Fixes the synthetic car_select jump for cold sessions, makes the loadout exit chain safe for real (non-synthetic) events, and adds a native->Kotlin GameEvents bridge (onMapLoaded/onRaceStarted/onRaceEnded/onUpgradesAccepted/ onCarSelected) so both the UI layer and a future native RatNet client can learn what the player picked - car id, accepted upgrades, and paint color (name + RGBA) are all resolved live from the game's own engine state rather than a static extracted table, so they stay correct for any car added later. Includes a Jetpack Compose overlay as a worked example of a UI-side GameEventListener consumer. Full investigation history, root causes, and the several dead ends ruled out along the way are documented in PROGRESS.md (cont. 30-63b). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
319 lines
13 KiB
C++
319 lines
13 KiB
C++
#include <cstring>
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <cerrno>
|
|
#include <android/log.h>
|
|
#include <jni.h>
|
|
#include "main.h"
|
|
#include "util/util.h"
|
|
#include <unistd.h>
|
|
#include <unwind.h>
|
|
#include <dlfcn.h>
|
|
#include <execinfo.h>
|
|
#include <link.h>
|
|
#include <sstream>
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include "util/armhook.h"
|
|
#include "util/armhooks.h"
|
|
#include "game_events.h"
|
|
#include "lan_event_injection.h"
|
|
|
|
void* libapp_base = NULL;
|
|
|
|
static int find_lib_callback(struct dl_phdr_info* info, size_t size, void* data) {
|
|
if (strstr(info->dlpi_name, "libapp.so")) {
|
|
libapp_base = (void*)info->dlpi_addr;
|
|
LOGD("Found libapp.so at base: 0x%08X", (uintptr_t)libapp_base);
|
|
return 1; // Останавливаем перебор
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool get_libapp_base() {
|
|
dl_iterate_phdr(find_lib_callback, NULL);
|
|
if (!libapp_base) {
|
|
Log("libapp.so not found in memory!");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
int (*sub_4087CC)() = nullptr;
|
|
int HOOK_sub_4087CC() {
|
|
Log("Its hooked sub_4087CC()!");
|
|
return sub_4087CC();
|
|
}
|
|
|
|
uintptr_t get_library_base(const char* lib_name) {
|
|
FILE* maps = fopen("/proc/self/maps", "r");
|
|
if (!maps) return 0;
|
|
|
|
char line[512];
|
|
uintptr_t base = 0;
|
|
|
|
while (fgets(line, sizeof(line), maps)) {
|
|
if (strstr(line, lib_name)) {
|
|
base = (uintptr_t)strtoul(strtok(line, "-"), NULL, 16);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(maps);
|
|
return base;
|
|
}
|
|
|
|
bool is_address_executable(void* addr) {
|
|
uintptr_t page = (uintptr_t)addr & ~(getpagesize() - 1);
|
|
return msync((void*)page, getpagesize(), MS_ASYNC) == 0;
|
|
}
|
|
|
|
bool is_memory_writable(void* addr, size_t size) {
|
|
uintptr_t page_start = (uintptr_t)addr & ~(getpagesize() - 1);
|
|
return mprotect((void*)page_start, size, PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
|
|
}
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
using namespace std;
|
|
|
|
// ---- RaceLoaderTask_BuildTrackScenePath hook (see ANALYSIS.md §6j) ----
|
|
// Target compiled in ARM mode (PUSH {R4-R11,LR}; ADD R11,SP,#0x1C - both
|
|
// position-independent, safe to relocate into the trampoline as-is).
|
|
#define BUILDTRACKSCENEPATH_OFFSET 0x2a8424
|
|
|
|
typedef int (*BuildTrackScenePathFn)(void* raceLoaderTask);
|
|
static BuildTrackScenePathFn orig_BuildTrackScenePath = nullptr;
|
|
|
|
// Deliberately different from any real event's track, so a successful
|
|
// override is visually unmistakable. region3/colorado was tried first and
|
|
// abandoned: it's cut/incomplete content in this build - models/environments/
|
|
// has no colorado/ folder at all (only chicago, desert, foothills, garage,
|
|
// newyork), even though region3_colorado_track2.scene.sb itself contains
|
|
// full embedded geometry. The scene's internal m3g loader still tries to
|
|
// open "/published/models/environments/colorado/region3_colorado_track2.m3g"
|
|
// as a loose file, which was never shipped -> geometry never loads -> every
|
|
// downstream consumer (spatial index, checkpoints, ...) sees empty/zero data
|
|
// and null-derefs, which is what the whole sub_53A5FC/sub_52A9B8/sub_52A620/
|
|
// sub_58E5E8 crash chain actually was (see ANALYSIS.md §6n/§6o). Switched to
|
|
// region4_chicago_track4/chicago instead: confirmed shipped and playable
|
|
// (event_01_race.prefabs.sb's real TrackName, verified start/finish actors
|
|
// in the scene, and a full models/environments/chicago/ folder present).
|
|
static const char* kOverrideTrackName = "region4_chicago_track4";
|
|
// Environment prefabs are per-track variants ("chicago1.prefabs.sb" ..
|
|
// "chicago6.prefabs.sb", matching "region4_chicago_track1".."track6"), not a
|
|
// single generic "chicago.prefabs.sb" - confirmed live: env="chicago" alone
|
|
// hit "Could not open database at published/prefabs/environments/chicago.
|
|
// prefabs.sb" followed by an immediate SIGSEGV. Only colorado shipped as one
|
|
// un-numbered colorado.prefabs.sb instead of colorado1..6 - further evidence
|
|
// region3/colorado is unfinished/cut content (see kOverrideTrackName above).
|
|
static const char* kOverrideEnvName = "chicago4";
|
|
|
|
int Hook_BuildTrackScenePath(void* a1) {
|
|
// a1[8] (word offset 8 = byte 32): pointer to the RaceDefinition-like
|
|
// struct. Track name is a {begin,end} pair at byte offsets +72/+76,
|
|
// environment name likewise at +100/+104. BuildTrackScenePath only
|
|
// READS these fields (never frees them), so we just repoint begin/end
|
|
// at our own static buffers instead of freeing/reallocating the
|
|
// originals - avoids guessing an unconfirmed capacity-field offset.
|
|
// The original buffers are deliberately leaked (two small allocations
|
|
// per race load - negligible).
|
|
void* raceDef = *(void**)((uint8_t*)a1 + 32);
|
|
if (raceDef) {
|
|
size_t trackLen = strlen(kOverrideTrackName);
|
|
*(const char**)((uint8_t*)raceDef + 72) = kOverrideTrackName;
|
|
*(const char**)((uint8_t*)raceDef + 76) = kOverrideTrackName + trackLen;
|
|
|
|
size_t envLen = strlen(kOverrideEnvName);
|
|
*(const char**)((uint8_t*)raceDef + 100) = kOverrideEnvName;
|
|
*(const char**)((uint8_t*)raceDef + 104) = kOverrideEnvName + envLen;
|
|
|
|
Log("BuildTrackScenePath hook fired: track -> %s, env -> %s", kOverrideTrackName, kOverrideEnvName);
|
|
} else {
|
|
Log("BuildTrackScenePath hook fired but raceDef (a1[8]) is NULL, skipping override");
|
|
}
|
|
return orig_BuildTrackScenePath(a1);
|
|
}
|
|
|
|
static bool InstallBuildTrackScenePathHook() {
|
|
uintptr_t target = (uintptr_t)libapp_base + BUILDTRACKSCENEPATH_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("BuildTrackScenePath hook: mmap trampoline failed");
|
|
return false;
|
|
}
|
|
|
|
uint32_t* tramp32 = (uint32_t*)tramp;
|
|
// Relocate the 2 displaced original ARM instructions verbatim (both
|
|
// confirmed position-independent: plain PUSH and ADD, no PC-relative
|
|
// addressing), then jump back into the function body past them.
|
|
tramp32[0] = target32[0];
|
|
tramp32[1] = target32[1];
|
|
tramp32[2] = 0xE51FF004; // LDR PC, [PC, #-4]
|
|
tramp32[3] = (uint32_t)(target + 8);
|
|
orig_BuildTrackScenePath = (BuildTrackScenePathFn)tramp;
|
|
|
|
uintptr_t page = target & ~((uintptr_t)getpagesize() - 1);
|
|
if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
|
|
Log("BuildTrackScenePath hook: mprotect target failed: %s", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
target32[0] = 0xE51FF004; // LDR PC, [PC, #-4]
|
|
// &Hook_BuildTrackScenePath has bit0 set (Thumb-compiled mpcore code),
|
|
// triggering the ARM->Thumb interworking switch on load into PC.
|
|
target32[1] = (uint32_t)(uintptr_t)&Hook_BuildTrackScenePath;
|
|
|
|
__builtin___clear_cache((char*)target, (char*)(target + 8));
|
|
__builtin___clear_cache((char*)tramp, (char*)tramp + 16);
|
|
|
|
Log("Installed RaceLoaderTask_BuildTrackScenePath hook at %p, trampoline=%p", (void*)target, tramp);
|
|
return true;
|
|
}
|
|
|
|
// ---- MapScreen constructor trace hook (temporary, RE discovery only) ----
|
|
// Purpose: capture the live `im::app::flow::nfs::MapScreen` instance pointer
|
|
// so we can read its "scroll" layout-entity (found via sub_1332B8's
|
|
// FindOrCreateLayoutEntity call with the literal name "scroll" - see
|
|
// PROGRESS.md) - a Transform-shaped object whose position (offset +36/+40)
|
|
// and scale (offset +44/+48) are hypothesized to be the map's current
|
|
// pan/zoom state, needed to convert a MapTrack's world-space bounds rect
|
|
// (found earlier, offsets +0x44.."+0x50") into real screen pixels.
|
|
#define MAPSCREEN_CTOR_OFFSET 0x1781BC
|
|
|
|
typedef void* (*MapScreenCtorFn)(void* a1);
|
|
static MapScreenCtorFn orig_MapScreenCtor = nullptr;
|
|
void* g_mapScreenInstance = nullptr;
|
|
|
|
void* Hook_MapScreenCtor(void* a1) {
|
|
void* result = orig_MapScreenCtor(a1);
|
|
g_mapScreenInstance = a1;
|
|
Log("MapScreen constructed: %p", a1);
|
|
return result;
|
|
}
|
|
|
|
static bool InstallMapScreenCtorTraceHook() {
|
|
uintptr_t target = (uintptr_t)libapp_base + MAPSCREEN_CTOR_OFFSET;
|
|
uint32_t* target32 = (uint32_t*)target;
|
|
|
|
// Confirmed ARM-mode, position-independent prologue this session
|
|
// (PUSH {R4-R11,LR}; ADD R11,SP,#0x1C), same trampoline pattern as the
|
|
// other hooks in this file.
|
|
void* tramp = mmap(nullptr, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC,
|
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (tramp == MAP_FAILED) {
|
|
Log("MapScreen ctor hook: mmap trampoline failed");
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
orig_MapScreenCtor = (MapScreenCtorFn)tramp;
|
|
|
|
uintptr_t page = target & ~((uintptr_t)getpagesize() - 1);
|
|
if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
|
|
Log("MapScreen ctor hook: mprotect target failed: %s", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
target32[0] = 0xE51FF004; // LDR PC, [PC, #-4]
|
|
target32[1] = (uint32_t)(uintptr_t)&Hook_MapScreenCtor;
|
|
|
|
__builtin___clear_cache((char*)target, (char*)(target + 8));
|
|
__builtin___clear_cache((char*)tramp, (char*)tramp + 16);
|
|
|
|
Log("Installed MapScreen ctor trace hook at %p, trampoline=%p", (void*)target, tramp);
|
|
return true;
|
|
}
|
|
|
|
// 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;
|
|
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
|
|
// from eventually crashing the process on entries our injection adds to the
|
|
// prefab cache. Independent of kEnableLanEventInjectionHook so it can be kept
|
|
// on even if injection itself is toggled off for testing.
|
|
static constexpr bool kEnableSoakTestDisableHook = true;
|
|
|
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|
JNIEnv* env = nullptr;
|
|
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) {
|
|
InitGameEvents(vm, env);
|
|
} else {
|
|
Log("JNI_OnLoad: GetEnv failed, GameEvents bridge not initialised");
|
|
}
|
|
|
|
if (get_libapp_base()) {
|
|
if (kEnableTrackSubstitutionHook) {
|
|
InstallBuildTrackScenePathHook();
|
|
}
|
|
if (kEnableLanEventInjectionHook) {
|
|
InstallMapTrackHandleEventHook();
|
|
}
|
|
if (kEnableSoakTestDisableHook) {
|
|
InstallSoakTestDisableHook();
|
|
}
|
|
if (kEnableMapScreenCtorTraceHook) {
|
|
InstallMapScreenCtorTraceHook();
|
|
}
|
|
InstallCopSoundsTickSkipHook();
|
|
InstallGetComponentNameSkipHook();
|
|
InstallStrlenNullGuardHook();
|
|
InstallInternStringDiagHook();
|
|
InstallFatalLogCallerTraceHook();
|
|
InstallResolveDisplayTextHook();
|
|
InstallResolveDisplayTextWrapperDiagHook();
|
|
InstallLayoutScreenCtorHook();
|
|
InstallModSlotSelectedHook();
|
|
InstallFireOutputDiagHook();
|
|
// NOT installed (cont.43): live-tested and found to break touch
|
|
// responsiveness on car_select once installed, for reasons not yet
|
|
// understood (sub_16C660 itself runs fine every frame through the
|
|
// hook per its own diagnostics - "returned 0" every ~16ms, no hang
|
|
// - yet taps stop registering; reproduced 5/5 tries with the hook
|
|
// 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;
|
|
// the FireOutput-level interception was widened instead (does not
|
|
// need this hook).
|
|
// InstallConfirmCarSelectionHook();
|
|
InstallFlowNodeTickHook();
|
|
}
|
|
|
|
return JNI_VERSION_1_6;
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_nfs_mod_mpcore_MultiplayerCore_bumpBackTraceToLogcat(JNIEnv *env, jobject thiz) {
|
|
//backtraceToLogcat();
|
|
}
|
|
|
|
// cont.44: lets Kotlin (eventually a real lobby-overlay button, for now a
|
|
// debug broadcast receiver - see GameActivityMain.kt) open car_select on
|
|
// demand instead of only automatically at boot. See
|
|
// TriggerOpenCarSelectOnDemand in lan_event_injection.h for the details.
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_nfs_mod_mpcore_MultiplayerCore_triggerCarSelectTest(JNIEnv *env, jobject thiz) {
|
|
TriggerOpenCarSelectOnDemand();
|
|
}
|
|
|
|
// cont.48: experimental TRUE direct jump to car_select, bypassing
|
|
// EventDetails entirely - see TriggerTrueDirectCarSelectJump in
|
|
// lan_event_injection.h for the details and the real risk involved.
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_nfs_mod_mpcore_MultiplayerCore_triggerTrueDirectCarSelectJump(JNIEnv *env, jobject thiz) {
|
|
TriggerTrueDirectCarSelectJump();
|
|
} |