Fix car class injection, two fresh-save crash bugs, medal-tracking regression; refactor lan_event_injection.h

- Hardcode car class enum for lobby car selection (fixed, small set by design)
- Fix GetCurrentCarColor() crashing on an unvalidated color index from an
  unzeroed hashmap-miss fallback record on a genuinely fresh save
- Default kEnableTrackSubstitutionHook to false: it substituted every race's
  track unconditionally, crashing the game's own scripted prologue race
- Remove GetComponentNameSkipHook from JNI_OnLoad: it unconditionally
  replaced a real name used by the same cache-context the per-event medal
  progress record resolves through, silently breaking medal/street-completion
  tracking for every real race
- Split lan_event_injection.h (3149 lines) into car_selection.h,
  crash_workarounds.h, mod_slot_tracking.h, and a shared
  util/hook_install.h trampoline helper; delete ~490 lines of confirmed-dead
  experimental code; trim comment-heavy sections to essential context

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 22:34:49 +03:00
co-authored by Claude Sonnet 5
parent cde392870e
commit 74ee49881d
6 changed files with 776 additions and 1566 deletions
+125
View File
@@ -0,0 +1,125 @@
#pragma once
// Reads the player's currently-selected car and its persisted paint color
// straight from the game's own live engine state (not a baked/static table),
// so this keeps working for any car added or modded in later. See
// PROGRESS.md cont.61/63/63b/64 for the full derivation of every offset
// below.
#include <cstdint>
#include <cstring>
#include "util/util.h"
extern void* libapp_base;
// The engine's central per-session state singleton (own RTTI-confirmed
// "s_Instance"/"GetInstance" assert strings). +24 = current car's ID string
// pointer, +56 = current car's CarDescription* (used below).
// SingletonInitialise (sub_244CA8) lazily constructs it if some code path
// reads it before its normal owner does - idempotent, safe to call directly.
#define SINGLETON_INSTANCE_OFFSET 0xAD2A08
#define SINGLETON_INITIALISE_OFFSET 0x244CA8
typedef int (*SingletonInitialiseFn)();
static SingletonInitialiseFn SingletonInitialise = nullptr;
// The player's owned-car registry (own "s_Instance"/"Not initialised" assert
// pattern, separate singleton from the one above) and its hashmap lookup:
// LookupCarRecord(registryPtr, &carIdKeySlot) walks buckets keyed by the
// car-id string's own interned pointer value, returning a 24-byte persistent
// record (`+4` = saved paint index) on a hit, or a lazily-`malloc`'d
// (never-zeroed) default record on a miss.
#define GET_CAR_REGISTRY_OFFSET 0x77B70
#define LOOKUP_CAR_RECORD_OFFSET 0x25102C
typedef int (*GetCarRegistryFn)();
typedef int (*LookupCarRecordFn)(int registryPtr, int* carIdKeySlot);
static GetCarRegistryFn GetCarRegistry = nullptr;
static LookupCarRecordFn LookupCarRecord = nullptr;
// CarDescription::GetPaintJobDescription(int paintJobIndex) - a plain
// vector-index accessor: `*(carDescPtr+104) + 112*paintJobIndex`. Its own
// out-of-range assert does NOT actually stop it from computing and
// dereferencing a wild pointer for a bad index - see the cont.66 fix below.
#define GET_PAINT_JOB_DESC_OFFSET 0xB3564
typedef int (*GetPaintJobDescriptionFn)(int carDescPtr, int paintJobIndex);
static GetPaintJobDescriptionFn GetPaintJobDescription = nullptr;
// sub_16692C - rebuilds car_select's browsable car list from the current
// SINGLETON_CARCLASS_FILTER_OFFSET value (same rebuild a live class-filter
// dropdown tap triggers). Resolved but currently unused: three attempts to
// call it programmatically (immediate post-construct, on first Tick, and
// deferred 500ms) all crashed live with a NULL component pointer - see
// PROGRESS.md cont.64. Left declared for a future, better-scoped attempt.
#define REBUILD_CAR_CLASS_LIST_OFFSET 0x16692C
typedef int (*RebuildCarClassListFn)(int screenInstance);
static RebuildCarClassListFn RebuildCarClassList = nullptr;
// Confirmed live (A/B/A across two cars): a direct, header-free pointer to
// the car's ASCII resource-id string (e.g. "ford_focus_rs500_2010_desc").
static const char* GetCurrentCarId() {
void* singletonPtr = *(void**)((uintptr_t)libapp_base + SINGLETON_INSTANCE_OFFSET);
if (!singletonPtr) return nullptr;
return *(const char**)((uint8_t*)singletonPtr + 24);
}
// PaintJobDescription's 112-byte layout (matches the unpacked resource
// schema exactly): six 16-byte string fields (+0 Name, +16
// DiffuseTextureFilePath, +32 DiffuseMaskFilePath, +48 BRDFFilePath, +64
// BRDFSpecularResponseFilePath, +80 NumberPlateTextureFilePath), then +96
// SwatchColor (packed RGBA, one byte each), +100 SwatchColor2, +104 Type,
// +108 UseVinylMap/padding.
struct CarColor {
const char* name;
int r, g, b, a;
};
// cont.66 CRITICAL FIX: GetPaintJobDescription does not actually clamp an
// out-of-range index - it logs an assert but still computes and returns
// `vectorBegin + 112*index` using the original, still out-of-range index.
// LookupCarRecord's own "not found" fallback record is `malloc`'d, never
// zeroed, so its `+4` (color index) can be uninitialized garbage - on a
// fresh save, before the car-record hashmap has any entries, this crashed
// car_select's own CONTINUE (every real race, not just this project's own
// test flow), making the game unplayable end to end. Fix: independently
// read the same vector bounds GetPaintJobDescription itself reads
// (carDescPtr+104/+108) and clamp locally before ever calling it.
static CarColor GetCurrentCarColor() {
CarColor result = {"", 0, 0, 0, 0};
if (!GetCarRegistry || !LookupCarRecord || !GetPaintJobDescription) return result;
void* singletonPtr = *(void**)((uintptr_t)libapp_base + SINGLETON_INSTANCE_OFFSET);
if (!singletonPtr) return result;
int registryPtr = GetCarRegistry();
if (!registryPtr) return result;
int* carIdKeySlot = (int*)((uint8_t*)singletonPtr + 24);
int recordPtr = LookupCarRecord(registryPtr, carIdKeySlot);
if (!recordPtr) return result;
int colorIndex = *(int*)((uint8_t*)(uintptr_t)recordPtr + 4);
int carDescPtr = *(int*)((uint8_t*)singletonPtr + 56);
if (!carDescPtr) return result;
uint32_t vecBegin = *(uint32_t*)((uint8_t*)(uintptr_t)carDescPtr + 104);
uint32_t vecEnd = *(uint32_t*)((uint8_t*)(uintptr_t)carDescPtr + 108);
if (vecEnd < vecBegin || (vecEnd - vecBegin) % 112 != 0) {
Log("GetCurrentCarColor: implausible PaintJobDescriptions vector (begin=0x%x end=0x%x) - bailing",
vecBegin, vecEnd);
return result;
}
int paintJobCount = (int)((vecEnd - vecBegin) / 112);
if (paintJobCount <= 0) return result;
if (colorIndex < 0 || colorIndex >= paintJobCount) {
Log("GetCurrentCarColor: colorIndex=%d out of range (count=%d) - clamping to 0", colorIndex, paintJobCount);
colorIndex = 0;
}
int pjPtr = GetPaintJobDescription(carDescPtr, colorIndex);
if (!pjPtr) return result;
result.name = *(const char**)(uintptr_t)pjPtr; // word[0] = Name's own begin pointer
uint32_t swatch = *(const uint32_t*)((uint8_t*)(uintptr_t)pjPtr + 96);
result.r = swatch & 0xFF;
result.g = (swatch >> 8) & 0xFF;
result.b = (swatch >> 16) & 0xFF;
result.a = (swatch >> 24) & 0xFF;
return result;
}
+200
View File
@@ -0,0 +1,200 @@
#pragma once
// Narrow crash workarounds found while getting the synthetic LAN event flow
// working (PROGRESS.md cont.29-34). Each guards ONE specific null/missing-
// data condition our under-registered synthetic RaceEvent/Actor triggers -
// none of them are meant to change behavior for real, fully-registered game
// objects. See crash_workarounds.h's own per-hook comments for the exact
// condition each one guards; kEnableDiagnosticAndTestHooks-style blanket
// installs are intentionally NOT used here (see the cont.67 lesson on
// GetComponentNameSkipHook below).
#include <cstdint>
#include <cstring>
#include <cerrno>
#include <sys/mman.h>
#include <unistd.h>
#include "util/util.h"
#include "util/hook_install.h"
extern void* libapp_base;
extern void* g_lastSyntheticRaceEvent; // declared in lan_event_injection.h
// im::app::sounds::CopSounds::Tick reads component data our minimal
// synthetic Actor doesn't provide (live SIGSEGV). Ambient audio system,
// unrelated to the actual car-select flow - skip its body entirely rather
// than replicate its full component requirements. Cosmetic only (no
// chase-sound animation), but note this skips the tick for EVERY actor,
// not just our synthetic one - real chase audio is silently disabled too.
#define COPSOUNDS_TICK_OFFSET 0x304AA0
typedef void (*CopSoundsTickFn)(int a1, int* deltaMs);
static CopSoundsTickFn orig_CopSoundsTick = nullptr;
extern "C" void Hook_CopSoundsTick(int a1, int* deltaMs) {
(void)a1;
(void)deltaMs;
// Deliberately not calling orig_CopSoundsTick - see comment above.
}
static bool InstallCopSoundsTickSkipHook() {
orig_CopSoundsTick = (CopSoundsTickFn)InstallArmTrampolineHook(
libapp_base, COPSOUNDS_TICK_OFFSET, (void*)&Hook_CopSoundsTick, "CopSoundsTick skip hook");
return orig_CopSoundsTick != nullptr;
}
// GetComponentName (a component-name-cache lookup): our synthetic Actor
// isn't in its hash table, so it falls into a "build an RTTI class-name
// error string" fallback that crashed (SIGSEGV). cont.67 CRITICAL LESSON:
// an earlier version of this hook unconditionally returned an empty-string
// sentinel for EVERY call (not just the crashing case), assuming the result
// was "purely cosmetic debug text" - it is not. GetComponentName's result
// gets WRITTEN into a real cache-context object field (via sub_240294,
// called from sub_17A99C - the same function real map-event processing
// uses), and blanket-replacing it silently broke the persistent per-event
// "medal earned" progress record for every real race, not just our
// synthetic test event - see PROGRESS.md cont.67 for the full live-bisected
// root-cause writeup. Left DISABLED (not installed from main.cpp) for this
// reason - only re-enable with a fix scoped to the exact crashing input/
// object, never a blanket substitution.
#define GET_COMPONENT_NAME_OFFSET 0x240548
#define EMPTY_STRING_SENTINEL_OFFSET 0xAC80E0
typedef char* (*GetComponentNameFn)(int a1, int* a2);
static GetComponentNameFn orig_GetComponentName = nullptr;
extern "C" char* Hook_GetComponentName(int a1, int* a2) {
(void)a1;
(void)a2;
return (char*)((uintptr_t)libapp_base + EMPTY_STRING_SENTINEL_OFFSET);
}
static bool InstallGetComponentNameSkipHook() {
orig_GetComponentName = (GetComponentNameFn)InstallArmTrampolineHook(
libapp_base, GET_COMPONENT_NAME_OFFSET, (void*)&Hook_GetComponentName, "GetComponentName skip hook");
return orig_GetComponentName != nullptr;
}
// A hand-optimized SWAR strlen() (sub_62F340) dereferences a NULL string
// pointer (fault addr 0x0) - likely reached while building an RTTI/class-
// name debug string for our under-registered synthetic actor, same family
// as the GetComponentName crash above but a different call site. Guard the
// leaf itself: return 0 for NULL, fall through to the real implementation
// otherwise - this one only ever changes behavior for an input that would
// otherwise crash, so it's safe to leave on unconditionally.
#define STRLEN_OFFSET 0x62F340
typedef int (*StrlenFn)(const char* s);
static StrlenFn orig_Strlen = nullptr;
extern "C" int Hook_Strlen(const char* s) {
if (!s) {
Log("Strlen null-guard: called with NULL, returning 0 instead of crashing");
return 0;
}
return orig_Strlen(s);
}
static bool InstallStrlenNullGuardHook() {
orig_Strlen = (StrlenFn)InstallArmTrampolineHook(
libapp_base, STRLEN_OFFSET, (void*)&Hook_Strlen, "Strlen null-guard hook");
return orig_Strlen != nullptr;
}
// sub_40A2B0 is the engine's generic "resolve display text for this key"
// call. On a lookup miss it renders `"XXXXX[" + rawText + "]"` (a real,
// pre-existing missing-localization marker) instead of plain text - our
// synthetic event's name/track fields were never registered as real
// string-table keys, so they always take this path. Rather than replicate
// the string-table registration format, let the original run unmodified
// and post-process its already-allocated output buffer: strip the marker
// in place (shrink-only memmove, no realloc, allocation base untouched so
// a later free() stays safe). Generic fix, not specific to our event - any
// raw-text fallback anywhere in the game renders cleanly instead of with
// the debug marker.
typedef int* (*ResolveDisplayTextFn)(int* outStr, int context, int key);
static ResolveDisplayTextFn orig_ResolveDisplayText = nullptr;
// Diagnostic-only: identifies which key/context resolves to genuinely empty
// content, so a synthetic RaceEvent's missing fields can be found. Budget-
// gated so a runaway caller can't flood logcat.
static volatile int g_emptyResolveLogBudget = 0;
extern "C" int* Hook_ResolveDisplayText(int* outStr, int context, int key) {
int* result = orig_ResolveDisplayText(outStr, context, key);
if (!outStr[0] || !outStr[1] || outStr[0] == outStr[1]) {
// A second lookup-miss branch inside sub_40A2B0 returns a plain
// empty result with no "XXXXX[" marker at all - nothing to strip.
if (g_emptyResolveLogBudget > 0) {
g_emptyResolveLogBudget--;
Log("DIAG ResolveDisplayText BLANK: context=0x%x key=%p key_as_str=\"%s\" caller=%p",
context, (void*)(uintptr_t)key,
key ? (const char*)(uintptr_t)key : "(null)",
__builtin_return_address(0));
}
return result;
}
uint16_t* begin = (uint16_t*)outStr[0];
uint16_t* end = (uint16_t*)outStr[1];
static const uint16_t kPrefix[] = {'X', 'X', 'X', 'X', 'X', '['};
ptrdiff_t len = end - begin;
if (len < 6 || memcmp(begin, kPrefix, sizeof(kPrefix)) != 0) return result;
uint16_t* p = end;
while (p > begin + 6 && *(p - 1) != ']') p--;
if (p <= begin + 6) return result; // no closing bracket found, leave as-is
uint16_t* closeBracket = p - 1;
ptrdiff_t innerLen = closeBracket - (begin + 6);
if (innerLen <= 0) {
if (g_emptyResolveLogBudget > 0) {
g_emptyResolveLogBudget--;
Log("DIAG ResolveDisplayText EMPTY: context=0x%x key=%p key_as_str=\"%s\" caller=%p",
context, (void*)(uintptr_t)key,
key ? (const char*)(uintptr_t)key : "(null)",
__builtin_return_address(0));
}
return result;
}
memmove(begin, begin + 6, innerLen * sizeof(uint16_t));
begin[innerLen] = 0;
outStr[1] = (int)(uintptr_t)(begin + innerLen);
return result;
}
static bool InstallResolveDisplayTextHook() {
orig_ResolveDisplayText = (ResolveDisplayTextFn)InstallArmTrampolineHook(
libapp_base, 0x40A2B0, (void*)&Hook_ResolveDisplayText, "ResolveDisplayText hook");
return orig_ResolveDisplayText != nullptr;
}
// sub_40A29C is the tiny wrapper - `*fieldPtr` then call sub_40A2B0(a1, a2,
// *fieldPtr) - that all real callers actually invoke (sub_40A2B0 is always
// reached via this wrapper's tail-call, so a hook on sub_40A2B0 alone can't
// see the true caller via __builtin_return_address). Hooking here instead
// exposes both the real caller and the field pointer itself, so a null
// field can be reported as "RaceEvent+N" by diffing against
// g_lastSyntheticRaceEvent. Diagnostic-only, budget-gated.
static volatile int g_wrapperNullFieldLogBudget = 0;
typedef int (*ResolveDisplayTextWrapperFn)(int a1, int a2, int* fieldPtr);
static ResolveDisplayTextWrapperFn orig_ResolveDisplayTextWrapper = nullptr;
extern "C" int Hook_ResolveDisplayTextWrapperDiag(int a1, int a2, int* fieldPtr) {
if (g_wrapperNullFieldLogBudget > 0) {
g_wrapperNullFieldLogBudget--;
ptrdiff_t offset = g_lastSyntheticRaceEvent
? ((uint8_t*)fieldPtr - (uint8_t*)g_lastSyntheticRaceEvent)
: -1;
int fieldVal = (fieldPtr) ? *fieldPtr : 0;
Log("DIAG sub_40A29C: fieldPtr=%p (RaceEvent+%ld) *fieldPtr=%p as_str=\"%s\" caller=%p",
(void*)fieldPtr, (long)offset, (void*)(uintptr_t)fieldVal,
fieldVal ? (const char*)(uintptr_t)fieldVal : "(null)",
__builtin_return_address(0));
}
return orig_ResolveDisplayTextWrapper(a1, a2, fieldPtr);
}
static bool InstallResolveDisplayTextWrapperDiagHook() {
orig_ResolveDisplayTextWrapper = (ResolveDisplayTextWrapperFn)InstallArmTrampolineHook(
libapp_base, 0x40A29C, (void*)&Hook_ResolveDisplayTextWrapperDiag, "ResolveDisplayTextWrapper diag hook");
return orig_ResolveDisplayTextWrapper != nullptr;
}
File diff suppressed because it is too large Load Diff
+45 -3
View File
@@ -235,7 +235,24 @@ static bool InstallMapScreenCtorTraceHook() {
// Flip to false to run the game completely unmodified (e.g. to capture a
// baseline/"before" comparison) - true installs the track-substitution hook.
// Just edit this and rebuild, no need to touch anything else.
static constexpr bool kEnableTrackSubstitutionHook = true;
//
// cont.66 CRITICAL FIX: Hook_BuildTrackScenePath (see its own comment)
// substitutes the track/env for EVERY race load with zero gating - not
// just our own synthetic LAN test event, but any real race too, including
// the game's own scripted prologue/tutorial race. Live-tested crash on a
// genuinely fresh save (SIGSEGV, null pointer deref, fault addr 0x14, on a
// background loading thread right after "BuildTrackScenePath hook fired"
// during the prologue's own PreRaceLoadingScreen) - the prologue's other
// scripted data (checkpoints/cutscene triggers/start-finish) still expects
// the ORIGINAL track, so substituting it mismatches and null-derefs
// downstream, same crash class already documented above (region3/colorado
// case). This was never hit before because every previous test session
// used a save that had already passed the prologue - this project's own
// "Track substitution scope" note ("only regular races need to work") was
// an *intent*, never actually enforced in code. Defaulting to `false` -
// this PoC toggle should only be flipped on deliberately, for a specific
// track-substitution test, not left on as the default running state.
static constexpr bool kEnableTrackSubstitutionHook = false;
static constexpr bool kEnableLanEventInjectionHook = true;
static constexpr bool kEnableMapScreenCtorTraceHook = false; // TEMP: isolating a reproducible SIGSEGV, see PROGRESS.md
// See ANALYSIS.md §6ff/§6gg: prevents a QA-only "Soak Test" auto-race feature
@@ -266,7 +283,32 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
InstallMapScreenCtorTraceHook();
}
InstallCopSoundsTickSkipHook();
InstallGetComponentNameSkipHook();
// cont.67 CONFIRMED ROOT CAUSE, permanently disabled: this hook made
// GetComponentName (sub_240548) unconditionally return an empty-string
// sentinel for EVERY call, not just the null-input crash case its own
// original comment described (cont.29/30 - a narrow SIGSEGV while
// triggering our synthetic LAN test event's own car-select flow).
// Live-bisected this session against a real, user-reported regression:
// real races completed and granted cash/SP correctly, but the
// per-event "medal earned" persistent record (read by
// MapTrack::AddEvent/sub_369AB0 via sub_77B70/sub_251188/sub_2515D0/
// sub_250F34 and a reflective "medal" property - feeds the
// street-completion-% badge on the map) was never written, on the
// player's very first tutorial/career race. Decompiling this hook's
// only relevant caller chain found sub_240294 does NOT just log
// GetComponentName's result for a debug label as originally assumed -
// it WRITES it into a named-cache-context object's own +8 field (the
// same kind of cache-context, via sub_7566C, that the medal-lookup
// chain also resolves through), and sub_240294 is itself called from
// sub_17A99C, the same real map-event-processing function this
// project's own FireEventOutput uses. With every hook EXCEPT this one
// installed, medal recording worked correctly live (confirmed twice);
// with this one also installed, it silently failed every time -
// isolating it as the sole cause. Left declared/installable below in
// case the original narrow crash needs revisiting, but must not ship
// installed - it silently corrupts real save progression for every
// player, not just the one synthetic test scenario it was written for.
// InstallGetComponentNameSkipHook();
InstallStrlenNullGuardHook();
InstallInternStringDiagHook();
InstallFatalLogCallerTraceHook();
@@ -283,7 +325,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
// installed vs 1/1 without). sub_16C660 is called at a much higher,
// more global frequency (~60/sec, from app boot onward) than any
// other function hooked in this project - too risky to keep
// chasing blind. See lan_event_injection.h for the full writeup;
// chasing blind. See lan_event_injection.h for the full writeup;
// the FireOutput-level interception was widened instead (does not
// need this hook).
// InstallConfirmCarSelectionHook();
+86
View File
@@ -0,0 +1,86 @@
#pragma once
// Tracks which upgrade the player picks in each loadout mod-slot picker, so
// the accepted set can be dispatched to Kotlin once the player exits back to
// the map. See PROGRESS.md cont.38/39 for the full derivation.
#include <cstdint>
#include "util/util.h"
#include "util/hook_install.h"
#include "game_events.h"
extern void* libapp_base;
// sub_37BF34(slotComponent, selectedEvent) commits a player's pick from the
// loadout screen's mod-picker into that slot's UI. Its only caller dispatches
// a UIRolloutSelectedEvent (type 1057) to EVERY slot's handler, not just the
// one whose picker was open - all slots see the identical event data, and
// only the slot whose own dirty flag (`a1+MODSLOT_DIRTY_FLAG_OFFSET`) is set
// actually applies it (the same flag sub_37BF34 itself checks, read here
// before the original call clears it). Recording without that check was
// live-tested and found to record the SAME pick for every slot from one tap.
#define MODSLOT_SELECTED_OFFSET 0x37BF34
#define MODSLOT_DIRTY_FLAG_OFFSET 289
#define MAX_TRACKED_MOD_SLOTS 8
struct ModSlotSelection {
int slotId;
int carMod; // 0 = no mod selected ("NONE"/empty slot)
};
static ModSlotSelection g_modSlotSelections[MAX_TRACKED_MOD_SLOTS] = {};
static int g_modSlotSelectionCount = 0;
static void RecordModSlotSelection(int slotId, int carMod) {
for (int i = 0; i < g_modSlotSelectionCount; i++) {
if (g_modSlotSelections[i].slotId == slotId) {
g_modSlotSelections[i].carMod = carMod;
Log("ModSlot: slot %d updated -> CarMod=0x%x", slotId, carMod);
return;
}
}
if (g_modSlotSelectionCount < MAX_TRACKED_MOD_SLOTS) {
g_modSlotSelections[g_modSlotSelectionCount].slotId = slotId;
g_modSlotSelections[g_modSlotSelectionCount].carMod = carMod;
g_modSlotSelectionCount++;
Log("ModSlot: slot %d recorded -> CarMod=0x%x", slotId, carMod);
} else {
Log("ModSlot: tracking array full (%d), dropping slot %d selection", MAX_TRACKED_MOD_SLOTS, slotId);
}
}
// Dispatches whatever's accumulated in g_modSlotSelections[] to Kotlin via
// GameEvents.onUpgradesAccepted - called once from Hook_LayoutScreenCtor,
// right as the controlled exit chain lands back on MapOverworld. Forward-
// declared in lan_event_injection.h since that hook is defined earlier in
// the file than this one. Deliberately does not reset
// g_modSlotSelectionCount afterward - see PROGRESS.md cont.39.
static void PersistAcceptedUpgrades() {
int slotIds[MAX_TRACKED_MOD_SLOTS];
int carMods[MAX_TRACKED_MOD_SLOTS];
for (int i = 0; i < g_modSlotSelectionCount; i++) {
slotIds[i] = g_modSlotSelections[i].slotId;
carMods[i] = g_modSlotSelections[i].carMod;
}
Log("PersistAcceptedUpgrades: exiting to map, dispatching %d accepted slot(s) to Kotlin",
g_modSlotSelectionCount);
FireUpgradesAccepted(slotIds, carMods, g_modSlotSelectionCount);
}
typedef int (*ModSlotSelectedFn)(int a1, int a2);
static ModSlotSelectedFn orig_ModSlotSelected = nullptr;
extern "C" int Hook_ModSlotSelected(int a1, int a2) {
bool applies = *(uint8_t*)((uint8_t*)(uintptr_t)a1 + MODSLOT_DIRTY_FLAG_OFFSET) != 0;
if (applies) {
int slotId = *(int*)((uint8_t*)(uintptr_t)a1 + 292);
int carMod = *(int*)((uint8_t*)(uintptr_t)a2 + 8);
RecordModSlotSelection(slotId, carMod);
}
return orig_ModSlotSelected(a1, a2);
}
static bool InstallModSlotSelectedHook() {
orig_ModSlotSelected = (ModSlotSelectedFn)InstallArmTrampolineHook(
libapp_base, MODSLOT_SELECTED_OFFSET, (void*)&Hook_ModSlotSelected, "ModSlotSelected hook");
return orig_ModSlotSelected != nullptr;
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <cerrno>
#include <sys/mman.h>
#include <unistd.h>
#include "util.h"
// Shared installer for this project's one hooking pattern: a 2-word-copy
// trampoline (preserves the target's original first two instructions, then
// jumps back past them) plus an `LDR PC, [PC, #-4]` absolute redirect at the
// target itself. Requires the target's first two instructions to be
// position-independent (e.g. `PUSH {...}; ADD/MOV ...`) - true for every
// call site in this project so far, each confirmed via IDA disasm before
// hooking.
//
// Returns a trampoline pointer (call through it, cast to the real function
// type, to reach the original behavior) or nullptr on failure.
static void* InstallArmTrampolineHook(void* libappBase, uintptr_t offset, void* hookFn, const char* debugName) {
uintptr_t target = (uintptr_t)libappBase + offset;
uint32_t* target32 = (uint32_t*)target;
void* tramp = mmap(nullptr, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (tramp == MAP_FAILED) {
Log("%s: mmap trampoline failed", debugName);
return nullptr;
}
uint32_t* tramp32 = (uint32_t*)tramp;
tramp32[0] = target32[0];
tramp32[1] = target32[1];
tramp32[2] = 0xE51FF004; // LDR PC, [PC, #-4]
tramp32[3] = (uint32_t)(target + 8);
uintptr_t page = target & ~((uintptr_t)getpagesize() - 1);
if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
Log("%s: mprotect target failed: %s", debugName, strerror(errno));
return nullptr;
}
target32[0] = 0xE51FF004; // LDR PC, [PC, #-4]
target32[1] = (uint32_t)(uintptr_t)hookFn;
__builtin___clear_cache((char*)target, (char*)(target + 8));
__builtin___clear_cache((char*)tramp, (char*)tramp + 16);
Log("Installed %s at %p, trampoline=%p", debugName, (void*)target, tramp);
return tramp;
}