port remaining stale rr us hooks

This commit is contained in:
patchzyy
2026-10-21 16:49:54 +02:00
parent 9dfbef00f5
commit a222d02fbf
5 changed files with 111 additions and 94 deletions
@@ -131,6 +131,8 @@ internal sealed class Installation
Workspace + "LocalBuild.ps1",
Workspace + "NativeBuildFlags.ps1",
Workspace + @"projects\mkwii\recomp.yml",
Workspace + @"projects\mkwii-ntsc-u\recomp.yml",
Workspace + @"projects\mkwii-ntsc-j\recomp.yml",
Workspace + @"runtime\CMakeLists.txt",
Workspace + @"runtime\assets\dsp\dsp_coef.bin"
];
@@ -1446,7 +1446,8 @@ internal static class SelfTests
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "CMake", "bin"));
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Ninja"));
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "llvm-mingw", "bin"));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "projects", "mkwii"));
foreach (var project in new[] { "mkwii", "mkwii-ntsc-u", "mkwii-ntsc-j" })
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "projects", project));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "src"));
Directory.CreateDirectory(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2",
"wc24"));
@@ -1473,7 +1474,8 @@ internal static class SelfTests
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}");
foreach (var project in new[] { "mkwii", "mkwii-ntsc-u", "mkwii-ntsc-j" })
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", project, "recomp.yml"), "profiles: {}");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"), "int main();");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "assets", "wii", "shared2", "wc24",
+4 -2
View File
@@ -1782,9 +1782,11 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
var selected = pul.SelectRegion(region);
if (string.Equals(profile?.Name, "retro-rewind", StringComparison.OrdinalIgnoreCase))
{
var corrected = RetroRewindRegionPatches.CorrectUsVoiceHook(selected, project?.Identity.GameId, image.Value);
var corrected = pul.IsCombined && !string.Equals(region, "P", StringComparison.OrdinalIgnoreCase)
? RetroRewindRegionPatches.CorrectUnportedUsHooks(selected, pul.SelectRegion("P"), project?.Identity.GameId, image.Value)
: selected;
if (!ReferenceEquals(corrected, selected))
Console.WriteLine("[translator] Corrected Retro Rewind US voice vtable hook: 0x808DBCD8 -> 0x808D7580.");
Console.WriteLine("[translator] Moved Retro Rewind's unported PAL vtable hooks to their NTSC-U slots.");
var esrb = RetroRewindRegionPatches.ApplyUsEsrbSkip(corrected, project?.Identity.GameId, image.Value);
if (!ReferenceEquals(esrb, corrected))
Console.WriteLine("[translator] Applied Retro Rewind's NTSC-U ESRB skip statically at 0x80604094.");
@@ -6,52 +6,54 @@ namespace Translator.Core.Mods.Mkwii;
public static class RetroRewindRegionPatches
{
private const uint PalVoiceHook = 0x808DBCD8;
private const uint UsVoiceHook = 0x808D7580;
private const uint PalRelStart = 0x805102E0;
// CustomCharacterVoice.cpp's HoldRosalinaLumaSound pointer retains its PAL address
// in the shipped US chunk. The clean US REL's relocated vtable identifies the slot;
// these surrounding entries distinguish it from other tables using HoldSoundLimited.
private static readonly (uint Address, uint Target)[] UsVoiceTable =
// Hooks the pack writes at PAL vtable slots that its NTSC-U port map leaves unported:
// PAL slot, NTSC-U slot, and the function that NTSC-U slot holds in the clean StaticR.rel.
private static readonly (uint Pal, uint Us, uint UsTarget)[] UnportedUsHooks =
[
(0x808D7540, 0x806FC03C),
(0x808D7544, 0x806FBC94),
(0x808D7570, 0x806FBC3C),
(0x808D7574, 0x806FBC34),
(0x808D7578, 0x806FA3A8),
(0x808D757C, 0x806FAA04),
(UsVoiceHook, 0x806FAB44),
(0x808D7584, 0x806FAD58),
(0x808D7588, 0x806FA7B0),
(0x808D758C, 0x806FA83C),
(0x808D7590, 0x806FA8D0),
(0x808D75BC, 0x808611E8),
(0x808D75C0, 0x806FA824)
(0x808B4D48, 0x808B0698, 0x80563BE8), // Kart::Damage::ApplyBananaDamage
(0x808B4DFC, 0x808B074C, 0x80563CD8), // Kart::Damage::ApplyZapperDamage
(0x808B4E14, 0x808B0764, 0x80563CCC), // Kart::Damage::ApplyShockDamage
(0x808CB02C, 0x808C5D24, 0x8072E068), // AI::EnemyRouteController::Update
(0x808DBCD8, 0x808D7580, 0x806FAB44), // Audio::RaceActor::HoldSoundLimited
];
public static KamekChunk CorrectUsVoiceHook(KamekChunk chunk, string? gameId, ProgramImage image)
/// <summary>
/// Moves the NTSC-U chunk's unported PAL vtable hooks to their NTSC-U slots. A StaticR.rel
/// address the NTSC-U chunk shares with the PAL chunk is always unported, so an unknown one fails.
/// </summary>
public static KamekChunk CorrectUnportedUsHooks(KamekChunk usChunk, KamekChunk palChunk, string? gameId,
ProgramImage image)
{
if (!string.Equals(gameId, "RMCE01", StringComparison.OrdinalIgnoreCase))
return chunk;
return usChunk;
if (usChunk.Commands.Count != palChunk.Commands.Count)
throw new InvalidDataException("Retro Rewind's PAL and NTSC-U chunks have different command lists.");
var matching = chunk.Commands.Where(c => c.AddressIsAbsolute && c.Address == PalVoiceHook).ToArray();
if (matching.Length == 0)
return chunk;
if (matching.Length != 1 || matching[0].Id != KamekCommandId.Addr32 ||
matching[0].Arguments.Count != 1 || matching[0].Arguments[0] >= chunk.CodeSize ||
chunk.Commands.Any(c => c.AddressIsAbsolute && c.Address == UsVoiceHook))
throw new InvalidDataException("Unexpected Retro Rewind US voice hook commands; cannot correct the PAL address.");
foreach (var (address, target) in UsVoiceTable)
var commands = usChunk.Commands.ToArray();
var changed = false;
for (var i = 0; i < commands.Length; i++)
{
if (!image.Contains(address, 4) ||
BinaryPrimitives.ReadUInt32BigEndian(image.Memory.AsSpan(image.GetOffset(address, 4), 4)) != target)
throw new InvalidDataException($"Retro Rewind US voice vtable check failed at 0x{address:X8}.");
var us = commands[i];
var pal = palChunk.Commands[i];
if (!us.AddressIsAbsolute || !pal.AddressIsAbsolute || us.Address != pal.Address || us.Address < PalRelStart)
continue;
var hook = UnportedUsHooks.FirstOrDefault(h => h.Pal == us.Address);
if (hook.Pal == 0)
throw new InvalidDataException($"Retro Rewind's NTSC-U chunk patches unported PAL address 0x{us.Address:X8}.");
if (us.Id != KamekCommandId.Addr32 || us.Arguments.Count != 1 || us.Arguments[0] >= usChunk.CodeSize)
throw new InvalidDataException($"Unexpected Retro Rewind command at PAL address 0x{us.Address:X8}.");
if (!image.Contains(hook.Us, 4) ||
BinaryPrimitives.ReadUInt32BigEndian(image.Memory.AsSpan(image.GetOffset(hook.Us, 4), 4)) != hook.UsTarget)
throw new InvalidDataException($"NTSC-U vtable slot check failed at 0x{hook.Us:X8}.");
commands[i] = us with { Address = hook.Us };
changed = true;
}
return new KamekChunk(chunk.Index, chunk.FileOffset, chunk.BssSize, chunk.CodeSize,
chunk.CtorStart, chunk.CtorEnd, chunk.ChunkSize, chunk.CodeBlob,
chunk.Commands.Select(c => ReferenceEquals(c, matching[0]) ? c with { Address = UsVoiceHook } : c).ToArray());
return changed
? new KamekChunk(usChunk.Index, usChunk.FileOffset, usChunk.BssSize, usChunk.CodeSize,
usChunk.CtorStart, usChunk.CtorEnd, usChunk.ChunkSize, usChunk.CodeBlob, commands)
: usChunk;
}
private const uint EsrbBranchSite = 0x80604094;
@@ -7,19 +7,31 @@ namespace Translator.Tests;
public sealed class RetroRewindRegionPatchesTests
{
[Fact]
public void CorrectsOnlyTheStaleUsPointerAndPreservesItsModuleTarget()
{
var hook = Command(KamekCommandId.Addr32, 0x808DBCD8);
var other = Command(KamekCommandId.Branch, 0x807B9F34);
var chunk = Chunk(hook, other);
var corrected = RetroRewindRegionPatches.CorrectUsVoiceHook(chunk, "RMCE01", VoiceImage());
private static readonly (uint Pal, uint Us, uint Target)[] UnportedHooks =
[
(0x808B4D48, 0x808B0698, 0x80563BE8),
(0x808B4DFC, 0x808B074C, 0x80563CD8),
(0x808B4E14, 0x808B0764, 0x80563CCC),
(0x808CB02C, 0x808C5D24, 0x8072E068),
(0x808DBCD8, 0x808D7580, 0x806FAB44),
];
Assert.Equal(0x808D7580u, corrected.Commands[0].Address);
Assert.Equal(hook.Arguments, corrected.Commands[0].Arguments);
Assert.Same(other, corrected.Commands[1]);
Assert.Equal(0x808DBCD8u, chunk.Commands[0].Address);
Assert.Same(chunk.CodeBlob, corrected.CodeBlob);
[Fact]
public void MovesEveryUnportedHookToItsUsSlotAndKeepsPortedOnes()
{
var pal = Chunk([.. UnportedHooks.Select(h => Command(KamekCommandId.Addr32, h.Pal)),
Command(KamekCommandId.Branch, 0x807C8994)]);
var us = Chunk([.. UnportedHooks.Select(h => Command(KamekCommandId.Addr32, h.Pal)),
Command(KamekCommandId.Branch, 0x807B9F34)]);
var corrected = RetroRewindRegionPatches.CorrectUnportedUsHooks(us, pal, "RMCE01", SlotImage());
for (var i = 0; i < UnportedHooks.Length; i++)
{
Assert.Equal(UnportedHooks[i].Us, corrected.Commands[i].Address);
Assert.Equal(us.Commands[i].Arguments, corrected.Commands[i].Arguments);
}
Assert.Same(us.Commands[^1], corrected.Commands[^1]);
Assert.Same(us.CodeBlob, corrected.CodeBlob);
}
[Theory]
@@ -28,44 +40,46 @@ public sealed class RetroRewindRegionPatchesTests
public void LeavesOtherRegionsUntouched(string gameId)
{
var chunk = Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8));
Assert.Same(chunk, RetroRewindRegionPatches.CorrectUsVoiceHook(chunk, gameId, VoiceImage()));
Assert.Same(chunk, RetroRewindRegionPatches.CorrectUnportedUsHooks(chunk, chunk, gameId, SlotImage()));
}
[Fact]
public void AcceptsAnAlreadyCorrectedPackWithoutRewritingIt()
public void AcceptsAPackWhosePortMapCoversTheHooks()
{
var chunk = Chunk(Command(KamekCommandId.Addr32, 0x808D7580));
Assert.Same(chunk, RetroRewindRegionPatches.CorrectUsVoiceHook(chunk, "RMCE01", VoiceImage()));
}
[Theory]
[InlineData(0x808D7540u)]
[InlineData(0x808D7580u)]
[InlineData(0x808D75C0u)]
public void RejectsTheCorrectionWhenTheVtableDoesNotMatch(uint address)
{
var image = VoiceImage();
image.Memory[image.GetOffset(address)] ^= 1;
Assert.Throws<InvalidDataException>(() => RetroRewindRegionPatches.CorrectUsVoiceHook(
Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8)), "RMCE01", image));
}
[Theory]
[InlineData(KamekCommandId.Write32)]
[InlineData(KamekCommandId.Branch)]
public void RejectsAnUnexpectedCommandAtThePalAddress(KamekCommandId id)
{
Assert.Throws<InvalidDataException>(() => RetroRewindRegionPatches.CorrectUsVoiceHook(
Chunk(Command(id, 0x808DBCD8)), "RMCE01", VoiceImage()));
var pal = Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8));
var us = Chunk(Command(KamekCommandId.Addr32, 0x808D7580));
Assert.Same(us, RetroRewindRegionPatches.CorrectUnportedUsHooks(us, pal, "RMCE01", SlotImage()));
}
[Fact]
public void RejectsDuplicateOrConflictingHooks()
public void RejectsAnUnknownUnportedRelAddress()
{
var hook = Command(KamekCommandId.Addr32, 0x808DBCD8);
foreach (var other in new[] { hook, Command(KamekCommandId.Addr32, 0x808D7580) })
Assert.Throws<InvalidDataException>(() => RetroRewindRegionPatches.CorrectUsVoiceHook(
Chunk(hook, other), "RMCE01", VoiceImage()));
var chunk = Chunk(Command(KamekCommandId.Addr32, 0x808B4D60));
Assert.Throws<InvalidDataException>(() =>
RetroRewindRegionPatches.CorrectUnportedUsHooks(chunk, chunk, "RMCE01", SlotImage()));
}
[Fact]
public void IgnoresSharedAddressesOutsideStaticR()
{
var chunk = Chunk(Command(KamekCommandId.Write32, 0x80001A80));
Assert.Same(chunk, RetroRewindRegionPatches.CorrectUnportedUsHooks(chunk, chunk, "RMCE01", SlotImage()));
}
[Fact]
public void RejectsAMismatchedSlotOrCommand()
{
var hook = Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8));
var image = SlotImage();
image.Memory[image.GetOffset(0x808D7580)] ^= 1;
Assert.Throws<InvalidDataException>(() =>
RetroRewindRegionPatches.CorrectUnportedUsHooks(hook, hook, "RMCE01", image));
var write = Chunk(Command(KamekCommandId.Write32, 0x808DBCD8));
Assert.Throws<InvalidDataException>(() =>
RetroRewindRegionPatches.CorrectUnportedUsHooks(write, write, "RMCE01", SlotImage()));
Assert.Throws<InvalidDataException>(() => RetroRewindRegionPatches.CorrectUnportedUsHooks(
hook, Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8), Command(KamekCommandId.Branch, 0x80000100)),
"RMCE01", SlotImage()));
}
private static readonly uint[] EsrbHook =
@@ -136,18 +150,13 @@ public sealed class RetroRewindRegionPatchesTests
private static KamekChunk Chunk(params KamekCommand[] commands) =>
new(1, 0, 0x20, 0x200, 0, 0, 0x220, new byte[0x200], commands);
private static ProgramImage VoiceImage()
private static ProgramImage SlotImage()
{
var memory = new byte[0x84];
uint[] entries =
[
0x806FC03C, 0x806FBC94, 0x806FBC3C, 0x806FBC34, 0x806FA3A8, 0x806FAA04,
0x806FAB44, 0x806FAD58, 0x806FA7B0, 0x806FA83C, 0x806FA8D0, 0x808611E8, 0x806FA824
];
int[] offsets = [0, 4, 0x30, 0x34, 0x38, 0x3C, 0x40, 0x44, 0x48, 0x4C, 0x50, 0x7C, 0x80];
for (var i = 0; i < entries.Length; i++)
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(offsets[i], 4), entries[i]);
var range = AddressRange.FromStartAndSize(0x808D7540, (uint)memory.Length);
const uint start = 0x808B0000;
var memory = new byte[0x28000];
foreach (var (_, us, target) in UnportedHooks)
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan((int)(us - start), 4), target);
var range = AddressRange.FromStartAndSize(start, (uint)memory.Length);
return new ProgramImage(memory, range, default, range, "fixture", range.Start);
}
}