Added support for nand saving in Face Training and Nintendo DS Guide (fixes #6), calculate nand save size from header, fixed Band Brothers save initialization

This commit is contained in:
Gericom
2025-12-28 17:34:06 +01:00
parent 6d12399ba4
commit 9f6311014d
10 changed files with 216 additions and 41 deletions

View File

@@ -4,34 +4,51 @@
#include "SaveList.h"
#include "SaveListFactory.h"
#include "fileInfo.h"
#include "gameCode.h"
#include "CardSaveArranger.h"
#define SAVE_LIST_PATH "/_pico/savelist.bin"
#define DEFAULT_SAVE_SIZE (512 * 1024)
#define SAVE_FILL_VALUE 0xFF
#define NTR_NAND_BLOCK_SIZE 0x20000
#define TWL_NAND_BLOCK_SIZE 0x80000
#define NAND_RW_REGION_END 0x07A00000
static const u8 sNandSaveId[16] = { 0xEC, 0x00, 0x9E, 0xA1, 0x51, 0x65, 0x34, 0x35, 0x30, 0x35, 0x30, 0x31, 0x19, 0x19, 0x02, 0x0A };
static const u8 sBandBrothersSaveId[12] = { 0x48, 0x8A, 0x00, 0x00, 0x42, 0x42, 0x44, 0x58, 0x31, 0x32, 0x33, 0x34 };
static const u8 sJamWithTheBandSaveId[16] = { 0xEC, 0x00, 0x9E, 0xA1, 0x51, 0x65, 0x34, 0x35, 0x30, 0x35, 0x30, 0x31, 0x19, 0x19, 0x02, 0x0A };
bool CardSaveArranger::SetupCardSave(u32 gameCode, const TCHAR* savePath) const
bool CardSaveArranger::SetupCardSave(const nds_header_ntr_t* header, const TCHAR* savePath) const
{
SaveList* saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
u32 saveSize = DEFAULT_SAVE_SIZE;
auto saveType = CardSaveType::None;
if (saveList)
u32 saveSize = DEFAULT_SAVE_SIZE;
if (header->nandBackupRegionStart != 0)
{
const auto saveListEntry = saveList->FindEntry(gameCode);
if (!saveListEntry)
saveType = CardSaveType::Nand;
u32 blockSize = header->IsTwlRom() ? TWL_NAND_BLOCK_SIZE : NTR_NAND_BLOCK_SIZE;
u32 nandBackupRegionStart = header->nandBackupRegionStart * blockSize;
saveSize = NAND_RW_REGION_END - nandBackupRegionStart;
LOG_DEBUG("NAND save. Size: 0x%X.", saveSize);
}
else
{
SaveList* saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
if (saveList)
{
LOG_WARNING("Game code %c%c%c%c not found in save list\n",
gameCode & 0xFF, (gameCode >> 8) & 0xFF, (gameCode >> 16) & 0xFF, gameCode >> 24);
const auto saveListEntry = saveList->FindEntry(header->gameCode);
if (!saveListEntry)
{
LOG_WARNING("Game code %c%c%c%c not found in save list\n",
header->gameCode & 0xFF, (header->gameCode >> 8) & 0xFF,
(header->gameCode >> 16) & 0xFF, header->gameCode >> 24);
}
else
{
saveType = saveListEntry->GetSaveType();
saveSize = saveListEntry->GetSaveSize();
saveListEntry->Dump();
}
delete saveList;
}
else
{
saveType = saveListEntry->GetSaveType();
saveSize = saveListEntry->GetSaveSize();
saveListEntry->Dump();
}
delete saveList;
}
if (saveSize == 0)
{
@@ -88,17 +105,29 @@ bool CardSaveArranger::SetupCardSave(u32 gameCode, const TCHAR* savePath) const
offset += bytesToWrite;
}
if (saveType == CardSaveType::Nand)
// Additional save initialization
if (header->gameCode == GAMECODE("AXBJ"))
{
// NAND save games have a read-only NAND save id at the end of their save area.
// Jam with the Band checks this id and errors if it cannot find it.
// See https://github.com/melonDS-emu/melonDS/blob/3a3388c4c50e8735af125c1af4d89e457f5e9035/src/NDSCart.cpp#L1067
// Write save id for Band Brothers
UINT bytesWritten = 0;
if (f_lseek(file.get(), 0) != FR_OK ||
f_write(file.get(), sBandBrothersSaveId, sizeof(sBandBrothersSaveId), &bytesWritten) != FR_OK ||
bytesWritten != sizeof(sBandBrothersSaveId))
{
LOG_FATAL("Failed to write Band Brothers save id\n");
return false;
}
}
else if (header->gameCode == GAMECODE("UXBP"))
{
// Write save id for Jam with the Band
// See also https://github.com/melonDS-emu/melonDS/blob/3a3388c4c50e8735af125c1af4d89e457f5e9035/src/NDSCart.cpp#L1067
UINT bytesWritten = 0;
if (f_lseek(file.get(), saveSize - 0x800) != FR_OK ||
f_write(file.get(), sNandSaveId, sizeof(sNandSaveId), &bytesWritten) != FR_OK ||
bytesWritten != sizeof(sNandSaveId))
f_write(file.get(), sJamWithTheBandSaveId, sizeof(sJamWithTheBandSaveId), &bytesWritten) != FR_OK ||
bytesWritten != sizeof(sJamWithTheBandSaveId))
{
LOG_FATAL("Failed to write NAND save id\n");
LOG_FATAL("Failed to write Jam with the Band save id\n");
return false;
}
}

View File

@@ -1,12 +1,13 @@
#pragma once
#include "ndsHeader.h"
/// @brief Class for setting up the save file for retail card roms.
class CardSaveArranger
{
public:
/// @brief Sets up the save file at \p savePath for a retail card rom with the given \p gameCode.
/// @param gameCode The game code of the retail card rom.
/// @param header The header of the retail card rom.
/// @param savePath The desired save file path.
/// @return \c true when setting up the save was successful, or \c false otherwise.
bool SetupCardSave(u32 gameCode, const TCHAR* savePath) const;
bool SetupCardSave(const nds_header_ntr_t* header, const TCHAR* savePath) const;
};

View File

@@ -205,7 +205,11 @@ void NdsLoader::Load(BootMode bootMode)
{
if (bootMode != BootMode::SdkResetSystem)
{
HandleCardSave();
if (!CardSaveArranger().SetupCardSave(&_romHeader, _savePath))
{
ErrorDisplay().PrintError("Failed to setup save file.");
return;
}
}
HandleAntiPiracy();
@@ -608,14 +612,6 @@ bool NdsLoader::TryLoadRomHeader(u32 romOffset)
return true;
}
void NdsLoader::HandleCardSave()
{
if (!CardSaveArranger().SetupCardSave(_romHeader.gameCode, _savePath))
{
ErrorDisplay().PrintError("Failed to setup save file.");
}
}
void NdsLoader::HandleAntiPiracy()
{
auto apList = ApListFactory().CreateFromFile(AP_LIST_PATH);

View File

@@ -56,7 +56,6 @@ private:
void ClearMainMemory();
void CreateRomClusterTable();
bool TryLoadRomHeader(u32 romOffset);
void HandleCardSave();
void HandleAntiPiracy();
void RemapWram();
bool TryLoadArm9();

View File

@@ -13,7 +13,9 @@
#include "patches/arm9/OSResetSystemPatch.h"
#include "patches/arm9/PokemonDownloaderArm9Patch.h"
#include "patches/arm9/DSProtectArm9Patch.h"
#include "patches/arm9/NandSave/FaceTrainingNandSavePatch.h"
#include "patches/arm9/NandSave/JamWithTheBandNandSavePatch.h"
#include "patches/arm9/NandSave/NintendoDSGuideNandSavePatch.h"
#include "patches/arm9/NandSave/WarioWareDiyNandSavePatch.h"
#include "patches/arm9/OverlayPatches/FsStartOverlayHookPatch.h"
#include "patches/arm9/OverlayPatches/DSProtectPatches/DSProtectOverlayPatch.h"
@@ -390,6 +392,18 @@ void Arm9Patcher::AddGameSpecificPatches(
patchCollection.AddPatch(new JamWithTheBandNandSavePatch());
break;
}
// Face Training
case GAMECODE("USKV"):
{
patchCollection.AddPatch(new FaceTrainingNandSavePatch());
break;
}
// Nintendo DS Guide
case GAMECODE("UGDA"):
{
patchCollection.AddPatch(new NintendoDSGuideNandSavePatch());
break;
}
}
}

