Add cheat documentation, enable input repeat for L and R, show cheat category name

This commit is contained in:
Gericom
2026-03-08 13:03:26 +01:00
parent 43b1bf7afa
commit 6c34d9324d
22 changed files with 189 additions and 67 deletions

View File

@@ -15,6 +15,7 @@ This repository contains Pico Launcher, which is a front-end for [Pico Loader](h
- [Covers](docs/Covers.md)
- [Material Design 3 and custom themes](docs/Themes.md)
- Support for background music (see [Themes](docs/Themes.md))
- Support for cheats (See [Cheats](docs/Cheats.md))
General usage documentation can be found here: [Usage](docs/Usage.md).

View File

@@ -45,7 +45,7 @@ App::App(IAppSettingsService& appSettingsService, IBgmService& bgmService)
, _bgmService(bgmService)
, _inputProvider(&_inputSource)
, _inputRepeater(&_inputProvider,
InputKey::DpadLeft | InputKey::DpadRight | InputKey::DpadUp | InputKey::DpadDown,
InputKey::DpadLeft | InputKey::DpadRight | InputKey::DpadUp | InputKey::DpadDown | InputKey::L | InputKey::R,
25, 8)
, _romBrowserController(&appSettingsService, &_ioTaskQueue, &_bgTaskQueue)
, _displaySettingsBottomSheetViewModel(&_romBrowserController)

View File

@@ -1,27 +1,48 @@
#pragma once
#include "CheatTreeItem.h"
class Cheat : public CheatTreeItem
/// @brief Class representing a single cheat.
class Cheat
{
public:
Cheat()
: _cheatData(nullptr), _cheatDataLength(0) { }
Cheat() { }
Cheat(const char* name, const char* description, u32* flagsPointer, const void* cheatData, u32 cheatDataLength)
: CheatTreeItem(name, description), _flagsPointer(flagsPointer)
: _name(name), _description(description), _flagsPointer(flagsPointer)
, _cheatData(cheatData), _cheatDataLength(cheatDataLength) { }
/// @brief Gets the name of this cheat.
/// @return A pointer to the name of this cheat.
const char* GetName() const
{
return _name;
}
/// @brief Gets the description of this cheat.
/// @return A pointer to the description of this cheat.
const char* GetDescription() const
{
return _description;
}
/// @brief Gets a pointer to the data of this cheat.
/// @param cheatDataLength The length of the cheat data is returned in this reference.
/// @return A pointer to the cheat data.
/// This pointer is only valid for the lifetime of the \see GameCheats instance this cheat belongs to.
const void* GetCheatData(u32& cheatDataLength) const
{
cheatDataLength = _cheatDataLength;
return _cheatData;
}
/// @brief Gets whether this cheat is active (enabled) or not.
/// @return \c true when this cheat is active, or \c false when not active.
bool GetIsCheatActive() const
{
return ((*_flagsPointer >> 24) & 1) == 1;
}
/// @brief Sets whether this cheat is active (enabled) or not.
/// @param isCheatActive \c true to enable this cheat, or \c false to disable this cheat.
void SetIsCheatActive(bool isCheatActive) const
{
u32 flags = *_flagsPointer;
@@ -34,7 +55,9 @@ public:
}
private:
u32* _flagsPointer;
const void* _cheatData;
u32 _cheatDataLength;
const char* _name = nullptr;
const char* _description = nullptr;
u32* _flagsPointer = nullptr;
const void* _cheatData = nullptr;
u32 _cheatDataLength = 0;
};

View File

@@ -1,19 +1,16 @@
#pragma once
#include <memory>
#include "CheatTreeItem.h"
#include "Cheat.h"
#include "ICheatCategory.h"
class CheatCategory : public CheatTreeItem, public ICheatCategory
/// @brief Class representing a cheat category, containing sub-categories and cheats.
class CheatCategory : public ICheatCategory
{
public:
CheatCategory()
: _isMaxOneCheatActive(false), _subCategories(nullptr), _numberOfSubCategories(0)
, _cheats(nullptr), _numberOfCheats(0) { }
CheatCategory() { }
CheatCategory(const char* name, const char* description, bool isMaxOneCheatActive,
CheatCategory* subCategories, u32 numberOfSubCategories, Cheat* cheats, u32 numberOfCheats)
: CheatTreeItem(name, description), _isMaxOneCheatActive(isMaxOneCheatActive)
: _name(name), _description(description), _isMaxOneCheatActive(isMaxOneCheatActive)
, _subCategories(subCategories), _numberOfSubCategories(numberOfSubCategories)
, _cheats(cheats), _numberOfCheats(numberOfCheats) { }
@@ -56,6 +53,18 @@ public:
}
}
const char* GetName() const override
{
return _name;
}
/// @brief Gets the description of this cheat category.
/// @return A pointer to the description of this cheat category.
const char* GetDescription() const
{
return _description;
}
bool GetIsMaxOneCheatActive() const override
{
return _isMaxOneCheatActive;
@@ -74,9 +83,11 @@ public:
}
private:
bool _isMaxOneCheatActive;
CheatCategory* _subCategories;
u32 _numberOfSubCategories;
Cheat* _cheats;
u32 _numberOfCheats;
const char* _name = nullptr;
const char* _description = nullptr;
bool _isMaxOneCheatActive = false;
CheatCategory* _subCategories = nullptr;
u32 _numberOfSubCategories = 0;
Cheat* _cheats = nullptr;
u32 _numberOfCheats = 0;
};

