Support Kamek v2 Code.pul files (#126)

This commit is contained in:
Michael G
2026-09-03 01:52:17 -04:00
committed by GitHub
parent dec9ed8df1
commit ffc7244277
3 changed files with 44 additions and 7 deletions
@@ -6,6 +6,12 @@ namespace Translator.Core.Parsing.Kamek;
public sealed class KamekChunk
{
public const uint Magic0 = 0x4B616D65;
/// <summary>
/// Kamek v2 is still emitted by current Retro Rewind releases. Its command
/// stream matches v3, but combined v2 files carry each chunk length only in
/// the outer size table.
/// </summary>
public const uint Magic1V2 = 0x6B000002;
public const uint Magic1 = 0x6B000003;
public const int HeaderSize = 0x20;
@@ -58,7 +64,7 @@ public sealed class KamekChunk
}
return BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)) == Magic0 &&
BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset + 4, 4)) == Magic1;
IsSupportedFormatVersion(BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset + 4, 4)));
}
public static KamekChunk Parse(byte[] data, int offset, int expectedSize, int index)
@@ -74,7 +80,7 @@ public sealed class KamekChunk
var magic0 = reader.ReadUInt32();
var magic1 = reader.ReadUInt32();
if (magic0 != Magic0 || magic1 != Magic1)
if (magic0 != Magic0 || !IsSupportedFormatVersion(magic1))
{
throw new InvalidDataException($"Kamek chunk {index} at 0x{offset:X} has invalid magic 0x{magic0:X8}/0x{magic1:X8}.");
}
@@ -83,9 +89,15 @@ public sealed class KamekChunk
var codeSize = reader.ReadUInt32();
var ctorStart = reader.ReadUInt32();
var ctorEnd = reader.ReadUInt32();
var chunkSize = reader.ReadUInt32();
var declaredChunkSize = reader.ReadUInt32();
_ = reader.ReadUInt32(); // reserved
// v3 writes each chunk's length in its header. v2 leaves that field at
// zero and relies on the combined-file table (or the raw file length).
var chunkSize = magic1 == Magic1V2
? checked((uint)(expectedSize != 0 ? expectedSize : data.Length - offset))
: declaredChunkSize;
if (chunkSize < HeaderSize + codeSize)
{
throw new InvalidDataException($"Kamek chunk {index} chunkSize 0x{chunkSize:X} is smaller than header+code.");
@@ -171,4 +183,7 @@ public sealed class KamekChunk
KamekCommandId.BranchLink => 1,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, "Unsupported Kamek command id")
};
private static bool IsSupportedFormatVersion(uint magic1) =>
magic1 is Magic1V2 or Magic1;
}
@@ -33,7 +33,7 @@ public sealed class KamekPulFile
if (!KamekChunk.HasMagic(data, 0))
{
throw new InvalidDataException("File is neither a combined Code.pul nor a raw Kamek v3 chunk.");
throw new InvalidDataException("File is neither a combined Code.pul nor a raw supported Kamek chunk.");
}
var rawChunk = KamekChunk.Parse(data, 0, expectedSize: 0, index: 0);
@@ -49,6 +49,27 @@ public class KamekPulFileTests
Assert.Equal(0x20u, pul.SelectRegion("E").BssSize);
}
[Fact]
public void ParsesKamekV2CombinedChunks()
{
var p = BuildChunk(0x10, [0x4E, 0x80, 0x00, 0x20],
[BuildCommand(KamekCommandId.Branch, true, 0x8053369C, [0x100])], KamekChunk.Magic1V2);
var combined = new byte[0x10 + p.Length];
WriteU32(combined, 0, (uint)p.Length);
p.CopyTo(combined, 0x10);
var pul = KamekPulFile.Parse(combined);
Assert.True(pul.IsCombined);
Assert.Equal(0x10u, pul.SelectRegion("P").BssSize);
Assert.Equal(KamekCommandId.Branch, pul.SelectRegion("P").Commands.Single().Id);
var raw = KamekPulFile.Parse(p);
Assert.False(raw.IsCombined);
Assert.Equal((uint)p.Length, raw.Chunks.Single().ChunkSize);
}
[Fact]
public void EncodesPpcBranchLikeKamek()
{
@@ -83,18 +104,19 @@ public class KamekPulFileTests
Assert.Equal(chunk.Commands.Count, chunk.CommandCounts.Values.Sum());
}
private static byte[] BuildChunk(uint bssSize, byte[] code, byte[][] commands)
private static byte[] BuildChunk(uint bssSize, byte[] code, byte[][] commands,
uint magic1 = KamekChunk.Magic1)
{
var commandSize = commands.Sum(c => c.Length);
var chunkSize = KamekChunk.HeaderSize + code.Length + commandSize;
var data = new byte[chunkSize];
WriteU32(data, 0x00, KamekChunk.Magic0);
WriteU32(data, 0x04, KamekChunk.Magic1);
WriteU32(data, 0x04, magic1);
WriteU32(data, 0x08, bssSize);
WriteU32(data, 0x0C, (uint)code.Length);
WriteU32(data, 0x10, 0);
WriteU32(data, 0x14, 0);
WriteU32(data, 0x18, (uint)chunkSize);
WriteU32(data, 0x18, magic1 == KamekChunk.Magic1V2 ? 0u : (uint)chunkSize);
code.CopyTo(data, KamekChunk.HeaderSize);
var offset = KamekChunk.HeaderSize + code.Length;