mirror of
https://github.com/LNH-team/pico-loader.git
synced 2026-04-11 21:13:52 +02:00
Initial work on patch list
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -49,4 +49,5 @@ Thumbs.db
|
||||
picoLoader*.bin
|
||||
.vscode/
|
||||
data/aplist.bin
|
||||
data/savelist.bin
|
||||
data/savelist.bin
|
||||
data/patchlist.bin
|
||||
5
Makefile
5
Makefile
@@ -1,6 +1,6 @@
|
||||
.PHONY: loader9 loader7 clean
|
||||
|
||||
all: checklibtwl loader9 loader7 apList saveList
|
||||
all: checklibtwl loader9 loader7 apList saveList patchList
|
||||
|
||||
PICO_PLATFORM ?= DSPICO
|
||||
|
||||
@@ -22,6 +22,9 @@ apList: picoLoaderConverter data/aplist.csv
|
||||
saveList: picoLoaderConverter data/savelist.csv
|
||||
dotnet tools/PicoLoaderConverter/PicoLoaderConverter/bin/Debug/net9.0/PicoLoaderConverter.dll savelist -i data/savelist.csv -o data/savelist.bin
|
||||
|
||||
patchList: picoLoaderConverter data/patchlist.json
|
||||
dotnet tools/PicoLoaderConverter/PicoLoaderConverter/bin/Debug/net9.0/PicoLoaderConverter.dll patchlist -i data/patchlist.json -o data/patchlist.bin
|
||||
|
||||
clean:
|
||||
$(MAKE) -f Makefile.arm7 clean
|
||||
$(MAKE) -f Makefile.arm9 clean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "common.h"
|
||||
#include "ApListFactory.h"
|
||||
|
||||
ApList* ApListFactory::CreateFromFile(const TCHAR *path)
|
||||
std::unique_ptr<ApList> ApListFactory::CreateFromFile(const TCHAR* path)
|
||||
{
|
||||
FIL file;
|
||||
if (f_open(&file, path, FA_OPEN_EXISTING | FA_READ) != FR_OK)
|
||||
@@ -20,5 +20,5 @@ ApList* ApListFactory::CreateFromFile(const TCHAR *path)
|
||||
}
|
||||
f_close(&file);
|
||||
|
||||
return new ApList(std::move(entries), entryCount);
|
||||
return std::make_unique<ApList>(std::move(entries), entryCount);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include "ApList.h"
|
||||
|
||||
/// @brief Factory for creating \see ApList instances.
|
||||
@@ -8,5 +9,5 @@ public:
|
||||
/// @brief Creates an \see ApList instance from the file at the given \p path.
|
||||
/// @param path The ap list file path.
|
||||
/// @return A pointer to the constructed \see ApList instance, or \c nullptr if construction failed.
|
||||
ApList* CreateFromFile(const TCHAR* path);
|
||||
std::unique_ptr<ApList> CreateFromFile(const TCHAR* path);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ bool CardSaveArranger::SetupCardSave(const nds_header_ntr_t* header, const TCHAR
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveList* saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
|
||||
auto saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
|
||||
if (saveList)
|
||||
{
|
||||
const auto saveListEntry = saveList->FindEntry(header->gameCode);
|
||||
@@ -44,7 +44,6 @@ bool CardSaveArranger::SetupCardSave(const nds_header_ntr_t* header, const TCHAR
|
||||
saveSize = saveListEntry->GetSaveSize();
|
||||
saveListEntry->Dump();
|
||||
}
|
||||
delete saveList;
|
||||
}
|
||||
}
|
||||
if (saveSize == 0)
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
#include "TwlAes.h"
|
||||
#include "DSMode.h"
|
||||
#include "Arm7IoRegisterClearer.h"
|
||||
#include "PatchListFactory.h"
|
||||
#include "NdsLoader.h"
|
||||
|
||||
#define AP_LIST_PATH "/_pico/aplist.bin"
|
||||
#define PATCH_LIST_PATH "/_pico/patchlist.bin"
|
||||
#define BIOS_NDS7_PATH "/_pico/biosnds7.rom"
|
||||
|
||||
typedef void (*entrypoint_t)(void);
|
||||
@@ -290,6 +292,8 @@ void NdsLoader::Load(BootMode bootMode)
|
||||
// wait for arm9 patches to be ready
|
||||
receiveFromArm9();
|
||||
|
||||
HandleGameSpecificPatches();
|
||||
|
||||
LOG_DEBUG("Arm9 patches done\n");
|
||||
|
||||
ApplyArm7Patches();
|
||||
@@ -438,6 +442,63 @@ void NdsLoader::InsertArgv()
|
||||
HOMEBREW_ARGV->length = argSize;
|
||||
}
|
||||
|
||||
void NdsLoader::HandleGameSpecificPatches()
|
||||
{
|
||||
auto patchList = PatchListFactory().CreateFromFile(PATCH_LIST_PATH);
|
||||
if (patchList)
|
||||
{
|
||||
auto entry = patchList->FindEntry(_romHeader.gameCode, _romHeader.softwareVersion);
|
||||
if (entry)
|
||||
{
|
||||
auto patch = &entry->firstPatch;
|
||||
for (int patchIndex = 0; patchIndex < entry->patchCount; patchIndex++)
|
||||
{
|
||||
switch (patch->common.patchType)
|
||||
{
|
||||
case PatchListPatchType::Replace:
|
||||
{
|
||||
LOG_DEBUG("Applying replace patch. %d bytes to 0x%08X\n",
|
||||
patch->replacePatch.dataLength, patch->replacePatch.address);
|
||||
memcpy((void*)patch->replacePatch.address, patch->replacePatch.data, patch->replacePatch.dataLength);
|
||||
patch = (const PatchListEntryPatch*)&patch->replacePatch.data[(patch->replacePatch.dataLength + 3) & ~3];
|
||||
break;
|
||||
}
|
||||
case PatchListPatchType::Metafortress:
|
||||
{
|
||||
LOG_DEBUG("Applying Metafortress patch. %d addresses\n", patch->metafortressPatch.addressCount);
|
||||
auto addresses = patch->metafortressPatch.addresses;
|
||||
for (uint32_t addressIndex = 0; addressIndex < patch->metafortressPatch.addressCount; addressIndex++)
|
||||
{
|
||||
uint32_t address = addresses[addressIndex];
|
||||
if ((address & 1) != 0)
|
||||
{
|
||||
// thumb
|
||||
*(u16*)(address & ~1) = 0x4280; // cmp r0, r0
|
||||
}
|
||||
else
|
||||
{
|
||||
// arm
|
||||
*(u32*)address = 0xE1500000; // cmp r0, r0
|
||||
}
|
||||
}
|
||||
patch = (const PatchListEntryPatch*)&addresses[patch->metafortressPatch.addressCount];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
LOG_ERROR("Unknown patch type\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DEBUG("No entry found in patch list\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NdsLoader::HandleHomebrewPatching()
|
||||
{
|
||||
if (_launcherPath != nullptr && _launcherPath[0] != 0)
|
||||
@@ -660,8 +721,6 @@ void NdsLoader::HandleAntiPiracy()
|
||||
{
|
||||
LOG_DEBUG("No entry found in ap list\n");
|
||||
}
|
||||
|
||||
delete apList;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ private:
|
||||
char driveLetter, const char* deviceName, const char* path, u8 flags, u8 accessRights);
|
||||
void SetupDsiDeviceList();
|
||||
void InsertArgv();
|
||||
void HandleGameSpecificPatches();
|
||||
void HandleHomebrewPatching();
|
||||
bool TrySetupDsiWareSave();
|
||||
bool TryDecryptSecureArea();
|
||||
|
||||
32
arm7/source/loader/PatchList.cpp
Normal file
32
arm7/source/loader/PatchList.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#include "common.h"
|
||||
#include <algorithm>
|
||||
#include "PatchList.h"
|
||||
|
||||
const PatchListEntry* PatchList::FindEntry(u32 gameCode, u8 gameVersion)
|
||||
{
|
||||
auto header = reinterpret_cast<const PatchListHeader*>(_fileContents.get());
|
||||
u32 count = header->entryCount;
|
||||
auto entries = header->headerEntries;
|
||||
if (count != 0)
|
||||
{
|
||||
const auto gameEntry = std::lower_bound(entries, entries + count, gameCode,
|
||||
[gameVersion] (const PatchListHeaderEntry& entry, u32 value)
|
||||
{
|
||||
if (entry.gameCode == value)
|
||||
{
|
||||
return entry.gameVersion < gameVersion;
|
||||
}
|
||||
else
|
||||
{
|
||||
return entry.gameCode < value;
|
||||
}
|
||||
});
|
||||
|
||||
if (gameEntry != entries + count && gameEntry->gameCode == gameCode && gameEntry->gameVersion == gameVersion)
|
||||
{
|
||||
return reinterpret_cast<const PatchListEntry*>(_fileContents.get() + gameEntry->offset);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
71
arm7/source/loader/PatchList.h
Normal file
71
arm7/source/loader/PatchList.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
enum class PatchListPatchType : u8
|
||||
{
|
||||
Replace,
|
||||
Metafortress
|
||||
};
|
||||
|
||||
struct PatchListHeaderEntry
|
||||
{
|
||||
u32 gameCode;
|
||||
u32 gameVersion : 8;
|
||||
u32 offset : 24;
|
||||
};
|
||||
|
||||
struct PatchListHeader
|
||||
{
|
||||
u32 entryCount;
|
||||
PatchListHeaderEntry headerEntries[1];
|
||||
};
|
||||
|
||||
union PatchListEntryPatch
|
||||
{
|
||||
struct
|
||||
{
|
||||
PatchListPatchType patchType;
|
||||
u8 reserved;
|
||||
} common;
|
||||
struct
|
||||
{
|
||||
PatchListPatchType patchType;
|
||||
u8 reserved;
|
||||
u16 dataLength;
|
||||
u32 address;
|
||||
u8 data[1];
|
||||
} replacePatch;
|
||||
struct
|
||||
{
|
||||
PatchListPatchType patchType;
|
||||
u8 reserved;
|
||||
u16 addressCount;
|
||||
u32 addresses[1];
|
||||
} metafortressPatch;
|
||||
};
|
||||
|
||||
static_assert(offsetof(PatchListEntryPatch, replacePatch.dataLength) == 2);
|
||||
static_assert(offsetof(PatchListEntryPatch, replacePatch.address) == 4);
|
||||
static_assert(offsetof(PatchListEntryPatch, replacePatch.data) == 8);
|
||||
static_assert(offsetof(PatchListEntryPatch, metafortressPatch.addressCount) == 2);
|
||||
static_assert(offsetof(PatchListEntryPatch, metafortressPatch.addresses) == 4);
|
||||
|
||||
struct PatchListEntry
|
||||
{
|
||||
u16 length;
|
||||
u16 patchCount;
|
||||
PatchListEntryPatch firstPatch;
|
||||
};
|
||||
|
||||
/// @brief Class representing a patch list.
|
||||
class PatchList
|
||||
{
|
||||
public:
|
||||
PatchList(std::unique_ptr<const u8[]> fileContents)
|
||||
: _fileContents(std::move(fileContents)) { }
|
||||
|
||||
const PatchListEntry* FindEntry(u32 gameCode, u8 gameVersion);
|
||||
|
||||
private:
|
||||
std::unique_ptr<const u8[]> _fileContents;
|
||||
};
|
||||
24
arm7/source/loader/PatchListFactory.cpp
Normal file
24
arm7/source/loader/PatchListFactory.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "common.h"
|
||||
#include "PatchListFactory.h"
|
||||
|
||||
std::unique_ptr<PatchList> PatchListFactory::CreateFromFile(const TCHAR* path)
|
||||
{
|
||||
FIL file;
|
||||
if (f_open(&file, path, FA_OPEN_EXISTING | FA_READ) != FR_OK)
|
||||
{
|
||||
LOG_FATAL("Failed to open patch list file\n");
|
||||
return nullptr;
|
||||
}
|
||||
u32 fileSize = f_size(&file);
|
||||
auto fileContents = std::make_unique_for_overwrite<u8[]>(fileSize);
|
||||
UINT bytesRead = 0;
|
||||
FRESULT result = f_read(&file, fileContents.get(), fileSize, &bytesRead);
|
||||
if (result != FR_OK || bytesRead != fileSize)
|
||||
{
|
||||
LOG_FATAL("Failed to read patch list file\n");
|
||||
return nullptr;
|
||||
}
|
||||
f_close(&file);
|
||||
|
||||
return std::make_unique<PatchList>(std::move(fileContents));
|
||||
}
|
||||
12
arm7/source/loader/PatchListFactory.h
Normal file
12
arm7/source/loader/PatchListFactory.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "PatchList.h"
|
||||
|
||||
/// @brief Factory for creating \see ApList instances.
|
||||
class PatchListFactory
|
||||
{
|
||||
public:
|
||||
/// @brief Creates an \see PatchList instance from the file at the given \p path.
|
||||
/// @param path The patch list file path.
|
||||
/// @return A pointer to the constructed \see PatchList instance, or \c nullptr if construction failed.
|
||||
std::unique_ptr<PatchList> CreateFromFile(const TCHAR* path);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "common.h"
|
||||
#include "SaveListFactory.h"
|
||||
|
||||
SaveList* SaveListFactory::CreateFromFile(const TCHAR *path)
|
||||
std::unique_ptr<SaveList> SaveListFactory::CreateFromFile(const TCHAR* path)
|
||||
{
|
||||
FIL file;
|
||||
if (f_open(&file, path, FA_OPEN_EXISTING | FA_READ) != FR_OK)
|
||||
@@ -20,5 +20,5 @@ SaveList* SaveListFactory::CreateFromFile(const TCHAR *path)
|
||||
}
|
||||
f_close(&file);
|
||||
|
||||
return new SaveList(std::move(entries), entryCount);
|
||||
return std::make_unique<SaveList>(std::move(entries), entryCount);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include "SaveList.h"
|
||||
|
||||
/// @brief Factory for creating \see SaveList instances.
|
||||
@@ -8,5 +9,5 @@ public:
|
||||
/// @brief Creates a \see SaveList instance from the file at the given \p path.
|
||||
/// @param path The save list file path.
|
||||
/// @return A pointer to the constructed \see SaveList instance, or \c nullptr if construction failed.
|
||||
SaveList* CreateFromFile(const TCHAR* path);
|
||||
std::unique_ptr<SaveList> CreateFromFile(const TCHAR* path);
|
||||
};
|
||||
|
||||
97
data/patchlist.json
Normal file
97
data/patchlist.json
Normal file
@@ -0,0 +1,97 @@
|
||||
[
|
||||
{
|
||||
"gameCode": "YPTE",
|
||||
"gameRevision": 0,
|
||||
"gameName": "Puppy Palace (USA)",
|
||||
"patches": [
|
||||
{
|
||||
"type": "replace",
|
||||
"description": "Avoid stack corruption by save read check. This patch makes the game read 0 instead of 2 bytes. The game never checks the result.",
|
||||
"address": "02029DD8",
|
||||
"data": "00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameCode": "YPTP",
|
||||
"gameRevision": 0,
|
||||
"gameName": "My Puppy Shop (Europe)",
|
||||
"patches": [
|
||||
{
|
||||
"type": "replace",
|
||||
"description": "Avoid stack corruption by save read check. This patch makes the game read 0 instead of 2 bytes. The game never checks the result.",
|
||||
"address": "02029C98",
|
||||
"data": "00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameCode": "YPTJ",
|
||||
"gameRevision": 0,
|
||||
"gameName": "Machi no Pet-ya-san DS - 200 Piki Wan-chan Daishuugou (Japan)",
|
||||
"patches": [
|
||||
{
|
||||
"type": "replace",
|
||||
"description": "Avoid stack corruption by save read check. This patch makes the game read 0 instead of 2 bytes. The game never checks the result.",
|
||||
"address": "0203ADAC",
|
||||
"data": "00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameCode": "BDUE",
|
||||
"gameRevision": 0,
|
||||
"gameName": "C.O.P. - The Recruit (USA)",
|
||||
"patches": [
|
||||
{
|
||||
"type": "metafortress",
|
||||
"addresses": [
|
||||
"02022284", "020224C8", "02022540", "020225AC", "02022620", "02022690", "0202270C", "02022770",
|
||||
"02023774", "0202389C", "02023B6C", "02023BD8", "02023C4C", "02023CBC", "02023D38", "02023D9C",
|
||||
"02023E18", "02024424", "02025F48", "02026408", "02026480", "020278E8", "02028468", "02028A5C",
|
||||
"02028B94", "02028C44", "02028D00", "02028DA4", "02028E5C", "0202C4FC", "0202C578", "0202C60C",
|
||||
"0202C688", "0202C7D8", "0202CD60", "0202D3CC", "0202D920", "0202DA80", "0202DAFC", "0202E2C8",
|
||||
"0202E5CC", "0202E638", "0202E78C", "0202EB98", "0202ECE4", "0202ED54", "0202F42C", "0202F9F8",
|
||||
"02030128", "02030194", "020304F8", "02030564", "020305D8", "02030AA4", "02030CF0", "02030D9C",
|
||||
"02030E18", "02031234", "020312AC", "02031348", "020313BC", "0203145C", "02031520", "02031584",
|
||||
"02031600", "020316E8", "020318B8", "02031974", "020319E8", "02031AF8", "02031B74", "02031BD8",
|
||||
"02031C8C", "02031D30", "02031DA8", "02031F80", "02031FF4", "02032114", "02032190", "02032318",
|
||||
"020323B8", "02032514", "0203258C", "02032764", "02032810", "0203288C", "020329B4", "020330EC",
|
||||
"02033168", "020332F0", "0203BA64", "0203C6EC", "0203C760", "0203D540", "0203EBB0", "0203EC14",
|
||||
"0203F4CC", "0203F538", "0203FA44", "0203FAB0", "0203FFBC", "02040A84", "02040B00", "02041664",
|
||||
"020416E0", "02041B24", "02041B9C", "020423F8", "02042870", "020428E0", "02042EA0", "02044128",
|
||||
"020441A4", "020447B8", "02048E10", "02049764", "020497D8", "02049848", "0204A1A0", "0204A204",
|
||||
"0205B5C4", "0205B630", "0205F030", "0205F09C", "0205F110", "0208C5EC", "0208C668"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameCode": "BDUP",
|
||||
"gameRevision": 0,
|
||||
"gameName": "C.O.P. - The Recruit (Europe)",
|
||||
"patches": [
|
||||
{
|
||||
"type": "metafortress",
|
||||
"addresses": [
|
||||
"02022284", "020224C8", "02022540", "020225AC", "02022620", "02022690", "0202270C", "02022770",
|
||||
"02023774", "0202389C", "02023B6C", "02023BD8", "02023C4C", "02023CBC", "02023D38", "02023D9C",
|
||||
"02023E18", "02024424", "02025F48", "02026408", "02026480", "020278E8", "02028468", "02028A5C",
|
||||
"02028B94", "02028C44", "02028D00", "02028DA4", "02028E5C", "0202C4FC", "0202C578", "0202C60C",
|
||||
"0202C688", "0202C7D8", "0202CD60", "0202D3CC", "0202D920", "0202DA80", "0202DAFC", "0202E2C8",
|
||||
"0202E5CC", "0202E638", "0202E78C", "0202EB98", "0202ECE4", "0202ED54", "0202F42C", "0202F9F8",
|
||||
"02030128", "02030194", "020304F8", "02030564", "020305D8", "02030AA4", "02030CF0", "02030D9C",
|
||||
"02030E18", "02031234", "020312AC", "02031348", "020313BC", "0203145C", "02031520", "02031584",
|
||||
"02031600", "020316E8", "020318B8", "02031974", "020319E8", "02031AF8", "02031B74", "02031BD8",
|
||||
"02031C8C", "02031D30", "02031DA8", "02031F80", "02031FF4", "02032114", "02032190", "02032318",
|
||||
"020323B8", "02032514", "0203258C", "02032764", "02032810", "0203288C", "020329B4", "020330EC",
|
||||
"02033168", "020332F0", "0203BA64", "0203C6EC", "0203C760", "0203D540", "0203EBB0", "0203EC14",
|
||||
"0203F4CC", "0203F538", "0203FA44", "0203FAB0", "0203FFBC", "02040A84", "02040B00", "02041664",
|
||||
"020416E0", "02041B24", "02041B9C", "020423F8", "02042870", "020428E0", "02042EA0", "02044128",
|
||||
"020441A4", "020447B8", "02048E10", "02049764", "020497D8", "02049848", "0204A1A0", "0204A204",
|
||||
"0205B5C4", "0205B630", "0205F030", "0205F09C", "0205F110", "0208C5EC", "0208C668"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
using CsvHelper.Configuration;
|
||||
using CsvHelper;
|
||||
using System.Globalization;
|
||||
using PicoLoaderConverter.Common;
|
||||
|
||||
namespace PicoLoaderConverter.ApList;
|
||||
|
||||
@@ -112,7 +113,7 @@ sealed class ApListFactory
|
||||
private ApListEntry ConvertFromCvsApListEntry(CsvApListEntry csvApListEntry)
|
||||
{
|
||||
return new ApListEntry(
|
||||
GameCodeToUint(csvApListEntry.GameCode),
|
||||
GameCodeHelper.GameCodeToUint(csvApListEntry.GameCode),
|
||||
(byte)csvApListEntry.GameVersion,
|
||||
ParseDSProtectVersion(csvApListEntry.DSProtectVersion),
|
||||
(byte)csvApListEntry.DSProtectFunctionMask,
|
||||
@@ -138,16 +139,6 @@ sealed class ApListFactory
|
||||
};
|
||||
}
|
||||
|
||||
private uint GameCodeToUint(string gameCode)
|
||||
{
|
||||
if (gameCode.Length != 4)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Game code '{gameCode}' is not valid. It must consist of exactly 4 characters.", nameof(gameCode));
|
||||
}
|
||||
return (uint)gameCode[0] | ((uint)gameCode[1] << 8) | ((uint)gameCode[2] << 16) | ((uint)gameCode[3] << 24);
|
||||
}
|
||||
|
||||
private DSProtectVersion ParseDSProtectVersion(string dsProtectVersion)
|
||||
{
|
||||
return dsProtectVersion switch
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace PicoLoaderConverter.Common;
|
||||
|
||||
static class GameCodeHelper
|
||||
{
|
||||
public static uint GameCodeToUint(string gameCode)
|
||||
{
|
||||
if (gameCode.Length != 4)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Game code '{gameCode}' is not valid. It must consist of exactly 4 characters.", nameof(gameCode));
|
||||
}
|
||||
return gameCode[0] | (uint)gameCode[1] << 8 | (uint)gameCode[2] << 16 | (uint)gameCode[3] << 24;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PicoLoaderConverter.Json;
|
||||
|
||||
sealed class JsonHexBytesConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(byte[]);
|
||||
}
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
string? text = reader.Value as string;
|
||||
var bytesString = text?.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [];
|
||||
return bytesString.Select(byteString =>
|
||||
{
|
||||
if (byteString.StartsWith("0x"))
|
||||
{
|
||||
byteString = byteString[2..];
|
||||
}
|
||||
return Convert.ToByte(byteString, 16);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PicoLoaderConverter.Json;
|
||||
|
||||
sealed class JsonHexNumberConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(uint);
|
||||
}
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
string? text = reader.Value as string;
|
||||
if (text?.StartsWith("0x") is true)
|
||||
{
|
||||
text = text[2..];
|
||||
}
|
||||
return Convert.ToUInt32(text, 16);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PicoLoaderConverter.PatchList;
|
||||
|
||||
sealed class PatchList
|
||||
{
|
||||
public IReadOnlyList<PatchListEntry> Entries { get; }
|
||||
|
||||
public PatchList(IEnumerable<PatchListEntry> entries)
|
||||
{
|
||||
Entries = entries.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PicoLoaderConverter.PatchList;
|
||||
|
||||
sealed class PatchListEntry
|
||||
{
|
||||
public string GameCode { get; init; } = string.Empty;
|
||||
public byte GameVersion { get; init; }
|
||||
public string GameName { get; init; } = string.Empty;
|
||||
public PatchListEntryPatch[] Patches { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
using PicoLoaderConverter.Json;
|
||||
|
||||
namespace PicoLoaderConverter.PatchList;
|
||||
|
||||
sealed class PatchListEntryPatch
|
||||
{
|
||||
public PatchType Type { get; init; }
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
// replace
|
||||
[JsonConverter(typeof(JsonHexNumberConverter))]
|
||||
public uint Address { get; init; }
|
||||
|
||||
[JsonConverter(typeof(JsonHexBytesConverter))]
|
||||
public byte[] Data { get; init; } = [];
|
||||
|
||||
// metafortress
|
||||
[JsonProperty(ItemConverterType = typeof(JsonHexNumberConverter))]
|
||||
public uint[] Addresses { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Newtonsoft.Json;
|
||||
using PicoLoaderConverter.Common;
|
||||
|
||||
namespace PicoLoaderConverter.PatchList;
|
||||
|
||||
sealed class PatchListFactory
|
||||
{
|
||||
public PatchList FromJson(string json)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<PatchListEntry[]>(json) ?? [];
|
||||
return new PatchList(result);
|
||||
}
|
||||
|
||||
public byte[] ToBinary(PatchList patchList)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(memoryStream);
|
||||
var sortedEntries = patchList.Entries
|
||||
.OrderBy(entry => GameCodeHelper.GameCodeToUint(entry.GameCode))
|
||||
.ThenBy(entry => entry.GameVersion)
|
||||
.ToArray();
|
||||
writer.Write(sortedEntries.Length);
|
||||
foreach (var entry in sortedEntries)
|
||||
{
|
||||
writer.Write(GameCodeHelper.GameCodeToUint(entry.GameCode));
|
||||
writer.Write(0);
|
||||
}
|
||||
for (int i = 0; i < sortedEntries.Length; i++)
|
||||
{
|
||||
var entry = sortedEntries[i];
|
||||
long entryStart = writer.BaseStream.Position;
|
||||
writer.BaseStream.Position = 4 + i * 8 + 4;
|
||||
writer.Write((uint)((entryStart << 8) | entry.GameVersion));
|
||||
writer.BaseStream.Position = entryStart;
|
||||
writer.Write((ushort)0);
|
||||
writer.Write((ushort)entry.Patches.Length);
|
||||
foreach (var patch in entry.Patches)
|
||||
{
|
||||
WritePatch(writer, patch);
|
||||
}
|
||||
long entryEnd = writer.BaseStream.Position;
|
||||
writer.BaseStream.Position = entryStart;
|
||||
writer.Write((ushort)(entryEnd - entryStart));
|
||||
writer.BaseStream.Position = entryEnd;
|
||||
|
||||
}
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
private void WritePatch(BinaryWriter writer, PatchListEntryPatch patch)
|
||||
{
|
||||
writer.Write((byte)patch.Type);
|
||||
writer.Write((byte)0);
|
||||
switch (patch.Type)
|
||||
{
|
||||
case PatchType.Replace:
|
||||
{
|
||||
writer.Write((ushort)patch.Data.Length);
|
||||
writer.Write(patch.Address);
|
||||
writer.Write(patch.Data);
|
||||
while ((writer.BaseStream.Position % 4) != 0)
|
||||
{
|
||||
writer.Write((byte)0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PatchType.Metafortress:
|
||||
{
|
||||
writer.Write((ushort)patch.Addresses.Length);
|
||||
foreach (uint address in patch.Addresses)
|
||||
{
|
||||
writer.Write(address);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PicoLoaderConverter.PatchList;
|
||||
|
||||
enum PatchType
|
||||
{
|
||||
Replace,
|
||||
Metafortress
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,8 @@ static class Program
|
||||
/// </summary>
|
||||
private static readonly Type[] sVerbs = [
|
||||
typeof(ApListConverterVerb),
|
||||
typeof(SaveListConverterVerb)
|
||||
typeof(SaveListConverterVerb),
|
||||
typeof(PatchListConverterVerb)
|
||||
];
|
||||
|
||||
static void Main(string[] args)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using CsvHelper;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using PicoLoaderConverter.Common;
|
||||
|
||||
namespace PicoLoaderConverter.SaveList;
|
||||
|
||||
@@ -97,7 +98,7 @@ sealed class SaveListFactory
|
||||
}
|
||||
|
||||
return new SaveListEntry(
|
||||
GameCodeToUint(csvSaveListEntry.GameCode),
|
||||
GameCodeHelper.GameCodeToUint(csvSaveListEntry.GameCode),
|
||||
ParseSaveType(csvSaveListEntry.SaveType),
|
||||
(byte)(csvSaveListEntry.SaveSize == 0 ? 0 : BitOperations.Log2((uint)csvSaveListEntry.SaveSize)));
|
||||
}
|
||||
@@ -113,16 +114,6 @@ sealed class SaveListFactory
|
||||
};
|
||||
}
|
||||
|
||||
private uint GameCodeToUint(string gameCode)
|
||||
{
|
||||
if (gameCode.Length != 4)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Game code '{gameCode}' is not valid. It must consist of exactly 4 characters.", nameof(gameCode));
|
||||
}
|
||||
return gameCode[0] | (uint)gameCode[1] << 8 | (uint)gameCode[2] << 16 | (uint)gameCode[3] << 24;
|
||||
}
|
||||
|
||||
private CardSaveType ParseSaveType(string saveType)
|
||||
{
|
||||
return saveType.ToLowerInvariant() switch
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using CommandLine;
|
||||
using PicoLoaderConverter.PatchList;
|
||||
|
||||
namespace PicoLoaderConverter.Verbs;
|
||||
|
||||
[Verb("patchlist", HelpText = "Convert patch list from json to bin.")]
|
||||
sealed class PatchListConverterVerb : IConverterVerb
|
||||
{
|
||||
[Option('i', Required = true, HelpText = "Input .json file.")]
|
||||
public required string InputFile { get; init; }
|
||||
|
||||
[Option('o', Required = true, HelpText = "Output .bin file.")]
|
||||
public required string OutputFile { get; init; }
|
||||
|
||||
public void Run()
|
||||
{
|
||||
// Convert patch list from json to bin
|
||||
var factory = new PatchListFactory();
|
||||
var patchList = factory.FromJson(File.ReadAllText(InputFile));
|
||||
var binaryList = factory.ToBinary(patchList);
|
||||
File.WriteAllBytes(OutputFile, binaryList);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user