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>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <iomanip>
|
||||
#include "util/armhook.h"
|
||||
#include "util/armhooks.h"
|
||||
#include "game_events.h"
|
||||
#include "lan_event_injection.h"
|
||||
|
||||
void* libapp_base = NULL;
|
||||
@@ -173,11 +174,70 @@ static bool InstallBuildTrackScenePathHook() {
|
||||
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
|
||||
@@ -185,6 +245,12 @@ static constexpr bool kEnableLanEventInjectionHook = true;
|
||||
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) {
|
||||
@@ -196,6 +262,32 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
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;
|
||||
@@ -205,4 +297,23 @@ 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();
|
||||
}
|
||||
Reference in New Issue
Block a user