diff --git a/translator/src/Translator.Core/Parsing/Kamek/KamekChunk.cs b/translator/src/Translator.Core/Parsing/Kamek/KamekChunk.cs index c8af538..a2b6c2b 100644 --- a/translator/src/Translator.Core/Parsing/Kamek/KamekChunk.cs +++ b/translator/src/Translator.Core/Parsing/Kamek/KamekChunk.cs @@ -6,6 +6,12 @@ namespace Translator.Core.Parsing.Kamek; public sealed class KamekChunk { public const uint Magic0 = 0x4B616D65; + /// + /// 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. + /// + 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; } diff --git a/translator/src/Translator.Core/Parsing/Kamek/KamekPulFile.cs b/translator/src/Translator.Core/Parsing/Kamek/KamekPulFile.cs index 75306fe..0cd46a9 100644 --- a/translator/src/Translator.Core/Parsing/Kamek/KamekPulFile.cs +++ b/translator/src/Translator.Core/Parsing/Kamek/KamekPulFile.cs @@ -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); diff --git a/translator/tests/Translator.Tests/KamekPulFileTests.cs b/translator/tests/Translator.Tests/KamekPulFileTests.cs index 528b7bb..7444651 100644 --- a/translator/tests/Translator.Tests/KamekPulFileTests.cs +++ b/translator/tests/Translator.Tests/KamekPulFileTests.cs @@ -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;