View File

@@ -1,25 +0,0 @@
#pragma once
class CheatTreeItem
{
public:
const char* GetName() const
{
return _name;
}
const char* GetDescription() const
{
return _description;
}
protected:
const char* _name;
const char* _description;
CheatTreeItem()
: _name(nullptr), _description(nullptr) { }
CheatTreeItem(const char* name, const char* description)
: _name(name), _description(description) { }
};

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ICheatRepository.h"
/// @brief Class implementing an empty cheat repository.
class EmptyCheatRepository : public ICheatRepository
{
public:

View File

@@ -3,6 +3,7 @@
#include "CheatCategory.h"
#include "ICheatCategory.h"
/// @brief Class holding the cheats for a game.
class GameCheats : public ICheatCategory
{
public:
@@ -24,16 +25,13 @@ public:
}
}
/// @brief Gets the name of the game.
/// @return A pointer to the name of the game.
const char* GetGameName() const
{
return _gameName;
}
bool GetIsMaxOneCheatActive() const override
{
return false;
}
const CheatCategory* GetCategories(u32& numberOfCategories) const override
{
numberOfCategories = _numberOfCategories;
@@ -46,6 +44,9 @@ public:
return _cheats;
}
/// @brief Gets a pointer to the cheat data.
/// @param cheatDataLength The length of the cheat data is returned in this reference.
/// @return A pointer to the cheat data. This pointer is only valid for the lifetime of this \see GameCheats instance.
u8* GetCheatData(u32& cheatDataLength) const
{
cheatDataLength = _cheatDataLength;
@@ -66,4 +67,15 @@ private:
u32 _numberOfCategories;
Cheat* _cheats;
u32 _numberOfCheats;
// From ICheatCategory
const char* GetName() const override
{
return "";
}
bool GetIsMaxOneCheatActive() const override
{
return false;
}
};

View File

@@ -3,13 +3,28 @@
class CheatCategory;
class Cheat;
/// @brief Interface for a collection of cheats.
class ICheatCategory
{
public:
virtual ~ICheatCategory() = default;
/// @brief Gets the name of this category.
/// @return The name of this category.
virtual const char* GetName() const = 0;
/// @brief Indicates if only one cheat in this category is allowed to be on or not.
/// @return \c true when only one cheat is allowed to be active in this category, or \c false otherwise.
virtual bool GetIsMaxOneCheatActive() const = 0;
/// @brief Gets the sub-categories of this category.
/// @param numberOfCategories The number of categories is returned through this reference.
/// @return A pointer to an array of categories. This may be \c nullptr when \p numberOfCategories is 0.
virtual const CheatCategory* GetCategories(u32& numberOfCategories) const;
/// @brief Gets the cheats in this category.
/// @param numberOfCheats The number of cheats is returned through this reference.
/// @return A pointer to an array of cheats. This may be \c nullptr when \p numberOfCheats is 0.
virtual const Cheat* GetCheats(u32& numberOfCheats) const;
protected:

View File

@@ -2,12 +2,19 @@
#include "GameCheats.h"
#include "fat/FastFileRef.h"
/// @brief Interface for a repository providing access to cheats.
class ICheatRepository
{
public:
virtual ~ICheatRepository() { }
/// @brief Gets the available cheats for the given \p romFile.
/// @param romFile Reference to the rom file.
/// @return A unique pointer to the found cheats, or an empty unique pointer when no cheats were found.
virtual std::unique_ptr<GameCheats> GetCheatsForGame(const FastFileRef& romFile) const = 0;
/// @brief Writes the enable/disabled status of the given \p cheats.
/// @param cheats The cheats to update.
virtual void UpdateEnabledCheatsForGame(const std::unique_ptr<GameCheats>& cheats) const = 0;
protected:

View File

@@ -3,9 +3,13 @@
#include "GameCheats.h"
#include "picoLoader7.h"
/// @brief Factory for creating Pico Loader compatible cheat data.
class PicoLoaderCheatDataFactory
{
public:
/// @brief Converts the given \p gameCheats to Pico Loader format. Only the enabled cheats will be included.
/// @param gameCheats The cheats to convert.
/// @return Pointer to the created cheat data.
pload_cheats_t* CreateCheatData(const std::unique_ptr<GameCheats>& gameCheats) const;
private:

View File

@@ -1,5 +1,6 @@
#pragma once
/// @brief Struct representing an index entry of usrcheat.dat.
struct usr_cheat_index_entry_t
{
u32 gameCode;

View File

@@ -4,6 +4,7 @@
#include "ICheatRepository.h"
#include "UsrCheatDat.h"
/// @brief Class implementing a cheat repository for usrcheat.dat.
class UsrCheatRepository : public ICheatRepository
{
public:

View File

@@ -2,8 +2,12 @@
#include <memory>
#include "UsrCheatRepository.h"
/// @brief Factory for constructing a \see UsrCheatRepository for a usrcheat.dat file.
class UsrCheatRepositoryFactory
{
public:
/// @brief Constructs a \see UsrCheatRepository for the given \p usrCheatDatPath.
/// @param usrCheatDatPath The path to the usrcheat.dat file.
/// @return A unique pointer to the constructed repository, or an empty unique pointer when construction failed.
std::unique_ptr<UsrCheatRepository> FromUsrCheatDat(const TCHAR* usrCheatDatPath);
};

View File

@@ -24,7 +24,7 @@ CheatsViewModel::CheatsViewModel(const FileInfo& romFileInfo, IRomBrowserControl
});
}
void CheatsViewModel::ItemActivated()
void CheatsViewModel::ActivateSelectedItem()
{
auto cheatCategory = GetCurrentCheatCategory();
u32 numberOfCategories = 0;
@@ -58,21 +58,25 @@ void CheatsViewModel::ItemActivated()
}
}
void CheatsViewModel::Back()
bool CheatsViewModel::NavigateUp()
{
if (_categoryStackLevel == 0)
{
Close();
return false;
}
else
{
_selectedItem = _categoryStack[_categoryStackLevel].index;
_categoryStack[_categoryStackLevel--] = { nullptr, 0 };
return true;
}
}
void CheatsViewModel::Close()
{
_categoryStack.fill({ nullptr, 0 });
if (_changed)
{
// Save which cheats are enabled/disabled

View File

@@ -10,26 +10,57 @@
class CheatsViewModel
{
public:
/// @brief Enum representing the state of the cheats panel.
enum class State
{
/// @brief Cheats are being loaded.
Loading,
/// @brief No cheats were found.
NoCheats,
/// @brief Cheats are being displayed.
DisplayCheats
};
CheatsViewModel(const FileInfo& romFileInfo, IRomBrowserController* romBrowserController);
void ItemActivated();
void Back();
/// @brief Activates the selected cheat or category.
void ActivateSelectedItem();
/// @brief Navigates up in the cheat hierachy, or closes the cheats panel when at the root.
/// @return \c true when navigation happened in the cheats tree, or \c false when the cheats panel was closed.
bool NavigateUp();
/// @brief Closes the cheats panel.
void Close();
/// @brief Disables all cheats.
void DisableAllCheats();
/// @brief Gets the current state of the cheats panel.
/// @return The current state of the cheats panel.
State GetState() const { return _state; }
/// @brief Gets the current cheat category.
/// @return The current cheat category.
const ICheatCategory* GetCurrentCheatCategory() const { return _categoryStack[_categoryStackLevel].cheatCategory; }
/// @brief Gets the index of the selected item.
/// @return The index of the selected item.
constexpr int GetSelectedItem() const { return _selectedItem; }
/// @brief Sets the index of the selected item.
/// @param selectedItem The index of the selected item to set.
void SetSelectedItem(int selectedItem) { _selectedItem = selectedItem; }
/// @brief Returns whether the category name should be displayed.
/// @return \c true when the category name should be displayed, or \c false otherwise.
bool ShouldShowCategoryName() const
{
return _categoryStackLevel > 0;
}
private:
struct CategoryStackEntry
{

View File

@@ -7,6 +7,7 @@
class MaterialColorScheme;
class IFontRepository;
/// @brief List item view for the cheats panel, representing a single cheat or cheat category.
class CheatListItemView : public ViewContainer
{
public:

View File

@@ -4,6 +4,7 @@
#include "cheats/Cheat.h"
#include "CheatListItemView.h"
/// @brief Recycler adapter for cheats.
class CheatsAdapter : public RecyclerAdapter
{
public:

View File

@@ -16,6 +16,9 @@
#define TITLE_LABEL_X 20
#define TITLE_LABEL_Y 16
#define CATEGORY_NAME_LABEL_X (TITLE_LABEL_X + 43)
#define CATEGORY_NAME_LABEL_Y (TITLE_LABEL_Y + 1)
#define NO_CHEATS_FOUND_LABEL_X 20
#define NO_CHEATS_FOUND_LABEL_Y 36
@@ -32,7 +35,7 @@ CheatsBottomSheetView::CheatsBottomSheetView(std::unique_ptr<CheatsViewModel> vi
FocusManager* focusManager)
: _viewModel(std::move(viewModel))
, _titleLabel(64, 16, 25, fontRepository->GetFont(FontType::Medium11))
, _noCheatsFoundLabel(96, 16, 25, fontRepository->GetFont(FontType::Regular10))
, _secondaryLabel(177, 16, 64, fontRepository->GetFont(FontType::Regular10))
, _descriptionLabel(224, 16, 256, fontRepository->GetFont(FontType::Medium7_5))
, _cheatListRecycler(std::make_unique<RecyclerView>(
LIST_X, LIST_Y, LIST_WIDTH, LIST_HEIGHT, RecyclerView::Mode::VerticalList))
@@ -41,11 +44,12 @@ CheatsBottomSheetView::CheatsBottomSheetView(std::unique_ptr<CheatsViewModel> vi
, _focusManager(focusManager)
{
_titleLabel.SetText(u"Cheats");
_noCheatsFoundLabel.SetText(u"No cheats found.");
_secondaryLabel.SetText(u"No cheats found.");
_secondaryLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Ellipsis);
_descriptionLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Marquee);
_descriptionLabel.SetText("");
AddChildTail(&_titleLabel);
AddChildTail(&_noCheatsFoundLabel);
AddChildTail(&_secondaryLabel);
AddChildTail(&_descriptionLabel);
AddChildTail(_cheatListRecycler.get());
}
@@ -80,7 +84,14 @@ void CheatsBottomSheetView::InitVram(const VramContext& vramContext)
void CheatsBottomSheetView::Update()
{
_titleLabel.SetPosition(TITLE_LABEL_X, _position.y + TITLE_LABEL_Y);
_noCheatsFoundLabel.SetPosition(NO_CHEATS_FOUND_LABEL_X, _position.y + NO_CHEATS_FOUND_LABEL_Y);
if (_viewModel->GetState() == CheatsViewModel::State::DisplayCheats)
{
_secondaryLabel.SetPosition(CATEGORY_NAME_LABEL_X, _position.y + CATEGORY_NAME_LABEL_Y);
}
else
{
_secondaryLabel.SetPosition(NO_CHEATS_FOUND_LABEL_X, _position.y + NO_CHEATS_FOUND_LABEL_Y);
}
_descriptionLabel.SetPosition(DESCRIPTION_LABEL_X, _position.y + DESCRIPTION_LABEL_Y);
_cheatListRecycler->SetPosition(LIST_X, _position.y + LIST_Y);
if (_viewModel->GetState() == CheatsViewModel::State::DisplayCheats)
@@ -168,11 +179,12 @@ void CheatsBottomSheetView::Draw(GraphicsContext& graphicsContext)
_titleLabel.SetForegroundColor(_materialColorScheme->onSurface);
_titleLabel.Draw(graphicsContext);
if (_viewModel->GetState() == CheatsViewModel::State::NoCheats)
if (_viewModel->GetState() == CheatsViewModel::State::NoCheats ||
_viewModel->ShouldShowCategoryName())
{
_noCheatsFoundLabel.SetBackgroundColor(backColor);
_noCheatsFoundLabel.SetForegroundColor(_materialColorScheme->onSurface);
_noCheatsFoundLabel.Draw(graphicsContext);
_secondaryLabel.SetBackgroundColor(backColor);
_secondaryLabel.SetForegroundColor(_materialColorScheme->onSurfaceVariant);
_secondaryLabel.Draw(graphicsContext);
}
_descriptionLabel.SetBackgroundColor(backColor);
@@ -190,9 +202,10 @@ bool CheatsBottomSheetView::HandleInput(const InputProvider& inputProvider, Focu
if (focusManager.IsFocusInside(_cheatListRecycler.get()))
{
auto oldCategory = _viewModel->GetCurrentCheatCategory();
_viewModel->ItemActivated();
_viewModel->ActivateSelectedItem();
if (oldCategory != _viewModel->GetCurrentCheatCategory())
{
_secondaryLabel.SetText(_viewModel->GetCurrentCheatCategory()->GetName());
UpdateCheatList();
}
@@ -202,8 +215,8 @@ bool CheatsBottomSheetView::HandleInput(const InputProvider& inputProvider, Focu
else if (inputProvider.Triggered(InputKey::B))
{
auto oldCategory = _viewModel->GetCurrentCheatCategory();
_viewModel->Back();
if (oldCategory != _viewModel->GetCurrentCheatCategory())
if (_viewModel->NavigateUp() &&
oldCategory != _viewModel->GetCurrentCheatCategory())
{
UpdateCheatList();
}

View File

@@ -11,6 +11,7 @@ class MaterialColorScheme;
class IFontRepository;
class IVramManager;
/// @brief Bottom sheet for browsing and enabling/disabling cheats.
class CheatsBottomSheetView : public BottomSheetView
{
public:
@@ -40,7 +41,7 @@ public:
private:
std::unique_ptr<CheatsViewModel> _viewModel;
Label2DView _titleLabel;
Label2DView _noCheatsFoundLabel;
Label2DView _secondaryLabel;
Label2DView _descriptionLabel;
std::unique_ptr<RecyclerView> _cheatListRecycler;
CheatsAdapter* _cheatsAdapter = nullptr;

15
docs/Cheats.md Normal file
View File

@@ -0,0 +1,15 @@
# Cheats
Pico Launcher supports cheats from a `usrcheat.dat` file placed at `/_pico/usrcheat.dat`. The file stores the cheat codes, as well as which cheats are enabled. When a game is started which has cheats enabled, the enabled cheats are passed to Pico Loader.
## Usage
To display the available cheats highlight a game in the rom browser and press Y to access the cheats panel.
![Cheats panel](images/Cheats.png)
### Controls
- DPAD up/down: Scroll through the list of cheats.
- L and R: Scroll quickly when there are many cheats.
- A: Toggle a cheat on/off, or go into a cheat category.
- B: Go up in the cheat hierarchy, or close the cheats panel when at the root.
- Y: Close the cheats panel.
- X: Disable all cheats.

View File

@@ -12,6 +12,7 @@ From here you can browse your SD card to launch homebrew and games.
- A: Open a folder, or to launch a homebrew or game.
- B: Go to the parent folder or close a menu.
- L and R: Scroll quickly when there are many items in a folder.
- Y: Open the cheats panel (see [Cheats](Cheats.md)).
The back arrow on the top left of the bottom screen can also be used to go up to the parent folder.

BIN
docs/images/Cheats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB