Initial work on patch list

This commit is contained in:
Gericom
2026-01-24 21:06:14 +01:00
parent edf18f25e2
commit fe2eff8ffe
27 changed files with 543 additions and 35 deletions

View File

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

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,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; } = [];
}

View File

@@ -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; } = [];
}

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>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>
</Project>

View File

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

View File

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

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