Add support for custom BMP icons and custom NDS banners for games and folders. Closes #53 and Closes #62

This commit is contained in:
Augusto Daniele
2026-07-05 11:27:48 -03:00
committed by GitHub
parent a845f03686
commit f90d0a1aae
40 changed files with 841 additions and 187 deletions

View File

@@ -0,0 +1,80 @@
#include "common.h"
#include <string.h>
#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;
}

View File

@@ -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;
};

View File

@@ -1,6 +1,5 @@
#include "common.h"
#include <string.h>
#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;
}
}

View File

@@ -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<SdFolder> _ndsCoversFolder;
std::unique_ptr<SdFolder> _gbaCoversFolder;
std::unique_ptr<SdFolder> _userCoversFolder;
const SdFolder* GetCoverFolder(const char* coverFolderName) const;
};

View File

@@ -35,7 +35,7 @@ TaskResult<void> BannerListFileRecyclerAdapter::BindView(SharedPtr<View> view, i
if (internalFileInfo)
{
const char16_t* gameTitle = internalFileInfo->GetGameTitle();
if (gameTitle)
if (gameTitle && gameTitle[0] != 0)
{
listItemView->SetGameTitle(gameTitle);
fileNameAsTitle = false;

View File

@@ -1,11 +1,15 @@
#include "common.h"
#include <string.h>
#include "FileType/CustomIconInternalFileInfo.h"
#include "FileInfoManager.h"
FileInfoManager::FileInfoManager(std::unique_ptr<const FileInfo*[]> items, u32 itemCount, const ICoverRepository& coverRepository)
FileInfoManager::FileInfoManager(std::unique_ptr<const FileInfo*[]> items, u32 itemCount, const ICoverRepository& coverRepository,
const IIconRepository& iconRepository, const IBannerRepository& bannerRepository)
: _items(std::move(items)), _itemCount(itemCount)
, _extraFileInfo(std::make_unique<ExtraFileInfo[]>(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<const InternalFileInfo>(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;
}
}

View File

@@ -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<const FileInfo*[]> items, u32 itemCount, const ICoverRepository& coverRepository);
FileInfoManager(std::unique_ptr<const FileInfo*[]> 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<FileCover> 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> fileCover;
};
@@ -43,4 +54,6 @@ private:
u32 _itemCount;
std::unique_ptr<ExtraFileInfo[]> _extraFileInfo;
const ICoverRepository& _coverRepository;
};
const IIconRepository& _iconRepository;
const IBannerRepository& _bannerRepository;
};

View File

@@ -5,6 +5,7 @@
#include <libtwl/dma/dmaNitro.h>
#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>();
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();

View File

@@ -0,0 +1,35 @@
#include "common.h"
#include <libtwl/dma/dmaNitro.h>
#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]);
}

View File

@@ -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<BmpFileIconData> iconData)
: _iconData(std::move(iconData)) { }
void UploadGraphics() override;
void Draw(GraphicsContext& graphicsContext, const Rgb<8, 8, 8>& backgroundColor) override;
private:
SharedPtr<BmpFileIconData> _iconData;
};

View File

@@ -0,0 +1,83 @@
#include "common.h"
#include <memory>
#include <string.h>
#include <nds/arm9/cache.h>
#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>();
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> 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<u8[]>(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);
}
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <memory>
#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> file);
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdlib>
#include <nds/ndstypes.h>
/// @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;
}
};

View File

@@ -0,0 +1,19 @@
#include "common.h"
#include <string.h>
#include <memory>
#include "fat/File.h"
#include "BnrInternalFileInfo.h"
BnrInternalFileInfo::BnrInternalFileInfo(const FastFileRef& bnrFileRef, const char* gameCode)
{
auto file = std::make_unique<File>();
file->Open(bnrFileRef, FA_READ);
if (gameCode)
{
strncpy(_gameCode, gameCode, 4);
}
_hasBanner = ReadBannerChunks(*file, file->GetSize());
}

View File

@@ -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);
};

View File

@@ -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

View File

@@ -0,0 +1,29 @@
#pragma once
#include <memory>
#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<BmpFileIconData> iconData, std::unique_ptr<const InternalFileInfo> 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<FileIcon> CreateGameIcon() const override
{
return std::make_unique<BmpFileIcon>(_iconData);
}
private:
SharedPtr<BmpFileIconData> _iconData;
std::unique_ptr<const InternalFileInfo> _wrapped;
};

