30 Commits

Author SHA1 Message Date
Gericom
127de36b1c Update changelog 2026-03-29 17:03:39 +02:00
Gericom
5442c02341 Add changelog 2026-03-29 16:20:27 +02:00
Gericom
cf9ce63db5 Add various extra customization options for custom themes. Fixes #40 2026-03-29 14:48:12 +02:00
Gericom
53727e5fdd Avoid having a single frame where the icon was not displayed on the top screen after selecting a different rom 2026-03-29 12:21:20 +02:00
Gericom
9ca3e38668 Enable marquee for file names on the top screen. Fixes #22 2026-03-29 11:56:13 +02:00
Gericom
3f780fdd69 Improve error handling for parsing banners. Fixes #18 2026-03-29 11:47:08 +02:00
Gericom
7c06abf224 Hide files/dirs with hidden attribute and files/dirs starting with a period. Fixes #13, fixes #23 2026-03-29 09:55:45 +02:00
Gericom
2c142caa98 Enable debug information 2026-03-29 09:41:51 +02:00
Gericom
b7d7f9f352 Change cheat implementation to show cheats in database order, fix some bugs
- AdvancedPaletteManager incorrectly handled negative y positions
- FocusManager still had a pointer to a view that was destroyed in the cheats panel. After changing focus, memory got corrupted.
2026-03-15 13:28:59 +01:00
Gericom
601fd6371e Do not attempt to draw NdsFileIcon when the vram address is not yet set 2026-03-15 10:22:33 +01:00
Gericom
a4ecea6802 Fix some bugs in the cheats panel 2026-03-14 10:55:10 +01:00
Gericom
6c34d9324d Add cheat documentation, enable input repeat for L and R, show cheat category name 2026-03-08 13:03:26 +01:00
Gericom
43b1bf7afa Fix vram corruption when using the cheat panel in vertical grid mode
The graphics of an animated nds icon are now not all uploaded to vram at the same time.
There are now 1024 bytes available for each icon (previously 4096).
Double buffering is used to upload the new icon frame every time it changes.
2026-03-08 11:32:48 +01:00
Gericom
12ebd482d4 Add option to disable all cheats by pressing X in the cheat panel 2026-03-07 14:51:58 +01:00
Gericom
4d9318b0b9 Fix label marquee speed, add cheat description label 2026-03-01 17:12:56 +01:00
Gericom
10431c4615 Remember selected index when returning from cheat category 2026-03-01 16:24:05 +01:00
Gericom
e2e42115e7 Add marquee for long cheat names 2026-03-01 16:18:06 +01:00
Gericom
a9425eea7c Add cheats not found label 2026-03-01 13:43:31 +01:00
Gericom
7f35d524ae Fix a few bugs related to the cheat panel 2026-02-28 17:32:40 +01:00
Gericom
126b898ac4 Run CI on all branches 2026-02-28 17:10:57 +01:00
Gericom
f54a379ff2 Further work on support for cheats
Cheats can now be enabled/disabled and games can be launched with cheats
2026-02-28 17:00:02 +01:00
Gericom
dddee0bb94 Initial work on implementing support for cheats 2026-02-22 20:28:35 +01:00
Gericom
f73c8b0547 Merge pull request #21 from oakwoodwolf/develop
Update Usage.md
2026-02-08 16:35:52 +01:00
Roy Flaherty
a102068433 Update Usage.md 2026-02-03 21:42:24 +00:00
Gericom
d76d46ea73 Added support for Pico Loader API v2. This makes it possible to return to Pico Launcher from homebrew. 2026-01-10 17:08:06 +01:00
Gericom
e4c2fafa74 Updated to latest blocksds (Fixes #14) 2026-01-04 11:00:33 +01:00
Gericom
e2a8a540e9 Merge pull request #12 from lifehackerhansol/workflow-release
workflow: add release pipeline
2025-12-04 13:41:05 +01:00
lifehackerhansol
17d8e3b4f8 workflow: add release pipeline
- Minor change in push pipeline to remove spaces from artifact name
- Create release pipeline with the same steps that will upload a zipped
  package to every published release automatically
2025-12-03 21:09:03 -08:00
Gericom
9863dbf631 Merge pull request #10 from lifehackerhansol/workflow-remove-dotnet-env
workflow: remove unnecessary dotnet environment variables
2025-11-29 14:41:52 +01:00
lifehackerhansol
c77bf774d4 workflow: remove unnecessary dotnet environment variables
- This is not needed in pico-launcher.
2025-11-29 04:43:35 -08:00
92 changed files with 2596 additions and 280 deletions

View File

@@ -2,7 +2,6 @@ name: Build Pico Launcher
on:
push:
branches: ["develop"]
paths-ignore:
- 'README.md'
pull_request:
@@ -14,12 +13,8 @@ on:
jobs:
pico_launcher:
runs-on: ubuntu-latest
container: skylyrac/blocksds:slim-v1.13.1
container: skylyrac/blocksds:slim-v1.16.0
name: Build Pico Launcher
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -34,4 +29,4 @@ jobs:
path: |
_pico/
LAUNCHER.nds
name: Pico Launcher
name: Pico_Launcher

39
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Build Pico Launcher release
on:
release:
types: [published]
jobs:
pico_launcher:
runs-on: ubuntu-latest
container: skylyrac/blocksds:slim-v1.16.0
name: Build Pico Launcher
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: true
- name: Install zip
run:
apt-get update && apt-get -y install zip
- name: Run build script
run: |
make
- name: Publish build to GH Actions
uses: actions/upload-artifact@v4
with:
path: |
_pico/
LAUNCHER.nds
name: Pico_Launcher
- name: Package for release
run: |
mkdir Pico_Launcher
cp -r _pico/ LAUNCHER.nds Pico_Launcher
cd Pico_Launcher && zip -r $PWD.zip *
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |
Pico_Launcher.zip

27
CHANGELOG.md Normal file
View File

@@ -0,0 +1,27 @@
# Changelog
## [Unreleased]
## [v1.2.0] - 29 Mar 2026
### Added
- Support for cheats with Pico Loader API v3
- Hide files/dirs with hidden attribute, or with a name starting with a period
- New customization options for custom themes
- Position of elements on the top screen
- Text colors
- Blend colors
### Changed
- File name on the top screen now uses marquee when too long
### Fixed
- Improve error handling for banners to better detect if a rom has a valid banner
## [v1.1.0] - 11 Jan 2026
### Added
- Support for Pico Loader API v2. This makes it possible to return to Pico Launcher from supported homebrew applications.
## [v1.0.0] - 25 Nov 2025
- Initial release

View File

@@ -105,17 +105,17 @@ INCLUDEFLAGS := $(foreach path,$(INCLUDEDIRS),-I$(path)) \
LIBDIRSFLAGS := $(foreach path,$(LIBDIRS),-L$(path)/lib)
ASFLAGS += -x assembler-with-cpp $(DEFINES) $(INCLUDEFLAGS) \
$(ARCH) -ffunction-sections -fdata-sections \
$(ARCH) -g -ffunction-sections -fdata-sections \
-specs=$(SPECS)
CFLAGS += -std=gnu17 $(WARNFLAGS) $(DEFINES) $(INCLUDEFLAGS) \
$(ARCH) -O2 -ffunction-sections -fdata-sections \
$(ARCH) -g -O2 -ffunction-sections -fdata-sections \
-fno-devirtualize-speculatively \
-Werror=return-type \
-specs=$(SPECS)
CXXFLAGS += -std=gnu++23 $(WARNFLAGS) $(DEFINES) $(INCLUDEFLAGS) \
$(ARCH) -O2 -ffunction-sections -fdata-sections \
$(ARCH) -g -O2 -ffunction-sections -fdata-sections \
-fno-exceptions -fno-rtti \
-fno-devirtualize-speculatively \
-Werror=return-type \

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

@@ -8,5 +8,119 @@
"g": 217,
"b": 255
},
"darkTheme": false
"darkTheme": false,
"topIcon": {
"position": {
"x": 24,
"y": 132
},
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"topBannerTextLine0": {
"position": {
"x": 70,
"y": 126
},
"width": 176,
"textColor": {
"r": 30,
"g": 30,
"b": 30
},
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"topBannerTextLine1": {
"position": {
"x": 70,
"y": 141
},
"width": 176,
"textColor": {
"r": 30,
"g": 30,
"b": 30
},
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"topBannerTextLine2": {
"position": {
"x": 70,
"y": 155
},
"width": 176,
"textColor": {
"r": 30,
"g": 30,
"b": 30
},
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"topFileNameText": {
"position": {
"x": 18,
"y": 170
},
"width": 220,
"textColor": {
"r": 30,
"g": 30,
"b": 30
},
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"gridIcon": {
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"bannerListIcon": {
"blendColor": {
"r": 200,
"g": 200,
"b": 200
}
},
"bannerListTextLine0": {
"textColor": {
"r": 30,
"g": 30,
"b": 30
}
},
"bannerListTextLine1": {
"textColor": {
"r": 30,
"g": 30,
"b": 30
}
},
"bannerListTextLine2": {
"textColor": {
"r": 30,
"g": 30,
"b": 30
}
}
}

View File

@@ -0,0 +1,7 @@
# tile format
-gt
# graphics bit depth is 4 (16 color)
-gB4
-p!

BIN
arm9/gfx/cheatSelector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -0,0 +1,7 @@
# tile format
-gt
# graphics bit depth is 4 (16 color)
-gB4
-p!

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -0,0 +1,7 @@
# tile format
-gt
# graphics bit depth is 4 (16 color)
-gB4
-p!

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

7
arm9/gfx/folderIcon.grit Normal file
View File

@@ -0,0 +1,7 @@
# tile format
-gt
# graphics bit depth is 4 (16 color)
-gB4
-p!

BIN
arm9/gfx/folderIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

View File

@@ -20,6 +20,7 @@
#include "romBrowser/DisplayMode/RomBrowserDisplayModeFactory.h"
#include "romBrowser/Theme/Material/MaterialThemeFileIconFactory.h"
#include "romBrowser/views/NdsGameDetailsBottomSheetView.h"
#include "romBrowser/views/cheats/CheatsBottomSheetView.h"
#include "romBrowser/views/DisplaySettingsBottomSheetView.h"
#include "bgm/AudioStreamPlayer.h"
#include "bgm/BgmService.h"
@@ -44,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)
@@ -295,10 +296,15 @@ void App::HandleTrigger(RomBrowserStateTrigger trigger, RomBrowserState newState
void App::HandleShowGameInfoTrigger()
{
auto gameInfoDialog = std::make_unique<NdsGameDetailsBottomSheetView>(
&_romBrowserController, &_theme->GetMaterialColorScheme(), _theme->GetFontRepository());
gameInfoDialog->SetGraphics(_chipViewVram);
_dialogPresenter.ShowDialog(std::move(gameInfoDialog));
// auto gameInfoDialog = std::make_unique<NdsGameDetailsBottomSheetView>(
// &_romBrowserController, &_theme->GetMaterialColorScheme(), _theme->GetFontRepository());
// gameInfoDialog->SetGraphics(_chipViewVram);
// _dialogPresenter.ShowDialog(std::move(gameInfoDialog));
auto cheatsViewModel = std::make_unique<CheatsViewModel>(_romBrowserController.GetTriggerFileInfo(), &_romBrowserController);
auto cheatsDialog = std::make_unique<CheatsBottomSheetView>(
std::move(cheatsViewModel), &_theme->GetMaterialColorScheme(), _theme->GetFontRepository(), &_focusManager);
_dialogPresenter.ShowDialog(std::move(cheatsDialog));
}
void App::HandleHideGameInfoTrigger()

View File

@@ -0,0 +1,190 @@
#pragma once
/// @brief Class representing a cheat or a cheat category.
class CheatEntry
{
public:
/// @brief Dummy empty constructor.
CheatEntry()
: _isCheatCategory(false), _flagsPointer(nullptr), _cheatData(nullptr), _cheatDataLength(0) { }
/// @brief Constructor for a cheat.
CheatEntry(const char* name, const char* description, u32* flagsPointer, const void* cheatData, u32 cheatDataLength)
: _name(name), _description(description), _isCheatCategory(false), _flagsPointer(flagsPointer)
, _cheatData(cheatData), _cheatDataLength(cheatDataLength) { }
/// @brief Constructor for a category.
CheatEntry(const char* name, const char* description, bool isMaxOneCheatActive, CheatEntry* subEntries, u32 numberOfSubEntries)
: _name(name), _description(description), _isCheatCategory(true), _isMaxOneCheatActive(isMaxOneCheatActive)
, _subEntries(subEntries), _numberOfSubEntries(numberOfSubEntries) { }
CheatEntry(const CheatEntry& other) = delete;
CheatEntry(CheatEntry&& other)
: _isCheatCategory(false), _flagsPointer(nullptr), _cheatData(nullptr), _cheatDataLength(0)
{
*this = std::move(other);
}
~CheatEntry()
{
if (_isCheatCategory && _subEntries != nullptr)
{
delete[] _subEntries;
}
}
CheatEntry& operator=(const CheatEntry& other) = delete;
CheatEntry& operator=(CheatEntry&& other)
{
if (_isCheatCategory && _subEntries != nullptr)
{
delete[] _subEntries;
_subEntries = nullptr;
}
_name = other._name;
other._name = nullptr;
_description = other._description;
other._description = nullptr;
_isCheatCategory = other.IsCheatCategory();
if (_isCheatCategory)
{
_isMaxOneCheatActive = other._isMaxOneCheatActive;
_subEntries = other._subEntries;
other._subEntries = nullptr;
_numberOfSubEntries = other._numberOfSubEntries;
other._numberOfSubEntries = 0;
}
else
{
_flagsPointer = other._flagsPointer;
other._flagsPointer = nullptr;
_cheatData = other._cheatData;
other._cheatData = nullptr;
_cheatDataLength = other._cheatDataLength;
other._cheatDataLength = 0;
}
return *this;
}
/// @brief Gets the name of this cheat entry.
/// @return A pointer to the name of this cheat entry.
const char* GetName() const
{
return _name;
}
/// @brief Gets the description of this cheat entry.
/// @return A pointer to the description of this cheat entry.
const char* GetDescription() const
{
return _description;
}
/// @brief When this entry is a cheat, 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 the \see GameCheats instance this cheat belongs to.
/// If this entry is not a cheat, \c nullptr is returned.
const void* GetCheatData(u32& cheatDataLength) const
{
if (_isCheatCategory)
{
cheatDataLength = 0;
return nullptr;
}
else
{
cheatDataLength = _cheatDataLength;
return _cheatData;
}
}
/// @brief Gets whether this entry is an active (enabled) cheat or not.
/// @return \c true when this entry is an active cheat, or \c false otherwise.
bool GetIsCheatActive() const
{
if (_isCheatCategory)
{
return false;
}
else
{
return ((*_flagsPointer >> 24) & 1) == 1;
}
}
/// @brief If this entry is a cheat, 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
{
if (!_isCheatCategory)
{
u32 flags = *_flagsPointer;
flags &= ~(1 << 24);
if (isCheatActive)
{
flags |= 1 << 24;
}
*_flagsPointer = flags;
}
}
/// @brief Indicates if this entry is a cheat category or not.
/// @return \c true when this entry is a cheat category, or \c false when this entry is a cheat.
bool IsCheatCategory() const
{
return _isCheatCategory;
}
/// @brief Indicates if this entry is a cheat category in which only one cheat is allowed to be on at a time or not.
/// @return \c true when this entry is a cheat category in which only one cheat is allowed
/// to be active at a time, or \c false otherwise.
bool GetIsMaxOneCheatActive() const
{
return _isCheatCategory && _isMaxOneCheatActive;
}
/// @brief Gets the sub-entries of this entry.
/// @param numberOfSubEntries The number of sub-entries is returned through this reference.
/// @return A pointer to an array of entries. This may be \c nullptr when \p numberOfSubEntries is 0.
const CheatEntry* GetSubEntries(u32& numberOfSubEntries) const
{
if (_isCheatCategory)
{
numberOfSubEntries = _numberOfSubEntries;
return _subEntries;
}
else
{
numberOfSubEntries = 0;
return nullptr;
}
}
private:
const char* _name = nullptr;
const char* _description = nullptr;
bool _isCheatCategory;
union
{
struct
{
// For cheat
u32* _flagsPointer;
const void* _cheatData;
u32 _cheatDataLength;
};
struct
{
// For category
bool _isMaxOneCheatActive;
CheatEntry* _subEntries;
u32 _numberOfSubEntries;
};
};
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "ICheatRepository.h"
/// @brief Class implementing an empty cheat repository.
class EmptyCheatRepository : public ICheatRepository
{
public:
std::unique_ptr<GameCheats> GetCheatsForGame(const FastFileRef& romFile) const override
{
return nullptr;
}
void UpdateEnabledCheatsForGame(const std::unique_ptr<GameCheats>& cheats) const override
{
// Do nothing
}
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include "CheatEntry.h"
/// @brief Class holding the cheats for a game.
class GameCheats : public CheatEntry
{
public:
GameCheats(std::unique_ptr<u8[]> cheatData, u32 cheatDataLength, u32 fileOffset, const char* gameName,
CheatEntry* subEntries, u32 numberOfSubEntries)
: CheatEntry(gameName, "", false, subEntries, numberOfSubEntries)
, _cheatData(std::move(cheatData)), _cheatDataLength(cheatDataLength)
, _fileOffset(fileOffset) { }
/// @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;
return _cheatData.get();
}
u32 GetFileOffset() const
{
return _fileOffset;
}
private:
std::unique_ptr<u8[]> _cheatData;
u32 _cheatDataLength;
u32 _fileOffset;
};

View File

@@ -0,0 +1,22 @@
#pragma once
#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:
ICheatRepository() { }
};

View File

@@ -0,0 +1,80 @@
#include "common.h"
#include <string.h>
#include "PicoLoaderCheatDataFactory.h"
pload_cheats_t* PicoLoaderCheatDataFactory::CreateCheatData(const std::unique_ptr<GameCheats>& gameCheats) const
{
pload_cheats_t* cheatData = nullptr;
if (gameCheats)
{
u32 totalNumberOfCheats = 0;
u32 requiredSize = GetCheatEntryRequiredSize(gameCheats.get(), totalNumberOfCheats);
if (totalNumberOfCheats != 0)
{
requiredSize += sizeof(u32) * 2;
cheatData = (pload_cheats_t*)new u8[requiredSize];
cheatData->length = requiredSize;
cheatData->numberOfCheats = totalNumberOfCheats;
u8* buffer = (u8*)&cheatData->firstCheat;
GetCheatEntryData(gameCheats.get(), buffer);
}
}
return cheatData;
}
u32 PicoLoaderCheatDataFactory::GetCheatEntryRequiredSize(const CheatEntry* cheatEntry, u32& totalNumberOfCheats) const
{
u32 requiredSize = 0;
if (cheatEntry->IsCheatCategory())
{
u32 numberOfSubEntries = 0;
auto subEntries = cheatEntry->GetSubEntries(numberOfSubEntries);
for (u32 i = 0; i < numberOfSubEntries; i++)
{
requiredSize += GetCheatEntryRequiredSize(&subEntries[i], totalNumberOfCheats);
}
}
else
{
if (cheatEntry->GetIsCheatActive())
{
u32 cheatDataLength = 0;
cheatEntry->GetCheatData(cheatDataLength);
requiredSize += sizeof(u32) + ((cheatDataLength + 7) & ~7);
totalNumberOfCheats++;
}
}
return requiredSize;
}
void PicoLoaderCheatDataFactory::GetCheatEntryData(const CheatEntry* cheatEntry, u8*& buffer) const
{
if (cheatEntry->IsCheatCategory())
{
u32 numberOfSubEntries = 0;
auto subEntries = cheatEntry->GetSubEntries(numberOfSubEntries);
for (u32 i = 0; i < numberOfSubEntries; i++)
{
GetCheatEntryData(&subEntries[i], buffer);
}
}
else
{
if (cheatEntry->GetIsCheatActive())
{
u32 cheatDataLength = 0;
auto cheatData = cheatEntry->GetCheatData(cheatDataLength);
u32 paddedCheatDataLength = (cheatDataLength + 7) & ~7;
*(u32*)buffer = paddedCheatDataLength;
buffer += sizeof(u32);
memcpy(buffer, cheatData, cheatDataLength);
if (cheatDataLength != paddedCheatDataLength)
{
memset(buffer + cheatDataLength, 0, paddedCheatDataLength - cheatDataLength);
}
buffer += paddedCheatDataLength;
}
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <memory>
#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:
u32 GetCheatEntryRequiredSize(const CheatEntry* cheatEntry, u32& totalNumberOfCheats) const;
void GetCheatEntryData(const CheatEntry* cheatEntry, u8*& buffer) const;
};

View File

@@ -0,0 +1,12 @@
#pragma once
/// @brief Struct representing an index entry of usrcheat.dat.
struct usr_cheat_index_entry_t
{
u32 gameCode;
u32 headerCrc32;
u32 offset;
u32 padding;
};
static_assert(sizeof(usr_cheat_index_entry_t) == 16);

View File

@@ -0,0 +1,225 @@
#include "common.h"
#include <algorithm>
#include <string.h>
#include "UsrCheatRepository.h"
#define CRCPOLY 0xEDB88320
static u32 crc32(const void* buffer, u32 length)
{
u32 crc = ~0u;
const u8* p = (u8*)buffer;
while (length--)
{
crc ^= *p++;
for (int i = 0; i < 8; i++)
{
crc = (crc >> 1) ^ ((crc & 1) ? CRCPOLY : 0);
}
}
return crc;
}
std::unique_ptr<GameCheats> UsrCheatRepository::GetCheatsForGame(const FastFileRef& romFile) const
{
auto file = std::make_unique<File>();
file->Open(romFile, FA_READ);
auto headerBuffer = std::make_unique_for_overwrite<u8[]>(512);
if (!file->ReadExact(headerBuffer.get(), 512))
{
LOG_ERROR("Could not read rom header\n");
return nullptr;
}
file->Close();
u32 gameCode = *(u32*)(headerBuffer.get() + 0xC);
u32 crc = crc32(headerBuffer.get(), 512);
headerBuffer.reset();
return GetCheatsForGame(gameCode, crc);
}
void UsrCheatRepository::UpdateEnabledCheatsForGame(const std::unique_ptr<GameCheats>& cheats) const
{
u32 cheatDataLength = 0;
auto cheatData = cheats->GetCheatData(cheatDataLength);
if (_usrCheatFile->Seek(cheats->GetFileOffset()) != FR_OK)
{
LOG_ERROR("Failed to seek to cheat data\n");
return;
}
u32 bytesWritten = 0;
if (_usrCheatFile->Write(cheatData, cheatDataLength, bytesWritten) != FR_OK ||
bytesWritten != cheatDataLength)
{
LOG_ERROR("Failed to write cheat data\n");
return;
}
if (_usrCheatFile->Sync() != FR_OK)
{
LOG_ERROR("Failed to flush cheat data\n");
}
}
std::unique_ptr<GameCheats> UsrCheatRepository::GetCheatsForGame(u32 gameCode, u32 headerCrc32) const
{
auto index = FindIndex(gameCode, headerCrc32);
if (index == nullptr)
{
LOG_DEBUG("Cheats not found for %c%c%c%c - 0x%X\n",
gameCode & 0xFF, (gameCode >> 8) & 0xFF, (gameCode >> 16) & 0xFF, gameCode >> 24,
headerCrc32);
return nullptr;
}
const u32 cheatDataLength = index->padding; // padding was set to the size in UsrCheatRepositoryFactory
auto cheatData = std::make_unique_for_overwrite<u8[]>(cheatDataLength);
if (_usrCheatFile->Seek(index->offset) != FR_OK)
{
LOG_ERROR("Failed to seek to cheat data\n");
return nullptr;
}
if (!_usrCheatFile->ReadExact(cheatData.get(), cheatDataLength))
{
LOG_ERROR("Failed to read cheat data\n");
return nullptr;
}
u8* ptr = cheatData.get();
// game name
const char* gameName = (const char*)ptr;
ptr += strlen(gameName) + 1;
// padding
ptr = (u8*)(((u32)ptr + 3) & ~3); // 32-bit align
// flags
u32 flags = *(u32*)ptr;
u32 totalNumberOfItems = flags & 0x0FFFFFFF;
// u32 gameActive = flags >> 28;
ptr += 4;
// master codes
ptr += 8 * 4;
auto entries = new CheatEntry[totalNumberOfItems];
u32 entryCount = 0;
while (ptr < cheatData.get() + cheatDataLength)
{
u32 itemFlags = *(u32*)ptr;
bool isCategory = ((itemFlags >> 28) & 1) == 1;
if (isCategory)
{
entries[entryCount++] = ParseCategory(ptr);
}
else
{
entries[entryCount++] = ParseCheat(ptr);
}
}
auto actualEntries = new CheatEntry[entryCount];
std::move(entries, entries + entryCount, actualEntries);
delete[] entries;
return std::make_unique<GameCheats>(
std::move(cheatData), cheatDataLength, index->offset, gameName, actualEntries, entryCount);
}
const usr_cheat_index_entry_t* UsrCheatRepository::FindIndex(u32 gameCode, u32 headerCrc32) const
{
if (_numberOfIndices != 0)
{
const auto index = std::lower_bound(_sortedIndices.get(), _sortedIndices.get() + _numberOfIndices, gameCode,
[headerCrc32] (const usr_cheat_index_entry_t& entry, u32 value)
{
if (entry.gameCode != value)
{
return entry.gameCode < value;
}
return entry.headerCrc32 < headerCrc32;
});
if (index != _sortedIndices.get() + _numberOfIndices &&
index->gameCode == gameCode &&
index->headerCrc32 == headerCrc32)
{
return index;
}
}
return nullptr;
}
CheatEntry UsrCheatRepository::ParseCategory(u8*& ptr) const
{
// flags
u32 itemFlags = *(u32*)ptr;
ptr += 4;
u32 numberOfItems = itemFlags & 0x00FFFFFF;
bool isMaxOneCheatActive = ((itemFlags >> 24) & 1) == 1;
// item name
const char* itemName = (const char*)ptr;
ptr += strlen(itemName) + 1;
// item description
const char* itemDescription = (const char*)ptr;
ptr += strlen(itemDescription) + 1;
// padding
ptr = (u8*)(((u32)ptr + 3) & ~3); // 32-bit align
auto entries = new CheatEntry[numberOfItems];
for (u32 i = 0; i < numberOfItems; i++)
{
u32 itemFlags = *(u32*)ptr;
bool isCategory = ((itemFlags >> 28) & 1) == 1;
if (isCategory)
{
entries[i] = ParseCategory(ptr);
}
else
{
entries[i] = ParseCheat(ptr);
}
}
return CheatEntry(itemName, itemDescription, isMaxOneCheatActive, entries, numberOfItems);
}
CheatEntry UsrCheatRepository::ParseCheat(u8*& ptr) const
{
// flags
u32* flagsPtr = (u32*)ptr;
ptr += 4;
// item name
const char* itemName = (const char*)ptr;
ptr += strlen(itemName) + 1;
// item description
const char* itemDescription = (const char*)ptr;
ptr += strlen(itemDescription) + 1;
// padding
ptr = (u8*)(((u32)ptr + 3) & ~3); // 32-bit align
// number of code words
u32 numberOfCodeWords = *(u32*)ptr;
ptr += 4;
const void* cheatData = ptr;
// code
ptr += numberOfCodeWords * 4;
return CheatEntry(itemName, itemDescription, flagsPtr, cheatData, numberOfCodeWords * 4);
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <memory>
#include "fat/File.h"
#include "ICheatRepository.h"
#include "UsrCheatDat.h"
/// @brief Class implementing a cheat repository for usrcheat.dat.
class UsrCheatRepository : public ICheatRepository
{
public:
UsrCheatRepository(std::unique_ptr<File> usrCheatDatFile,
std::unique_ptr<usr_cheat_index_entry_t[]> sortedIndices, u32 numberOfIndices)
: _usrCheatFile(std::move(usrCheatDatFile))
, _sortedIndices(std::move(sortedIndices)), _numberOfIndices(numberOfIndices) { }
std::unique_ptr<GameCheats> GetCheatsForGame(const FastFileRef& romFile) const override;
void UpdateEnabledCheatsForGame(const std::unique_ptr<GameCheats>& cheats) const override;
private:
std::unique_ptr<File> _usrCheatFile;
std::unique_ptr<usr_cheat_index_entry_t[]> _sortedIndices;
u32 _numberOfIndices;
std::unique_ptr<GameCheats> GetCheatsForGame(u32 gameCode, u32 headerCrc32) const;
const usr_cheat_index_entry_t* FindIndex(u32 gameCode, u32 headerCrc32) const;
CheatEntry ParseCategory(u8*& ptr) const;
CheatEntry ParseCheat(u8*& ptr) const;
};

View File

@@ -0,0 +1,87 @@
#include "common.h"
#include <algorithm>
#include <string.h>
#include "fat/File.h"
#include "UsrCheatDat.h"
#include "UsrCheatRepositoryFactory.h"
std::unique_ptr<UsrCheatRepository> UsrCheatRepositoryFactory::FromUsrCheatDat(const TCHAR* usrCheatDatPath)
{
auto file = std::make_unique<File>();
if (file->Open(usrCheatDatPath, FA_READ | FA_WRITE) != FR_OK)
{
LOG_ERROR("Failed to open %s\n", usrCheatDatPath);
return nullptr;
}
char header[12];
if (!file->ReadExact(header, sizeof(header)))
{
LOG_ERROR("Failed to read usr cheat header\n");
return nullptr;
}
if (memcmp(header, "R4 CheatCode", sizeof(header)))
{
LOG_ERROR("Invalid usr cheat header\n");
return nullptr;
}
// Read index
if (file->Seek(0x100) != FR_OK)
{
LOG_ERROR("Failed to seek to usr cheat index\n");
return nullptr;
}
usr_cheat_index_entry_t firstEntry;
if (!file->ReadExact(&firstEntry, sizeof(firstEntry)))
{
LOG_ERROR("Failed to read first index\n");
return nullptr;
}
u32 numberOfIndices = 0;
if (firstEntry.offset != 0)
{
numberOfIndices = (firstEntry.offset - 0x100 - sizeof(usr_cheat_index_entry_t)) / sizeof(usr_cheat_index_entry_t);
}
if (file->Seek(0x100) != FR_OK)
{
LOG_ERROR("Failed to seek to usr cheat index\n");
return nullptr;
}
auto indices = std::make_unique_for_overwrite<usr_cheat_index_entry_t[]>(numberOfIndices);
if (!file->ReadExact(indices.get(), numberOfIndices * sizeof(usr_cheat_index_entry_t)))
{
LOG_ERROR("Failed to read first index\n");
return nullptr;
}
// Set padding field to the size of the cheat data
if (numberOfIndices > 0)
{
for (u32 i = 0; i < numberOfIndices - 1; i++)
{
indices[i].padding = indices[i + 1].offset - indices[i].offset;
}
indices[numberOfIndices - 1].padding = file->GetSize() - indices[numberOfIndices - 1].offset;
}
// sort by gameCode, then by headerCrc32
std::sort(indices.get(), indices.get() + numberOfIndices,
[] (const usr_cheat_index_entry_t& a, const usr_cheat_index_entry_t& b)
{
if (a.gameCode != b.gameCode)
{
return a.gameCode < b.gameCode;
}
return a.headerCrc32 < b.headerCrc32;
});
return std::make_unique<UsrCheatRepository>(std::move(file), std::move(indices), numberOfIndices);
}

View File

@@ -0,0 +1,13 @@
#pragma once
#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

@@ -75,3 +75,5 @@ public:
return Rgb(r + other.r, g + other.g, b + other.b);
}
};
using Rgb8 = Rgb<8, 8, 8>;

View File

@@ -65,7 +65,7 @@ template <>
class Task<void> : public TaskBase
{
protected:
virtual TaskResult<void> ExecuteFunc() const = 0;
virtual TaskResult<void> ExecuteFunc() = 0;
private:
TaskState ExecuteDirect() override
@@ -79,11 +79,11 @@ template <class T, typename FuncType>
class FuncTask : public Task<T>
{
public:
FuncTask(const FuncType& function)
: _function(function) { }
FuncTask(FuncType&& function)
: _function(std::move(function)) { }
private:
const FuncType _function;
FuncType _function;
TaskResult<T> ExecuteFunc() const override { return _function((const volatile u8&)this->_cancelRequested); }
TaskResult<T> ExecuteFunc() override { return _function((const volatile u8&)this->_cancelRequested); }
};

View File

@@ -79,12 +79,12 @@ public:
template <typename FuncType>
[[gnu::noinline]]
auto Enqueue(const FuncType& function) -> QueueTask<decltype(TaskResultToResultType(function(*new vu8())))>
auto Enqueue(FuncType&& function) -> QueueTask<decltype(TaskResultToResultType(function(*new vu8())))>
{
using TaskType = FuncTask<decltype(TaskResultToResultType(function(*new vu8()))), FuncType>;
// static_assert(sizeof(TaskType) <= MaxTaskSize, "Task is too big for this pool");
void* slot = GetSlot();
auto task = new (slot) TaskType(function);
auto task = new (slot) TaskType(std::move(function));
Enqueue(task);
return QueueTask(task, this);
}

View File

@@ -60,6 +60,16 @@ extern "C" void* memalign(size_t alignment, size_t size)
return result;
}
extern "C" void* calloc(size_t num, size_t size)
{
void* result = malloc(num * size);
if (result)
{
memset(result, 0, num * size);
}
return result;
}
void* operator new(std::size_t blocksize)
{
return malloc(blocksize);

View File

@@ -89,14 +89,8 @@ u32 AdvancedPaletteManagerBase::TryMerge(PaletteRow* rows, const PaletteRow& new
u32 AdvancedPaletteManagerBase::AllocRowInternal(PaletteRow* rows, const IPalette& palette, int yStart, int yEnd)
{
if (yStart < 0)
{
yStart = 0;
}
if (yEnd > 192)
{
yEnd = 192;
}
yStart = std::clamp(yStart, 0, 192);
yEnd = std::clamp(yEnd, 0, 192);
u32 newIdx = _usedRows;
PaletteRow& newRow = rows[newIdx];

View File

@@ -84,6 +84,7 @@ public:
u32 AllocRow(const IPalette& palette, int yStart, int yEnd) override
{
yEnd++; // avoid back to back allocation
return AllocRowInternal(_rows[_curSet], palette, yStart, yEnd);
}

View File

@@ -28,24 +28,28 @@ int nft2_findGlyphIdxForCharacter(const nft2_header_t* font, u16 character)
}
static inline void renderGlyph(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride, bool a5i3)
int xPos, int yPos, const nft2_string_render_params_t* renderParams, u8* dst, u32 stride, bool a5i3)
{
int yOffset = glyph->spacingTop;
u32 xStart = xPos < 0 ? -xPos : 0;
u32 yStart = yPos + yOffset < 0 ? -(yPos + yOffset) : 0;
int xEnd = glyph->glyphWidth;
if (xPos + xEnd > width)
if (xPos + xEnd > (int)renderParams->width)
{
// by returning we only render complete glyphs
return;
// old code for rendering partial glyphs
// xEnd = width - xPos;
if (renderParams->onlyRenderWholeGlyphs)
{
return;
}
xEnd = renderParams->width - xPos;
}
int yEnd = glyph->glyphHeight;
if (yPos + yOffset + yEnd > height)
yEnd = height - (yPos + yOffset); // allow partial glyphs in the vertical direction
if (yPos + yOffset + yEnd > (int)renderParams->height)
{
yEnd = renderParams->height - (yPos + yOffset); // allow partial glyphs in the vertical direction
}
const u8* glyphData = &font->glyphDataPtr[glyph->dataOffset];
glyphData += yStart * ((glyph->glyphWidth + 1) >> 1);
@@ -97,15 +101,15 @@ static inline void renderGlyph(const nft2_header_t* font, const nft2_glyph_t* gl
}
static ITCM_CODE void renderGlyphTiled(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride)
int xPos, int yPos, const nft2_string_render_params_t* renderParams, u8* dst, u32 stride)
{
renderGlyph(font, glyph, xPos, yPos, width, height, dst, stride, false);
renderGlyph(font, glyph, xPos, yPos, renderParams, dst, stride, false);
}
static ITCM_CODE void renderGlyphA5I3(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride)
int xPos, int yPos, const nft2_string_render_params_t* renderParams, u8* dst, u32 stride)
{
renderGlyph(font, glyph, xPos, yPos, width, height, dst, stride, true);
renderGlyph(font, glyph, xPos, yPos, renderParams, dst, stride, true);
}
ITCM_CODE void nft2_renderString(const nft2_header_t* font, const char16_t* string, u8* dst, u32 stride,
@@ -134,18 +138,18 @@ ITCM_CODE void nft2_renderString(const nft2_header_t* font, const char16_t* stri
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphTiled(font, glyph, xPos, yPos, renderParams, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
renderParams->textWidth = textWidth;
renderParams->textWidth = textWidth - renderParams->x;
}
ITCM_CODE void nft2_measureString(const nft2_header_t* font, const char16_t* string, u32& width, u32& height)
@@ -265,11 +269,11 @@ ITCM_CODE void nft2_renderStringEllipsis(const nft2_header_t* font, const char16
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphTiled(font, glyph, xPos, yPos, renderParams, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
@@ -287,11 +291,11 @@ ITCM_CODE void nft2_renderStringEllipsis(const nft2_header_t* font, const char16
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphTiled(font, glyph, xPos, yPos, renderParams, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
@@ -309,16 +313,16 @@ ITCM_CODE void nft2_renderStringEllipsis(const nft2_header_t* font, const char16
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
renderGlyphTiled(font, glyph, xPos, yPos, renderParams, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
renderParams->textWidth = textWidth;
renderParams->textWidth = textWidth - renderParams->x;
}

View File

@@ -39,6 +39,7 @@ struct nft2_string_render_params_t
u32 textWidth;
// u32 textHeight;
bool a5i3;
bool onlyRenderWholeGlyphs = true;
};
/// @brief Prepares the ntf2 data of the given \p font for runtime use.

View File

@@ -9,6 +9,10 @@
#include "gui/palette/GradientPalette.h"
#include "LabelView.h"
#define MARQUEE_START_FRAMES 60
#define MARQUEE_STEP_FRAMES 3
#define MARQUEE_END_FRAMES 90
LabelView::LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font, bool a5i3)
: _width(width), _height(height)
, _maxStringLength(maxStringLength), _font(font)
@@ -34,14 +38,30 @@ LabelView::LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_head
SetText(u"");
}
void LabelView::Update()
{
if (_ellipsisStyleChanged)
{
RestartMarquee();
UpdateTileBuffer();
_ellipsisStyleChanged = false;
}
else if (_ellipsisStyle == EllipsisStyle::Marquee)
{
UpdateMarquee();
}
}
void LabelView::SetTextBuffer(const char* text)
{
StringUtil::Copy(_textBuffer.get(), text, _maxStringLength + 1);
RestartMarquee();
}
void LabelView::SetTextBuffer(const char16_t* text)
{
StringUtil::Copy(_textBuffer.get(), text, _maxStringLength + 1);
RestartMarquee();
}
void LabelView::SetTextBuffer(const char16_t* text, u32 length)
@@ -50,6 +70,7 @@ void LabelView::SetTextBuffer(const char16_t* text, u32 length)
if (copyLength > 0)
memcpy(_textBuffer.get(), text, copyLength * 2);
_textBuffer[copyLength] = 0;
RestartMarquee();
}
void LabelView::UpdateTileBuffer()
@@ -63,10 +84,19 @@ void LabelView::UpdateTileBuffer()
renderParams.width = _width;
renderParams.height = _height;
renderParams.a5i3 = _a5i3;
if (_ellipsis)
if (_ellipsisStyle == EllipsisStyle::Ellipsis)
{
nft2_renderStringEllipsis(_font, _textBuffer.get(), _tileBuffer.get(), _actualWidth, &renderParams, u" ... ");
}
else
{
if (_ellipsisStyle == EllipsisStyle::Marquee)
{
renderParams.x = -_marqueeOffset;
renderParams.onlyRenderWholeGlyphs = false;
}
nft2_renderString(_font, _textBuffer.get(), _tileBuffer.get(), _actualWidth, &renderParams);
}
_newStringWidth = renderParams.textWidth;
}
else
@@ -119,3 +149,59 @@ QueueTask<void> LabelView::SetTextAsync(TaskQueueBase* taskQueue, const char16_t
SetTextBuffer(text, length);
return UpdateTileBufferAsync(taskQueue);
}
void LabelView::RestartMarquee()
{
_marqueeOffset = 0;
_marqueeCounter = MARQUEE_START_FRAMES;
_marqueeState = MarqueeState::StartWait;
}
void LabelView::UpdateMarquee()
{
UpdateTileBuffer();
if (_newStringWidth <= _width)
{
_marqueeOffset = 0;
}
else
{
switch (_marqueeState)
{
case MarqueeState::StartWait:
{
if (--_marqueeCounter <= 0)
{
_marqueeState = MarqueeState::Moving;
_marqueeOffset = 0;
_marqueeCounter = MARQUEE_STEP_FRAMES;
}
break;
}
case MarqueeState::Moving:
{
if (--_marqueeCounter == 0)
{
_marqueeCounter = MARQUEE_STEP_FRAMES;
_marqueeOffset++;
if (_newStringWidth - _marqueeOffset < _width)
{
_marqueeState = MarqueeState::EndWait;
_marqueeCounter = MARQUEE_END_FRAMES;
}
}
break;
}
case MarqueeState::EndWait:
{
if (--_marqueeCounter <= 0)
{
_marqueeState = MarqueeState::StartWait;
_marqueeCounter = MARQUEE_START_FRAMES;
_marqueeOffset = 0;
}
break;
}
}
}
}

View File

@@ -14,6 +14,15 @@ class MaterialGraphicsContext;
class LabelView : public View
{
public:
enum class EllipsisStyle
{
None,
Ellipsis,
Marquee
};
void Update() override;
void SetText(const char* text);
void SetText(const char16_t* text);
void SetText(const char16_t* text, u32 length);
@@ -43,7 +52,14 @@ public:
return Rectangle(_position, _width, _height);
}
void SetEllipsis(bool ellipsis) { _ellipsis = ellipsis; }
void SetEllipsisStyle(EllipsisStyle ellipsisStyle)
{
if (_ellipsisStyle != ellipsisStyle)
{
_ellipsisStyle = ellipsisStyle;
_ellipsisStyleChanged = true;
}
}
protected:
u32 _width;
@@ -61,14 +77,30 @@ protected:
Rgb<8, 8, 8> _backgroundColor;
Rgb<8, 8, 8> _foregroundColor;
int _paletteRow = -1;
bool _ellipsis = false;
EllipsisStyle _ellipsisStyle = EllipsisStyle::None;
bool _a5i3;
LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font, bool a5i3);
void SetTextBuffer(const char* text);
void SetTextBuffer(const char16_t* text);
void SetTextBuffer(const char16_t* text, u32 length);
virtual void UpdateTileBuffer();
QueueTask<void> UpdateTileBufferAsync(TaskQueueBase* taskQueue);
LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font, bool a5i3);
private:
enum class MarqueeState
{
StartWait,
Moving,
EndWait
};
MarqueeState _marqueeState = MarqueeState::StartWait;
int _marqueeOffset = 0;
int _marqueeCounter = 0;
bool _ellipsisStyleChanged = false;
void RestartMarquee();
void UpdateMarquee();
};

View File

@@ -39,6 +39,10 @@ void RecyclerView::SetAdapter(const RecyclerAdapter* adapter, int initialSelecte
_viewPool.reset();
_viewPoolFreeCount = 0;
_viewPoolTotalCount = 0;
_xOffset = 0;
_yOffset = 0;
_curRangeStart = 0;
_curRangeLength = 0;
}
_adapter = adapter;
_adapter->GetViewSize(_itemWidth, _itemHeight);
@@ -305,7 +309,7 @@ View* RecyclerView::MoveFocusVertical(View* currentFocus, FocusMoveDirection dir
bool RecyclerView::HandleInput(const InputProvider& inputProvider, FocusManager& focusManager)
{
if (inputProvider.Triggered(InputKey::L | InputKey::R))
if (_itemCount != 0 && inputProvider.Triggered(InputKey::L | InputKey::R))
{
int direction = inputProvider.Triggered(InputKey::L) ? 1 : -1;
int selected = _selectedItem->itemIdx;

View File

@@ -167,6 +167,11 @@ int main(int argc, char* argv[])
rtc_init();
if (argc >= 1)
{
pload_setLauncherPath(argv[0]);
}
memset(&gFatFs, 0, sizeof(gFatFs));
if (dldi_init())
{

View File

@@ -10,6 +10,7 @@
#include <libtwl/ipc/ipcFifoSystem.h>
#include "ipcChannels.h"
#include "fat/File.h"
#include "core/StringUtil.h"
#include "picoLoaderBootstrap.h"
#define PICO_LOADER_9_PATH "/_pico/picoLoader9.bin"
@@ -18,7 +19,9 @@
typedef void (*pico_loader_9_func_t)(void);
static pload_params_t sLoadParams;
static char sLauncherPath[256] alignas(32);
static PicoLoaderBootDrive sBootDrive;
static const pload_cheats_t* sCheatData = nullptr;
pload_params_t* pload_getLoadParams()
{
@@ -30,6 +33,16 @@ void pload_setBootDrive(PicoLoaderBootDrive bootDrive)
sBootDrive = bootDrive;
}
void pload_setLauncherPath(const char* launcherPath)
{
StringUtil::Copy(sLauncherPath, launcherPath, sizeof(sLauncherPath));
}
void pload_setCheatData(const pload_cheats_t* cheatData)
{
sCheatData = cheatData;
}
void pload_start()
{
mem_setVramAMapping(MEM_VRAM_AB_LCDC);
@@ -78,8 +91,17 @@ void pload_start()
DC_InvalidateAll();
IC_InvalidateAll();
((pload_header7_t*)0x06840000)->bootDrive = sBootDrive;
dma_ntrCopy16(3, &sLoadParams, &((pload_header7_t*)0x06840000)->loadParams, sizeof(pload_params_t));
auto header = (pload_header7_t*)0x06840000;
header->bootDrive = sBootDrive;
dma_ntrCopy16(3, &sLoadParams, &header->loadParams, sizeof(pload_params_t));
if (header->apiVersion >= 2)
{
dma_ntrCopy16(3, &sLauncherPath, &header->v2.launcherPath, sizeof(header->v2.launcherPath));
}
if (header->apiVersion >= 3)
{
header->v3.cheats = sCheatData;
}
mem_setVramCMapping(MEM_VRAM_C_ARM7_00000);
mem_setVramDMapping(MEM_VRAM_D_ARM7_20000);
ipc_sendFifoMessage(IPC_CHANNEL_LOADER, 1);

View File

@@ -3,4 +3,6 @@
pload_params_t* pload_getLoadParams();
void pload_setBootDrive(PicoLoaderBootDrive bootDrive);
void pload_setLauncherPath(const char* launcherPath);
void pload_setCheatData(const pload_cheats_t* cheatData);
void pload_start();

View File

@@ -4,15 +4,15 @@
#include "FileInfo.h"
FileInfo::FileInfo(const FileInfo& fileInfo)
: _type(fileInfo._type), _fastFileRef(fileInfo._fastFileRef)
: _type(fileInfo._type), _fastFileRef(fileInfo._fastFileRef), _attributes(fileInfo._attributes)
{
u32 bufferLength = strlen(fileInfo.GetFileName()) + 1;
_name = std::make_unique_for_overwrite<TCHAR[]>(bufferLength);
StringUtil::Copy(_name.get(), fileInfo.GetFileName(), bufferLength);
}
FileInfo::FileInfo(const TCHAR* fileName, const FileType* type, const FastFileRef& fastFileRef)
: _type(type), _fastFileRef(fastFileRef)
FileInfo::FileInfo(const TCHAR* fileName, const FileType* type, const FastFileRef& fastFileRef, u8 attributes)
: _type(type), _fastFileRef(fastFileRef), _attributes(attributes)
{
u32 bufferLength = strlen(fileName) + 1;
_name = std::make_unique_for_overwrite<TCHAR[]>(bufferLength);

View File

@@ -10,7 +10,7 @@ class FileInfo
public:
FileInfo() { }
FileInfo(const FileInfo& fileInfo);
FileInfo(const TCHAR* fileName, const FileType* type, const FastFileRef& fastFileRef);
FileInfo(const TCHAR* fileName, const FileType* type, const FastFileRef& fastFileRef, u8 attributes);
FileInfo &operator=(FileInfo&& rhs)
{
@@ -35,8 +35,13 @@ public:
const FastFileRef& GetFastFileRef() const { return _fastFileRef; }
bool IsReadOnly() const { return _attributes & AM_RDO; }
bool IsHidden() const { return _attributes & AM_HID; }
bool IsSystem() const { return _attributes & AM_SYS; }
private:
std::unique_ptr<TCHAR[]> _name;
const FileType* _type;
FastFileRef _fastFileRef;
u8 _attributes;
};

View File

@@ -5,7 +5,7 @@
class PaletteManager;
class GraphicsContext;
#define FILE_ICON_VRAM_SIZE 4096
#define FILE_ICON_VRAM_SIZE 1024
/// @brief Abstract base class representing a file icon.
class FileIcon
@@ -13,9 +13,17 @@ class FileIcon
public:
virtual ~FileIcon() = 0;
/// @brief Uploads the graphics of this icon to the specified \p vram address.
/// @param vram The vram address to load the graphics to.
virtual void UploadGraphics(vu16* vram) const = 0;
/// @brief Sets the OBJ vram address and offset to use.
/// @param objVramAddress The OBJ vram address to use.
/// @param objVramOffset The OBJ vram offset to use.
virtual void SetVramAddress(vu16* objVramAddress, u32 objVramOffset)
{
_vramAddress = objVramAddress;
_vramOffset = objVramOffset;
}
/// @brief Uploads the graphics of this icon to the vram address specified by SetVramAddress.
virtual void UploadGraphics() = 0;
/// @brief Updates this icon.
virtual void Update() { }
@@ -25,10 +33,6 @@ public:
/// @param backgroundColor The color on which the icon is drawn.
virtual void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) = 0;
/// @brief Sets the OBJ vram offset of this icon.
/// @param offset The OBJ vram offset.
void SetObjVramOffset(u32 offset) { _vramOffset = offset; }
/// @brief Sets the icon animation frame.
/// @param frame The animation frame.
void SetAnimFrame(u32 frame)
@@ -50,6 +54,7 @@ public:
}
protected:
vu16* _vramAddress = nullptr;
u32 _vramOffset = 0;
u32 _frame = 0;
Point _position;

View File

@@ -1,5 +1,4 @@
#include "common.h"
#include <libtwl/math/mathDiv.h>
#include <libtwl/dma/dmaNitro.h>
#include "gui/PaletteManager.h"
#include "gui/OamManager.h"
@@ -30,25 +29,36 @@ NdsFileIcon::NdsFileIcon(const nds_banner_t* banner)
break;
}
else
{
length += token.duration;
}
}
_animLength = length;
_tokenStartTimes[NDS_BANNER_ANIM_TOKEN_COUNT] = _animLength;
}
}
void NdsFileIcon::UploadGraphics(vu16* vram) const
void NdsFileIcon::SetVramAddress(vu16* objVramAddress, u32 objVramOffset)
{
if (_animated)
dma_ntrCopy32(3, _banner->animation.iconGfx, vram, sizeof(_banner->animation.iconGfx));
else
dma_ntrCopy32(3, _banner->iconGfx, vram, sizeof(_banner->iconGfx));
FileIcon::SetVramAddress(objVramAddress, objVramOffset);
_currentVramSlot = 0;
_currentGfxIdx = -1;
}
void NdsFileIcon::UploadGraphics()
{
if (_vramAddress != nullptr && !_animated)
{
dma_ntrCopy32(3, _banner->iconGfx, _vramAddress, sizeof(_banner->iconGfx));
}
}
void NdsFileIcon::Update()
{
if (!_animated)
{
return;
}
_frame %= _animLength;
@@ -71,48 +81,31 @@ void NdsFileIcon::Update()
break;
}
else if (_frame < midTime)
{
end = mid - 1;
}
else
{
start = mid + 1;
}
}
_animTokenIdx = start;
}
if (++_frame == _animLength)
{
_frame = 0;
// if (++_durationCounter < _banner->animation.animTokens[_animTokenIdx].duration)
// return;
// _durationCounter = 0;
// if (++_animTokenIdx >= NDS_BANNER_ANIM_TOKEN_COUNT)
// {
// _animTokenIdx = 0;
// return;
// }
// if (_banner->animation.animTokens[_animTokenIdx].duration == NDS_BANNER_ANIM_DURATION_CONTROL_FRAME)
// {
// switch (_banner->animation.animTokens[_animTokenIdx].control)
// {
// case NDS_BANNER_ANIM_CONTROL_LOOP:
// _animTokenIdx = 0;
// break;
// case NDS_BANNER_ANIM_CONTROL_STOP:
// _animTokenIdx--;
// break;
// }
// }
}
}
void NdsFileIcon::Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor)
{
if (!graphicsContext.IsVisible(Rectangle(_position, 32, 32)))
if (!graphicsContext.IsVisible(Rectangle(_position, 32, 32)) ||
_vramAddress == nullptr)
{
return;
}
const u16* palette = _animated
? _banner->animation.iconPltt[_banner->animation.animTokens[_animTokenIdx].plttIdx]
@@ -123,7 +116,17 @@ void NdsFileIcon::Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& bac
u32 vramOffset = _vramOffset;
if (_animated)
vramOffset += _banner->animation.animTokens[_animTokenIdx].gfxIdx * 512;
{
int gfxIdx = _banner->animation.animTokens[_animTokenIdx].gfxIdx;
if (gfxIdx != _currentGfxIdx)
{
_currentVramSlot = 1 - _currentVramSlot;
_currentGfxIdx = gfxIdx;
dma_ntrCopy32(3, &_banner->animation.iconGfx[gfxIdx][0], (u8*)_vramAddress + (_currentVramSlot * NDS_BANNER_ICON_SIZE), NDS_BANNER_ICON_SIZE);
}
vramOffset += _currentVramSlot * NDS_BANNER_ICON_SIZE;
}
auto builder = OamBuilder::OamWithSize<32, 32>(
_position, vramOffset >> 7)

View File

@@ -8,7 +8,8 @@ class NdsFileIcon : public FileIcon
public:
explicit NdsFileIcon(const nds_banner_t* banner);
void UploadGraphics(vu16* vram) const override;
void SetVramAddress(vu16* objVramAddress, u32 objVramOffset) override;
void UploadGraphics() override;
void Update() override;
void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) override;
@@ -16,9 +17,11 @@ private:
const nds_banner_t* _banner;
bool _animated;
int _animTokenIdx;
// int _durationCounter;
u32 _lastAnimToken;
u32 _animLength;
u16 _tokenStartTimes[65];
bool _loop;
int _currentVramSlot = 0;
int _currentGfxIdx = -1;
};

