From a222d02fbfccca0d709d26530ce03da9cee02b60 Mon Sep 17 00:00:00 2001
From: patchzyy <64382339+patchzyy@users.noreply.github.com>
Date: Wed, 21 Oct 2026 16:49:54 +0200
Subject: [PATCH] port remaining stale rr us hooks
---
.../WiiCompiled.Setup.Windows/Installation.cs | 2 +
.../WiiCompiled.Setup.Windows/SelfTests.cs | 6 +-
translator/src/Translator.Cli/Program.cs | 6 +-
.../Mods/Mkwii/RetroRewindRegionPatches.cs | 76 ++++++------
.../RetroRewindRegionPatchesTests.cs | 115 ++++++++++--------
5 files changed, 111 insertions(+), 94 deletions(-)
diff --git a/Launcher/WiiCompiled.Setup.Windows/Installation.cs b/Launcher/WiiCompiled.Setup.Windows/Installation.cs
index 3b0c1af..d1e7040 100644
--- a/Launcher/WiiCompiled.Setup.Windows/Installation.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/Installation.cs
@@ -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"
];
diff --git a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
index c751841..9536687 100644
--- a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
@@ -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",
diff --git a/translator/src/Translator.Cli/Program.cs b/translator/src/Translator.Cli/Program.cs
index 793e7e7..85224e9 100644
--- a/translator/src/Translator.Cli/Program.cs
+++ b/translator/src/Translator.Cli/Program.cs
@@ -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.");
diff --git a/translator/src/Translator.Core/Mods/Mkwii/RetroRewindRegionPatches.cs b/translator/src/Translator.Core/Mods/Mkwii/RetroRewindRegionPatches.cs
index 10468a2..04f7572 100644
--- a/translator/src/Translator.Core/Mods/Mkwii/RetroRewindRegionPatches.cs
+++ b/translator/src/Translator.Core/Mods/Mkwii/RetroRewindRegionPatches.cs
@@ -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)
+ ///
+ /// 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.
+ ///
+ 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;
diff --git a/translator/tests/Translator.Tests/RetroRewindRegionPatchesTests.cs b/translator/tests/Translator.Tests/RetroRewindRegionPatchesTests.cs
index 4289946..feb1ece 100644
--- a/translator/tests/Translator.Tests/RetroRewindRegionPatchesTests.cs
+++ b/translator/tests/Translator.Tests/RetroRewindRegionPatchesTests.cs
@@ -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(() => RetroRewindRegionPatches.CorrectUsVoiceHook(
- Chunk(Command(KamekCommandId.Addr32, 0x808DBCD8)), "RMCE01", image));
- }
-
- [Theory]
- [InlineData(KamekCommandId.Write32)]
- [InlineData(KamekCommandId.Branch)]
- public void RejectsAnUnexpectedCommandAtThePalAddress(KamekCommandId id)
- {
- Assert.Throws(() => 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(() => RetroRewindRegionPatches.CorrectUsVoiceHook(
- Chunk(hook, other), "RMCE01", VoiceImage()));
+ var chunk = Chunk(Command(KamekCommandId.Addr32, 0x808B4D60));
+ Assert.Throws(() =>
+ 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(() =>
+ RetroRewindRegionPatches.CorrectUnportedUsHooks(hook, hook, "RMCE01", image));
+ var write = Chunk(Command(KamekCommandId.Write32, 0x808DBCD8));
+ Assert.Throws(() =>
+ RetroRewindRegionPatches.CorrectUnportedUsHooks(write, write, "RMCE01", SlotImage()));
+ Assert.Throws(() => 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);
}
}