View File

@@ -0,0 +1,57 @@
#include "common.h"
#include "gameCode.h"
#include "patches/platform/LoaderPlatform.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "ReadNandSaveAsm.h"
#include "WriteNandSaveAsm.h"
#include "FaceTrainingNandSavePatch.h"
// This code was based on nds-bootstrap:
// https://github.com/DS-Homebrew/nds-bootstrap/blob/89f27d1392a68436695d0050992ee84258ef41bc/retail/bootloader/source/arm7/patch_arm9.c#L2531
bool FaceTrainingNandSavePatch::FindPatchTarget(PatchContext& patchContext)
{
return true;
}
void FaceTrainingNandSavePatch::ApplyPatch(PatchContext& patchContext)
{
auto sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
(
patchContext.GetPatchHeap(),
SHARED_SAVE_FILE_INFO
);
auto loaderPlatform = patchContext.GetLoaderPlatform();
auto readNandSavePatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<ReadNandSavePatchCode>
(
patchContext.GetPatchHeap(),
sectorRemapPatchCode,
loaderPlatform->CreateSdReadPatchCode(patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap())
);
auto writeNandSavePatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<WriteNandSavePatchCode>
(
patchContext.GetPatchHeap(),
sectorRemapPatchCode,
loaderPlatform->CreateSdWritePatchCode(patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap())
);
// u32 nandInit(void* data)
*(u32*)0x020E2AEC = 0xE3A00001; // mov r0, #1
*(u32*)0x020E2AF0 = 0xE12FFF1E; // bx lr
// u32 nandResume(void)
*(u32*)0x020E2EC0 = 0xE3A00000; // mov r0, #0
*(u32*)0x020E2EC4 = 0xE12FFF1E; // bx lr
// u32 nandError(void)
*(u32*)0x020E3150 = 0xE3A00000; // mov r0, #0
*(u32*)0x020E3154 = 0xE12FFF1E; // bx lr
// u32 nandWrite(const void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x020E2BF0 = 0xE51FF004; // ldr pc,= patch_writeNandSave
*(u32*)0x020E2BF4 = (u32)writeNandSavePatchCode->GetWriteNandSaveFunction();
// u32 nandRead(void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x020E2F3C = 0xE51FF004; // ldr pc,= patch_readNandSave
*(u32*)0x020E2F40 = (u32)readNandSavePatchCode->GetReadNandSaveFunction();
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "patches/Patch.h"
class FunctionSignature;
/// @brief Arm9 patch to redirect Face Training nand saving to the SD card.
class FaceTrainingNandSavePatch : public Patch
{
public:
bool FindPatchTarget(PatchContext& patchContext) override;
void ApplyPatch(PatchContext& patchContext) override;
};

View File

@@ -0,0 +1,61 @@
#include "common.h"
#include "gameCode.h"
#include "patches/platform/LoaderPlatform.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "ReadNandSaveAsm.h"
#include "WriteNandSaveAsm.h"
#include "NintendoDSGuideNandSavePatch.h"
// This code was based on nds-bootstrap:
// https://github.com/DS-Homebrew/nds-bootstrap/blob/89f27d1392a68436695d0050992ee84258ef41bc/retail/bootloader/source/arm7/patch_arm9.c#L2531
bool NintendoDSGuideNandSavePatch::FindPatchTarget(PatchContext& patchContext)
{
return true;
}
void NintendoDSGuideNandSavePatch::ApplyPatch(PatchContext& patchContext)
{
auto sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
(
patchContext.GetPatchHeap(),
SHARED_SAVE_FILE_INFO
);
auto loaderPlatform = patchContext.GetLoaderPlatform();
auto readNandSavePatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<ReadNandSavePatchCode>
(
patchContext.GetPatchHeap(),
sectorRemapPatchCode,
loaderPlatform->CreateSdReadPatchCode(patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap())
);
auto writeNandSavePatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<WriteNandSavePatchCode>
(
patchContext.GetPatchHeap(),
sectorRemapPatchCode,
loaderPlatform->CreateSdWritePatchCode(patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap())
);
// u32 nandInit(void* data)
*(u32*)0x02009298 = 0xE3A00001; // mov r0, #1
*(u32*)0x0200929C = 0xE12FFF1E; // bx lr
// u32 nandResume(void)
*(u32*)0x020098C8 = 0xE3A00000; // mov r0, #0
*(u32*)0x020098CC = 0xE12FFF1E; // bx lr
// u32 nandState(void)
*(u32*)0x02009AA8 = 0xE1A00000; //nop
*(u32*)0x02009AB0 = 0x06600000;
// u32 nandError(void)
*(u32*)0x02009AB4 = 0xE3A00000; // mov r0, #0
*(u32*)0x02009AB8 = 0xE12FFF1E; // bx lr
// u32 nandWrite(const void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x0200961C = 0xE51FF004; // ldr pc,= patch_writeNandSave
*(u32*)0x02009620 = (u32)writeNandSavePatchCode->GetWriteNandSaveFunction();
// u32 nandRead(void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x02009940 = 0xE51FF004; // ldr pc,= patch_readNandSave
*(u32*)0x02009944 = (u32)readNandSavePatchCode->GetReadNandSaveFunction();
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "patches/Patch.h"
class FunctionSignature;
/// @brief Arm9 patch to redirect Nintendo DS Guide nand saving to the SD card.
class NintendoDSGuideNandSavePatch : public Patch
{
public:
bool FindPatchTarget(PatchContext& patchContext) override;
void ApplyPatch(PatchContext& patchContext) override;
};

View File

@@ -5455,13 +5455,7 @@ UBRE;flash;0x40000
UBRJ;flash;0x40000
UBRP;flash;0x40000
UEIJ;eeprom;0x2000
UGDA;none;0x0
UNSJ;flash;0x40000
UORE;nand;0x1000000
UORJ;nand;0x1000000
UORP;nand;0x1000000
USKV;nand;0x4000000
UXBP;nand;0x800000
UZCJ;flash;0x2000000
UZPD;flash;0x20000
UZPF;flash;0x20000
1 gameCode saveType saveSize
5455 UBRJ flash 0x40000
5456 UBRP flash 0x40000
5457 UEIJ eeprom 0x2000
UGDA none 0x0
5458 UNSJ flash 0x40000
UORE nand 0x1000000
UORJ nand 0x1000000
UORP nand 0x1000000
USKV nand 0x4000000
UXBP nand 0x800000
5459 UZCJ flash 0x2000000
5460 UZPD flash 0x20000
5461 UZPF flash 0x20000