View File

@@ -2,52 +2,60 @@
#include <string.h>
#include <nds/arm9/cache.h>
#include "fat/File.h"
#include "romBrowser/ICoverRepository.h"
#include "NdsInternalFileInfo.h"
NdsInternalFileInfo::NdsInternalFileInfo(const FastFileRef& fastFileRef)
{
const auto file = std::make_unique<File>();
_hasBanner = false;
memset(_gameCode, 0, sizeof(_gameCode));
file->Open(fastFileRef, FA_READ);
if (file->Seek(0xC) != FR_OK)
return;
u32 bytesRead;
if (file->Read(_gameCode, 4, bytesRead) != FR_OK)
return;
if (file->Seek(0x68) != FR_OK)
return;
u32 bannerOffset;
if (file->Read(&bannerOffset, 4, bytesRead) != FR_OK)
return;
if (bannerOffset == 0)
return;
if (file->Seek(bannerOffset) != FR_OK)
return;
if (file->Read(&_banner, 0xA00, bytesRead) != FR_OK)
return;
if (_banner.header.version >= NDS_BANNER_VERSION_103)
if (file->Seek(0xC) != FR_OK ||
!file->ReadExact(_gameCode, 4) ||
file->Seek(0x68) != FR_OK ||
!file->ReadExact(&bannerOffset, 4) ||
bannerOffset == 0 ||
bannerOffset >= file->GetSize() ||
file->Seek(bannerOffset) != FR_OK ||
!file->ReadExact(&_banner, 0x840))
{
if (file->Read(((u8*)&_banner) + 0xA00, 0x19C0, bytesRead) != FR_OK)
return;
return;
}
if (_banner.header.version >= NDS_BANNER_VERSION_2 &&
!file->ReadExact(((u8*)&_banner) + 0x840, 0x100))
{
return;
}
if (_banner.header.version >= NDS_BANNER_VERSION_3 &&
!file->ReadExact(((u8*)&_banner) + 0x940, 0x100))
{
return;
}
if (_banner.header.version >= NDS_BANNER_VERSION_103 &&
!file->ReadExact(((u8*)&_banner) + 0xA40, 0x1980))
{
return;
}
_hasBanner = true;
DC_FlushRange(&_banner, sizeof(_banner));
}
const char16_t* NdsInternalFileInfo::GetGameTitle() const
{
if (!_hasBanner)
return nullptr;
return _banner.title[NDS_BANNER_TITLE_LANGUAGE_ENGLISH];
return _hasBanner
? _banner.title[NDS_BANNER_TITLE_LANGUAGE_ENGLISH]
: nullptr;
}
std::unique_ptr<FileIcon> NdsInternalFileInfo::CreateGameIcon() const
{
return _hasBanner
? std::make_unique<NdsFileIcon>(&_banner)
: nullptr;
}

