using System.Buffers.Binary; using Translator.Core.Mods; using Translator.Core.Parsing.Kamek; using Xunit; namespace Translator.Tests; /// /// Pins base-translation reuse decisions: a Code.pul that touches nothing inside a translated /// function should be reusable, and one that moves a patch on/off a translated function must not be. /// public class BaseTranslationModAwarenessTests { // Two translated functions with a gap between them, so an address can be provably outside both. private static readonly Dictionary FunctionEnds = new() { [0x80005F34] = 0x8000608C, [0x80543BB4] = 0x80543C40 }; [Fact] public void ReusesTheBaseTranslationForAPulThatPatchesTheSameTranslatedFunctions() { using var original = TemporaryPul( Absolute(KamekCommandId.BranchLink, 0x80005F40), Absolute(KamekCommandId.Write32, 0x80001920)); using var candidate = TemporaryPul( // The low-memory write is gone and a new one appears, but neither is inside a translated // function, so the translation could not have seen either. Absolute(KamekCommandId.BranchLink, 0x80005F40), Absolute(KamekCommandId.Write32, 0x80003000)); var awareness = Awareness(original.Path); Assert.True(awareness.CoversCodePul("retro-rewind", candidate.Path, out var reason), reason); } [Fact] public void RefusesAPulThatMovesAPatchOntoADifferentTranslatedFunction() { using var original = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40)); using var candidate = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80543BB8)); var awareness = Awareness(original.Path); Assert.False(awareness.CoversCodePul("retro-rewind", candidate.Path, out var reason)); Assert.Contains("80543BB8", reason, StringComparison.OrdinalIgnoreCase); } [Fact] public void RefusesAPulThatStopsPatchingATranslatedFunction() { using var original = TemporaryPul( Absolute(KamekCommandId.BranchLink, 0x80005F40), Absolute(KamekCommandId.Branch, 0x80543BB8)); using var candidate = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40)); var awareness = Awareness(original.Path); Assert.False(awareness.CoversCodePul("retro-rewind", candidate.Path, out _)); } [Fact] public void RefusesAProfileTheBaseTranslationNeverSaw() { using var original = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40)); var awareness = Awareness(original.Path); Assert.False(awareness.CoversCodePul("some-other-mod", original.Path, out var reason)); Assert.Contains("retro-rewind", reason, StringComparison.Ordinal); } [Fact] public void RefusesWhenSeveralProfilesShapedTheTranslation() { using var first = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40)); using var second = TemporaryPul(Absolute(KamekCommandId.Branch, 0x80543BB8)); var awareness = BaseTranslationModAwareness.Create(null, 2, [ ("retro-rewind", "P", new string('a', 64), BaseTranslationModAwareness.PatchedAddresses(first.Path, "P")), ("other", "P", new string('b', 64), BaseTranslationModAwareness.PatchedAddresses(second.Path, "P")) ], FunctionEnds); Assert.False(awareness.CoversCodePul("retro-rewind", first.Path, out var reason)); Assert.Contains("2 mod profile", reason, StringComparison.Ordinal); } [Fact] public void SurvivesAWriteAndReadRoundTrip() { using var original = TemporaryPul( Absolute(KamekCommandId.BranchLink, 0x80005F40), Absolute(KamekCommandId.Write32, 0x80001920)); var path = Path.Combine(Path.GetTempPath(), $"mkwc-awareness-{Guid.NewGuid():N}.json"); try { BaseTranslationModAwarenessFile.WriteIfChangedAtomic(path, Awareness(original.Path)); var round = BaseTranslationModAwarenessFile.Read(path); Assert.Equal(2, round.TranslatedFunctionCount); Assert.Equal(4, round.TranslatedFunctionRanges.Count); // Only the patch inside a translated function is recorded as consequential. Assert.Equal([0x80005F40u], round.Profiles[0].ConsequentialPatchedAddresses); Assert.True(round.CoversCodePul("retro-rewind", original.Path, out var reason), reason); } finally { File.Delete(path); } } private static BaseTranslationModAwareness Awareness(string codePulPath) => BaseTranslationModAwareness.Create(null, FunctionEnds.Count, [("retro-rewind", "P", new string('a', 64), BaseTranslationModAwareness.PatchedAddresses(codePulPath, "P"))], FunctionEnds); private static TemporaryFile TemporaryPul(params byte[][] commands) { var file = new TemporaryFile(); File.WriteAllBytes(file.Path, BuildChunk([0x60, 0x00, 0x00, 0x00], commands)); return file; } private static byte[] BuildChunk(byte[] code, byte[][] commands) { var chunkSize = KamekChunk.HeaderSize + code.Length + commands.Sum(command => command.Length); var data = new byte[chunkSize]; BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x00), KamekChunk.Magic0); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x04), KamekChunk.Magic1); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x08), 0x10); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x0C), (uint)code.Length); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x18), (uint)chunkSize); code.CopyTo(data, KamekChunk.HeaderSize); var offset = KamekChunk.HeaderSize + code.Length; foreach (var command in commands) { command.CopyTo(data, offset); offset += command.Length; } return data; } private static byte[] Absolute(KamekCommandId id, uint address) { var data = new byte[12]; BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0), ((uint)(byte)id << 24) | 0x00FFFFFEu); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), address); return data; } private sealed class TemporaryFile : IDisposable { public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"mkwc-pul-{Guid.NewGuid():N}.pul"); public void Dispose() => File.Delete(Path); } }