From f90d0a1aae1cf01bd6cb1b7cf2d7cbb70978d33e Mon Sep 17 00:00:00 2001 From: Augusto Daniele <12686734+tasken@users.noreply.github.com> Date: Sun, 5 Jul 2026 11:27:48 -0300 Subject: [PATCH] Add support for custom BMP icons and custom NDS banners for games and folders. Closes #53 and Closes #62 --- CHANGELOG.md | 4 + README.md | 1 + arm9/source/romBrowser/BannerRepository.cpp | 80 +++++++++++++++++ arm9/source/romBrowser/BannerRepository.h | 10 +++ arm9/source/romBrowser/CoverRepository.cpp | 56 ++---------- arm9/source/romBrowser/CoverRepository.h | 11 +-- .../BannerListFileRecyclerAdapter.cpp | 2 +- arm9/source/romBrowser/FileInfoManager.cpp | 38 ++++++-- arm9/source/romBrowser/FileInfoManager.h | 21 ++++- .../romBrowser/FileType/BmpFileCover.cpp | 10 ++- .../romBrowser/FileType/BmpFileIcon.cpp | 35 ++++++++ arm9/source/romBrowser/FileType/BmpFileIcon.h | 19 ++++ .../romBrowser/FileType/BmpFileIconData.cpp | 83 +++++++++++++++++ .../romBrowser/FileType/BmpFileIconData.h | 24 +++++ arm9/source/romBrowser/FileType/BmpHeader.h | 52 +++++++++++ .../FileType/Bnr/BnrInternalFileInfo.cpp | 19 ++++ .../FileType/Bnr/BnrInternalFileInfo.h | 10 +++ .../romBrowser/FileType/CustomFileType.h | 6 -- .../FileType/CustomIconInternalFileInfo.h | 29 ++++++ arm9/source/romBrowser/FileType/FileType.h | 4 - .../romBrowser/FileType/Gba/GbaFileType.h | 2 - .../FileType/Gba/GbaInternalFileInfo.cpp | 7 +- .../FileType/Gba/GbaInternalFileInfo.h | 2 +- .../romBrowser/FileType/Nds/NdsFileType.h | 2 - .../FileType/Nds/NdsInternalFileInfo.cpp | 40 +-------- .../FileType/Nds/NdsInternalFileInfo.h | 15 +--- .../FileType/NdsBannerInternalFileInfo.cpp | 89 +++++++++++++++++++ .../FileType/NdsBannerInternalFileInfo.h | 26 ++++++ arm9/source/romBrowser/IBannerRepository.h | 16 ++++ arm9/source/romBrowser/IIconRepository.h | 17 ++++ .../source/romBrowser/IRomBrowserController.h | 4 + arm9/source/romBrowser/IconRepository.cpp | 65 ++++++++++++++ arm9/source/romBrowser/IconRepository.h | 10 +++ arm9/source/romBrowser/RepositoryBase.cpp | 49 ++++++++++ arm9/source/romBrowser/RepositoryBase.h | 20 +++++ .../romBrowser/RomBrowserController.cpp | 10 +++ arm9/source/romBrowser/RomBrowserController.h | 6 ++ .../viewModels/RomBrowserViewModel.cpp | 3 +- .../views/RomBrowserTopScreenView.cpp | 78 +++++++--------- docs/Customization.md | 53 +++++++++++ 40 files changed, 841 insertions(+), 187 deletions(-) create mode 100644 arm9/source/romBrowser/BannerRepository.cpp create mode 100644 arm9/source/romBrowser/BannerRepository.h create mode 100644 arm9/source/romBrowser/FileType/BmpFileIcon.cpp create mode 100644 arm9/source/romBrowser/FileType/BmpFileIcon.h create mode 100644 arm9/source/romBrowser/FileType/BmpFileIconData.cpp create mode 100644 arm9/source/romBrowser/FileType/BmpFileIconData.h create mode 100644 arm9/source/romBrowser/FileType/BmpHeader.h create mode 100644 arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.cpp create mode 100644 arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.h create mode 100644 arm9/source/romBrowser/FileType/CustomIconInternalFileInfo.h create mode 100644 arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.cpp create mode 100644 arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.h create mode 100644 arm9/source/romBrowser/IBannerRepository.h create mode 100644 arm9/source/romBrowser/IIconRepository.h create mode 100644 arm9/source/romBrowser/IconRepository.cpp create mode 100644 arm9/source/romBrowser/IconRepository.h create mode 100644 arm9/source/romBrowser/RepositoryBase.cpp create mode 100644 arm9/source/romBrowser/RepositoryBase.h create mode 100644 docs/Customization.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f9fc9fe..476adee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added +- Support for custom BMP icons for games and folders - by @tasken +- Support for custom NDS banners (custom titles, subtitles and animated icons) for games and folders - by @tasken + ### Fixed - Top screen cover is now displayed/hidden correctly when placed partially or fully off-screen - DSi banners with missing DSi part now fall back to the DS icon diff --git a/README.md b/README.md index b96fcde..6743bf1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains Pico Launcher, which is a front-end for [Pico Loader](h - Coverflow - [File associations](docs/FileAssociations.md) - [Covers](docs/Covers.md) +- [Custom Icons & Banners](docs/Customization.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)) diff --git a/arm9/source/romBrowser/BannerRepository.cpp b/arm9/source/romBrowser/BannerRepository.cpp new file mode 100644 index 0000000..7c95099 --- /dev/null +++ b/arm9/source/romBrowser/BannerRepository.cpp @@ -0,0 +1,80 @@ +#include "common.h" +#include +#include "core/mini-printf.h" +#include "fat/Directory.h" +#include "FileType/NullFileTypeProvider.h" +#include "FileType/Bnr/BnrInternalFileInfo.h" +#include "SdFolderFactory.h" +#include "fat/File.h" +#include "BannerRepository.h" + +void BannerRepository::Initialize() +{ + InitializeFolders("/_pico/banners/"); +} + +InternalFileInfo* BannerRepository::GetBannerForFile(const FileInfo& fileInfo, const char* gameCode) const +{ + char nameBuffer[270]; + const auto& fileType = fileInfo.GetFileType(); + + if (fileType->GetClassification() == FileTypeClassification::Folder) + { + // Look for banner.bnr inside the folder (path relative to FatFs CWD = current browse dir). + // Scan with the already-open directory handle so the match can be turned directly into a + // FastFileRef, instead of stat'ing then re-opening the same path by name. + Directory folderDir; + if (folderDir.Open(fileInfo.GetFileName()) == FR_OK) + { + FILINFO folderFileInfo; + while (folderDir.Read(&folderFileInfo) == FR_OK && folderFileInfo.fname[0] != 0) + { + if (!(folderFileInfo.fattrib & AM_DIR) && !strcasecmp(folderFileInfo.fname, "banner.bnr")) + { + auto* bnr = new BnrInternalFileInfo( + FastFileRef(folderDir.GetFatFsDirectory(), &folderFileInfo), nullptr); + if (bnr->HasBanner()) + { + return bnr; + } + delete bnr; + break; + } + } + } + + return nullptr; + } + + const FileInfo* bnrFile = nullptr; + + // Try to get a banner based on the filename in the user folder + if (_userFolder) + { + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bnr", fileInfo.GetFileName()); + bnrFile = _userFolder->BinarySearch(nameBuffer); + } + + // Try to get a banner based on an internal game code + if (!bnrFile && gameCode) + { + const auto* bannerFolder = GetFileTypeFolder(fileType->GetShortName()); + if (bannerFolder) + { + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bnr", gameCode); + bnrFile = bannerFolder->BinarySearch(nameBuffer); + } + } + + if (bnrFile) + { + auto* bnr = new BnrInternalFileInfo(bnrFile->GetFastFileRef(), gameCode); + if (bnr->HasBanner()) + { + return bnr; + } + delete bnr; + } + + return nullptr; +} diff --git a/arm9/source/romBrowser/BannerRepository.h b/arm9/source/romBrowser/BannerRepository.h new file mode 100644 index 0000000..d190f6b --- /dev/null +++ b/arm9/source/romBrowser/BannerRepository.h @@ -0,0 +1,10 @@ +#pragma once +#include "IBannerRepository.h" +#include "RepositoryBase.h" + +class BannerRepository : public RepositoryBase, public IBannerRepository +{ +public: + void Initialize() override; + InternalFileInfo* GetBannerForFile(const FileInfo& fileInfo, const char* gameCode) const override; +}; diff --git a/arm9/source/romBrowser/CoverRepository.cpp b/arm9/source/romBrowser/CoverRepository.cpp index 180a391..5c8cfb0 100644 --- a/arm9/source/romBrowser/CoverRepository.cpp +++ b/arm9/source/romBrowser/CoverRepository.cpp @@ -1,6 +1,5 @@ #include "common.h" -#include -#include "core/StringUtil.h" +#include "core/mini-printf.h" #include "FileType/NullFileTypeProvider.h" #include "FileType/BmpFileCover.h" #include "FileType/InternalFileInfo.h" @@ -9,22 +8,7 @@ void CoverRepository::Initialize() { - NullFileTypeProvider fileTypeProvider; - _ndsCoversFolder = SdFolderFactory(&fileTypeProvider).CreateFromPath("/_pico/covers/nds"); - if (_ndsCoversFolder) - { - _ndsCoversFolder->SortByNameInPlace(); - } - _gbaCoversFolder = SdFolderFactory(&fileTypeProvider).CreateFromPath("/_pico/covers/gba"); - if (_gbaCoversFolder) - { - _gbaCoversFolder->SortByNameInPlace(); - } - _userCoversFolder = SdFolderFactory(&fileTypeProvider).CreateFromPath("/_pico/covers/user"); - if (_userCoversFolder) - { - _userCoversFolder->SortByNameInPlace(); - } + InitializeFolders("/_pico/covers/"); } FileCover* CoverRepository::GetCoverForFile(const FileInfo& fileInfo, const InternalFileInfo* internalFileInfo) const @@ -37,32 +21,22 @@ FileCover* CoverRepository::GetCoverForFile(const FileInfo& fileInfo, const Inte const FileInfo* coverFile = nullptr; // Try to get a cover based on the filename in the user folder - if (_userCoversFolder) + if (_userFolder) { - u32 length = StringUtil::Copy(nameBuffer, fileInfo.GetFileName(), sizeof(nameBuffer) - 5); - nameBuffer[length + 0] = '.'; - nameBuffer[length + 1] = 'b'; - nameBuffer[length + 2] = 'm'; - nameBuffer[length + 3] = 'p'; - nameBuffer[length + 4] = 0; - coverFile = _userCoversFolder->BinarySearch(nameBuffer); + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bmp", fileInfo.GetFileName()); + coverFile = _userFolder->BinarySearch(nameBuffer); } // Try to get a cover based on an internal game code if (!coverFile && internalFileInfo) { - const auto* coverFolder = GetCoverFolder(fileType->GetShortName()); + const auto* coverFolder = GetFileTypeFolder(fileType->GetShortName()); if (coverFolder) { const char* gameCode = internalFileInfo->GetGameCode(); if (gameCode) { - u32 length = StringUtil::Copy(nameBuffer, gameCode, sizeof(nameBuffer) - 5); - nameBuffer[length + 0] = '.'; - nameBuffer[length + 1] = 'b'; - nameBuffer[length + 2] = 'm'; - nameBuffer[length + 3] = 'p'; - nameBuffer[length + 4] = 0; + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bmp", gameCode); } coverFile = coverFolder->BinarySearch(nameBuffer); @@ -86,19 +60,3 @@ FileCover* CoverRepository::GetCoverForFile(const FileInfo& fileInfo, const Inte return fileType->CreateFileCover(fileInfo.GetFileName()); } - -const SdFolder* CoverRepository::GetCoverFolder(const char* coverFolderName) const -{ - if (!strcmp(coverFolderName, "nds")) - { - return _ndsCoversFolder.get(); - } - else if (!strcmp(coverFolderName, "gba")) - { - return _gbaCoversFolder.get(); - } - else - { - return nullptr; - } -} diff --git a/arm9/source/romBrowser/CoverRepository.h b/arm9/source/romBrowser/CoverRepository.h index c6d4731..e074b75 100644 --- a/arm9/source/romBrowser/CoverRepository.h +++ b/arm9/source/romBrowser/CoverRepository.h @@ -1,18 +1,11 @@ #pragma once #include "ICoverRepository.h" -#include "SdFolder.h" +#include "RepositoryBase.h" -class CoverRepository : public ICoverRepository +class CoverRepository : public RepositoryBase, public ICoverRepository { public: void Initialize() override; FileCover* GetCoverForFile( const FileInfo& fileInfo, const InternalFileInfo* internalFileInfo) const override; - -private: - std::unique_ptr _ndsCoversFolder; - std::unique_ptr _gbaCoversFolder; - std::unique_ptr _userCoversFolder; - - const SdFolder* GetCoverFolder(const char* coverFolderName) const; }; diff --git a/arm9/source/romBrowser/DisplayMode/BannerListFileRecyclerAdapter.cpp b/arm9/source/romBrowser/DisplayMode/BannerListFileRecyclerAdapter.cpp index 572ac26..6fb1474 100644 --- a/arm9/source/romBrowser/DisplayMode/BannerListFileRecyclerAdapter.cpp +++ b/arm9/source/romBrowser/DisplayMode/BannerListFileRecyclerAdapter.cpp @@ -35,7 +35,7 @@ TaskResult BannerListFileRecyclerAdapter::BindView(SharedPtr view, i if (internalFileInfo) { const char16_t* gameTitle = internalFileInfo->GetGameTitle(); - if (gameTitle) + if (gameTitle && gameTitle[0] != 0) { listItemView->SetGameTitle(gameTitle); fileNameAsTitle = false; diff --git a/arm9/source/romBrowser/FileInfoManager.cpp b/arm9/source/romBrowser/FileInfoManager.cpp index 3b96f60..8fdb573 100644 --- a/arm9/source/romBrowser/FileInfoManager.cpp +++ b/arm9/source/romBrowser/FileInfoManager.cpp @@ -1,11 +1,15 @@ #include "common.h" #include +#include "FileType/CustomIconInternalFileInfo.h" #include "FileInfoManager.h" -FileInfoManager::FileInfoManager(std::unique_ptr items, u32 itemCount, const ICoverRepository& coverRepository) +FileInfoManager::FileInfoManager(std::unique_ptr items, u32 itemCount, const ICoverRepository& coverRepository, + const IIconRepository& iconRepository, const IBannerRepository& bannerRepository) : _items(std::move(items)), _itemCount(itemCount) , _extraFileInfo(std::make_unique(itemCount)) - , _coverRepository(coverRepository) { } + , _coverRepository(coverRepository) + , _iconRepository(iconRepository) + , _bannerRepository(bannerRepository) { } FileInfoManager::~FileInfoManager() { @@ -17,10 +21,29 @@ FileInfoManager::~FileInfoManager() void FileInfoManager::LoadFileInfo(int index) { - auto internalFileInfo = _extraFileInfo[index].internalFileInfo; - if (!internalFileInfo) + if (_extraFileInfo[index].loaded) { - internalFileInfo = _items[index]->CreateInternalFileInfo(); + return; + } + + const InternalFileInfo* internalFileInfo = _items[index]->CreateInternalFileInfo(); + const char* gameCode = internalFileInfo ? internalFileInfo->GetGameCode() : nullptr; + + // A custom banner (.bnr) takes priority and replaces the internal file info entirely. + auto customBanner = _bannerRepository.GetBannerForFile(*_items[index], gameCode); + if (customBanner) + { + delete internalFileInfo; + internalFileInfo = customBanner; + } + else + { + // A custom icon (.bmp) wraps the existing internal file info, overriding only the icon. + auto iconData = _iconRepository.GetIconForFile(*_items[index], gameCode); + if (iconData) + { + internalFileInfo = new CustomIconInternalFileInfo(std::move(iconData), std::unique_ptr(internalFileInfo)); + } } if (!_extraFileInfo[index].fileCover.Lock()) @@ -29,10 +52,13 @@ void FileInfoManager::LoadFileInfo(int index) } _extraFileInfo[index].internalFileInfo = internalFileInfo; + _extraFileInfo[index].loaded = true; } void FileInfoManager::ReleaseFileInfo(int index) { + _extraFileInfo[index].loaded = false; + auto internalFileInfo = _extraFileInfo[index].internalFileInfo; if (internalFileInfo) { @@ -57,4 +83,4 @@ int FileInfoManager::GetItemIndex(const char* fileName) } } return -1; -} \ No newline at end of file +} diff --git a/arm9/source/romBrowser/FileInfoManager.h b/arm9/source/romBrowser/FileInfoManager.h index 8a3395f..442dad4 100644 --- a/arm9/source/romBrowser/FileInfoManager.h +++ b/arm9/source/romBrowser/FileInfoManager.h @@ -4,13 +4,16 @@ #include "FileInfo.h" #include "FileType/FileCover.h" #include "ICoverRepository.h" +#include "IIconRepository.h" +#include "IBannerRepository.h" #include "core/AtomicSharedPtr.h" #include "FileType/InternalFileInfo.h" class FileInfoManager { public: - FileInfoManager(std::unique_ptr items, u32 itemCount, const ICoverRepository& coverRepository); + FileInfoManager(std::unique_ptr items, u32 itemCount, const ICoverRepository& coverRepository, + const IIconRepository& iconRepository, const IBannerRepository& bannerRepository); ~FileInfoManager(); const InternalFileInfo* GetInternalFileInfo(int index) @@ -18,13 +21,20 @@ public: return _extraFileInfo[index].internalFileInfo; } + /// @brief Whether LoadFileInfo() has finished for this item. Unlike checking + /// GetInternalFileInfo() for null, this distinguishes "still loading" from + /// "loaded, and there's legitimately nothing" (e.g. a folder with no custom icon). + bool IsFileInfoLoaded(int index) const + { + return _extraFileInfo[index].loaded; + } + SharedPtr GetFileCover(int index) { return _extraFileInfo[index].fileCover.Lock(); } void LoadFileInfo(int index); - void ReleaseFileInfo(int index); int GetItemIndex(const char* fileName); @@ -35,7 +45,8 @@ public: private: struct ExtraFileInfo { - const InternalFileInfo* internalFileInfo; + bool loaded = false; + const InternalFileInfo* internalFileInfo = nullptr; AtomicSharedPtr fileCover; }; @@ -43,4 +54,6 @@ private: u32 _itemCount; std::unique_ptr _extraFileInfo; const ICoverRepository& _coverRepository; -}; \ No newline at end of file + const IIconRepository& _iconRepository; + const IBannerRepository& _bannerRepository; +}; diff --git a/arm9/source/romBrowser/FileType/BmpFileCover.cpp b/arm9/source/romBrowser/FileType/BmpFileCover.cpp index 9db6eae..27f85e5 100644 --- a/arm9/source/romBrowser/FileType/BmpFileCover.cpp +++ b/arm9/source/romBrowser/FileType/BmpFileCover.cpp @@ -5,6 +5,7 @@ #include #include "fat/File.h" #include "core/math/ColorConverter.h" +#include "BmpHeader.h" #include "BmpFileCover.h" BmpFileCover::BmpFileCover(const FastFileRef& coverFileRef) @@ -12,8 +13,11 @@ BmpFileCover::BmpFileCover(const FastFileRef& coverFileRef) const auto file = std::make_unique(); file->Open(coverFileRef, FA_READ); - if (!file->ReadExact(_coverBuffer, 0x436)) + if (!file->ReadExact(_coverBuffer, 0x436) || + !BmpHeader::Validate(_coverBuffer, 128, 96, 8)) + { return; + } u32 dataOffset = _coverBuffer[0xA] | (_coverBuffer[0xB] << 8) | (_coverBuffer[0xC] << 16) | (_coverBuffer[0xD] << 24); @@ -24,12 +28,14 @@ BmpFileCover::BmpFileCover(const FastFileRef& coverFileRef) u32 g = *paletteData32++; u32 r = *paletteData32++; paletteData32++; - _palette[i] = ColorConverter::ToXBGR555(Rgb<5, 5, 5>(Rgb<8, 8, 8>(r, g, b))); + _palette[i] = ColorConverter::ToXBGR555(Rgb<5, 5, 5>(Rgb8(r, g, b))); } if (file->Seek(dataOffset) != FR_OK || !file->ReadExact(_coverBuffer, sizeof(_coverBuffer))) + { return; + } file->Close(); diff --git a/arm9/source/romBrowser/FileType/BmpFileIcon.cpp b/arm9/source/romBrowser/FileType/BmpFileIcon.cpp new file mode 100644 index 0000000..52791cd --- /dev/null +++ b/arm9/source/romBrowser/FileType/BmpFileIcon.cpp @@ -0,0 +1,35 @@ +#include "common.h" +#include +#include "gui/OamBuilder.h" +#include "gui/palette/DirectPalette.h" +#include "gui/GraphicsContext.h" +#include "BmpFileIcon.h" + +void BmpFileIcon::UploadGraphics() +{ + if (_vramAddress != nullptr) + { + dma_ntrCopy32(3, _iconData->GetGfx(), _vramAddress, BmpFileIconData::GfxSize); + } +} + +void BmpFileIcon::Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) +{ + if (!graphicsContext.IsVisible(Rectangle(_position, 32, 32)) || + _vramAddress == nullptr) + { + return; + } + + u32 paletteRowIdx = graphicsContext.GetPaletteManager().AllocRow( + DirectPalette(_iconData->GetPltt()), _position.y, _position.y + 32); + + // Character offset in OBJ VRAM is in 128-byte units for 32x32 4 bpp sprites + auto builder = OamBuilder::OamWithSize<32, 32>( + _position, _vramOffset >> 7) + .WithPalette16(paletteRowIdx) + .WithPriority(graphicsContext.GetPriority()); + + gfx_oam_entry_t* oam = graphicsContext.GetOamManager().AllocOams(1); + builder.Build(oam[0]); +} diff --git a/arm9/source/romBrowser/FileType/BmpFileIcon.h b/arm9/source/romBrowser/FileType/BmpFileIcon.h new file mode 100644 index 0000000..944cf19 --- /dev/null +++ b/arm9/source/romBrowser/FileType/BmpFileIcon.h @@ -0,0 +1,19 @@ +#pragma once +#include "FileIcon.h" +#include "BmpFileIconData.h" +#include "core/SharedPtr.h" + +/// @brief Icon of a file loaded from an external 32x32 4 bpp 16-color BMP file. +class BmpFileIcon : public FileIcon +{ +public: + explicit BmpFileIcon(SharedPtr iconData) + : _iconData(std::move(iconData)) { } + + void UploadGraphics() override; + + void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) override; + +private: + SharedPtr _iconData; +}; diff --git a/arm9/source/romBrowser/FileType/BmpFileIconData.cpp b/arm9/source/romBrowser/FileType/BmpFileIconData.cpp new file mode 100644 index 0000000..6a3728f --- /dev/null +++ b/arm9/source/romBrowser/FileType/BmpFileIconData.cpp @@ -0,0 +1,83 @@ +#include "common.h" +#include +#include +#include +#include "fat/File.h" +#include "core/math/ColorConverter.h" +#include "BmpHeader.h" +#include "BmpFileIconData.h" + +BmpFileIconData::BmpFileIconData(const FastFileRef& iconFileRef) +{ + auto file = std::make_unique(); + file->Open(iconFileRef, FA_READ); + + memset(_iconGfx, 0, sizeof(_iconGfx)); + memset(_iconPltt, 0, sizeof(_iconPltt)); + Load(std::move(file)); + DC_FlushRange(_iconGfx, sizeof(_iconGfx)); + DC_FlushRange(_iconPltt, sizeof(_iconPltt)); +} + +void BmpFileIconData::Load(std::unique_ptr file) +{ + // BMP file header (14) + DIB header (40) + 16-color palette (64) + u8 headerAndPalette[118]; + if (!file->ReadExact(headerAndPalette, sizeof(headerAndPalette)) || + !BmpHeader::Validate(headerAndPalette, 32, 32, 4)) + { + return; + } + + u32 dataOffset = headerAndPalette[0xA] | (headerAndPalette[0xB] << 8) | + (headerAndPalette[0xC] << 16) | (headerAndPalette[0xD] << 24); + + if (dataOffset < sizeof(headerAndPalette)) + { + return; + } + + const bool topDown = BmpHeader::IsTopDown(headerAndPalette); + + const u8* paletteData = &headerAndPalette[0x36]; + for (u32 i = 0; i < 16; i++) + { + u32 b = *paletteData++; + u32 g = *paletteData++; + u32 r = *paletteData++; + paletteData++; + _iconPltt[i] = ColorConverter::ToGBGR565(Rgb<8, 8, 8>(r, g, b)); + } + + // Heap-allocate the staging buffer so it doesn't live on the task thread stack. + auto rawPixelData = std::make_unique(GfxSize); + if (!rawPixelData || + file->Seek(dataOffset) != FR_OK || + !file->ReadExact(rawPixelData.get(), GfxSize)) + { + memset(_iconPltt, 0, sizeof(_iconPltt)); + return; + } + + // Convert BMP rows (bottom-up or top-down) to the DS tiled 4 bpp sprite format. + // BMP is high-nibble-first; DS tiles are low-nibble-first -- swap nibbles per 4-byte group. + for (int y = 0; y < 32; y++) + { + // Bottom-up BMP (normal, positive height): row 0 is the bottom of the image. + // Top-down BMP (negative height): row 0 is the top of the image. + const u8* srcRowPtr = topDown + ? rawPixelData.get() + y * 16 + : rawPixelData.get() + (31 - y) * 16; + + int ty = y / 8; + int py = y % 8; + + for (int tx = 0; tx < 4; tx++) + { + u32 val; + memcpy(&val, srcRowPtr + tx * 4, 4); + val = ((val >> 4) & 0x0F0F0F0F) | ((val & 0x0F0F0F0F) << 4); + memcpy(&_iconGfx[(ty * 4 + tx) * 32 + py * 4], &val, 4); + } + } +} diff --git a/arm9/source/romBrowser/FileType/BmpFileIconData.h b/arm9/source/romBrowser/FileType/BmpFileIconData.h new file mode 100644 index 0000000..4479a25 --- /dev/null +++ b/arm9/source/romBrowser/FileType/BmpFileIconData.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include "fat/FastFileRef.h" + +class File; + +/// @brief Decoded graphics and palette of a 32x32 4 bpp 16-color BMP file icon, +/// loaded once and shared by BmpFileIcon instances. +class alignas(32) BmpFileIconData +{ +public: + static constexpr u32 GfxSize = 512; + + explicit BmpFileIconData(const FastFileRef& iconFileRef); + + const u8* GetGfx() const { return _iconGfx; } + const u16* GetPltt() const { return _iconPltt; } + +private: + u8 _iconGfx[GfxSize] alignas(32); + u16 _iconPltt[16] alignas(32); + + void Load(std::unique_ptr file); +}; diff --git a/arm9/source/romBrowser/FileType/BmpHeader.h b/arm9/source/romBrowser/FileType/BmpHeader.h new file mode 100644 index 0000000..643f786 --- /dev/null +++ b/arm9/source/romBrowser/FileType/BmpHeader.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include + +/// @brief Static helpers for reading and validating BITMAPFILEHEADER + BITMAPINFOHEADER +/// fields from a raw BMP buffer. +struct BmpHeader +{ + /// @brief Reads the (possibly negative) biHeight field of a BMP's DIB header. + /// @param bmpHeader Buffer containing BMP data. Must be at least 50 bytes. + /// @return The image height. Negative when the BMP is stored top-down. + static s32 GetHeight(const u8* bmpHeader) + { + return (s32)(bmpHeader[0x16] | (bmpHeader[0x17] << 8) | (bmpHeader[0x18] << 16) | (bmpHeader[0x19] << 24)); + } + + /// @brief Validates BITMAPFILEHEADER + BITMAPINFOHEADER fields from a raw BMP buffer. + /// @param bmpHeader Buffer containing BMP data. Must be at least 50 bytes. + /// @param expectedWidth Expected width of the image. + /// @param expectedHeight Expected height of the image. + /// @param expectedBpp Expected bits per pixel. + /// @return \c true when valid, or \c false otherwise. + static bool Validate(const u8* bmpHeader, u32 expectedWidth, u32 expectedHeight, u32 expectedBpp) + { + if (bmpHeader[0] != 'B' || bmpHeader[1] != 'M') + { + return false; + } + + u32 dibSize = bmpHeader[0x0E] | (bmpHeader[0x0F] << 8) | (bmpHeader[0x10] << 16) | (bmpHeader[0x11] << 24); + u32 width = bmpHeader[0x12] | (bmpHeader[0x13] << 8) | (bmpHeader[0x14] << 16) | (bmpHeader[0x15] << 24); + u32 bpp = bmpHeader[0x1C] | (bmpHeader[0x1D] << 8); + u32 comp = bmpHeader[0x1E] | (bmpHeader[0x1F] << 8) | (bmpHeader[0x20] << 16) | (bmpHeader[0x21] << 24); + u32 clrUsed = bmpHeader[0x2E] | (bmpHeader[0x2F] << 8) | (bmpHeader[0x30] << 16) | (bmpHeader[0x31] << 24); + + return dibSize == 40 + && width == expectedWidth + && (u32)std::abs(GetHeight(bmpHeader)) == expectedHeight + && bpp == expectedBpp + && comp == 0 + && (clrUsed == 0 || clrUsed == (1u << expectedBpp)); + } + + /// @brief Returns \c true if the BMP stores rows top-to-bottom (negative biHeight). + /// @param bmpHeader The raw BMP buffer. + /// @return \c true when top-down, or \c false otherwise. + /// @note Call only after Validate() succeeds. + static bool IsTopDown(const u8* bmpHeader) + { + return GetHeight(bmpHeader) < 0; + } +}; diff --git a/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.cpp b/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.cpp new file mode 100644 index 0000000..4bd63e7 --- /dev/null +++ b/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.cpp @@ -0,0 +1,19 @@ +#include "common.h" +#include +#include +#include "fat/File.h" +#include "BnrInternalFileInfo.h" + +BnrInternalFileInfo::BnrInternalFileInfo(const FastFileRef& bnrFileRef, const char* gameCode) +{ + auto file = std::make_unique(); + file->Open(bnrFileRef, FA_READ); + + if (gameCode) + { + strncpy(_gameCode, gameCode, 4); + } + + _hasBanner = ReadBannerChunks(*file, file->GetSize()); +} + diff --git a/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.h b/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.h new file mode 100644 index 0000000..d4e6beb --- /dev/null +++ b/arm9/source/romBrowser/FileType/Bnr/BnrInternalFileInfo.h @@ -0,0 +1,10 @@ +#pragma once +#include "../NdsBannerInternalFileInfo.h" +#include "fat/FastFileRef.h" + +/// @brief Internal file info loaded from an external .bnr file. +class alignas(32) BnrInternalFileInfo : public NdsBannerInternalFileInfo +{ +public: + BnrInternalFileInfo(const FastFileRef& bnrFileRef, const char* gameCode); +}; diff --git a/arm9/source/romBrowser/FileType/CustomFileType.h b/arm9/source/romBrowser/FileType/CustomFileType.h index 25145db..2e37e39 100644 --- a/arm9/source/romBrowser/FileType/CustomFileType.h +++ b/arm9/source/romBrowser/FileType/CustomFileType.h @@ -34,12 +34,6 @@ public: : FileType::CreateFileCover(fileName); }; - bool HasInternalFileInfo() const override - { - return _baseFileType != nullptr - && _baseFileType->HasInternalFileInfo(); - } - InternalFileInfo* CreateInternalFileInfo(const FastFileRef& fastFileRef) const override { return _baseFileType != nullptr diff --git a/arm9/source/romBrowser/FileType/CustomIconInternalFileInfo.h b/arm9/source/romBrowser/FileType/CustomIconInternalFileInfo.h new file mode 100644 index 0000000..af3b921 --- /dev/null +++ b/arm9/source/romBrowser/FileType/CustomIconInternalFileInfo.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include "InternalFileInfo.h" +#include "BmpFileIcon.h" +#include "BmpFileIconData.h" +#include "core/SharedPtr.h" + +/// @brief Wraps any InternalFileInfo and overrides its icon with a custom BMP icon. +class CustomIconInternalFileInfo : public InternalFileInfo +{ +public: + CustomIconInternalFileInfo(SharedPtr iconData, std::unique_ptr wrapped) + : _iconData(std::move(iconData)), _wrapped(std::move(wrapped)) {} + + ~CustomIconInternalFileInfo() override = default; + + const char* GetGameCode() const override { return _wrapped ? _wrapped->GetGameCode() : nullptr; } + const char16_t* GetGameTitle() const override { return _wrapped ? _wrapped->GetGameTitle() : nullptr; } + FileCover* CreateGameCover() const override { return _wrapped ? _wrapped->CreateGameCover() : nullptr; } + + std::unique_ptr CreateGameIcon() const override + { + return std::make_unique(_iconData); + } + +private: + SharedPtr _iconData; + std::unique_ptr _wrapped; +}; diff --git a/arm9/source/romBrowser/FileType/FileType.h b/arm9/source/romBrowser/FileType/FileType.h index f102181..7cb4ac3 100644 --- a/arm9/source/romBrowser/FileType/FileType.h +++ b/arm9/source/romBrowser/FileType/FileType.h @@ -45,10 +45,6 @@ public: return new UnknownFileCover(); }; - /// @brief Returns if this file format has internal file info or not. - /// @return \c true if this file format has internal file info, or \c false otherwise. - virtual bool HasInternalFileInfo() const { return false; } - /// @brief Reads the internal file info of the file specified by \p fastFileRef. /// @param fastFileRef The file to get the internal file info of. /// @return The internal file info of the specified file when successful, or \c nullptr otherwise. diff --git a/arm9/source/romBrowser/FileType/Gba/GbaFileType.h b/arm9/source/romBrowser/FileType/Gba/GbaFileType.h index e13384c..8998d87 100644 --- a/arm9/source/romBrowser/FileType/Gba/GbaFileType.h +++ b/arm9/source/romBrowser/FileType/Gba/GbaFileType.h @@ -16,8 +16,6 @@ public: return themeFileIconFactory->CreateGenericFileIcon(fileName); } - bool HasInternalFileInfo() const override { return true; } - InternalFileInfo* CreateInternalFileInfo(const FastFileRef& fastFileRef) const override { return new GbaInternalFileInfo(fastFileRef); diff --git a/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.cpp b/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.cpp index 18a52bb..2e1fc40 100644 --- a/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.cpp +++ b/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.cpp @@ -14,7 +14,8 @@ GbaInternalFileInfo::GbaInternalFileInfo(const FastFileRef& fastFileRef) if (file->Seek(0xAC) != FR_OK) return; - u32 bytesRead; - if (file->Read(_gameCode, 4, bytesRead) != FR_OK) - return; + if (!file->ReadExact(_gameCode, 4)) + { + memset(_gameCode, 0, sizeof(_gameCode)); + } } diff --git a/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.h b/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.h index 44e5f7b..18890d7 100644 --- a/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.h +++ b/arm9/source/romBrowser/FileType/Gba/GbaInternalFileInfo.h @@ -12,4 +12,4 @@ public: private: char _gameCode[5]; -}; \ No newline at end of file +}; diff --git a/arm9/source/romBrowser/FileType/Nds/NdsFileType.h b/arm9/source/romBrowser/FileType/Nds/NdsFileType.h index 96def4d..d3b0e7c 100644 --- a/arm9/source/romBrowser/FileType/Nds/NdsFileType.h +++ b/arm9/source/romBrowser/FileType/Nds/NdsFileType.h @@ -16,8 +16,6 @@ public: return themeFileIconFactory->CreateNdsFileIcon(fileName); } - bool HasInternalFileInfo() const override { return true; } - InternalFileInfo* CreateInternalFileInfo(const FastFileRef& fastFileRef) const override { return new NdsInternalFileInfo(fastFileRef); diff --git a/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.cpp b/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.cpp index 54c01a4..e5eae24 100644 --- a/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.cpp +++ b/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.cpp @@ -1,14 +1,11 @@ #include "common.h" #include -#include #include "fat/File.h" #include "NdsInternalFileInfo.h" NdsInternalFileInfo::NdsInternalFileInfo(const FastFileRef& fastFileRef) { const auto file = std::make_unique(); - memset(_gameCode, 0, sizeof(_gameCode)); - file->Open(fastFileRef, FA_READ); u32 bannerOffset; @@ -18,44 +15,11 @@ NdsInternalFileInfo::NdsInternalFileInfo(const FastFileRef& fastFileRef) !file->ReadExact(&bannerOffset, 4) || bannerOffset == 0 || bannerOffset >= file->GetSize() || - file->Seek(bannerOffset) != FR_OK || - !file->ReadExact(&_banner, 0x840)) + file->Seek(bannerOffset) != FR_OK) { 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)); + _hasBanner = ReadBannerChunks(*file, file->GetSize() - bannerOffset); } -const char16_t* NdsInternalFileInfo::GetGameTitle() const -{ - return _hasBanner - ? _banner.title[NDS_BANNER_TITLE_LANGUAGE_ENGLISH] - : nullptr; -} - -std::unique_ptr NdsInternalFileInfo::CreateGameIcon() const -{ - return _hasBanner - ? std::make_unique(&_banner) - : nullptr; -} diff --git a/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.h b/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.h index 736870a..c98a749 100644 --- a/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.h +++ b/arm9/source/romBrowser/FileType/Nds/NdsInternalFileInfo.h @@ -1,22 +1,13 @@ #pragma once -#include "../InternalFileInfo.h" +#include "../NdsBannerInternalFileInfo.h" #include "ndsBanner.h" -#include "NdsFileIcon.h" #include "fat/FastFileRef.h" /// @brief Internal file info for nds roms. -class alignas(32) NdsInternalFileInfo : public InternalFileInfo +class alignas(32) NdsInternalFileInfo : public NdsBannerInternalFileInfo { public: explicit NdsInternalFileInfo(const FastFileRef& fastFileRef); - constexpr const char* GetGameCode() const override { return _gameCode; } - const char16_t* GetGameTitle() const override; - std::unique_ptr CreateGameIcon() const override; const nds_banner_t& GetBanner() const { return _banner; } - -private: - nds_banner_t _banner alignas(32); - bool _hasBanner = false; - char _gameCode[5]; -}; \ No newline at end of file +}; diff --git a/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.cpp b/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.cpp new file mode 100644 index 0000000..e51daf4 --- /dev/null +++ b/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.cpp @@ -0,0 +1,89 @@ +#include "common.h" +#include +#include +#include "fat/File.h" +#include "Nds/NdsFileIcon.h" +#include "NdsBannerInternalFileInfo.h" + +bool NdsBannerInternalFileInfo::ReadBannerChunks(File& file, u32 availableSize) +{ + memset(&_banner, 0, sizeof(_banner)); + + if (availableSize < 0x240) // Must have at least header + icon + palette (576 bytes) + { + return false; + } + + u32 toRead = availableSize < 0x840 ? availableSize : 0x840; + if (!file.ReadExact(&_banner, toRead)) + { + return false; + } + + // Read Version 2 Chinese Title override (0x100 bytes) + if (_banner.header.version >= NDS_BANNER_VERSION_2 && availableSize >= 0x940 && + !file.ReadExact(((u8*)&_banner) + 0x840, 0x100)) + { + return false; + } + + // Read Version 3 Korean Title override (0x100 bytes) + if (_banner.header.version >= NDS_BANNER_VERSION_3 && availableSize >= 0xA40 && + !file.ReadExact(((u8*)&_banner) + 0x940, 0x100)) + { + return false; + } + + // Read Version 103 (DSi) Animation Frames, Palettes, and Sequence (0x1980 bytes) + if (_banner.header.version >= NDS_BANNER_VERSION_103 && availableSize >= 0x23C0 && + !file.ReadExact(((u8*)&_banner) + 0xA40, 0x1980)) + { + return false; + } + + DC_FlushRange(&_banner, sizeof(_banner)); + return true; +} + +std::unique_ptr NdsBannerInternalFileInfo::CreateGameIcon() const +{ + return _hasBanner + ? std::make_unique(&_banner) + : nullptr; +} + +const char* NdsBannerInternalFileInfo::GetGameCode() const +{ + return _gameCode[0] != 0 ? _gameCode : nullptr; +} + +const char16_t* NdsBannerInternalFileInfo::GetGameTitle() const +{ + if (!_hasBanner) + { + return nullptr; + } + + const char16_t* title = _banner.title[NDS_BANNER_TITLE_LANGUAGE_ENGLISH]; + if (title && title[0] != 0) + { + return title; + } + + title = _banner.title[NDS_BANNER_TITLE_LANGUAGE_JAPANESE]; + if (title && title[0] != 0) + { + return title; + } + + for (int i = NDS_BANNER_TITLE_LANGUAGE_FRENCH; i <= NDS_BANNER_TITLE_LANGUAGE_KOREAN; i++) + { + title = _banner.title[i]; + if (title && title[0] != 0) + { + return title; + } + } + + return nullptr; +} diff --git a/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.h b/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.h new file mode 100644 index 0000000..d2997b6 --- /dev/null +++ b/arm9/source/romBrowser/FileType/NdsBannerInternalFileInfo.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include "InternalFileInfo.h" +#include "Nds/ndsBanner.h" + +class File; + +class alignas(32) NdsBannerInternalFileInfo : public InternalFileInfo +{ +public: + std::unique_ptr CreateGameIcon() const override; + const char* GetGameCode() const override; + const char16_t* GetGameTitle() const override; + bool HasBanner() const { return _hasBanner; } + +protected: + bool _hasBanner = false; + char _gameCode[5] = {}; + nds_banner_t _banner alignas(32); + + /// @brief Reads banner chunks from the current file position. + /// @param file The file to read from. + /// @param availableSize The number of bytes remaining from that position. + /// @return True on success, or false otherwise. + bool ReadBannerChunks(File& file, u32 availableSize); +}; diff --git a/arm9/source/romBrowser/IBannerRepository.h b/arm9/source/romBrowser/IBannerRepository.h new file mode 100644 index 0000000..895474b --- /dev/null +++ b/arm9/source/romBrowser/IBannerRepository.h @@ -0,0 +1,16 @@ +#pragma once + +class FileInfo; +class InternalFileInfo; + +class IBannerRepository +{ +public: + virtual ~IBannerRepository() = default; + + virtual void Initialize() = 0; + virtual InternalFileInfo* GetBannerForFile(const FileInfo& fileInfo, const char* gameCode) const = 0; + +protected: + IBannerRepository() = default; +}; diff --git a/arm9/source/romBrowser/IIconRepository.h b/arm9/source/romBrowser/IIconRepository.h new file mode 100644 index 0000000..85ff9a7 --- /dev/null +++ b/arm9/source/romBrowser/IIconRepository.h @@ -0,0 +1,17 @@ +#pragma once +#include "core/SharedPtr.h" +#include "FileType/BmpFileIconData.h" + +class FileInfo; + +class IIconRepository +{ +public: + virtual ~IIconRepository() = default; + + virtual void Initialize() = 0; + virtual SharedPtr GetIconForFile(const FileInfo& fileInfo, const char* gameCode) const = 0; + +protected: + IIconRepository() = default; +}; diff --git a/arm9/source/romBrowser/IRomBrowserController.h b/arm9/source/romBrowser/IRomBrowserController.h index 4c4f6b6..343d75a 100644 --- a/arm9/source/romBrowser/IRomBrowserController.h +++ b/arm9/source/romBrowser/IRomBrowserController.h @@ -8,6 +8,8 @@ class RomBrowserViewModel; class FileInfo; class TaskQueueBase; class ICoverRepository; +class IIconRepository; +class IBannerRepository; class ICheatRepository; class IRomBrowserController @@ -34,6 +36,8 @@ public: virtual TaskQueueBase* GetIoTaskQueue() const = 0; virtual TaskQueueBase* GetBgTaskQueue() const = 0; virtual const ICoverRepository& GetCoverRepository() const = 0; + virtual const IIconRepository& GetIconRepository() const = 0; + virtual const IBannerRepository& GetBannerRepository() const = 0; virtual const ICheatRepository& GetCheatRepository() const = 0; virtual const RomBrowserDisplaySettings& GetRomBrowserDisplaySettings() const = 0; diff --git a/arm9/source/romBrowser/IconRepository.cpp b/arm9/source/romBrowser/IconRepository.cpp new file mode 100644 index 0000000..e8d0d56 --- /dev/null +++ b/arm9/source/romBrowser/IconRepository.cpp @@ -0,0 +1,65 @@ +#include "common.h" +#include +#include "core/mini-printf.h" +#include "fat/Directory.h" +#include "FileType/NullFileTypeProvider.h" +#include "FileType/BmpFileIconData.h" +#include "SdFolderFactory.h" +#include "IconRepository.h" + +void IconRepository::Initialize() +{ + InitializeFolders("/_pico/icons/"); +} + +SharedPtr IconRepository::GetIconForFile(const FileInfo& fileInfo, const char* gameCode) const +{ + char nameBuffer[256]; + const auto& fileType = fileInfo.GetFileType(); + + if (fileType->GetClassification() == FileTypeClassification::Folder) + { + // Look for icon.bmp inside the folder (path relative to FatFs CWD = current browse dir). + // Scan with the already-open directory handle so the match can be turned directly into a + // FastFileRef, instead of stat'ing then re-opening the same path by name. + Directory folderDir; + if (folderDir.Open(fileInfo.GetFileName()) == FR_OK) + { + FILINFO folderFileInfo; + while (folderDir.Read(&folderFileInfo) == FR_OK && folderFileInfo.fname[0] != 0) + { + if (!(folderFileInfo.fattrib & AM_DIR) && !strcasecmp(folderFileInfo.fname, "icon.bmp")) + { + return SharedPtr::MakeShared( + FastFileRef(folderDir.GetFatFsDirectory(), &folderFileInfo)); + } + } + } + + return nullptr; + } + + const FileInfo* iconFile = nullptr; + + // Try to get an icon based on the filename in the user folder + if (_userFolder) + { + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bmp", fileInfo.GetFileName()); + iconFile = _userFolder->BinarySearch(nameBuffer); + } + + // Try to get an icon based on an internal game code + if (!iconFile && gameCode) + { + const auto* iconFolder = GetFileTypeFolder(fileType->GetShortName()); + if (iconFolder) + { + mini_snprintf(nameBuffer, sizeof(nameBuffer), "%s.bmp", gameCode); + iconFile = iconFolder->BinarySearch(nameBuffer); + } + } + + return iconFile + ? SharedPtr::MakeShared(iconFile->GetFastFileRef()) + : nullptr; +} diff --git a/arm9/source/romBrowser/IconRepository.h b/arm9/source/romBrowser/IconRepository.h new file mode 100644 index 0000000..cc1691e --- /dev/null +++ b/arm9/source/romBrowser/IconRepository.h @@ -0,0 +1,10 @@ +#pragma once +#include "IIconRepository.h" +#include "RepositoryBase.h" + +class IconRepository : public RepositoryBase, public IIconRepository +{ +public: + void Initialize() override; + SharedPtr GetIconForFile(const FileInfo& fileInfo, const char* gameCode) const override; +}; diff --git a/arm9/source/romBrowser/RepositoryBase.cpp b/arm9/source/romBrowser/RepositoryBase.cpp new file mode 100644 index 0000000..ce93597 --- /dev/null +++ b/arm9/source/romBrowser/RepositoryBase.cpp @@ -0,0 +1,49 @@ +#include "common.h" +#include +#include "core/StringUtil.h" +#include "FileType/NullFileTypeProvider.h" +#include "SdFolderFactory.h" +#include "RepositoryBase.h" + +void RepositoryBase::InitializeFolders(const char* basePath) +{ + NullFileTypeProvider fileTypeProvider; + SdFolderFactory folderFactory(&fileTypeProvider); + char path[64]; + + u32 len = StringUtil::Copy(path, basePath, sizeof(path)); + + StringUtil::Copy(path + len, "nds", sizeof(path) - len); + _ndsFolder = folderFactory.CreateFromPath(path); + if (_ndsFolder) + { + _ndsFolder->SortByNameInPlace(); + } + + StringUtil::Copy(path + len, "gba", sizeof(path) - len); + _gbaFolder = folderFactory.CreateFromPath(path); + if (_gbaFolder) + { + _gbaFolder->SortByNameInPlace(); + } + + StringUtil::Copy(path + len, "user", sizeof(path) - len); + _userFolder = folderFactory.CreateFromPath(path); + if (_userFolder) + { + _userFolder->SortByNameInPlace(); + } +} + +const SdFolder* RepositoryBase::GetFileTypeFolder(const char* shortName) const +{ + if (!strcmp(shortName, "nds")) + { + return _ndsFolder.get(); + } + if (!strcmp(shortName, "gba")) + { + return _gbaFolder.get(); + } + return nullptr; +} diff --git a/arm9/source/romBrowser/RepositoryBase.h b/arm9/source/romBrowser/RepositoryBase.h new file mode 100644 index 0000000..33caa47 --- /dev/null +++ b/arm9/source/romBrowser/RepositoryBase.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include "SdFolder.h" + +class RepositoryBase +{ +protected: + std::unique_ptr _ndsFolder; + std::unique_ptr _gbaFolder; + std::unique_ptr _userFolder; + + /// @brief Scans directories and populates the folder cache under the given base path. + /// @param basePath The base path of the folders. + void InitializeFolders(const char* basePath); + + /// @brief Gets the cached folder corresponding to the file type suffix name. + /// @param shortName Suffix folder name (e.g. "nds" or "gba"). + /// @return A pointer to the cached \see SdFolder, or \c nullptr otherwise. + const SdFolder* GetFileTypeFolder(const char* shortName) const; +}; diff --git a/arm9/source/romBrowser/RomBrowserController.cpp b/arm9/source/romBrowser/RomBrowserController.cpp index 234899a..1a4eabc 100644 --- a/arm9/source/romBrowser/RomBrowserController.cpp +++ b/arm9/source/romBrowser/RomBrowserController.cpp @@ -143,6 +143,16 @@ void RomBrowserController::HandleNavigateTrigger() _coverRepository = std::make_unique(); _coverRepository->Initialize(); } + if (!_iconRepository) + { + _iconRepository = std::make_unique(); + _iconRepository->Initialize(); + } + if (!_bannerRepository) + { + _bannerRepository = std::make_unique(); + _bannerRepository->Initialize(); + } if (!_cheatRepository) { _cheatRepository = UsrCheatRepositoryFactory().FromUsrCheatDat("/_pico/usrcheat.dat"); diff --git a/arm9/source/romBrowser/RomBrowserController.h b/arm9/source/romBrowser/RomBrowserController.h index 3b2ba4f..f8ddc7b 100644 --- a/arm9/source/romBrowser/RomBrowserController.h +++ b/arm9/source/romBrowser/RomBrowserController.h @@ -7,6 +7,8 @@ #include "core/task/TaskQueue.h" #include "IRomBrowserController.h" #include "CoverRepository.h" +#include "IconRepository.h" +#include "BannerRepository.h" #include "FileType/ExtensionFileTypeProvider.h" #include "services/settings/IAppSettingsService.h" #include "cheats/ICheatRepository.h" @@ -40,6 +42,8 @@ public: TaskQueueBase* GetIoTaskQueue() const override { return _ioTaskQueue; } TaskQueueBase* GetBgTaskQueue() const override { return _bgTaskQueue; } const ICoverRepository& GetCoverRepository() const override { return *_coverRepository; } + const IIconRepository& GetIconRepository() const override { return *_iconRepository; } + const IBannerRepository& GetBannerRepository() const override { return *_bannerRepository; } const ICheatRepository& GetCheatRepository() const override { return *_cheatRepository; } void SetRomBrowserDisplaySettings(const RomBrowserDisplaySettings& romBrowserDisplaySettings) override; @@ -66,6 +70,8 @@ private: QueueTask _navigateTask; bool _saveSettingsPending = false; std::unique_ptr _coverRepository; + std::unique_ptr _iconRepository; + std::unique_ptr _bannerRepository; ExtensionFileTypeProvider _fileTypeProvider; std::unique_ptr _cheatRepository; diff --git a/arm9/source/romBrowser/viewModels/RomBrowserViewModel.cpp b/arm9/source/romBrowser/viewModels/RomBrowserViewModel.cpp index 37f91c0..8784931 100644 --- a/arm9/source/romBrowser/viewModels/RomBrowserViewModel.cpp +++ b/arm9/source/romBrowser/viewModels/RomBrowserViewModel.cpp @@ -36,7 +36,8 @@ RomBrowserViewModel::RomBrowserViewModel(IRomBrowserController* romBrowserContro u64 endTick = gTickCounter.GetValue(); LOG_DEBUG("Filter + sort took: %d us\n", (u32)TickCounter::TicksToMicroSeconds(endTick - startTick)); _fileInfoManager = std::make_unique(std::move(sortedFilteredFiles), - filteredCount, _romBrowserController->GetCoverRepository()); + filteredCount, _romBrowserController->GetCoverRepository(), _romBrowserController->GetIconRepository(), + _romBrowserController->GetBannerRepository()); _selectedItem = _fileInfoManager->GetItemIndex(initialSelectedFileName); } diff --git a/arm9/source/romBrowser/views/RomBrowserTopScreenView.cpp b/arm9/source/romBrowser/views/RomBrowserTopScreenView.cpp index 2563e04..17280d1 100644 --- a/arm9/source/romBrowser/views/RomBrowserTopScreenView.cpp +++ b/arm9/source/romBrowser/views/RomBrowserTopScreenView.cpp @@ -44,64 +44,48 @@ void RomBrowserTopScreenView::InitVram(const VramContext& vramContext) void RomBrowserTopScreenView::Update() { int selectedItem = _viewModel->GetSelectedItem(); - if (selectedItem != _lastSelectedItem) + if (selectedItem != _lastSelectedItem && selectedItem >= 0) { auto& fileInfoManager = _viewModel->GetFileInfoManager(); - const auto& item = fileInfoManager.GetItem(selectedItem); - if (item.GetFileType()->HasInternalFileInfo()) + // GetInternalFileInfo() covers both game banners and custom icon overrides (the + // latter apply to any file type, including folders), so check it directly instead + // of branching on FileType::HasInternalFileInfo() - that's a static per-type property + // and knows nothing about a per-item custom icon. IsFileInfoLoaded() distinguishes + // "still loading" from "loaded, and there's legitimately nothing" so this waits for + // the io thread instead of flashing the previous item's icon while undecided. + if (fileInfoManager.IsFileInfoLoaded(selectedItem)) { + const auto& item = fileInfoManager.GetItem(selectedItem); auto info = fileInfoManager.GetInternalFileInfo(selectedItem); - if (info) + + bool fileNameAsTitle = true; + const char16_t* gameTitle = info ? info->GetGameTitle() : nullptr; + if (gameTitle && gameTitle[0] != 0) { - bool fileNameAsTitle = true; - const char16_t* gameTitle = info->GetGameTitle(); - if (gameTitle) - { - _fileInfoView->SetGameTitleAsync(_viewModel->GetBgTaskQueue(), gameTitle); - fileNameAsTitle = false; - } - - _selectedFileIcon = info->CreateGameIcon(); - if (!_selectedFileIcon) - { - _selectedFileIcon = item.GetFileType()->CreateFileIcon("", _themeFileIconFactory); - } - if (_selectedFileIcon) - { - _selectedFileIcon->SetAnimFrame(_viewModel->GetIconFrameCounter()); - _iconGraphicsUploaded = false; - } - _fileInfoView->SetIcon(std::move(_selectedFileIcon)); - _fileInfoView->SetFileNameAsync(_viewModel->GetBgTaskQueue(), item.GetFileName(), fileNameAsTitle); - - _lastSelectedItem = selectedItem; - - auto cover = fileInfoManager.GetFileCover(selectedItem); - if (cover.IsValid()) - { - _selectedFileCover = std::move(cover); - _coverGraphicsUploaded = false; - } + _fileInfoView->SetGameTitleAsync(_viewModel->GetBgTaskQueue(), gameTitle); + fileNameAsTitle = false; } - } - else - { + + _selectedFileIcon = info ? info->CreateGameIcon() : nullptr; + if (!_selectedFileIcon) + { + _selectedFileIcon = item.GetFileType()->CreateFileIcon("", _themeFileIconFactory); + } + if (_selectedFileIcon) + { + _selectedFileIcon->SetAnimFrame(_viewModel->GetIconFrameCounter()); + _iconGraphicsUploaded = false; + } + _fileInfoView->SetIcon(std::move(_selectedFileIcon)); + _fileInfoView->SetFileNameAsync(_viewModel->GetBgTaskQueue(), item.GetFileName(), fileNameAsTitle); + + _lastSelectedItem = selectedItem; + auto cover = fileInfoManager.GetFileCover(selectedItem); if (cover.IsValid()) { _selectedFileCover = std::move(cover); _coverGraphicsUploaded = false; - - _selectedFileIcon = item.GetFileType()->CreateFileIcon("", _themeFileIconFactory); - if (_selectedFileIcon) - { - _selectedFileIcon->SetAnimFrame(_viewModel->GetIconFrameCounter()); - _iconGraphicsUploaded = false; - } - _fileInfoView->SetIcon(std::move(_selectedFileIcon)); - _fileInfoView->SetFileNameAsync(_viewModel->GetBgTaskQueue(), item.GetFileName(), true); - - _lastSelectedItem = selectedItem; } } } diff --git a/docs/Customization.md b/docs/Customization.md new file mode 100644 index 0000000..41dd29b --- /dev/null +++ b/docs/Customization.md @@ -0,0 +1,53 @@ +# Customization +Pico Launcher supports custom icons and banners for files and folders by placing files in the right locations on the SD card. Icons and banners follow the same folder structure: nds and gba files by game code, user files by filename, and folder-level overrides. + +## Icons +Custom icons are `.bmp` files. DS roms already show the icon from their internal banner, so custom icons are mainly useful for GBA roms and other file types. + +### Icons by game code +Place a `.bmp` file under `/_pico/icons/nds/` or `/_pico/icons/gba/`. For example: +- `/_pico/icons/gba/ABCD.bmp` - icon for the GBA game with game code `ABCD` +- `/_pico/icons/nds/EFGH.bmp` - icon for the NDS game with game code `EFGH` (overrides the built-in banner icon) + +### User icons (by filename) +Place a `.bmp` file under `/_pico/icons/user/`. For example, `myGame.gba.bmp` will be used for any file named `myGame.gba`. This takes precedence over a game code based icon and over the internal banner icon of DS roms. + +### Folder icons +Place a file named `icon.bmp` directly inside the folder you want to customise. For example: +- `/GBA Games/icon.bmp` - icon for the `GBA Games` folder +- `/GBA Games/Racing/icon.bmp` - icon for the `Racing` subfolder + +Each folder's `icon.bmp` is independent - two folders with the same name can have different icons. Since `.bmp` files are not a recognised ROM type, `icon.bmp` is never shown in the rom browser. + +A folder can also use a `banner.bnr` file (see [Folder banners](#folder-banners)) for its icon, in the same location. If both `banner.bnr` and `icon.bmp` exist, `banner.bnr` takes priority. + +### Icon priority +User icon (`/_pico/icons/user/`) > game code based icon (`/_pico/icons/nds/` or `/_pico/icons/gba/`) > internal banner > theme default. +Folder icons are looked up independently and do not participate in the above priority chain: `banner.bnr` > `icon.bmp` > theme default. + +### Icon format +Icons must be **32×32 pixels, 4 bpp (16 colors), uncompressed `.bmp`** files, with the first palette color treated as transparent. + +## Banners +Custom banners are `.bnr` files containing custom game titles, subtitles and (animated) icons. This is especially useful for adding custom titles and (animated) icons to GBA ROMs or other systems. + +### Banners by game code +Place a `.bnr` file under `/_pico/banners/nds/` or `/_pico/banners/gba/`. For example: +- `/_pico/banners/gba/ABCD.bnr` - banner for the GBA game with game code `ABCD` +- `/_pico/banners/nds/EFGH.bnr` - banner for the NDS game with game code `EFGH` (overrides the built-in banner) + +### User banners (by filename) +Place a `.bnr` file under `/_pico/banners/user/`. For example, `myGame.gba.bnr` will be used for any file named `myGame.gba`. + +### Folder banners +Place a file named `banner.bnr` directly inside the folder you want to customise. For example: +- `/GBA Games/banner.bnr` - custom banner for the `GBA Games` folder + +This gives the folder a custom icon (static or animated) and replaces its displayed name with the banner's title, anywhere the folder's name is shown. The folder's actual name on the SD card (used for navigation) is unchanged. Folder banners do not affect the folder's cover image. + +### Banner priority +User banner (`/_pico/banners/user/`) > game code based banner (`/_pico/banners/nds/` or `/_pico/banners/gba/`) > built-in ROM banner. +Folder banners (`banner.bnr`) are looked up independently and do not participate in the above priority chain. + +### Banner format +Banners must be standard Nintendo DS(i) banner files (usually named `.bnr` or `banner.bin`). Both static icons and animated DSi-style icons are supported.