View File

@@ -12,18 +12,11 @@ public:
constexpr const char* GetGameCode() const override { return _gameCode; }
const char16_t* GetGameTitle() const override;
std::unique_ptr<FileIcon> CreateGameIcon() const override
{
if (!_hasBanner)
return nullptr;
return std::make_unique<NdsFileIcon>(&_banner);
}
std::unique_ptr<FileIcon> CreateGameIcon() const override;
const nds_banner_t& GetBanner() const { return _banner; }
private:
nds_banner_t _banner alignas(32);
bool _hasBanner;
bool _hasBanner = false;
char _gameCode[5];
};

View File

@@ -46,10 +46,11 @@ typedef struct
} nds_banner_anim_token_t;
#define NDS_BANNER_ANIM_TOKEN_COUNT 64
#define NDS_BANNER_ICON_SIZE 512
typedef struct
{
u8 iconGfx[8][0x200];
u8 iconGfx[8][NDS_BANNER_ICON_SIZE];
u16 iconPltt[8][16];
nds_banner_anim_token_t animTokens[NDS_BANNER_ANIM_TOKEN_COUNT];
} nds_banner_anim_t;
@@ -57,7 +58,7 @@ typedef struct
typedef struct
{
nds_banner_header_t header;
u8 iconGfx[0x200];
u8 iconGfx[NDS_BANNER_ICON_SIZE];
u16 iconPltt[16];
char16_t title[16][128];
nds_banner_anim_t animation;

View File

@@ -7,9 +7,12 @@
#include "gui/palette/DirectPalette.h"
#include "StaticIcon.h"
void StaticIcon::UploadGraphics(vu16* vram) const
void StaticIcon::UploadGraphics()
{
dma_ntrCopy32(3, _tileData, vram, 512);
if (_vramAddress != nullptr)
{
dma_ntrCopy32(3, _tileData, _vramAddress, 512);
}
}
void StaticIcon::Update()

View File

@@ -7,7 +7,7 @@ public:
StaticIcon(const u8* tileData, const u16* paletteData)
: _tileData(tileData), _paletteData(paletteData) { }
void UploadGraphics(vu16* vram) const override;
void UploadGraphics() override;
void Update() override;
void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) override;

