Added support for NAND saving in WarioWare D.I.Y. and Jam with the Band (#6)

Also improved speed of creating save files
This commit is contained in:
Gericom
2025-12-28 13:18:30 +01:00
parent 64e020182a
commit 6d12399ba4
17 changed files with 409 additions and 27 deletions

View File

@@ -1,4 +1,5 @@
#include "common.h"
#include <algorithm>
#include <string.h>
#include "SaveList.h"
#include "SaveListFactory.h"
@@ -9,10 +10,13 @@
#define DEFAULT_SAVE_SIZE (512 * 1024)
#define SAVE_FILL_VALUE 0xFF
static const u8 sNandSaveId[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
{
SaveList* saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
u32 saveSize = DEFAULT_SAVE_SIZE;
auto saveType = CardSaveType::None;
if (saveList)
{
const auto saveListEntry = saveList->FindEntry(gameCode);
@@ -23,6 +27,7 @@ bool CardSaveArranger::SetupCardSave(u32 gameCode, const TCHAR* savePath) const
}
else
{
saveType = saveListEntry->GetSaveType();
saveSize = saveListEntry->GetSaveSize();
saveListEntry->Dump();
}
@@ -50,8 +55,9 @@ bool CardSaveArranger::SetupCardSave(u32 gameCode, const TCHAR* savePath) const
return false;
}
auto ffBuffer = std::make_unique<u8[]>(512);
memset(ffBuffer.get(), SAVE_FILL_VALUE, 512);
const u32 ffBufferSize = 32 * 1024;
auto ffBuffer = std::make_unique<u8[]>(ffBufferSize);
memset(ffBuffer.get(), SAVE_FILL_VALUE, ffBufferSize);
u32 offset = initialSize;
// Align to 512 bytes
@@ -68,17 +74,33 @@ bool CardSaveArranger::SetupCardSave(u32 gameCode, const TCHAR* savePath) const
offset += remainingTo512;
}
// Write in 512-byte blocks
// Write in up to 32kb blocks
while (offset < saveSize)
{
u32 bytesToWrite = std::min<u32>(saveSize - offset, ffBufferSize);
UINT bytesWritten = 0;
if (f_write(file.get(), ffBuffer.get(), 512, &bytesWritten) != FR_OK ||
bytesWritten != 512)
if (f_write(file.get(), ffBuffer.get(), bytesToWrite, &bytesWritten) != FR_OK ||
bytesWritten != bytesToWrite)
{
LOG_FATAL("Failed to expand save file\n");
return false;
}
offset += 512;
offset += bytesToWrite;
}
if (saveType == CardSaveType::Nand)
{
// 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
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))
{
LOG_FATAL("Failed to write NAND save id\n");
return false;
}
}
}

View File

@@ -1,5 +1,6 @@
#include "common.h"
#include <string.h>
#include <algorithm>
#include <memory>
#include "DsiWareSaveArranger.h"
@@ -70,7 +71,7 @@ bool DsiWareSaveArranger::SetupDsiWareSaveFile(const TCHAR* savePath, u32 saveSi
if (f_lseek(file.get(), saveSize) != FR_OK ||
f_lseek(file.get(), 0) != FR_OK)
{
LOG_FATAL("Failed to create private save file\n");
LOG_FATAL("Failed to create DSiWare save file\n");
return false;
}
@@ -80,21 +81,28 @@ bool DsiWareSaveArranger::SetupDsiWareSaveFile(const TCHAR* savePath, u32 saveSi
if (f_write(file.get(), fatHeader.get(), sizeof(fat_header_t), &bytesWritten) != FR_OK ||
bytesWritten != sizeof(fat_header_t))
{
LOG_FATAL("Failed to format private save file\n");
LOG_FATAL("Failed to format DSiWare save file\n");
return false;
}
memset(fatHeader.get(), 0, 512);
fatHeader.reset();
while (f_tell(file.get()) < saveSize)
const u32 clearBufferSize = 32 * 1024;
auto clearBuffer = std::make_unique<u8[]>(clearBufferSize);
memset(clearBuffer.get(), 0, clearBufferSize);
u32 offset = f_tell(file.get());
while (offset < saveSize)
{
u32 bytesToWrite = std::min<u32>(saveSize - offset, clearBufferSize);
bytesWritten = 0;
if (f_write(file.get(), fatHeader.get(), 512, &bytesWritten) != FR_OK ||
bytesWritten != 512)
if (f_write(file.get(), clearBuffer.get(), bytesToWrite, &bytesWritten) != FR_OK ||
bytesWritten != bytesToWrite)
{
LOG_FATAL("Failed to format private save file\n");
LOG_FATAL("Failed to format DSiWare save file\n");
return false;
}
offset += bytesToWrite;
}
}

View File

@@ -7,13 +7,13 @@
class SaveListEntry
{
u32 gameCode;
u8 saveType; // see CardSaveType
CardSaveType saveType;
u8 saveSize; // 0 or 1 << x
u8 reserved[2]; // for possible future use
public:
u32 GetGameCode() const { return gameCode; }
CardSaveType GetSaveType() const { return static_cast<CardSaveType>(saveType); }
CardSaveType GetSaveType() const { return saveType; }
u32 GetSaveSize() const { return saveSize == 0 ? 0 : (1u << saveSize); }
void Dump() const

View File

@@ -13,6 +13,8 @@
#include "patches/arm9/OSResetSystemPatch.h"
#include "patches/arm9/PokemonDownloaderArm9Patch.h"
#include "patches/arm9/DSProtectArm9Patch.h"
#include "patches/arm9/NandSave/JamWithTheBandNandSavePatch.h"
#include "patches/arm9/NandSave/WarioWareDiyNandSavePatch.h"
#include "patches/arm9/OverlayPatches/FsStartOverlayHookPatch.h"
#include "patches/arm9/OverlayPatches/DSProtectPatches/DSProtectOverlayPatch.h"
#include "patches/arm9/OverlayPatches/DSProtectPatches/DSProtectPuyoPuyo7Patch.h"
@@ -374,6 +376,20 @@ void Arm9Patcher::AddGameSpecificPatches(
overlayHookPatch->AddOverlayPatch(new PokemonIrApPatch(PokemonIrVersion::Bw2));
break;
}
// WarioWare: D.I.Y.
case GAMECODE("UORE"):
case GAMECODE("UORP"):
case GAMECODE("UORJ"):
{
patchCollection.AddPatch(new WarioWareDiyNandSavePatch());
break;
}
// Jam with the Band
case GAMECODE("UXBP"):
{
patchCollection.AddPatch(new JamWithTheBandNandSavePatch());
break;
}
}
}

View File

@@ -1,6 +1,6 @@
#pragma once
#include "sections.h"
#include "../SectorRemapPatchCode.h"
#include "SectorRemapPatchCode.h"
#include "fileInfo.h"
DEFINE_SECTION_SYMBOLS(saveoffsettosdsector);

View File

@@ -8,8 +8,7 @@
#include "patches/arm7/VerifySaveAsm.h"
#include "patches/FunctionSignature.h"
#include "patches/platform/LoaderPlatform.h"
#include "patches/arm7/SaveOffsetToSdSectorAsm.h"
#include "patches/OffsetToSectorRemapAsm.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "patches/arm7/CardiTaskThreadPatchAsm.h"
#include "thumbInstructions.h"
#include "CardiTaskThreadPatch.h"
@@ -95,14 +94,11 @@ void CardiTaskThreadPatch::ApplyPatch(PatchContext& patchContext)
u32 patch1Size = SECTION_SIZE(patch_carditaskthread);
void* patch1Address = patchContext.GetPatchHeap().Alloc(patch1Size);
auto loaderPlatform = patchContext.GetLoaderPlatform();
const SdReadPatchCode* readPatchCode;
const SdWritePatchCode* writePatchCode;
const SectorRemapPatchCode* sectorRemapPatchCode;
readPatchCode = loaderPlatform->CreateSdReadPatchCode(
auto readPatchCode = loaderPlatform->CreateSdReadPatchCode(
patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap());
writePatchCode = loaderPlatform->CreateSdWritePatchCode(
auto writePatchCode = loaderPlatform->CreateSdWritePatchCode(
patchContext.GetPatchCodeCollection(), patchContext.GetPatchHeap());
sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
auto sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
(
patchContext.GetPatchHeap(),
(const save_file_info_t*)((u32)SHARED_SAVE_FILE_INFO - 0x02F00000 + 0x02700000)

View File

@@ -5,9 +5,8 @@
#include "patches/arm7/ReadSaveAsm.h"
#include "patches/arm7/WriteSaveAsm.h"
#include "patches/arm7/VerifySaveAsm.h"
#include "patches/arm7/SaveOffsetToSdSectorAsm.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "patches/platform/LoaderPlatform.h"
#include "patches/OffsetToSectorRemapAsm.h"
#include "patches/arm7/CardiTaskThreadPatchAsm.h"
#include "CardiDoTaskFromArm9Patch.h"

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 "JamWithTheBandNandSavePatch.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 JamWithTheBandNandSavePatch::FindPatchTarget(PatchContext& patchContext)
{
return true;
}
void JamWithTheBandNandSavePatch::ApplyPatch(PatchContext& patchContext)
{
auto sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
(
patchContext.GetPatchHeap(),
(const save_file_info_t*)((u32)SHARED_SAVE_FILE_INFO - 0x02F00000 + 0x02700000)
);
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*)0x020613CC = 0xE3A00001; // mov r0, #1
*(u32*)0x020613D0 = 0xE12FFF1E; // bx lr
// u32 nandResume(void)
*(u32*)0x02061A4C = 0xE3A00000; // mov r0, #0
*(u32*)0x02061A50 = 0xE12FFF1E; // bx lr
// u32 nandError(void)
*(u32*)0x02061C24 = 0xE3A00000; // mov r0, #0
*(u32*)0x02061C28 = 0xE12FFF1E; // bx lr
// u32 nandWrite(const void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x0206176C = 0xE51FF004; // ldr pc,= patch_writeNandSave
*(u32*)0x02061770 = (u32)writeNandSavePatchCode->GetWriteNandSaveFunction();
// u32 nandRead(void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)0x02061AC4 = 0xE51FF004; // ldr pc,= patch_readNandSave
*(u32*)0x02061AC8 = (u32)readNandSavePatchCode->GetReadNandSaveFunction();
}

View File

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

View File

@@ -0,0 +1,29 @@
#pragma once
#include "patches/PatchCode.h"
#include "sections.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "patches/platform/SdReadPatchCode.h"
DEFINE_SECTION_SYMBOLS(patch_readnandsave);
extern "C" bool patch_readNandSave(void* dst, u32 nandByteOffset, u32 byteLength, u32 dmaChannel);
extern u32 patch_readNandSave_save_offset_to_sd_sector_asm_address;
extern u32 patch_readNandSave_sdread_asm_address;
class ReadNandSavePatchCode : public PatchCode
{
public:
ReadNandSavePatchCode(PatchHeap& patchHeap, const SaveOffsetToSdSectorPatchCode* saveOffsetToSdSectorPatchCode,
const SdReadPatchCode* sdReadPatchCode)
: PatchCode(SECTION_START(patch_readnandsave), SECTION_SIZE(patch_readnandsave), patchHeap)
{
patch_readNandSave_save_offset_to_sd_sector_asm_address = (u32)saveOffsetToSdSectorPatchCode->GetRemapFunction();
patch_readNandSave_sdread_asm_address = (u32)sdReadPatchCode->GetSdReadFunction();
}
const void* GetReadNandSaveFunction() const
{
return GetAddressAtTarget((void*)patch_readNandSave);
}
};

View File

@@ -0,0 +1,57 @@
.cpu arm946e-s
.section "patch_readnandsave", "ax"
.syntax unified
.thumb
// r0 = memory dst
// r1 = nand byte offset
// r2 = byte length
// returns r0 = bool success
.global patch_readNandSave
.type patch_readNandSave, %function
patch_readNandSave:
push {r0,r1,r2,r4,r5,r6,lr}
pop {r4,r5,r6}
lsrs r6, r6, #9 // remaining number of sectors = byte length / 512
loop:
// while remaining number of sectors > 0
cmp r6, #0
beq end
movs r0, r5 // nand byte offset
ldr r3, patch_readNandSave_save_offset_to_sd_sector_asm_address
blx r3
mov r2, lr // sectors to read
// if sectors to read > remaining number of sectors
cmp r2, r6
bls 1f
movs r2, r6 // sectors to read = remaining number of sectors
1:
subs r6, r2 // remaining number of sectors -= sectors to read
movs r1, r4 // memory dst
lsls r3, r2, #9
adds r4, r3 // memory dst += sectors to read * 512
adds r5, r3 // nand byte offset += sectors to read * 512
ldr r3, patch_readNandSave_sdread_asm_address
blx r3
b loop
end:
movs r0, #1
pop {r4,r5,r6,pc}
.balign 4
.pool
.global patch_readNandSave_save_offset_to_sd_sector_asm_address
patch_readNandSave_save_offset_to_sd_sector_asm_address:
.word 0
.global patch_readNandSave_sdread_asm_address
patch_readNandSave_sdread_asm_address:
.word 0
.end

View File

@@ -0,0 +1,85 @@
#include "common.h"
#include "gameCode.h"
#include "patches/platform/LoaderPlatform.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "ReadNandSaveAsm.h"
#include "WriteNandSaveAsm.h"
#include "WarioWareDiyNandSavePatch.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 WarioWareDiyNandSavePatch::FindPatchTarget(PatchContext& patchContext)
{
_sdPatchEntry = nullptr;
switch (patchContext.GetGameCode())
{
case GAMECODE("UORE"):
{
_sdPatchEntry = (u8*)0x02002C04;
break;
}
case GAMECODE("UORP"):
{
_sdPatchEntry = (u8*)0x02002CA4;
break;
}
case GAMECODE("UORJ"):
{
_sdPatchEntry = (u8*)0x02002BE4;
break;
}
}
return _sdPatchEntry != nullptr;
}
void WarioWareDiyNandSavePatch::ApplyPatch(PatchContext& patchContext)
{
if (_sdPatchEntry == nullptr)
{
return;
}
auto sectorRemapPatchCode = patchContext.GetPatchCodeCollection().AddUniquePatchCode<SaveOffsetToSdSectorPatchCode>
(
patchContext.GetPatchHeap(),
(const save_file_info_t*)((u32)SHARED_SAVE_FILE_INFO - 0x02F00000 + 0x02700000)
);
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*)(_sdPatchEntry + 0x50C) = 0xE3A00001; // mov r0, #1
*(u32*)(_sdPatchEntry + 0x510) = 0xE12FFF1E; // bx lr
// u32 nandWait(void)
*(u32*)(_sdPatchEntry + 0xC9C) = 0xE12FFF1E; // bx lr
// u32 nandState(void)
*(u32*)(_sdPatchEntry + 0xEB0) = 0xE3A00003; // mov r0, #3
*(u32*)(_sdPatchEntry + 0xEB4) = 0xE12FFF1E; // bx lr
// u32 nandError(void)
*(u32*)(_sdPatchEntry + 0xEC8) = 0xE3A00000; // mov r0, #0
*(u32*)(_sdPatchEntry + 0xECC) = 0xE12FFF1E; // bx lr
// u32 nandWrite(void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)(_sdPatchEntry + 0x958) = 0xE51FF004; // ldr pc,= patch_writeNandSave
*(u32*)(_sdPatchEntry + 0x95C) = (u32)writeNandSavePatchCode->GetWriteNandSaveFunction();
// u32 nandRead(void* memory, u32 flash, u32 size, u32 dmaChannel)
*(u32*)(_sdPatchEntry + 0xD24) = 0xE51FF004; // ldr pc,= patch_readNandSave
*(u32*)(_sdPatchEntry + 0xD28) = (u32)readNandSavePatchCode->GetReadNandSaveFunction();
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include "patches/Patch.h"
class FunctionSignature;
/// @brief Arm9 patch to redirect WarioWare D.I.Y. nand saving to the SD card.
class WarioWareDiyNandSavePatch : public Patch
{
public:
bool FindPatchTarget(PatchContext& patchContext) override;
void ApplyPatch(PatchContext& patchContext) override;
private:
u8* _sdPatchEntry = nullptr;
};

View File

@@ -0,0 +1,29 @@
#pragma once
#include "patches/PatchCode.h"
#include "sections.h"
#include "patches/SaveOffsetToSdSectorAsm.h"
#include "patches/platform/SdWritePatchCode.h"
DEFINE_SECTION_SYMBOLS(patch_writenandsave);
extern "C" bool patch_writeNandSave(const void* src, u32 nandByteOffset, u32 byteLength, u32 dmaChannel);
extern u32 patch_writeNandSave_save_offset_to_sd_sector_asm_address;
extern u32 patch_writeNandSave_sdwrite_asm_address;
class WriteNandSavePatchCode : public PatchCode
{
public:
WriteNandSavePatchCode(PatchHeap& patchHeap, const SaveOffsetToSdSectorPatchCode* saveOffsetToSdSectorPatchCode,
const SdWritePatchCode* sdWritePatchCode)
: PatchCode(SECTION_START(patch_writenandsave), SECTION_SIZE(patch_writenandsave), patchHeap)
{
patch_writeNandSave_save_offset_to_sd_sector_asm_address = (u32)saveOffsetToSdSectorPatchCode->GetRemapFunction();
patch_writeNandSave_sdwrite_asm_address = (u32)sdWritePatchCode->GetSdWriteFunction();
}
const void* GetWriteNandSaveFunction() const
{
return GetAddressAtTarget((void*)patch_writeNandSave);
}
};

View File

@@ -0,0 +1,57 @@
.cpu arm946e-s
.section "patch_writenandsave", "ax"
.syntax unified
.thumb
// r0 = memory src
// r1 = nand byte offset
// r2 = byte length
// returns r0 = bool success
.global patch_writeNandSave
.type patch_writeNandSave, %function
patch_writeNandSave:
push {r0,r1,r2,r4,r5,r6,lr}
pop {r4,r5,r6}
lsrs r6, r6, #9 // remaining number of sectors = byte length / 512
loop:
// while remaining number of sectors > 0
cmp r6, #0
beq end
movs r0, r5 // nand byte offset
ldr r3, patch_writeNandSave_save_offset_to_sd_sector_asm_address
blx r3
mov r2, lr // sectors to read
// if sectors to read > remaining number of sectors
cmp r2, r6
bls 1f
movs r2, r6 // sectors to read = remaining number of sectors
1:
subs r6, r2 // remaining number of sectors -= sectors to read
movs r1, r4 // memory src
lsls r3, r2, #9
adds r4, r3 // memory src += sectors to read * 512
adds r5, r3 // nand byte offset += sectors to read * 512
ldr r3, patch_writeNandSave_sdwrite_asm_address
blx r3
b loop
end:
movs r0, #1
pop {r4,r5,r6,pc}
.balign 4
.pool
.global patch_writeNandSave_save_offset_to_sd_sector_asm_address
patch_writeNandSave_save_offset_to_sd_sector_asm_address:
.word 0
.global patch_writeNandSave_sdwrite_asm_address
patch_writeNandSave_sdwrite_asm_address:
.word 0
.end

View File

@@ -1,7 +1,7 @@
#pragma once
/// @brief Enum representing the save type.
enum class CardSaveType
enum class CardSaveType : u8
{
/// @brief The game has no save.
None = 0,