View File

@@ -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.

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -12,4 +12,4 @@ public:
private:
char _gameCode[5];
};
};

View File

@@ -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);

View File

@@ -1,14 +1,11 @@
#include "common.h"
#include <string.h>
#include <nds/arm9/cache.h>
#include "fat/File.h"
#include "NdsInternalFileInfo.h"
NdsInternalFileInfo::NdsInternalFileInfo(const FastFileRef& fastFileRef)
{
const auto file = std::make_unique<File>();
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<FileIcon> NdsInternalFileInfo::CreateGameIcon() const
{
return _hasBanner
? std::make_unique<NdsFileIcon>(&_banner)
: nullptr;
}

View File

@@ -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<FileIcon> CreateGameIcon() const override;
const nds_banner_t& GetBanner() const { return _banner; }
private:
nds_banner_t _banner alignas(32);
bool _hasBanner = false;
char _gameCode[5];
};
};

View File

@@ -0,0 +1,89 @@
#include "common.h"
#include <string.h>
#include <nds/arm9/cache.h>
#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<FileIcon> NdsBannerInternalFileInfo::CreateGameIcon() const
{
return _hasBanner
? std::make_unique<NdsFileIcon>(&_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;
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <memory>
#include "InternalFileInfo.h"
#include "Nds/ndsBanner.h"
class File;
class alignas(32) NdsBannerInternalFileInfo : public InternalFileInfo
{
public:
std::unique_ptr<FileIcon> 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);
};

View File

@@ -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;
};

View File

@@ -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<BmpFileIconData> GetIconForFile(const FileInfo& fileInfo, const char* gameCode) const = 0;
protected:
IIconRepository() = default;
};

View File

@@ -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;

View File

@@ -0,0 +1,65 @@
#include "common.h"
#include <string.h>
#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<BmpFileIconData> 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<BmpFileIconData>::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<BmpFileIconData>::MakeShared(iconFile->GetFastFileRef())
: nullptr;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "IIconRepository.h"
#include "RepositoryBase.h"
class IconRepository : public RepositoryBase, public IIconRepository
{
public:
void Initialize() override;
SharedPtr<BmpFileIconData> GetIconForFile(const FileInfo& fileInfo, const char* gameCode) const override;
};

View File

@@ -0,0 +1,49 @@
#include "common.h"
#include <string.h>
#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;
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <memory>
#include "SdFolder.h"
class RepositoryBase
{
protected:
std::unique_ptr<SdFolder> _ndsFolder;
std::unique_ptr<SdFolder> _gbaFolder;
std::unique_ptr<SdFolder> _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;
};

View File

@@ -143,6 +143,16 @@ void RomBrowserController::HandleNavigateTrigger()
_coverRepository = std::make_unique<CoverRepository>();
_coverRepository->Initialize();
}
if (!_iconRepository)
{
_iconRepository = std::make_unique<IconRepository>();
_iconRepository->Initialize();
}
if (!_bannerRepository)
{
_bannerRepository = std::make_unique<BannerRepository>();
_bannerRepository->Initialize();
}
if (!_cheatRepository)
{
_cheatRepository = UsrCheatRepositoryFactory().FromUsrCheatDat("/_pico/usrcheat.dat");

View File

@@ -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<void> _navigateTask;
bool _saveSettingsPending = false;
std::unique_ptr<CoverRepository> _coverRepository;
std::unique_ptr<IconRepository> _iconRepository;
std::unique_ptr<BannerRepository> _bannerRepository;
ExtensionFileTypeProvider _fileTypeProvider;
std::unique_ptr<ICheatRepository> _cheatRepository;

View File

@@ -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<FileInfoManager>(std::move(sortedFilteredFiles),
filteredCount, _romBrowserController->GetCoverRepository());
filteredCount, _romBrowserController->GetCoverRepository(), _romBrowserController->GetIconRepository(),
_romBrowserController->GetBannerRepository());
_selectedItem = _fileInfoManager->GetItemIndex(initialSelectedFileName);
}

View File

@@ -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;
}
}
}