View File

@@ -8,6 +8,7 @@ class RomBrowserViewModel;
class FileInfo;
class TaskQueueBase;
class ICoverRepository;
class ICheatRepository;
class IRomBrowserController
{
@@ -17,7 +18,7 @@ public:
virtual void NavigateUp() = 0;
virtual void NavigateToPath(const TCHAR* name) = 0;
virtual void LaunchFile(const FileInfo& fileInfo) = 0;
virtual void ShowGameInfo() = 0;
virtual void ShowGameInfo(const FileInfo& fileInfo) = 0;
virtual void HideGameInfo() = 0;
virtual void ShowDisplaySettings() = 0;
virtual void HideDisplaySettings() = 0;
@@ -33,11 +34,14 @@ public:
virtual TaskQueueBase* GetIoTaskQueue() const = 0;
virtual TaskQueueBase* GetBgTaskQueue() const = 0;
virtual const ICoverRepository& GetCoverRepository() const = 0;
virtual const ICheatRepository& GetCheatRepository() const = 0;
virtual const RomBrowserDisplaySettings& GetRomBrowserDisplaySettings() const = 0;
virtual void SetRomBrowserDisplaySettings(
const RomBrowserDisplaySettings& romBrowserDisplaySettings) = 0;
virtual const FileInfo& GetTriggerFileInfo() const = 0;
};
inline IRomBrowserController::~IRomBrowserController() { }

View File

@@ -6,6 +6,9 @@
#include "FileType/FileType.h"
#include "SdFolderFactory.h"
#include "services/settings/IAppSettingsService.h"
#include "cheats/UsrCheatRepositoryFactory.h"
#include "cheats/EmptyCheatRepository.h"
#include "cheats/PicoLoaderCheatDataFactory.h"
#include "RomBrowserController.h"
RomBrowserController::RomBrowserController(
@@ -23,14 +26,14 @@ void RomBrowserController::NavigateToPath(const TCHAR* name)
void RomBrowserController::LaunchFile(const FileInfo& fileInfo)
{
_launchFileInfo = FileInfo(fileInfo);
_triggerFileInfo = FileInfo(fileInfo);
_stateMachine.Fire(RomBrowserStateTrigger::Launch);
}
void RomBrowserController::ShowGameInfo()
void RomBrowserController::ShowGameInfo(const FileInfo& fileInfo)
{
// Currently disabled, as the game info panel is not complete
// _stateMachine.Fire(RomBrowserStateTrigger::ShowGameInfo);
_triggerFileInfo = FileInfo(fileInfo);
_stateMachine.Fire(RomBrowserStateTrigger::ShowGameInfo);
}
void RomBrowserController::HideGameInfo()
@@ -140,6 +143,15 @@ void RomBrowserController::HandleNavigateTrigger()
_coverRepository = std::make_unique<CoverRepository>();
_coverRepository->Initialize();
}
if (!_cheatRepository)
{
_cheatRepository = UsrCheatRepositoryFactory().FromUsrCheatDat("/_pico/usrcheat.dat");
if (!_cheatRepository)
{
// When usrcheat.dat is not found or cannot be read use a dummy empty cheat repository
_cheatRepository = std::make_unique<EmptyCheatRepository>();
}
}
u64 startTick = gTickCounter.GetValue();
_navigateFileName = nullptr;
@@ -178,26 +190,9 @@ void RomBrowserController::HandleLaunchTrigger()
LOG_DEBUG("RomBrowserStateTrigger::Launch\n");
_ioTaskQueue->Enqueue([this] (const vu8& cancelRequested)
{
f_getcwd(_navigatePath, sizeof(_navigatePath) / sizeof(_navigatePath[0]));
int idx = strlcat(_navigatePath, "/", sizeof(_navigatePath));
if (_navigatePath[idx - 2] == '/')
_navigatePath[idx - 1] = 0;
strlcat(_navigatePath, _launchFileInfo.GetFileName(), sizeof(_navigatePath));
_appSettingsService->GetAppSettings().lastUsedFilePath = _navigatePath;
_appSettingsService->Save();
auto loadParams = pload_getLoadParams();
loadParams->savePath[0] = 0;
loadParams->arguments[0] = 0;
loadParams->argumentsLength = 0;
if (_launchFileInfo.GetFileType()->TrySetLaunchParameters(loadParams, _navigatePath))
{
gProcessManager.Goto<PicoLoaderProcess>();
}
else
{
LOG_FATAL("Failed to set launch parameters.\n");
}
UpdateLastUsedFilepath();
SetPicoLoaderParams();
LoadCheats();
return TaskResult<void>::Completed();
});
}
@@ -207,3 +202,39 @@ void RomBrowserController::HandleChangeDisplayModeTrigger()
LOG_DEBUG("RomBrowserStateTrigger::ChangeDisplayMode\n");
_romBrowserViewModel = SharedPtr(new RomBrowserViewModel(this));
}
void RomBrowserController::UpdateLastUsedFilepath()
{
f_getcwd(_navigatePath, sizeof(_navigatePath) / sizeof(_navigatePath[0]));
int idx = strlcat(_navigatePath, "/", sizeof(_navigatePath));
if (_navigatePath[idx - 2] == '/')
{
_navigatePath[idx - 1] = 0;
}
strlcat(_navigatePath, _triggerFileInfo.GetFileName(), sizeof(_navigatePath));
_appSettingsService->GetAppSettings().lastUsedFilePath = _navigatePath;
_appSettingsService->Save();
}
void RomBrowserController::SetPicoLoaderParams() const
{
auto loadParams = pload_getLoadParams();
loadParams->savePath[0] = 0;
loadParams->arguments[0] = 0;
loadParams->argumentsLength = 0;
if (_triggerFileInfo.GetFileType()->TrySetLaunchParameters(loadParams, _navigatePath))
{
gProcessManager.Goto<PicoLoaderProcess>();
}
else
{
LOG_FATAL("Failed to set launch parameters.\n");
}
}
void RomBrowserController::LoadCheats() const
{
auto cheats = _cheatRepository->GetCheatsForGame(_triggerFileInfo.GetFastFileRef());
auto cheatData = PicoLoaderCheatDataFactory().CreateCheatData(cheats);
pload_setCheatData(cheatData);
}

View File

@@ -9,6 +9,7 @@
#include "CoverRepository.h"
#include "FileType/ExtensionFileTypeProvider.h"
#include "services/settings/IAppSettingsService.h"
#include "cheats/ICheatRepository.h"
class RomBrowserController : public IRomBrowserController
{
@@ -23,7 +24,7 @@ public:
void NavigateToPath(const TCHAR* name) override;
void LaunchFile(const FileInfo& fileInfo) override;
void ShowGameInfo() override;
void ShowGameInfo(const FileInfo& fileInfo) override;
void HideGameInfo() override;
void ShowDisplaySettings() override;
void HideDisplaySettings() override;
@@ -39,6 +40,7 @@ public:
TaskQueueBase* GetIoTaskQueue() const override { return _ioTaskQueue; }
TaskQueueBase* GetBgTaskQueue() const override { return _bgTaskQueue; }
const ICoverRepository& GetCoverRepository() const override { return *_coverRepository; }
const ICheatRepository& GetCheatRepository() const override { return *_cheatRepository; }
void SetRomBrowserDisplaySettings(const RomBrowserDisplaySettings& romBrowserDisplaySettings) override;
@@ -47,6 +49,8 @@ public:
return _appSettingsService->GetAppSettings().romBrowserDisplaySettings;
}
virtual const FileInfo& GetTriggerFileInfo() const override { return _triggerFileInfo; }
private:
IAppSettingsService* _appSettingsService;
TaskQueueBase* _ioTaskQueue;
@@ -58,15 +62,19 @@ private:
RomBrowserStateMachine _stateMachine;
TCHAR _navigatePath[256];
TCHAR* _navigateFileName;
FileInfo _launchFileInfo;
FileInfo _triggerFileInfo;
QueueTask<void> _navigateTask;
bool _saveSettingsPending = false;
std::unique_ptr<CoverRepository> _coverRepository;
ExtensionFileTypeProvider _fileTypeProvider;
std::unique_ptr<ICheatRepository> _cheatRepository;
void HandleTrigger();
void HandleNavigateTrigger();
void HandleFolderLoadDoneTrigger();
void HandleLaunchTrigger();
void HandleChangeDisplayModeTrigger();
void UpdateLastUsedFilepath();
void SetPicoLoaderParams() const;
void LoadCheats() const;
};

View File

@@ -21,8 +21,10 @@ std::unique_ptr<const FileInfo*[]> SdFolder::FilterAndSort(
for (int i = 0; i < _fileCount; i++)
{
const FileInfo* file = _files[i];
bool isHidden = file->GetFileName()[0] == '.' || file->IsHidden();
auto classification = file->GetFileType()->GetClassification();
if (classification != FileTypeClassification::Unknown)
if (classification != FileTypeClassification::Unknown &&
(!isHidden || filterSortParams.includeHiddenFiles))
{
sortedFilteredFiles[filteredCount++] = file;
}

View File

@@ -32,7 +32,7 @@ std::unique_ptr<SdFolder> SdFolderFactory::CreateFromPath(const char* path) cons
? &FolderFileType::sInstance
: _fileTypeProvider->GetFileType(sdFileInfo->fname);
fileInfos[count++] = new FileInfo(sdFileInfo->fname, fileType,
FastFileRef(directory.GetFatFsDirectory(), sdFileInfo.get()));
FastFileRef(directory.GetFatFsDirectory(), sdFileInfo.get()), sdFileInfo->fattrib);
}
return std::make_unique<SdFolder>(fileInfos, count);

View File

@@ -7,9 +7,10 @@ class SdFolderFilterSortParams
public:
SdFolderSortType sortType = SdFolderSortType::Name;
SdFolderSortDirection sortDirection = SdFolderSortDirection::Ascending;
bool includeHiddenFiles = false;
SdFolderFilterSortParams() { }
SdFolderFilterSortParams(SdFolderSortType sortType, SdFolderSortDirection sortDirection)
: sortType(sortType), sortDirection(sortDirection) { }
SdFolderFilterSortParams(SdFolderSortType sortType, SdFolderSortDirection sortDirection, bool includeHiddenFiles)
: sortType(sortType), sortDirection(sortDirection), includeHiddenFiles(includeHiddenFiles) { }
};

View File

@@ -119,7 +119,6 @@ void MaterialBannerListItemView::Draw(GraphicsContext& graphicsContext)
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->SetPosition(6 + _position.x, 6 + _position.y);
_icon->Draw(graphicsContext, frontColor);
}

View File

@@ -24,23 +24,26 @@ MaterialFileIcon::MaterialFileIcon(const TCHAR* name, const MaterialColorScheme*
_displayName[i] = 0;
}
void MaterialFileIcon::UploadGraphics(vu16* vram) const
void MaterialFileIcon::UploadGraphics()
{
dma_ntrCopy32(3, GetIconTiles(), vram, 32 * 32 / 2);
if (_vramAddress != nullptr)
{
dma_ntrCopy32(3, GetIconTiles(), _vramAddress, 32 * 32 / 2);
auto font = _fontRepository->GetFont(FontType::Medium11);
u8 tileBuffer[32 * 16 / 2];
memset(tileBuffer, 0, sizeof(tileBuffer));
u32 textWidth, textHeight;
nft2_measureString(font, _displayName, textWidth, textHeight);
nft2_string_render_params_t renderParams;
renderParams.x = ((int)32 - (int)textWidth) / 2;
renderParams.y = 0;
renderParams.width = 32;
renderParams.height = 16;
renderParams.a5i3 = false;
nft2_renderString(font, _displayName, tileBuffer, 32, &renderParams);
memcpy((u8*)vram + largeFolderIconTilesLen, tileBuffer, sizeof(tileBuffer));
auto font = _fontRepository->GetFont(FontType::Medium11);
u8 tileBuffer[32 * 16 / 2];
memset(tileBuffer, 0, sizeof(tileBuffer));
u32 textWidth, textHeight;
nft2_measureString(font, _displayName, textWidth, textHeight);
nft2_string_render_params_t renderParams;
renderParams.x = ((int)32 - (int)textWidth) / 2;
renderParams.y = 0;
renderParams.width = 32;
renderParams.height = 16;
renderParams.a5i3 = false;
nft2_renderString(font, _displayName, tileBuffer, 32, &renderParams);
memcpy((u8*)_vramAddress + largeFolderIconTilesLen, tileBuffer, sizeof(tileBuffer));
}
}
void MaterialFileIcon::Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor)

View File

@@ -11,7 +11,7 @@ public:
MaterialFileIcon(const TCHAR* name, const MaterialColorScheme* materialColorScheme,
const IFontRepository* fontRepository);
void UploadGraphics(vu16* vram) const override;
void UploadGraphics() override;
void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) override;
protected:

View File

@@ -22,7 +22,7 @@ MaterialFileInfoCardView::MaterialFileInfoCardView(const MaterialColorScheme* ma
AddChildTail(&_firstLine);
AddChildTail(&_secondLine);
AddChildTail(&_thirdLine);
_filenameLabelView.SetEllipsis(true);
_filenameLabelView.SetEllipsisStyle(LabelView::EllipsisStyle::Marquee);
AddChildTail(&_filenameLabelView);
}
@@ -78,7 +78,6 @@ void MaterialFileInfoCardView::Draw(GraphicsContext& graphicsContext)
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->Draw(graphicsContext, fgColor);
}
}

View File

@@ -19,7 +19,7 @@ public:
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char* firstLine, bool ellipsis) override
{
_firstLine.SetEllipsis(ellipsis);
_firstLine.SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine.SetTextAsync(taskQueue, firstLine);
else
@@ -28,7 +28,7 @@ public:
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char16_t* firstLine, u32 length, bool ellipsis) override
{
_firstLine.SetEllipsis(ellipsis);
_firstLine.SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine.SetTextAsync(taskQueue, firstLine, length);
else

View File

@@ -47,7 +47,6 @@ void MaterialIconGridItemView::Draw(GraphicsContext& graphicsContext)
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->SetPosition(6 + _position.x, 6 + _position.y);
_icon->Draw(graphicsContext, frontColor);
}

View File

@@ -12,6 +12,7 @@
#include "core/math/RgbMixer.h"
#include "themes/material/MaterialColorScheme.h"
#include "themes/IFontRepository.h"
#include "themes/custom/CustomThemeInfo.h"
#include "bannerListItemBg0.h"
#include "bannerListItemBg1.h"
#include "bannerListItemBg2.h"
@@ -30,9 +31,10 @@
#define LINE_HEIGHT 16
#define MAX_LINE_STRING_LENGTH 50
CustomBannerListItemView::CustomBannerListItemView(const MaterialColorScheme* materialColorScheme,
const IFontRepository* fontRepository, u32 texVramOffset, u32 plttVramOffset,
u32 selectedTexVramOffset, u32 selectedPlttVramOffset, VBlankTextureLoader* vblankTextureLoader)
CustomBannerListItemView::CustomBannerListItemView(const CustomThemeInfo* customThemeInfo,
const MaterialColorScheme* materialColorScheme, const IFontRepository* fontRepository,
u32 texVramOffset, u32 plttVramOffset, u32 selectedTexVramOffset, u32 selectedPlttVramOffset,
VBlankTextureLoader* vblankTextureLoader)
: BannerListItemView(
std::make_unique<Label3DView>(LINE_WIDTH, LINE_HEIGHT, MAX_LINE_STRING_LENGTH,
fontRepository->GetFont(FontType::Medium10), vblankTextureLoader),
@@ -40,6 +42,7 @@ CustomBannerListItemView::CustomBannerListItemView(const MaterialColorScheme* ma
fontRepository->GetFont(FontType::Regular10), vblankTextureLoader),
std::make_unique<Label3DView>(LINE_WIDTH, LINE_HEIGHT, MAX_LINE_STRING_LENGTH,
fontRepository->GetFont(FontType::Regular10), vblankTextureLoader))
, _customThemeInfo(customThemeInfo)
, _materialColorScheme(materialColorScheme)
, _texVramOffset(texVramOffset)
, _plttVramOffset(plttVramOffset)
@@ -53,8 +56,6 @@ void CustomBannerListItemView::Draw(GraphicsContext& graphicsContext)
return;
}
auto backgroundColor = Rgb<8, 8, 8>(200, 200, 200);
Gx::MtxIdentity();
Gx::PolygonAttr(GX_LIGHTMASK_NONE, GX_POLYGON_MODE_MODULATE, GX_DISPLAY_MODE_FRONT,
false, false, false, GX_DEPTH_FUNC_LESS, false, 31, 0);
@@ -82,9 +83,9 @@ void CustomBannerListItemView::Draw(GraphicsContext& graphicsContext)
graphicsContext.SetPolygonId(1);
_firstLine->SetForegroundColor(Rgb<8, 8, 8>(30, 30, 30));
_secondLine->SetForegroundColor(Rgb<8, 8, 8>(30, 30, 30));
_thirdLine->SetForegroundColor(Rgb<8, 8, 8>(30, 30, 30));
_firstLine->SetForegroundColor(_customThemeInfo->bannerListTextLine0Info.GetTextColor());
_secondLine->SetForegroundColor(_customThemeInfo->bannerListTextLine1Info.GetTextColor());
_thirdLine->SetForegroundColor(_customThemeInfo->bannerListTextLine2Info.GetTextColor());
if (_lines == 1)
{
@@ -117,8 +118,7 @@ void CustomBannerListItemView::Draw(GraphicsContext& graphicsContext)
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->SetPosition(6 + _position.x, 6 + _position.y);
_icon->Draw(graphicsContext, backgroundColor);
_icon->Draw(graphicsContext, _customThemeInfo->bannerListIconInfo.GetBlendColor());
}
}

View File

@@ -4,11 +4,12 @@
class MaterialColorScheme;
class IFontRepository;
class CustomThemeInfo;
class CustomBannerListItemView : public BannerListItemView
{
public:
CustomBannerListItemView(const MaterialColorScheme* materialColorScheme,
CustomBannerListItemView(const CustomThemeInfo* customThemeInfo, const MaterialColorScheme* materialColorScheme,
const IFontRepository* fontRepository, u32 texVramOffset, u32 plttVramOffset,
u32 selectedTexVramOffset, u32 selectedPlttVramOffset, VBlankTextureLoader* vblankTextureLoader);
@@ -20,6 +21,7 @@ public:
}
private:
const CustomThemeInfo* _customThemeInfo;
const MaterialColorScheme* _materialColorScheme;
u32 _texVramOffset = 0;
u32 _plttVramOffset = 0;

View File

@@ -2,52 +2,52 @@
#include "gui/GraphicsContext.h"
#include "gui/IVramManager.h"
#include "themes/IFontRepository.h"
#include "themes/custom/CustomThemeInfo.h"
#include "CustomFileInfoView.h"
CustomFileInfoView::CustomFileInfoView(const IFontRepository* fontRepository)
: _firstLine(176, 16, 50, fontRepository->GetFont(FontType::Medium11))
, _secondLine(176, 16, 50, fontRepository->GetFont(FontType::Regular10))
, _thirdLine(176, 16, 50, fontRepository->GetFont(FontType::Regular10))
, _filenameLabelView(220, 16, 200, fontRepository->GetFont(FontType::Medium7_5))
, _backgroundColor(200, 200, 200), _textColor(30, 30, 30)
CustomFileInfoView::CustomFileInfoView(const CustomThemeInfo* customThemeInfo, const IFontRepository* fontRepository)
: _firstLine(customThemeInfo->topBannerTextLine0Info.GetWidth(), 16, 50, fontRepository->GetFont(FontType::Medium11))
, _secondLine(customThemeInfo->topBannerTextLine1Info.GetWidth(), 16, 50, fontRepository->GetFont(FontType::Regular10))
, _thirdLine(customThemeInfo->topBannerTextLine2Info.GetWidth(), 16, 50, fontRepository->GetFont(FontType::Regular10))
, _filenameLabelView(customThemeInfo->topFileNameTextInfo.GetWidth(), 16, 256, fontRepository->GetFont(FontType::Medium7_5))
, _customThemeInfo(customThemeInfo)
{
AddChildTail(&_firstLine);
AddChildTail(&_secondLine);
AddChildTail(&_thirdLine);
_filenameLabelView.SetEllipsis(true);
_filenameLabelView.SetEllipsisStyle(LabelView::EllipsisStyle::Marquee);
AddChildTail(&_filenameLabelView);
}
void CustomFileInfoView::Update()
{
BannerView::Update();
_firstLine.SetPosition(_position.x + 70, _position.y + 130 - 4);
_secondLine.SetPosition(_position.x + 70, _position.y + 145 - 4);
_thirdLine.SetPosition(_position.x + 70, _position.y + 159 - 4);
_filenameLabelView.SetPosition(_position.x + 18, _position.y + 170);
_firstLine.SetPosition(_customThemeInfo->topBannerTextLine0Info.GetPosition());
_secondLine.SetPosition(_customThemeInfo->topBannerTextLine1Info.GetPosition());
_thirdLine.SetPosition(_customThemeInfo->topBannerTextLine2Info.GetPosition());
_filenameLabelView.SetPosition(_customThemeInfo->topFileNameTextInfo.GetPosition());
if (_icon)
{
_icon->SetPosition(_position.x + 24, _position.y + 136 - 4);
_icon->SetPosition(_customThemeInfo->topIconInfo.GetPosition());
_icon->Update();
}
}
void CustomFileInfoView::Draw(GraphicsContext& graphicsContext)
{
_firstLine.SetBackgroundColor(_backgroundColor);
_firstLine.SetForegroundColor(_textColor);
_secondLine.SetBackgroundColor(_backgroundColor);
_secondLine.SetForegroundColor(_textColor);
_thirdLine.SetBackgroundColor(_backgroundColor);
_thirdLine.SetForegroundColor(_textColor);
_filenameLabelView.SetBackgroundColor(_backgroundColor);
_filenameLabelView.SetForegroundColor(_textColor);
_firstLine.SetBackgroundColor(_customThemeInfo->topBannerTextLine0Info.GetBlendColor());
_firstLine.SetForegroundColor(_customThemeInfo->topBannerTextLine0Info.GetTextColor());
_secondLine.SetBackgroundColor(_customThemeInfo->topBannerTextLine1Info.GetBlendColor());
_secondLine.SetForegroundColor(_customThemeInfo->topBannerTextLine1Info.GetTextColor());
_thirdLine.SetBackgroundColor(_customThemeInfo->topBannerTextLine2Info.GetBlendColor());
_thirdLine.SetForegroundColor(_customThemeInfo->topBannerTextLine2Info.GetTextColor());
_filenameLabelView.SetBackgroundColor(_customThemeInfo->topFileNameTextInfo.GetBlendColor());
_filenameLabelView.SetForegroundColor(_customThemeInfo->topFileNameTextInfo.GetTextColor());
BannerView::Draw(graphicsContext);
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->Draw(graphicsContext, _backgroundColor);
_icon->Draw(graphicsContext, _customThemeInfo->topIconInfo.GetBlendColor());
}
}

