Compare commits

...

6 Commits

Author SHA1 Message Date
patchzyy 4c166a7eb1 update to 0.2.36 2026-10-19 22:03:44 +02:00
patchzyy 29155eefbd Only trust relocated module pointers (#321)
* only trust relocated module pointers

* match relocation value too
2026-10-19 22:02:06 +02:00
patchzyy 46eb6b46f5 update to 0.2.35 (#316) 2026-10-19 13:36:47 +02:00
patchzyy 4f166e1403 Fix missed wii remote shakes (#313)
* keep wii remote shake peaks

* drop zeroed wii remote reports in sdl
2026-10-19 13:27:46 +02:00
patchzyy 6b4de9fbdb Update wpad.cpp (#312) 2026-10-18 19:29:51 +02:00
patchzyy 16bfe7b095 Fix prebuilt packaging (#311) 2026-10-18 18:51:56 +02:00
10 changed files with 501 additions and 33 deletions
+2 -1
View File
@@ -244,7 +244,8 @@ foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependency
# The translation identity covers the runtime's native registrations through this index (see
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
& $translator emit-native-index --project (Join-Path $workspace 'projects\mkwii\recomp.yml') --out $nativeIndex
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --out $nativeIndex
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
Assert-File $nativeIndex 'Runtime native index'
# A staged runtime that fingerprints differently from the shipped archive would make every
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.34</Version>
<Version>0.2.36</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+9 -8
View File
@@ -208,13 +208,14 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
so a workspace whose vendored tree moved on must not consume archives harvested from an older
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
Returns '' when absent.
#>
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
Where-Object { -not ($_.FullName.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') }) } |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
try {
@@ -245,13 +246,13 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
(Join-Path $root 'extern') + '\',
(Join-Path $root 'build') + '\'
)
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
# hashing them would make the two sides disagree by construction.
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
# Dot-paths (the submodule's .git pointer) are skipped by name, not by the hidden attribute:
# the payload zip drops that attribute, so the installed side would otherwise hash them.
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
Where-Object {
$full = $_.FullName
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
} |
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
$sha = [Security.Cryptography.SHA256]::Create()
+1 -1
View File
@@ -182,7 +182,7 @@ cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$a
# index, so it is extracted from exactly the bundled runtime sources.
echo "Extracting the runtime native index..."
"$appdir/usr/bin/translator-cli" emit-native-index \
--project "$appdir/workspace/projects/mkwii/recomp.yml" \
--native-source-dir "$appdir/workspace/runtime/src" \
--out "$appdir/workspace/runtime/native-index.json"
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
+1 -1
View File
@@ -153,7 +153,7 @@ done
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
# index, so it is extracted from exactly the packaged runtime sources.
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
"$host_translator" emit-native-index --project "$resources/workspace/projects/mkwii/recomp.yml" \
"$host_translator" emit-native-index --native-source-dir "$resources/workspace/runtime/src" \
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
mkdir -p "$resources/workspace/Launcher/macos"
@@ -372,6 +372,9 @@ static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
if (x == 0 && y == 0 && z == 0) {
return;
}
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
+12 -4
View File
@@ -93,12 +93,20 @@ extern "C" int32_t WUDGetStatus_HLE()
}
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
// WPADGetDataFormat reads the per-channel format set by WPADSetDataFormat. Can't reuse the
// translated SDK implementation: it dereferences Bluetooth control blocks that HLE'd WPADInit
// never constructs.
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
// Bluetooth control blocks that HLE'd WPADInit never constructs.
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
{
return g_state.contract.GetDataFormat(chan);
const int32_t status = g_state.contract.GetDataFormat(chan);
if (status < 0) {
return status;
}
switch (WiiRemoteInput::EffectiveKind(chan)) {
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
default: return 2;
}
}
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
+10 -14
View File
@@ -144,8 +144,8 @@ struct LastAcc {
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastAcc{};
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastNunchukAcc{};
// Any axis beyond this is not a reading the remote's +-3 g sensor can produce.
constexpr float kMaxPlausibleRemoteG = 4.0f;
// A zeroed remote report decodes to minus its zero-g point, at least this far out on every axis.
constexpr float kZeroReportMinG = 4.0f;
// Case-sensitive substring test that tolerates a null name.
bool NameContains(const char* name, const char* needle) {
@@ -250,10 +250,11 @@ bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) {
// True for a sample that cannot have come from the sensor. Over Bluetooth on
// Windows the remote delivers, a few times a minute, a report whose
// accelerometer bytes are all zero; SDL decodes that as -0x200 on every axis,
// i.e. (+5.12, -5.12, -5.12) g for the remote (100 units/g) and
// (+2.56, -2.56, -2.56) g for the Nunchuk (200 units/g). Handed to the game as
// is, one such frame is a full-lock steer plus a 9 g "shake". `g` is the
// accelerometer bytes are all zero; SDL decodes that as (+a, -b, -c) g with
// a, b, c the zero-g point over the units per g of each axis: about 5 g for the
// remote (factory calibration) and exactly 2.56 g for the Nunchuk (200 units/g).
// Handed to the game as is, one such frame is a full-lock steer plus a 9 g
// "shake". Single axes past 4 g are real readings of a hard shake. `g` is the
// uncorrected SDL sample.
bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
// An exact zero vector is SDL's sensor buffer before the first report, not
@@ -261,16 +262,11 @@ bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) {
return true;
}
const float zero = sensor == SDL_SENSOR_ACCEL ? 5.12f : 2.56f;
if (std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f) {
return true;
}
if (sensor == SDL_SENSOR_ACCEL) {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i]) > kMaxPlausibleRemoteG) return true;
}
return g[0] > kZeroReportMinG && g[1] < -kZeroReportMinG && g[2] < -kZeroReportMinG;
}
return false;
constexpr float zero = 2.56f;
return std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f;
}
// One accelerometer sample in g, SDL's sensor frame. The remote's own axes are
@@ -69,9 +69,24 @@ public static class ModFunctionDiscovery
// Kamek modules frequently register callbacks through static tables or lists that are
// walked indirectly by module startup code. Seed those native translations ahead of time.
// The Kamek blob is linked at offset zero, so a word inside it only holds a module address
// when a pointer relocation wrote one there; anything else is an instruction or data that
// happens to encode one. Images appended past the blob are not described by chunk commands.
var pointerRelocations = chunk.Commands
.Where(command => command.AddressIsRelative &&
command.Arguments.Count > 0 &&
command.Id is KamekCommandId.Addr32 or KamekCommandId.CondWritePointer)
.Select(command => (command.Address, KamekAddress.Resolve(command.Arguments[0], moduleGuestBase)))
.ToHashSet();
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
{
var value = BinaryPrimitives.ReadUInt32BigEndian(relocatedModuleImage.AsSpan(offset, 4));
// Comparing the value also rejects conditional writes that were skipped.
if ((uint)offset < chunk.CodeSize && !pointerRelocations.Contains(((uint)offset, value)))
{
continue;
}
if (LooksLikeFunctionPointerTarget(value, moduleGuestBase, moduleImageEnd, relocatedModuleImage))
{
AddImageAddress(value, "module data pointer");
@@ -28,7 +28,7 @@ public sealed class ModFunctionDiscoveryTests
ctorEnd: 0,
chunkSize: (uint)(KamekChunk.HeaderSize + code.Length),
codeBlob: code,
commands: []);
commands: [PointerRelocation(0x40, 0x50)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
@@ -368,7 +368,7 @@ public sealed class ModFunctionDiscoveryTests
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
commands: [PointerRelocation(0x20, 0x100)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
@@ -381,7 +381,7 @@ public sealed class ModFunctionDiscoveryTests
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x280, moduleBase + 0x200); // appended pointer table
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
@@ -655,6 +655,450 @@ public sealed class ModFunctionDiscoveryTests
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x43004]);
}
[Fact]
public void DiscoversVtableOnlyTailCallFunctionNextToInstructionThatEncodesItsTail()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x10000];
// Virtual call whose lwz encodes the address of the tail branch below.
WriteU32(code, 0x0FF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x0FF4, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x0FF8, 0x8061000Cu); // lwz r3,0xc(r1)
WriteU32(code, 0x0FFC, 0x8184BB50u); // lwz r12,-0x44b0(r4)
WriteU32(code, 0x1000, 0x7D8903A6u); // mtctr r12
WriteU32(code, 0x1004, 0x4E800421u); // bctrl
WriteU32(code, 0x1008, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x100C, 0x4E800020u); // blr
WriteU32(code, 0xBAF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0xBAF4, 0x7C0802A6u); // mflr r0
WriteU32(code, 0xBAF8, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0xBAFC, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0xBB00, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0xBB04, 0x4E800020u); // blr
// Frameless virtual reached only through the vtable; it ends in a tail call.
WriteU32(code, 0xBB08, 0x38000008u); // li r0,8
WriteU32(code, 0xBB0C, 0x3880FFFFu); // li r4,-1
WriteU32(code, 0xBB10, 0x90832AB4u); // stw r4,0x2ab4(r3)
WriteU32(code, 0xBB14, 0x38A00000u); // li r5,0
WriteU32(code, 0xBB18, 0x80C3072Cu); // lwz r6,0x72c(r3)
WriteU32(code, 0xBB1C, 0x7C0903A6u); // mtctr r0
WriteU32(code, 0xBB20, 0x80860000u); // lwz r4,0(r6)
WriteU32(code, 0xBB24, 0x38C60004u); // addi r6,r6,4
WriteU32(code, 0xBB28, 0x98A401E9u); // stb r5,0x1e9(r4)
WriteU32(code, 0xBB2C, 0x4200FFF4u); // bdnz -0xc
WriteU32(code, 0xBB30, 0x3C80818Au); // lis r4,0x818a
WriteU32(code, 0xBB34, 0x808441F8u); // lwz r4,0x41f8(r4)
WriteU32(code, 0xBB38, 0x80040024u); // lwz r0,0x24(r4)
WriteU32(code, 0xBB3C, 0x5400103Au); // slwi r0,r0,2
WriteU32(code, 0xBB40, 0x7C000034u); // cntlzw r0,r0
WriteU32(code, 0xBB44, 0x5400D97Eu); // srwi r0,r0,5
WriteU32(code, 0xBB48, 0x980328E1u); // stb r0,0x28e1(r3)
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
// Hook target that follows directly.
WriteU32(code, 0xBB54, 0x3CA0818Au); // lis r5,0x818a
WriteU32(code, 0xBB58, 0x80A541F8u); // lwz r5,0x41f8(r5)
WriteU32(code, 0xBB5C, 0x80050024u); // lwz r0,0x24(r5)
WriteU32(code, 0xBB60, 0x5400103Bu); // slwi. r0,r0,2
WriteU32(code, 0xBB64, 0x41820008u); // beq +8
WriteU32(code, 0xBB68, 0x38800001u); // li r4,1
WriteU32(code, 0xBB6C, EncodeBranch(moduleBase + 0xBB6C, 0x805F0DB4u)); // b base code
WriteU32(code, 0xBB70, 0x9421FFC0u); // stwu r1,-0x40(r1)
WriteU32(code, 0xBB74, 0x7C0802A6u); // mflr r0
WriteU32(code, 0xBB78, 0x4E800020u); // blr
WriteU32(code, 0xF000, moduleBase + 0xBB70); // vtable
WriteU32(code, 0xF004, moduleBase + 0xBB08);
var chunk = CodeChunk(
code,
PointerRelocation(0xF000, 0xBB70),
PointerRelocation(0xF004, 0xBB08),
AbsoluteCommand(KamekCommandId.BranchLink, 0x80601234u, 0xBB54));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0xBB08]);
Assert.Equal("BranchLink", starts[moduleBase + 0xBB54]);
Assert.Equal("module data pointer", starts[moduleBase + 0xBB70]);
Assert.False(starts.ContainsKey(moduleBase + 0xBB50));
}
[Fact]
public void IgnoresUnrelocatedInstructionWordThatEncodesTrampolineAddress()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x10000];
WriteU32(code, 0x100, 0x8184BB50u); // lwz r12,-0x44b0(r4)
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0xBB50);
}
[Fact]
public void IgnoresUnrelocatedWordThatEncodesFunctionEpilogueAddress()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x200, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x204, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x208, 0x93C10008u); // stw r30,8(r1)
WriteU32(code, 0x20C, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0x210, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x214, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x218, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
WriteU32(code, 0x224, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0x228, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x22C, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module prologue scan", starts[moduleBase + 0x200]);
Assert.False(starts.ContainsKey(moduleBase + 0x220));
}
[Fact]
public void DoesNotSplitFunctionAtAnyAddressEncodedByUnrelocatedInstructions()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x3000];
WriteU32(code, 0x2000, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x2004, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x2008, 0x90010014u); // stw r0,0x14(r1)
for (var offset = 0x200C; offset < 0x20F0; offset += 4)
{
WriteU32(code, offset, 0x38630001u); // addi r3,r3,1
}
WriteU32(code, 0x20F0, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0x20F4, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0x20F8, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x20FC, 0x4E800020u); // blr
// lwz r12,disp(r4) for every word of the function body.
for (var offset = 0x2004; offset < 0x2100; offset += 4)
{
WriteU32(code, 0x100 + (offset - 0x2004), 0x81840000u | (uint)offset);
}
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.Contains(starts, start => start.Address == moduleBase + 0x2000);
Assert.DoesNotContain(starts, start => start.Address > moduleBase + 0x2000 && start.Address < moduleBase + 0x2100);
}
[Fact]
public void DiscoversRelocatedPointerToFunctionThatOnlyLooksLikeAnEpilogue()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, moduleBase + 0x220);
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
WriteU32(code, 0x224, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x228, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0x100, 0x220)), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x220]);
}
[Fact]
public void DiscoversConditionallyWrittenModulePointer()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x80];
WriteU32(code, 0x40, moduleBase + 0x50);
WriteU32(code, 0x4C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x50, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x40, 0x50, 0));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresSkippedConditionalPointerWrite()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4), expected value did not match
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
WriteU32(code, 0x300, 0x38600002u); // li r3,2
WriteU32(code, 0x304, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x100, 0x300, 0x12345678u));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
}
[Fact]
public void IgnoresPointerRelocationOverwrittenByLaterWrite()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // Write32 result applied after the relocation
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
WriteU32(code, 0x2FC, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x300, 0x38600002u); // li r3,2
WriteU32(code, 0x304, 0x4E800020u); // blr
var chunk = CodeChunk(
code,
PointerRelocation(0x100, 0x300),
RelativeCommand(KamekCommandId.Write32, 0x100, 0x81840220u));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
}
[Theory]
[InlineData(KamekCommandId.Addr16Lo, 2u)]
[InlineData(KamekCommandId.Addr16Ha, 2u)]
[InlineData(KamekCommandId.Addr16Hi, 2u)]
[InlineData(KamekCommandId.Addr16Lo, 0u)]
[InlineData(KamekCommandId.Addr16Ha, 0u)]
[InlineData(KamekCommandId.Addr16Hi, 0u)]
[InlineData(KamekCommandId.Write32, 0u)]
[InlineData(KamekCommandId.CondWrite32, 0u)]
public void IgnoresWordWhoseOnlyRelocationIsNotAPointerWrite(KamekCommandId id, uint addressDelta)
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
var chunk = CodeChunk(code, RelativeCommand(id, 0x100 + addressDelta, 0x220));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220 && start.Reason == "module data pointer");
}
[Fact]
public void IgnoresAbsolutePointerWriteWhenGatingModuleWords()
{
const uint moduleBase = 0x81840000u;
var code = new byte[0x400];
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x220, 0x38600001u); // li r3,1
WriteU32(code, 0x224, 0x4E800020u); // blr
// A base-game pointer patch whose address field happens to match the module offset.
var chunk = CodeChunk(code, AbsoluteCommand(KamekCommandId.WritePointer, 0x100, 0x300));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
}
[Fact]
public void HonorsPointerRelocationInLastWordOfCode()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0xFC, moduleBase + 0x50);
WriteU32(code, 0x50, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0xFC, 0x50)), moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresUnrelocatedModuleAddressInLastWordOfCode()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0xFC, moduleBase + 0x50);
WriteU32(code, 0x50, 0x4E800020u); // blr
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x50);
}
[Fact]
public void ScansFirstAppendedWordWithoutRelocations()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x200];
WriteU32(relocatedImage, 0x50, 0x4E800020u); // blr
WriteU32(relocatedImage, 0x100, moduleBase + 0x50); // first appended word
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void IgnoresUnrelocatedCodeWordPointingIntoAppendedImage()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x1FC, 0x38630001u); // addi r3,r3,1
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x200);
}
[Fact]
public void StillRejectsRelocatedPointersToData()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x200];
"control\0"u8.ToArray().CopyTo(code, 0x80);
WriteU32(code, 0x20, moduleBase + 0x80);
WriteU32(code, 0x24, moduleBase + 0x180); // zero word
var chunk = CodeChunk(code, PointerRelocation(0x20, 0x80), PointerRelocation(0x24, 0x180));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x80);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x180);
}
[Fact]
public void DiscoversCtorEntriesWithAndWithoutPointerRelocations()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x100];
WriteU32(code, 0x40, moduleBase + 0x60);
WriteU32(code, 0x44, moduleBase + 0x70);
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x60, 0x38600000u); // li r3,0
WriteU32(code, 0x64, 0x4E800020u); // blr
WriteU32(code, 0x6C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x70, 0x38600001u); // li r3,1
WriteU32(code, 0x74, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0x40,
ctorEnd: 0x48,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: [PointerRelocation(0x40, 0x60)]);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("ctor", starts[moduleBase + 0x60]);
Assert.Equal("ctor", starts[moduleBase + 0x70]);
}
[Fact]
public void DiscoversHookAndRelocationBranchTargets()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x200];
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0x60, 0x38600001u); // li r3,1
WriteU32(code, 0x64, 0x4E800020u); // blr
WriteU32(code, 0x9C, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0xA0, 0x38600002u); // li r3,2
WriteU32(code, 0xA4, 0x4E800020u); // blr
WriteU32(code, 0xDC, 0x38630001u); // addi r3,r3,1
WriteU32(code, 0xE0, 0x38600003u); // li r3,3
WriteU32(code, 0xE4, 0x4E800020u); // blr
WriteU32(code, 0x100, EncodeBranch(moduleBase + 0x100, 0x80001000u) | 1u); // bl base code
var chunk = CodeChunk(
code,
AbsoluteCommand(KamekCommandId.Branch, 0x80001234u, 0x60),
AbsoluteCommand(KamekCommandId.BranchLink, 0x80001238u, 0xA0),
RelativeCommand(KamekCommandId.Rel24, 0x100, 0xE0));
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("Branch", starts[moduleBase + 0x60]);
Assert.Equal("BranchLink", starts[moduleBase + 0xA0]);
Assert.Equal("Rel24", starts[moduleBase + 0xE0]);
}
private static KamekChunk CodeChunk(byte[] code, params KamekCommand[] commands) =>
new(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: commands);
private static KamekCommand RelativeCommand(KamekCommandId id, uint offset, params uint[] arguments) =>
new(0, ((uint)id << 24) | offset, id, false, offset, arguments);
private static KamekCommand AbsoluteCommand(KamekCommandId id, uint address, params uint[] arguments) =>
new(0, ((uint)id << 24) | 0x00FFFFFEu, id, true, address, arguments);
private static KamekCommand PointerRelocation(uint offset, uint target) =>
RelativeCommand(KamekCommandId.Addr32, offset, target);
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);