Files
nfsmw-online/mpcore/src/main/cpp/main.cpp
T
megboyzzandClaude Sonnet 5 cde392870e Add LAN car_select flow: real event handling, GameEvents JNI bridge, live car/upgrade/color capture, Compose UI example
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>
2026-08-18 15:30:21 +03:00

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();
}