View File

@@ -5,18 +5,19 @@
class FileIcon;
class IFontRepository;
class CustomThemeInfo;
class CustomFileInfoView : public BannerView
{
public:
explicit CustomFileInfoView(const IFontRepository* fontRepository);
CustomFileInfoView(const CustomThemeInfo* customThemeInfo, const IFontRepository* fontRepository);
void Update() override;
void Draw(GraphicsContext& graphicsContext) override;
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char* firstLine, bool ellipsis) override
{
_firstLine.SetEllipsis(ellipsis);
_firstLine.SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine.SetTextAsync(taskQueue, firstLine);
else
@@ -25,7 +26,7 @@ public:
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char16_t* firstLine, u32 length, bool ellipsis) override
{
_firstLine.SetEllipsis(ellipsis);
_firstLine.SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine.SetTextAsync(taskQueue, firstLine, length);
else
@@ -67,6 +68,5 @@ private:
Label2DView _secondLine;
Label2DView _thirdLine;
Label2DView _filenameLabelView;
Rgb<8, 8, 8> _backgroundColor;
Rgb<8, 8, 8> _textColor;
const CustomThemeInfo* _customThemeInfo;
};

View File

@@ -5,6 +5,7 @@
#include "gui/OamBuilder.h"
#include "gui/IVramManager.h"
#include "gui/GraphicsContext.h"
#include "themes/custom/CustomThemeInfo.h"
#include "CustomIconGridItemView.h"
#define X_OFFSET (-2)
@@ -47,8 +48,7 @@ void CustomIconGridItemView::Draw(GraphicsContext& graphicsContext)
if (_icon)
{
_icon->SetObjVramOffset(_iconVramOffset);
_icon->SetPosition(6 + _position.x, 6 + _position.y);
_icon->Draw(graphicsContext, Rgb<8, 8, 8>(200, 200, 200));
_icon->Draw(graphicsContext, _customThemeInfo->gridIconInfo.GetBlendColor());
}
}

View File

@@ -1,12 +1,14 @@
#pragma once
#include "../../views/IconGridItemView.h"
class CustomThemeInfo;
class CustomIconGridItemView : public IconGridItemView
{
public:
CustomIconGridItemView(u32 texVramOffset, u32 plttVramOffset,
CustomIconGridItemView(const CustomThemeInfo* customThemeInfo, u32 texVramOffset, u32 plttVramOffset,
u32 selectedTexVramOffset, u32 selectedPlttVramOffset)
: _texVramOffset(texVramOffset), _plttVramOffset(plttVramOffset)
: _customThemeInfo(customThemeInfo), _texVramOffset(texVramOffset), _plttVramOffset(plttVramOffset)
, _selectedTexVramOffset(selectedTexVramOffset), _selectedPlttVramOffset(selectedPlttVramOffset) { }
void Draw(GraphicsContext& graphicsContext) override;
@@ -17,8 +19,9 @@ public:
}
private:
u32 _texVramOffset = 0;
u32 _plttVramOffset = 0;
u32 _selectedTexVramOffset = 0;
u32 _selectedPlttVramOffset = 0;
const CustomThemeInfo* _customThemeInfo;
u32 _texVramOffset;
u32 _plttVramOffset;
u32 _selectedTexVramOffset;
u32 _selectedPlttVramOffset;
};

View File

@@ -12,23 +12,24 @@ class MaterialColorScheme;
class ITheme;
class VramContext;
class IFontRepository;
class CustomThemeInfo;
class CustomRomBrowserViewFactory : public IRomBrowserViewFactory
{
public:
CustomRomBrowserViewFactory(const MaterialColorScheme* materialColorScheme,
CustomRomBrowserViewFactory(const CustomThemeInfo* customThemeInfo, const MaterialColorScheme* materialColorScheme,
const IFontRepository* fontRepository)
: _materialColorScheme(materialColorScheme), _fontRepository(fontRepository) { }
: _customThemeInfo(customThemeInfo), _materialColorScheme(materialColorScheme), _fontRepository(fontRepository) { }
IconGridItemView* CreateIconGridItemView() const override
{
return new CustomIconGridItemView(_gridCellTexVramOffset, _gridCellPlttVramOffset,
return new CustomIconGridItemView(_customThemeInfo, _gridCellTexVramOffset, _gridCellPlttVramOffset,
_gridCellSelectedTexVramOffset, _gridCellSelectedPlttVramOffset);
}
BannerListItemView* CreateBannerListItemView(VBlankTextureLoader* vblankTextureLoader) const override
{
return new CustomBannerListItemView(_materialColorScheme, _fontRepository,
return new CustomBannerListItemView(_customThemeInfo, _materialColorScheme, _fontRepository,
_bannerListCellTexVramOffset, _bannerListCellPlttVramOffset,
_bannerListCellSelectedTexVramOffset, _bannerListCellSelectedPlttVramOffset, vblankTextureLoader);
}
@@ -47,7 +48,7 @@ public:
std::unique_ptr<BannerView> CreateFileInfoView() const override
{
return std::make_unique<CustomFileInfoView>(_fontRepository);
return std::make_unique<CustomFileInfoView>(_customThemeInfo, _fontRepository);
}
std::unique_ptr<RecyclerViewBase> CreateCoverFlowRecyclerView() const override
@@ -77,6 +78,7 @@ private:
u32 _bannerListCellSelectedPlttVramOffset;
u32 _scrimTexVramOffset;
u32 _scrimPlttVramOffset;
const CustomThemeInfo* _customThemeInfo;
const MaterialColorScheme* _materialColorScheme;
const IFontRepository* _fontRepository;
};

View File

@@ -0,0 +1,135 @@
#include "common.h"
#include "cheats/ICheatRepository.h"
#include "fat/File.h"
#include "CheatsViewModel.h"
CheatsViewModel::CheatsViewModel(const FileInfo& romFileInfo, IRomBrowserController* romBrowserController)
: _romFileInfo(romFileInfo), _romBrowserController(romBrowserController)
{
_categoryStack.fill({ nullptr, 0 });
_loadCheatsTask = _romBrowserController->GetIoTaskQueue()->Enqueue([this] (const vu8& cancelRequested)
{
_cheats = _romBrowserController->GetCheatRepository().GetCheatsForGame(_romFileInfo.GetFastFileRef());
if (_cheats)
{
_categoryStack[0] = { _cheats.get(), 0 };
_state = State::DisplayCheats;
}
else
{
_state = State::NoCheats;
}
return TaskResult<void>::Completed();
});
}
void CheatsViewModel::ActivateSelectedItem()
{
if (_state != State::DisplayCheats)
{
// No cheats or not yet loaded
return;
}
auto cheatCategory = GetCurrentCheatCategory();
u32 numberOfSubEntries = 0;
auto subEntries = cheatCategory->GetSubEntries(numberOfSubEntries);
if (numberOfSubEntries == 0)
{
// There is nothing to activate
return;
}
auto& entry = subEntries[_selectedItem];
if (entry.IsCheatCategory())
{
// Category activated
if (_categoryStackLevel + 1 != _categoryStack.size())
{
_categoryStack[++_categoryStackLevel] = { &entry, (u32)_selectedItem };
_selectedItem = 0;
}
}
else
{
// Toggle cheat on/off
bool isEnabled = !entry.GetIsCheatActive();
if (isEnabled && cheatCategory->GetIsMaxOneCheatActive())
{
for (u32 i = 0; i < numberOfSubEntries; i++)
{
if (!subEntries[i].IsCheatCategory())
{
subEntries[i].SetIsCheatActive(false);
}
}
}
entry.SetIsCheatActive(isEnabled);
_changed = true;
}
}
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
_romBrowserController->GetIoTaskQueue()->Enqueue(
[romBrowserController = _romBrowserController, cheats = move(_cheats)] (const vu8& cancelRequested)
{
romBrowserController->GetCheatRepository().UpdateEnabledCheatsForGame(cheats);
return TaskResult<void>::Completed();
});
}
_romBrowserController->HideGameInfo();
}
void CheatsViewModel::DisableAllCheats()
{
if (_state == State::DisplayCheats)
{
DisableAllCheats(_cheats.get());
}
}
void CheatsViewModel::DisableAllCheats(const CheatEntry* cheatCategory)
{
u32 numberOfSubEntries = 0;
auto subEntries = cheatCategory->GetSubEntries(numberOfSubEntries);
for (u32 i = 0; i < numberOfSubEntries; i++)
{
auto& entry = subEntries[i];
if (entry.IsCheatCategory())
{
DisableAllCheats(&entry);
}
else
{
if (entry.GetIsCheatActive())
{
entry.SetIsCheatActive(false);
_changed = true;
}
}
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include <array>
#include <memory>
#include "core/task/TaskQueue.h"
#include "cheats/GameCheats.h"
#include "romBrowser/FileInfo.h"
#include "romBrowser/IRomBrowserController.h"
/// @brief View model for the cheats screen.
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);
/// @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 CheatEntry* 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
{
const CheatEntry* cheatCategory;
u32 index;
};
FileInfo _romFileInfo;
IRomBrowserController* _romBrowserController;
QueueTask<void> _loadCheatsTask;
std::unique_ptr<GameCheats> _cheats;
State _state = State::Loading;
int _selectedItem = -1;
bool _changed = false;
u32 _categoryStackLevel = 0;
std::array<CategoryStackEntry, 8> _categoryStack;
void DisableAllCheats(const CheatEntry* cheatCategory);
};

View File

@@ -1,5 +1,6 @@
#include "common.h"
#include <algorithm>
#include "romBrowser/FileType/Nds/NdsFileType.h"
#include "RomBrowserViewModel.h"
RomBrowserViewModel::RomBrowserViewModel(IRomBrowserController* romBrowserController, const char* initialSelectedFileName)
@@ -12,19 +13,19 @@ RomBrowserViewModel::RomBrowserViewModel(IRomBrowserController* romBrowserContro
default:
{
filterSortParams = SdFolderFilterSortParams(
SdFolderSortType::Name, SdFolderSortDirection::Ascending);
SdFolderSortType::Name, SdFolderSortDirection::Ascending, false);
break;
}
case RomBrowserSortMode::NameDescending:
{
filterSortParams = SdFolderFilterSortParams(
SdFolderSortType::Name, SdFolderSortDirection::Descending);
SdFolderSortType::Name, SdFolderSortDirection::Descending, false);
break;
}
case RomBrowserSortMode::LastModified:
{
filterSortParams = SdFolderFilterSortParams(
SdFolderSortType::LastModified, SdFolderSortDirection::Descending);
SdFolderSortType::LastModified, SdFolderSortDirection::Descending, false);
break;
}
}
@@ -34,7 +35,8 @@ RomBrowserViewModel::RomBrowserViewModel(IRomBrowserController* romBrowserContro
auto sortedFilteredFiles = sdFolder.FilterAndSort(filterSortParams, filteredCount);
u64 endTick = gTickCounter.GetValue();
LOG_DEBUG("Filter + sort took: %d us\n", (u32)TickCounter::TicksToMicroSeconds(endTick - startTick));
_fileInfoManager = std::make_unique<FileInfoManager>(std::move(sortedFilteredFiles), filteredCount, _romBrowserController->GetCoverRepository());
_fileInfoManager = std::make_unique<FileInfoManager>(std::move(sortedFilteredFiles),
filteredCount, _romBrowserController->GetCoverRepository());
_selectedItem = _fileInfoManager->GetItemIndex(initialSelectedFileName);
}
@@ -59,7 +61,8 @@ void RomBrowserViewModel::NavigateUp()
void RomBrowserViewModel::ShowGameInfo()
{
const auto& item = _fileInfoManager->GetItem(_selectedItem);
if (item.GetFileType()->GetClassification() == FileTypeClassification::Folder)
return;
_romBrowserController->ShowGameInfo();
if (item.GetFileType() == &NdsFileType::sInstance)
{
_romBrowserController->ShowGameInfo(item);
}
}

View File

@@ -28,7 +28,7 @@ public:
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char* firstLine, bool ellipsis) override
{
_firstLine->SetEllipsis(ellipsis);
_firstLine->SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine->SetTextAsync(taskQueue, firstLine);
else
@@ -37,7 +37,7 @@ public:
void SetFirstLineAsync(TaskQueueBase* taskQueue, const char16_t* firstLine, u32 length, bool ellipsis) override
{
_firstLine->SetEllipsis(ellipsis);
_firstLine->SetEllipsisStyle(ellipsis ? LabelView::EllipsisStyle::Ellipsis : LabelView::EllipsisStyle::None);
if (taskQueue)
_firstLine->SetTextAsync(taskQueue, firstLine, length);
else

View File

@@ -26,13 +26,17 @@ public:
void SetIcon(std::unique_ptr<FileIcon> icon)
{
_icon = std::move(icon);
if (_icon)
{
_icon->SetVramAddress(_iconVram, _iconVramOffset);
}
}
void UploadIconGraphics() const
{
if (_icon)
{
_icon->UploadGraphics(_iconVram);
_icon->UploadGraphics();
}
}

View File

@@ -27,13 +27,17 @@ public:
void SetIcon(std::unique_ptr<FileIcon> icon)
{
_icon = std::move(icon);
if (_icon)
{
_icon->SetVramAddress(_iconVram, _iconVramOffset);
}
}
void UploadIconGraphics() const
{
if (_icon)
{
_icon->UploadGraphics(_iconVram);
_icon->UploadGraphics();
}
}

View File

@@ -0,0 +1,99 @@
#include "common.h"
#include "themes/material/MaterialColorScheme.h"
#include "themes/IFontRepository.h"
#include "gui/palette/GradientPalette.h"
#include "gui/GraphicsContext.h"
#include "gui/OamBuilder.h"
#include "CheatListItemView.h"
#define ICON_X 4
#define ICON_Y 4
#define NAME_LABEL_X 24
#define NAME_LABEL_Y 5
CheatListItemView::CheatListItemView(const VramOffsets& vramOffsets,
const MaterialColorScheme* materialColorScheme, const IFontRepository* fontRepository)
: _nameLabel(196, 16, 256, fontRepository->GetFont(FontType::Regular10))
, _vramOffsets(vramOffsets)
, _materialColorScheme(materialColorScheme)
{
_nameLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Ellipsis);
AddChildTail(&_nameLabel);
}
void CheatListItemView::Update()
{
_nameLabel.SetPosition(_position.x + NAME_LABEL_X, _position.y + NAME_LABEL_Y);
if (_cheatEntry != nullptr && !_cheatEntry->IsCheatCategory())
{
_iconVramOffset = _cheatEntry->GetIsCheatActive()
? _vramOffsets.checkboxCheckedIconVramOffset
: _vramOffsets.checkboxUncheckedIconVramOffset;
}
if (IsFocused())
{
_nameLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Marquee);
}
else
{
_nameLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Ellipsis);
}
ViewContainer::Update();
}
void CheatListItemView::Draw(GraphicsContext& graphicsContext)
{
if (!graphicsContext.IsVisible(GetBounds()))
{
return;
}
auto backColor = _materialColorScheme->GetColor(md::sys::color::surfaceContainerLow);
if (IsFocused())
{
auto selectorFullColor = _materialColorScheme->GetColor(md::sys::color::onSurface);
backColor = RgbMixer::Lerp(backColor, selectorFullColor, 10, 100);
}
_nameLabel.SetBackgroundColor(backColor);
_nameLabel.SetForegroundColor(_materialColorScheme->onSurface);
if (IsFocused())
{
u32 selectorPaletteRow = graphicsContext.GetPaletteManager().AllocRow(
GradientPalette(backColor, backColor),
_position.y, _position.y + 24);
auto selectorOam = graphicsContext.GetOamManager().AllocOams(4);
OamBuilder::OamWithSize<64, 32>(_position.x, _position.y, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(selectorPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(selectorOam[0]);
OamBuilder::OamWithSize<64, 32>(_position.x + 64, _position.y, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(selectorPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(selectorOam[1]);
OamBuilder::OamWithSize<64, 32>(_position.x + 2 * 64, _position.y, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(selectorPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(selectorOam[2]);
OamBuilder::OamWithSize<64, 32>(_position.x + 2 * 64 + 32, _position.y, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(selectorPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(selectorOam[3]);
}
ViewContainer::Draw(graphicsContext);
if (graphicsContext.IsVisible(Rectangle(_position.x + ICON_X, _position.y + ICON_Y, 16, 16)))
{
u32 iconPaletteRow = graphicsContext.GetPaletteManager().AllocRow(
GradientPalette(backColor, _materialColorScheme->primary),
_position.y + ICON_Y, _position.y + ICON_Y + 16);
auto iconOam = graphicsContext.GetOamManager().AllocOams(1);
OamBuilder::OamWithSize<16, 16>(_position.x + ICON_X, _position.y + ICON_Y, _iconVramOffset >> 7)
.WithPalette16(iconPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(iconOam[0]);
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "gui/views/ViewContainer.h"
#include "gui/views/Label2DView.h"
#include "cheats/CheatEntry.h"
class MaterialColorScheme;
class IFontRepository;
/// @brief List item view for the cheats panel, representing a single cheat or cheat category.
class CheatListItemView : public ViewContainer
{
public:
struct VramOffsets
{
u32 folderIconVramOffset = 0;
u32 checkboxUncheckedIconVramOffset = 0;
u32 checkboxCheckedIconVramOffset = 0;
u32 cheatSelectorVramOffset = 0;
};
CheatListItemView(const VramOffsets& vramOffsets, const MaterialColorScheme* materialColorScheme, const IFontRepository* fontRepository);
void Update() override;
void Draw(GraphicsContext& graphicsContext) override;
Rectangle GetBounds() const override
{
return Rectangle(_position.x, _position.y, 224, 24);
}
void SetName(const char* name)
{
_nameLabel.SetText(name);
}
void SetEntry(const CheatEntry* cheatEntry)
{
_cheatEntry = cheatEntry;
_nameLabel.SetText(cheatEntry->GetName());
if (cheatEntry->IsCheatCategory())
{
_iconVramOffset = _vramOffsets.folderIconVramOffset;
}
}
private:
Label2DView _nameLabel;
VramOffsets _vramOffsets;
const MaterialColorScheme* _materialColorScheme;
u32 _iconVramOffset = 0;
const CheatEntry* _cheatEntry = nullptr;
};

View File

@@ -0,0 +1,56 @@
#pragma once
#include "gui/views/RecyclerAdapter.h"
#include "cheats/CheatEntry.h"
#include "CheatListItemView.h"
/// @brief Recycler adapter for cheats.
class CheatsAdapter : public RecyclerAdapter
{
public:
CheatsAdapter(const CheatEntry* cheatCategory, const MaterialColorScheme* materialColorScheme,
const IFontRepository* fontRepository, const CheatListItemView::VramOffsets& vramOffsets)
: _cheatCategory(cheatCategory), _materialColorScheme(materialColorScheme)
, _fontRepository(fontRepository), _vramOffsets(vramOffsets) { }
u32 GetItemCount() const override
{
u32 numberOfSubEntries = 0;
_cheatCategory->GetSubEntries(numberOfSubEntries);
return numberOfSubEntries;
}
void GetViewSize(int& width, int& height) const override
{
width = 224;
height = 24;
}
View* CreateView() const override
{
return new CheatListItemView(_vramOffsets, _materialColorScheme, _fontRepository);
}
void DestroyView(View* view) const override
{
delete (CheatListItemView*)view;
}
void BindView(View* view, int index) const override
{
auto listItemView = static_cast<CheatListItemView*>(view);
u32 numberOfSubEntries = 0;
auto subEntries = _cheatCategory->GetSubEntries(numberOfSubEntries);
listItemView->SetEntry(&subEntries[index]);
}
void ReleaseView(View* view, int index) const override
{
// Nothing to do
}
private:
const CheatEntry* _cheatCategory;
const MaterialColorScheme* _materialColorScheme;
const IFontRepository* _fontRepository;
CheatListItemView::VramOffsets _vramOffsets;
};

View File

@@ -0,0 +1,271 @@
#include "common.h"
#include "gui/GraphicsContext.h"
#include "themes/material/MaterialColorScheme.h"
#include "themes/IFontRepository.h"
#include "gui/input/InputProvider.h"
#include "gui/VramContext.h"
#include "gui/palette/GradientPalette.h"
#include "gui/OamBuilder.h"
#include "folderIcon.h"
#include "checkboxChecked.h"
#include "checkboxUnchecked.h"
#include "cheatSelector.h"
#include "gui/DescendingStackVramManager.h"
#include "CheatsBottomSheetView.h"
#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
#define DESCRIPTION_LABEL_X 16
#define DESCRIPTION_LABEL_Y 147
#define LIST_X 16
#define LIST_Y 36
#define LIST_WIDTH 224
#define LIST_HEIGHT 108
CheatsBottomSheetView::CheatsBottomSheetView(std::unique_ptr<CheatsViewModel> viewModel,
const MaterialColorScheme* materialColorScheme, const IFontRepository* fontRepository,
FocusManager* focusManager)
: _viewModel(std::move(viewModel))
, _titleLabel(64, 16, 25, fontRepository->GetFont(FontType::Medium11))
, _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))
, _materialColorScheme(materialColorScheme)
, _fontRepository(fontRepository)
, _focusManager(focusManager)
{
_titleLabel.SetText(u"Cheats");
_secondaryLabel.SetText(u"No cheats found.");
_secondaryLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Ellipsis);
_descriptionLabel.SetEllipsisStyle(LabelView::EllipsisStyle::Marquee);
_descriptionLabel.SetText("");
AddChildTail(&_titleLabel);
AddChildTail(&_secondaryLabel);
AddChildTail(&_descriptionLabel);
AddChildTail(_cheatListRecycler.get());
}
void CheatsBottomSheetView::InitVram(const VramContext& vramContext)
{
BottomSheetView::InitVram(vramContext);
const auto objVramManager = vramContext.GetObjVramManager();
if (objVramManager)
{
_vramOffsets.folderIconVramOffset = objVramManager->Alloc(folderIconTilesLen);
dma_ntrCopy32(3, folderIconTiles,
objVramManager->GetVramAddress(_vramOffsets.folderIconVramOffset), folderIconTilesLen);
_vramOffsets.checkboxUncheckedIconVramOffset = objVramManager->Alloc(checkboxUncheckedTilesLen);
dma_ntrCopy32(3, checkboxUncheckedTiles,
objVramManager->GetVramAddress(_vramOffsets.checkboxUncheckedIconVramOffset), checkboxUncheckedTilesLen);
_vramOffsets.checkboxCheckedIconVramOffset = objVramManager->Alloc(checkboxCheckedTilesLen);
dma_ntrCopy32(3, checkboxCheckedTiles,
objVramManager->GetVramAddress(_vramOffsets.checkboxCheckedIconVramOffset), checkboxCheckedTilesLen);
_vramOffsets.cheatSelectorVramOffset = objVramManager->Alloc(cheatSelectorTilesLen);
dma_ntrCopy32(3, cheatSelectorTiles,
objVramManager->GetVramAddress(_vramOffsets.cheatSelectorVramOffset), cheatSelectorTilesLen);
}
_objVramManager = vramContext.GetObjVramManager();
}
void CheatsBottomSheetView::Update()
{
_titleLabel.SetPosition(TITLE_LABEL_X, _position.y + TITLE_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)
{
if (_cheatsAdapter == nullptr && _objVramManager != nullptr)
{
_cheatsAdapter = new CheatsAdapter(
_viewModel->GetCurrentCheatCategory(), _materialColorScheme, _fontRepository, _vramOffsets);
_cheatListRecycler->SetAdapter(_cheatsAdapter);
// Ugly hack
_savedVramState = ((DescendingStackVramManager*)_objVramManager)->GetState();
_cheatListRecycler->InitVram(VramContext(nullptr, _objVramManager, nullptr, nullptr));
_cheatListRecycler->Focus(*_focusManager);
}
}
BottomSheetView::Update();
int selectedItem = _cheatListRecycler->GetSelectedItem();
if (selectedItem != _viewModel->GetSelectedItem())
{
_viewModel->SetSelectedItem(selectedItem);
UpdateDescriptionText();
}
}
void CheatsBottomSheetView::Draw(GraphicsContext& graphicsContext)
{
graphicsContext.SetClipArea(GetBounds());
u32 oldPrio = graphicsContext.SetPriority(1);
{
graphicsContext.SetClipArea(_cheatListRecycler->GetBounds());
_cheatListRecycler->Draw(graphicsContext);
graphicsContext.SetClipArea(GetBounds());
auto backColor = _materialColorScheme->GetColor(md::sys::color::surfaceContainerLow);
auto maskOam = graphicsContext.GetOamManager().AllocOams(8);
// Top
u32 maskPaletteRow = graphicsContext.GetPaletteManager().AllocRow(
GradientPalette(backColor, backColor),
_position.y + LIST_Y - 24, _position.y + LIST_Y);
OamBuilder::OamWithSize<64, 32>(LIST_X, _position.y + LIST_Y - 24, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[0]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 64, _position.y + LIST_Y - 24, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[1]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 2 * 64, _position.y + LIST_Y - 24, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[2]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 2 * 64 + 32, _position.y + LIST_Y - 24, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[3]);
// Bottom
if (graphicsContext.IsVisible(Rectangle(LIST_X, _position.y + LIST_Y + LIST_HEIGHT, 224, 24)))
{
maskPaletteRow = graphicsContext.GetPaletteManager().AllocRow(
GradientPalette(backColor, backColor),
_position.y + LIST_Y + LIST_HEIGHT, 192);
OamBuilder::OamWithSize<64, 32>(LIST_X, _position.y + LIST_Y + LIST_HEIGHT, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[4]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 64, _position.y + LIST_Y + LIST_HEIGHT, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[5]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 2 * 64, _position.y + LIST_Y + LIST_HEIGHT, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[6]);
OamBuilder::OamWithSize<64, 32>(LIST_X + 2 * 64 + 32, _position.y + LIST_Y + LIST_HEIGHT, _vramOffsets.cheatSelectorVramOffset >> 7)
.WithPalette16(maskPaletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(maskOam[7]);
}
_titleLabel.SetBackgroundColor(backColor);
_titleLabel.SetForegroundColor(_materialColorScheme->onSurface);
_titleLabel.Draw(graphicsContext);
if (_viewModel->GetState() == CheatsViewModel::State::NoCheats ||
_viewModel->ShouldShowCategoryName())
{
_secondaryLabel.SetBackgroundColor(backColor);
_secondaryLabel.SetForegroundColor(_materialColorScheme->onSurfaceVariant);
_secondaryLabel.Draw(graphicsContext);
}
_descriptionLabel.SetBackgroundColor(backColor);
_descriptionLabel.SetForegroundColor(_materialColorScheme->onSurfaceVariant);
_descriptionLabel.Draw(graphicsContext);
}
graphicsContext.SetPriority(oldPrio);
graphicsContext.ResetClipArea();
}
bool CheatsBottomSheetView::HandleInput(const InputProvider& inputProvider, FocusManager& focusManager)
{
if (inputProvider.Triggered(InputKey::A))
{
if (focusManager.IsFocusInside(_cheatListRecycler.get()))
{
auto oldCategory = _viewModel->GetCurrentCheatCategory();
_viewModel->ActivateSelectedItem();
if (oldCategory != _viewModel->GetCurrentCheatCategory())
{
_secondaryLabel.SetText(_viewModel->GetCurrentCheatCategory()->GetName());
UpdateCheatList();
}
return true;
}
}
else if (inputProvider.Triggered(InputKey::B))
{
auto oldCategory = _viewModel->GetCurrentCheatCategory();
if (_viewModel->NavigateUp() &&
oldCategory != _viewModel->GetCurrentCheatCategory())
{
UpdateCheatList();
}
return true;
}
else if (inputProvider.Triggered(InputKey::Y))
{
_viewModel->Close();
return true;
}
else if (inputProvider.Triggered(InputKey::X))
{
_viewModel->DisableAllCheats();
return true;
}
return false;
}
void CheatsBottomSheetView::UpdateCheatList()
{
// Need to unfocus first, otherwise the focus manager still contains a pointer to a view that is going to be destroyed
_focusManager->Unfocus();
auto oldAdapter = _cheatsAdapter;
_cheatsAdapter = new CheatsAdapter(
_viewModel->GetCurrentCheatCategory(), _materialColorScheme, _fontRepository, _vramOffsets);
_cheatListRecycler->SetAdapter(_cheatsAdapter, _viewModel->GetSelectedItem());
delete oldAdapter;
// Ugly hack
((DescendingStackVramManager*)_objVramManager)->SetState(_savedVramState);
_cheatListRecycler->InitVram(VramContext(nullptr, _objVramManager, nullptr, nullptr));
_cheatListRecycler->Focus(*_focusManager);
UpdateDescriptionText();
}
void CheatsBottomSheetView::UpdateDescriptionText()
{
int selectedItem = _viewModel->GetSelectedItem();
if (selectedItem < 0)
{
_descriptionLabel.SetText("");
}
else
{
auto cheatCategory = _viewModel->GetCurrentCheatCategory();
u32 numberOfSubEntries = 0;
auto subEntries = cheatCategory->GetSubEntries(numberOfSubEntries);
_descriptionLabel.SetText(subEntries[selectedItem].GetDescription());
}
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <memory>
#include "romBrowser/views/BottomSheetView.h"
#include "gui/views/Label2DView.h"
#include "gui/views/RecyclerView.h"
#include "romBrowser/viewModels/CheatsViewModel.h"
#include "CheatsAdapter.h"
#include "CheatListItemView.h"
class MaterialColorScheme;
class IFontRepository;
class IVramManager;
/// @brief Bottom sheet for browsing and enabling/disabling cheats.
class CheatsBottomSheetView : public BottomSheetView
{
public:
CheatsBottomSheetView(std::unique_ptr<CheatsViewModel> viewModel,
const MaterialColorScheme* materialColorScheme, const IFontRepository* fontRepository,
FocusManager* focusManager);
~CheatsBottomSheetView() override
{
_cheatListRecycler.reset();
if (_cheatsAdapter != nullptr)
{
delete _cheatsAdapter;
}
}
void InitVram(const VramContext& vramContext) override;
void Update() override;
void Draw(GraphicsContext& graphicsContext) override;
bool HandleInput(const InputProvider& inputProvider, FocusManager& focusManager) override;
void Focus(FocusManager& focusManager) override
{
_cheatListRecycler->Focus(focusManager);
}
private:
std::unique_ptr<CheatsViewModel> _viewModel;
Label2DView _titleLabel;
Label2DView _secondaryLabel;
Label2DView _descriptionLabel;
std::unique_ptr<RecyclerView> _cheatListRecycler;
CheatsAdapter* _cheatsAdapter = nullptr;
const MaterialColorScheme* _materialColorScheme;
const IFontRepository* _fontRepository;
IVramManager* _objVramManager = nullptr;
FocusManager* _focusManager;
CheatListItemView::VramOffsets _vramOffsets;
u32 _savedVramState = 0;
void UpdateCheatList();
void UpdateDescriptionText();
};

View File

@@ -0,0 +1,15 @@
#pragma once
#include "core/math/Point.h"
#include "core/math/Rgb.h"
class CustomBannerListTextElementInfo
{
public:
CustomBannerListTextElementInfo(const Rgb8& textColor)
: _textColor(textColor) { }
const Rgb8& GetTextColor() const { return _textColor; }
private:
Rgb8 _textColor;
};

View File

@@ -0,0 +1,15 @@
#pragma once
#include "core/math/Point.h"
#include "core/math/Rgb.h"
class CustomBottomIconInfo
{
public:
CustomBottomIconInfo(const Rgb8& blendColor)
: _blendColor(blendColor) { }
const Rgb8& GetBlendColor() const { return _blendColor; }
private:
Rgb8 _blendColor;
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "core/math/Point.h"
#include "core/math/Rgb.h"
class CustomTopTextElementInfo
{
public:
CustomTopTextElementInfo(const Point& position, u32 width, const Rgb8& textColor, const Rgb8& blendColor)
: _position(position), _width(width), _textColor(textColor), _blendColor(blendColor) { }
const Point& GetPosition() const { return _position; }
const u32 GetWidth() const { return _width; }
const Rgb8& GetTextColor() const { return _textColor; }
const Rgb8& GetBlendColor() const { return _blendColor; }
private:
Point _position;
u32 _width;
Rgb8 _textColor;
Rgb8 _blendColor;
};

View File

@@ -11,13 +11,164 @@
#include "romBrowser/views/IconButton3DView.h"
#include "CustomTheme.h"
#define JSON_RESERVED_SIZE 2048
#define JSON_RESERVED_SIZE 4096
#define KEY_COLOR_R "r"
#define KEY_COLOR_G "g"
#define KEY_COLOR_B "b"
#define KEY_POINT_X "x"
#define KEY_POINT_Y "y"
#define KEY_TOP_ICON "topIcon"
#define KEY_TOP_BANNER_TEXT_LINE_0 "topBannerTextLine0"
#define KEY_TOP_BANNER_TEXT_LINE_1 "topBannerTextLine1"
#define KEY_TOP_BANNER_TEXT_LINE_2 "topBannerTextLine2"
#define KEY_TOP_FILE_NAME_TEXT "topFileNameText"
#define KEY_GRID_ICON "gridIcon"
#define KEY_BANNER_LIST_ICON "bannerListIcon"
#define KEY_BANNER_LIST_TEXT_LINE_0 "bannerListTextLine0"
#define KEY_BANNER_LIST_TEXT_LINE_1 "bannerListTextLine1"
#define KEY_BANNER_LIST_TEXT_LINE_2 "bannerListTextLine2"
#define KEY_ELEMENT_POSITION "position"
#define KEY_ELEMENT_WIDTH "width"
#define KEY_ELEMENT_TEXT_COLOR "textColor"
#define KEY_ELEMENT_BLEND_COLOR "blendColor"
static const CustomThemeInfo sDefaultCustomThemeInfo
{
.topIconInfo = CustomTopIconInfo(Point(24, 132), Rgb8(200, 200, 200)),
.topBannerTextLine0Info = CustomTopTextElementInfo(Point(70, 126), 176, Rgb8(30, 30, 30), Rgb8(200, 200, 200)),
.topBannerTextLine1Info = CustomTopTextElementInfo(Point(70, 141), 176, Rgb8(30, 30, 30), Rgb8(200, 200, 200)),
.topBannerTextLine2Info = CustomTopTextElementInfo(Point(70, 155), 176, Rgb8(30, 30, 30), Rgb8(200, 200, 200)),
.topFileNameTextInfo = CustomTopTextElementInfo(Point(18, 170), 220, Rgb8(30, 30, 30), Rgb8(200, 200, 200)),
.gridIconInfo = CustomBottomIconInfo(Rgb8(200, 200, 200)),
.bannerListIconInfo = CustomBottomIconInfo(Rgb8(200, 200, 200)),
.bannerListTextLine0Info = CustomBannerListTextElementInfo(Rgb8(30, 30, 30)),
.bannerListTextLine1Info = CustomBannerListTextElementInfo(Rgb8(30, 30, 30)),
.bannerListTextLine2Info = CustomBannerListTextElementInfo(Rgb8(30, 30, 30))
};
static CustomTopBackgroundType parseTopBackgroundType(const char* topBackgroundTypeString)
{
return CustomTopBackgroundType::Bitmap;
}
static Rgb8 parseColor(const JsonObjectConst& json, const Rgb8& defaultColor)
{
if (json.isNull())
{
return defaultColor;
}
return Rgb8(
json[KEY_COLOR_R] | 0,
json[KEY_COLOR_G] | 0,
json[KEY_COLOR_B] | 0
);
}
static Point parsePoint(const JsonObjectConst& json, const Point& defaultPoint)
{
if (json.isNull())
{
return defaultPoint;
}
return Point(
json[KEY_POINT_X] | 0,
json[KEY_POINT_Y] | 0
);
}
static CustomBannerListTextElementInfo parseCustomBannerListTextElementInfo(
const JsonObjectConst& json, const CustomBannerListTextElementInfo& defaultInfo)
{
if (json.isNull())
{
return defaultInfo;
}
return CustomBannerListTextElementInfo(
parseColor(json[KEY_ELEMENT_TEXT_COLOR], defaultInfo.GetTextColor())
);
}
static CustomBottomIconInfo parseCustomBottomIconInfo(const JsonObjectConst& json, const CustomBottomIconInfo& defaultInfo)
{
if (json.isNull())
{
return defaultInfo;
}
return CustomBottomIconInfo(
parseColor(json[KEY_ELEMENT_BLEND_COLOR], defaultInfo.GetBlendColor())
);
}
static CustomTopIconInfo parseCustomTopIconInfo(const JsonObjectConst& json, const CustomTopIconInfo& defaultInfo)
{
if (json.isNull())
{
return defaultInfo;
}
return CustomTopIconInfo(
parsePoint(json[KEY_ELEMENT_POSITION], defaultInfo.GetPosition()),
parseColor(json[KEY_ELEMENT_BLEND_COLOR], defaultInfo.GetBlendColor())
);
}
static CustomTopTextElementInfo parseCustomTextElementInfo(
const JsonObjectConst& json, const CustomTopTextElementInfo& defaultInfo)
{
if (json.isNull())
{
return defaultInfo;
}
return CustomTopTextElementInfo(
parsePoint(json[KEY_ELEMENT_POSITION], defaultInfo.GetPosition()),
json[KEY_ELEMENT_WIDTH] | defaultInfo.GetWidth(),
parseColor(json[KEY_ELEMENT_TEXT_COLOR], defaultInfo.GetTextColor()),
parseColor(json[KEY_ELEMENT_BLEND_COLOR], defaultInfo.GetBlendColor())
);
}
static CustomThemeInfo parseCustomThemeInfo(const JsonDocument& json)
{
return CustomThemeInfo
{
.topIconInfo = parseCustomTopIconInfo(json[KEY_TOP_ICON], sDefaultCustomThemeInfo.topIconInfo),
.topBannerTextLine0Info = parseCustomTextElementInfo(
json[KEY_TOP_BANNER_TEXT_LINE_0], sDefaultCustomThemeInfo.topBannerTextLine0Info),
.topBannerTextLine1Info = parseCustomTextElementInfo(
json[KEY_TOP_BANNER_TEXT_LINE_1], sDefaultCustomThemeInfo.topBannerTextLine1Info),
.topBannerTextLine2Info = parseCustomTextElementInfo(
json[KEY_TOP_BANNER_TEXT_LINE_2], sDefaultCustomThemeInfo.topBannerTextLine2Info),
.topFileNameTextInfo = parseCustomTextElementInfo(
json[KEY_TOP_FILE_NAME_TEXT], sDefaultCustomThemeInfo.topFileNameTextInfo),
.gridIconInfo = parseCustomBottomIconInfo(json[KEY_GRID_ICON], sDefaultCustomThemeInfo.gridIconInfo),
.bannerListIconInfo = parseCustomBottomIconInfo(json[KEY_BANNER_LIST_ICON], sDefaultCustomThemeInfo.bannerListIconInfo),
.bannerListTextLine0Info = parseCustomBannerListTextElementInfo(
json[KEY_BANNER_LIST_TEXT_LINE_0], sDefaultCustomThemeInfo.bannerListTextLine0Info),
.bannerListTextLine1Info = parseCustomBannerListTextElementInfo(
json[KEY_BANNER_LIST_TEXT_LINE_1], sDefaultCustomThemeInfo.bannerListTextLine1Info),
.bannerListTextLine2Info = parseCustomBannerListTextElementInfo(
json[KEY_BANNER_LIST_TEXT_LINE_2], sDefaultCustomThemeInfo.bannerListTextLine2Info)
};
}
CustomTheme::CustomTheme(const TCHAR* folderName, const Rgb<8, 8, 8>& primaryColor, bool darkMode)
: Theme(folderName, primaryColor, darkMode)
, _customThemeInfo(sDefaultCustomThemeInfo)
, _romBrowserViewFactory(&_customThemeInfo, &_materialColorScheme, &_fontRepository) { }
void CustomTheme::LoadRomBrowserResources(const VramContext& mainVramContext, const VramContext& subVramContext)
{
const auto file = std::make_unique<File>();
@@ -39,6 +190,7 @@ void CustomTheme::LoadRomBrowserResources(const VramContext& mainVramContext, co
return;
_topBackgroundType = parseTopBackgroundType(json["topBackgroundType"].as<const char*>());
_customThemeInfo = parseCustomThemeInfo(json);
mem_setVramDMapping(MEM_VRAM_D_LCDC);
mem_setVramEMapping(MEM_VRAM_E_LCDC);

View File

@@ -7,19 +7,13 @@
#include "romBrowser/Theme/custom/CustomRomBrowserViewFactory.h"
#include "CustomTopBackgroundType.h"
#include "../DefaultFontRepository.h"
#include "CustomThemeInfo.h"
#include "../Theme.h"
class CustomTheme : public Theme
{
String<TCHAR, 64> _folderName;
CustomRomBrowserViewFactory _romBrowserViewFactory;
CustomTopBackgroundType _topBackgroundType;
DefaultFontRepository _fontRepository;
public:
CustomTheme(const TCHAR* folderName, const Rgb<8, 8, 8>& primaryColor, bool darkMode)
: Theme(folderName, primaryColor, darkMode)
, _romBrowserViewFactory(&_materialColorScheme, &_fontRepository) { }
CustomTheme(const TCHAR* folderName, const Rgb<8, 8, 8>& primaryColor, bool darkMode);
const IFontRepository* GetFontRepository() const override
{
@@ -47,4 +41,11 @@ public:
}
void LoadRomBrowserResources(const VramContext& mainVramContext, const VramContext& subVramContext) override;
private:
String<TCHAR, 64> _folderName;
CustomThemeInfo _customThemeInfo;
CustomRomBrowserViewFactory _romBrowserViewFactory;
CustomTopBackgroundType _topBackgroundType;
DefaultFontRepository _fontRepository;
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "CustomBannerListTextElementInfo.h"
#include "CustomBottomIconInfo.h"
#include "CustomTopIconInfo.h"
#include "CustomTextElementInfo.h"
struct CustomThemeInfo
{
CustomTopIconInfo topIconInfo;
CustomTopTextElementInfo topBannerTextLine0Info;
CustomTopTextElementInfo topBannerTextLine1Info;
CustomTopTextElementInfo topBannerTextLine2Info;
CustomTopTextElementInfo topFileNameTextInfo;
CustomBottomIconInfo gridIconInfo;
CustomBottomIconInfo bannerListIconInfo;
CustomBannerListTextElementInfo bannerListTextLine0Info;
CustomBannerListTextElementInfo bannerListTextLine1Info;
CustomBannerListTextElementInfo bannerListTextLine2Info;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "core/math/Point.h"
#include "core/math/Rgb.h"
class CustomTopIconInfo
{
public:
CustomTopIconInfo(const Point& position, const Rgb8& blendColor)
: _position(position), _blendColor(blendColor) { }
const Point& GetPosition() const { return _position; }
const Rgb8& GetBlendColor() const { return _blendColor; }
private:
Point _position;
Rgb8 _blendColor;
};

View File

@@ -1,7 +1,7 @@
#pragma once
/// @brief The Pico Loader API version supported by this header file.
#define PICO_LOADER_API_VERSION 1
#define PICO_LOADER_API_VERSION 3
/// @brief Enum to specify the drive to boot from.
typedef enum
@@ -35,6 +35,54 @@ typedef struct
char arguments[256];
} pload_params_t;
/// @brief Struct representing the API version 2 part of the header of picoLoader7.bin.
typedef struct
{
/// @brief The path of the rom to return to when exiting an application.
/// When this path is not set, no bootstub will be patched into homebrew applications.
char launcherPath[256];
} pload_header7_v2_t;
/// @brief Struct representing a single Action Replay cheat opcode.
typedef struct
{
/// @brief The first part of the opcode.
u32 a;
/// @brief The second part of the opcode.
u32 b;
} pload_cheat_opcode_t;
/// @brief Struct representing a single Action Replay cheat.
typedef struct
{
/// @brief Length of \see opcodes in bytes.
u32 length;
/// @brief The cheat opcodes.
pload_cheat_opcode_t opcodes[1];
} pload_cheat_t;
/// @brief Struct representing one or more cheats. Cheats are adjacent starting from the firstCheat field.
typedef struct
{
/// @brief Length of this stucture (length field + numberOfCheats field + all cheats).
u32 length;
/// @brief The number of cheats.
u32 numberOfCheats;
/// @brief The first cheat.
pload_cheat_t firstCheat;
} pload_cheats_t;
/// @brief Struct representing the API version 3 part of the header of picoLoader7.bin.
typedef struct
{
/// @brief Pointer to the cheats, or \c nullptr when there are no cheats.
const pload_cheats_t* cheats;
} pload_header7_v3_t;
/// @brief Struct representing the header of picoLoader7.bin.
typedef struct
{
@@ -52,4 +100,10 @@ typedef struct
/// @brief The load params, see \see pload_params_t.
pload_params_t loadParams;
/// @brief The API version 2 part of the header. Only access this when \see apiVersion >= 2.
pload_header7_v2_t v2;
/// @brief The API version 3 part of the header. Only access this when \see apiVersion >= 3.
pload_header7_v3_t v3;
} pload_header7_t;

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

@@ -53,5 +53,75 @@ These files can be created, for example, using [NitroPaint](https://github.com/G
The top screen background should include a box in which the banner text and icon of the selected item will be shown.
### Additional JSON properties
Custom themes support additional properties in the `theme.json` file to allow for more customization.
- **topIcon** - Properties of the icon displayed on the top screen.
- **topBannerTextLine0** - Properties of the first banner text line displayed on the top screen.
- **topBannerTextLine1** - Properties of the second banner text line displayed on the top screen.
- **topBannerTextLine2** - Properties of the third banner text line displayed on the top screen.
- **topFileNameText** - Properties of the file name text displayed on the top screen.
- **gridIcon** - Properties of the icons displayed on the bottom screen in grid display modes.
- **bannerListIcon** - Properties of the icons displayed on the bottom screen in banner list display mode.
- **bannerListTextLine0** - Properties of the first banner text line displayed on the bottom screen in banner list display mode.
- **bannerListTextLine1** - Properties of the second banner text line displayed on the bottom screen in banner list display mode.
- **bannerListTextLine2** - Properties of the third banner text line displayed on the bottom screen in banner list display mode.
Blend colors are used to fake translucency. They should be set to an approximation of the background color.
```json
{
"type": "custom",
"name": "Raspberry",
"description": "Theme based on raspberries.",
"author": "Gericom",
"primaryColor": { "r": 138, "g": 217, "b": 255 },
"darkTheme": false,
"topIcon": {
"position": { "x": 24, "y": 132 },
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"topBannerTextLine0": {
"position": { "x": 70, "y": 126 },
"width": 176,
"textColor": { "r": 30, "g": 30, "b": 30 },
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"topBannerTextLine1": {
"position": { "x": 70, "y": 141 },
"width": 176,
"textColor": { "r": 30, "g": 30, "b": 30 },
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"topBannerTextLine2": {
"position": { "x": 70, "y": 155 },
"width": 176,
"textColor": { "r": 30, "g": 30, "b": 30 },
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"topFileNameText": {
"position": { "x": 18, "y": 170 },
"width": 220,
"textColor": { "r": 30, "g": 30, "b": 30 },
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"gridIcon": {
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"bannerListIcon": {
"blendColor": { "r": 200, "g": 200, "b": 200 }
},
"bannerListTextLine0": {
"textColor": { "r": 30, "g": 30, "b": 30 }
},
"bannerListTextLine1": {
"textColor": { "r": 30, "g": 30, "b": 30 }
},
"bannerListTextLine2": {
"textColor": { "r": 30, "g": 30, "b": 30 }
}
}
```
## Background music
All themes support background music by placing DSP-ADPCM encoded `.bcstm` files in a `bgm` folder inside the theme folder. Looping is supported. When multiple `.bcstm` files are provided, the background music will be selected at random each time Pico Launcher is started.

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.
@@ -46,4 +47,4 @@ Settings are stored on your SD card in `/_pico/settings.json`. They can be edite
- `romBrowserSortMode` - Specified if folder contents should be sorted from A to Z (`NameAscending`), or from Z to A (`NameDescending`). This setting can be changed from within Pico Launcher.
- `theme`: Specifies the folder name of the theme to use. If the theme cannot be found, a default fallback theme will be used.
- `lastUsedFilePath` - Specifies the path of the most recently launched homebrew or game, such that it can be selected the next time Pico Launcher is started. It is automatically updated by Pico Launcher.
- `fileAssociations` - See [FileAssociations.md](/LAUNCHER/docs/FileAssociations.md) for information about how to use this setting.
- `fileAssociations` - See [FileAssociations.md](/docs/FileAssociations.md) for information about how to use this setting.

BIN
docs/images/Cheats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB