Merge branch 'feature/patch-list' into develop

This commit is contained in:
Gericom
2026-01-31 20:37:05 +01:00
29 changed files with 1587 additions and 37 deletions

View File

@@ -55,7 +55,7 @@ jobs:
- name: Package artifact - name: Package artifact
run: | run: |
mkdir Pico_Loader_${{ matrix.platform }} mkdir Pico_Loader_${{ matrix.platform }}
mv picoLoader7.bin data/aplist.bin data/savelist.bin Pico_Loader_${{ matrix.platform }} mv picoLoader7.bin data/aplist.bin data/patchlist.bin data/savelist.bin Pico_Loader_${{ matrix.platform }}
mv picoLoader9_${{ matrix.platform }}.bin Pico_Loader_${{ matrix.platform }}/picoLoader9.bin mv picoLoader9_${{ matrix.platform }}.bin Pico_Loader_${{ matrix.platform }}/picoLoader9.bin
- name: Publish build to GH Actions - name: Publish build to GH Actions
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -52,7 +52,7 @@ jobs:
- name: Package artifact - name: Package artifact
run: | run: |
mkdir Pico_Loader_${{ matrix.platform }} mkdir Pico_Loader_${{ matrix.platform }}
mv picoLoader7.bin data/aplist.bin data/savelist.bin Pico_Loader_${{ matrix.platform }} mv picoLoader7.bin data/aplist.bin data/patchlist.bin data/savelist.bin Pico_Loader_${{ matrix.platform }}
mv picoLoader9_${{ matrix.platform }}.bin Pico_Loader_${{ matrix.platform }}/picoLoader9.bin mv picoLoader9_${{ matrix.platform }}.bin Pico_Loader_${{ matrix.platform }}/picoLoader9.bin
- name: Publish build to GH Actions - name: Publish build to GH Actions
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

3
.gitignore vendored
View File

@@ -49,4 +49,5 @@ Thumbs.db
picoLoader*.bin picoLoader*.bin
.vscode/ .vscode/
data/aplist.bin data/aplist.bin
data/savelist.bin data/savelist.bin
data/patchlist.bin

View File

@@ -1,6 +1,6 @@
.PHONY: loader9 loader7 clean .PHONY: loader9 loader7 clean
all: checklibtwl loader9 loader7 apList saveList all: checklibtwl loader9 loader7 apList saveList patchList
PICO_PLATFORM ?= DSPICO PICO_PLATFORM ?= DSPICO
@@ -22,6 +22,9 @@ apList: picoLoaderConverter data/aplist.csv
saveList: picoLoaderConverter data/savelist.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 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: clean:
$(MAKE) -f Makefile.arm7 clean $(MAKE) -f Makefile.arm7 clean
$(MAKE) -f Makefile.arm9 clean $(MAKE) -f Makefile.arm9 clean

View File

@@ -1,7 +1,7 @@
#include "common.h" #include "common.h"
#include "ApListFactory.h" #include "ApListFactory.h"
ApList* ApListFactory::CreateFromFile(const TCHAR *path) std::unique_ptr<ApList> ApListFactory::CreateFromFile(const TCHAR* path)
{ {
FIL file; FIL file;
if (f_open(&file, path, FA_OPEN_EXISTING | FA_READ) != FR_OK) 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); f_close(&file);
return new ApList(std::move(entries), entryCount); return std::make_unique<ApList>(std::move(entries), entryCount);
} }

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include <memory>
#include "ApList.h" #include "ApList.h"
/// @brief Factory for creating \see ApList instances. /// @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. /// @brief Creates an \see ApList instance from the file at the given \p path.
/// @param path The ap list file path. /// @param path The ap list file path.
/// @return A pointer to the constructed \see ApList instance, or \c nullptr if construction failed. /// @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);
}; };

View File

@@ -29,7 +29,7 @@ bool CardSaveArranger::SetupCardSave(const nds_header_ntr_t* header, const TCHAR
} }
else else
{ {
SaveList* saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH); auto saveList = SaveListFactory().CreateFromFile(SAVE_LIST_PATH);
if (saveList) if (saveList)
{ {
const auto saveListEntry = saveList->FindEntry(header->gameCode); 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(); saveSize = saveListEntry->GetSaveSize();
saveListEntry->Dump(); saveListEntry->Dump();
} }
delete saveList;
} }
} }
if (saveSize == 0) if (saveSize == 0)

View File

@@ -26,9 +26,11 @@
#include "TwlAes.h" #include "TwlAes.h"
#include "DSMode.h" #include "DSMode.h"
#include "Arm7IoRegisterClearer.h" #include "Arm7IoRegisterClearer.h"
#include "PatchListFactory.h"
#include "NdsLoader.h" #include "NdsLoader.h"
#define AP_LIST_PATH "/_pico/aplist.bin" #define AP_LIST_PATH "/_pico/aplist.bin"
#define PATCH_LIST_PATH "/_pico/patchlist.bin"
#define BIOS_NDS7_PATH "/_pico/biosnds7.rom" #define BIOS_NDS7_PATH "/_pico/biosnds7.rom"
typedef void (*entrypoint_t)(void); typedef void (*entrypoint_t)(void);
@@ -290,6 +292,8 @@ void NdsLoader::Load(BootMode bootMode)
// wait for arm9 patches to be ready // wait for arm9 patches to be ready
receiveFromArm9(); receiveFromArm9();
HandleGameSpecificPatches();
LOG_DEBUG("Arm9 patches done\n"); LOG_DEBUG("Arm9 patches done\n");
ApplyArm7Patches(); ApplyArm7Patches();
@@ -438,6 +442,63 @@ void NdsLoader::InsertArgv()
HOMEBREW_ARGV->length = argSize; 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() void NdsLoader::HandleHomebrewPatching()
{ {
if (_launcherPath != nullptr && _launcherPath[0] != 0) if (_launcherPath != nullptr && _launcherPath[0] != 0)
@@ -660,8 +721,6 @@ void NdsLoader::HandleAntiPiracy()
{ {
LOG_DEBUG("No entry found in ap list\n"); LOG_DEBUG("No entry found in ap list\n");
} }
delete apList;
} }
} }

View File

@@ -79,6 +79,7 @@ private:
char driveLetter, const char* deviceName, const char* path, u8 flags, u8 accessRights); char driveLetter, const char* deviceName, const char* path, u8 flags, u8 accessRights);
void SetupDsiDeviceList(); void SetupDsiDeviceList();
void InsertArgv(); void InsertArgv();
void HandleGameSpecificPatches();
void HandleHomebrewPatching(); void HandleHomebrewPatching();
bool TrySetupDsiWareSave(); bool TrySetupDsiWareSave();
bool TryDecryptSecureArea(); bool TryDecryptSecureArea();

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

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

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

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

View File

@@ -1,7 +1,7 @@
#include "common.h" #include "common.h"
#include "SaveListFactory.h" #include "SaveListFactory.h"
SaveList* SaveListFactory::CreateFromFile(const TCHAR *path) std::unique_ptr<SaveList> SaveListFactory::CreateFromFile(const TCHAR* path)
{ {
FIL file; FIL file;
if (f_open(&file, path, FA_OPEN_EXISTING | FA_READ) != FR_OK) 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); f_close(&file);
return new SaveList(std::move(entries), entryCount); return std::make_unique<SaveList>(std::move(entries), entryCount);
} }

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include <memory>
#include "SaveList.h" #include "SaveList.h"
/// @brief Factory for creating \see SaveList instances. /// @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. /// @brief Creates a \see SaveList instance from the file at the given \p path.
/// @param path The save list file path. /// @param path The save list file path.
/// @return A pointer to the constructed \see SaveList instance, or \c nullptr if construction failed. /// @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);
}; };

1141
data/patchlist.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
using CsvHelper.Configuration; using CsvHelper.Configuration;
using CsvHelper; using CsvHelper;
using System.Globalization; using System.Globalization;
using PicoLoaderConverter.Common;
namespace PicoLoaderConverter.ApList; namespace PicoLoaderConverter.ApList;
@@ -112,7 +113,7 @@ sealed class ApListFactory
private ApListEntry ConvertFromCvsApListEntry(CsvApListEntry csvApListEntry) private ApListEntry ConvertFromCvsApListEntry(CsvApListEntry csvApListEntry)
{ {
return new ApListEntry( return new ApListEntry(
GameCodeToUint(csvApListEntry.GameCode), GameCodeHelper.GameCodeToUint(csvApListEntry.GameCode),
(byte)csvApListEntry.GameVersion, (byte)csvApListEntry.GameVersion,
ParseDSProtectVersion(csvApListEntry.DSProtectVersion), ParseDSProtectVersion(csvApListEntry.DSProtectVersion),
(byte)csvApListEntry.DSProtectFunctionMask, (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) private DSProtectVersion ParseDSProtectVersion(string dsProtectVersion)
{ {
return dsProtectVersion switch return dsProtectVersion switch

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
namespace PicoLoaderConverter.PatchList;
sealed class PatchList
{
public IReadOnlyList<PatchListEntry> Entries { get; }
public PatchList(IEnumerable<PatchListEntry> entries)
{
Entries = entries.ToArray();
}
}

View File

@@ -0,0 +1,8 @@
namespace PicoLoaderConverter.PatchList;
sealed class PatchListEntry
{
public string GameCode { get; init; } = string.Empty;
public byte GameVersion { get; init; }
public PatchListEntryPatch[] Patches { get; init; } = [];
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
using PicoLoaderConverter.Json;
namespace PicoLoaderConverter.PatchList;
sealed class PatchListEntryPatch
{
public PatchType Type { get; init; }
// 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; } = [];
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace PicoLoaderConverter.PatchList;
enum PatchType
{
Replace,
Metafortress
}

View File

@@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" /> <PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -12,7 +12,8 @@ static class Program
/// </summary> /// </summary>
private static readonly Type[] sVerbs = [ private static readonly Type[] sVerbs = [
typeof(ApListConverterVerb), typeof(ApListConverterVerb),
typeof(SaveListConverterVerb) typeof(SaveListConverterVerb),
typeof(PatchListConverterVerb)
]; ];
static void Main(string[] args) static void Main(string[] args)

View File

@@ -2,6 +2,7 @@
using CsvHelper; using CsvHelper;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using PicoLoaderConverter.Common;
namespace PicoLoaderConverter.SaveList; namespace PicoLoaderConverter.SaveList;
@@ -97,7 +98,7 @@ sealed class SaveListFactory
} }
return new SaveListEntry( return new SaveListEntry(
GameCodeToUint(csvSaveListEntry.GameCode), GameCodeHelper.GameCodeToUint(csvSaveListEntry.GameCode),
ParseSaveType(csvSaveListEntry.SaveType), ParseSaveType(csvSaveListEntry.SaveType),
(byte)(csvSaveListEntry.SaveSize == 0 ? 0 : BitOperations.Log2((uint)csvSaveListEntry.SaveSize))); (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) private CardSaveType ParseSaveType(string saveType)
{ {
return saveType.ToLowerInvariant() switch return saveType.ToLowerInvariant() switch

View File

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