Compare commits
3
Commits
e7c76fc2dd
...
1a771ae9ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a771ae9ca | ||
|
|
944aa68b96 | ||
|
|
239a9a6346 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+33
@@ -22,6 +22,39 @@
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="main">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-08-28T12:35:54.649730307Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=2B101JEGR07875" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="unitTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-08-28T12:35:54.649730307Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=2B101JEGR07875" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="androidTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-08-28T12:35:54.649730307Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=2B101JEGR07875" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+4
-4
@@ -9,10 +9,10 @@
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/mpcore" />
|
||||
<option value="$PROJECT_DIR$/savemanager" />
|
||||
<option value="$USER_HOME$/AndroidStudioProjects/NFSMostWanted128" />
|
||||
<option value="$USER_HOME$/AndroidStudioProjects/NFSMostWanted128/app" />
|
||||
<option value="$USER_HOME$/AndroidStudioProjects/NFSMostWanted128/mpcore" />
|
||||
<option value="$USER_HOME$/AndroidStudioProjects/NFSMostWanted128/savemanager" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
||||
Generated
+2
-1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
Generated
+1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$USER_HOME$/AndroidStudioProjects/NFSMostWanted128" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ea.ironmonkey
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import nfs.mod.mpcore.GameEventListener
|
||||
import nfs.mod.mpcore.GameEvents
|
||||
|
||||
// See DEBUG_MENU.md for the full design. Local-only Compose state for now -
|
||||
// Apply doesn't touch any game memory, no real balance getter/setter has
|
||||
// been located yet (see DEBUG_MENU.md §3). Exists purely so the debug panel
|
||||
// has something to edit while that RE work is pending.
|
||||
object DebugMoneyState {
|
||||
var amount by mutableStateOf(0L)
|
||||
private set
|
||||
|
||||
fun apply(newAmount: Long) {
|
||||
amount = newAmount
|
||||
}
|
||||
}
|
||||
|
||||
// Same GameEventListener-as-Compose-state adapter shape as CarSelectionState
|
||||
// (CarSelectionOverlay.kt) - tracks whether the map has loaded at least once
|
||||
// this session, which is when the debug button should appear.
|
||||
object DebugMenuVisibility : GameEventListener {
|
||||
var mapLoaded by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
override fun onMapLoaded() {
|
||||
mapLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
// Call once, e.g. from GameActivityMain.onCreate, to start receiving
|
||||
// onMapLoaded into DebugMenuVisibility.
|
||||
fun registerDebugMenuListener() {
|
||||
GameEvents.register(DebugMenuVisibility)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DebugMenuButton(visible: Boolean, modifier: Modifier = Modifier) {
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
AnimatedVisibility(visible = visible, modifier = modifier) {
|
||||
FloatingActionButton(onClick = { dialogOpen = true }) {
|
||||
Text("DBG")
|
||||
}
|
||||
}
|
||||
if (dialogOpen) {
|
||||
DebugMoneyDialog(onDismiss = { dialogOpen = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugMoneyDialog(onDismiss: () -> Unit) {
|
||||
var text by remember { mutableStateOf(DebugMoneyState.amount.toString()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Debug: Money") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { input -> text = input.filter(Char::isDigit) },
|
||||
label = { Text("Amount") },
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
DebugMoneyState.apply(text.toLongOrNull() ?: DebugMoneyState.amount)
|
||||
onDismiss()
|
||||
}) { Text("Apply") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import com.ea.ironmonkey.ObbHelper.getObbFileName
|
||||
import com.ea.ironmonkey.domain.AssetLocationType
|
||||
import com.ea.nimble.ApplicationLifecycle
|
||||
import com.ea.nimble.Global
|
||||
import nfs.mod.mpcore.DebugFeatures
|
||||
import nfs.mod.mpcore.GameInput
|
||||
import nfs.mod.mpcore.MultiplayerCore.loadCore
|
||||
import nfs.mod.mpcore.MultiplayerCore.loadEmulatedLibapp
|
||||
@@ -101,18 +102,30 @@ class GameActivityMain : AppCompatActivity(), DrawFrameListener {
|
||||
// "select car" button, letting the on-demand car_select-opening call be
|
||||
// exercised at an arbitrary moment (not just automatically at boot) for
|
||||
// live testing. See PROGRESS.md cont.44.
|
||||
// cont.78 FIX: BroadcastReceiver.onReceive runs on the main thread, but
|
||||
// every other call site in this file that touches native game state
|
||||
// (nativeOnPhysicalKeyDown/Up, dispatchSyntheticTap) wraps it in
|
||||
// gameGLSurfaceView.queueEvent{} to marshal onto the GL/render thread,
|
||||
// where MapScreen/FlowNode/etc. actually live. These two receivers were
|
||||
// the one place that called straight into JNI from the main thread -
|
||||
// a real cross-thread race, reproduced live as a consistent
|
||||
// "fault addr 0x8" SIGSEGV inside FireEventOutput on every single fire,
|
||||
// independent of save state or any mpcore hook. Wrapping in queueEvent
|
||||
// matches the pattern used everywhere else in this file.
|
||||
private val carSelectTestReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context?, intent: Intent?) {
|
||||
nfs.mod.mpcore.MultiplayerCore.triggerCarSelectTest()
|
||||
gameGLSurfaceView.queueEvent { nfs.mod.mpcore.MultiplayerCore.triggerCarSelectTest() }
|
||||
}
|
||||
}
|
||||
|
||||
// cont.48 DEBUG: fires the EXPERIMENTAL true-direct-jump variant via
|
||||
// `adb shell am broadcast -a nfs.mod.mpcore.TEST_TRUE_DIRECT_CARSELECT
|
||||
// -p com.ea.games.nfs13_arm`. See PROGRESS.md cont.48.
|
||||
// -p com.ea.games.nfs13_arm` (nfs13_mod on the native32/launcher build -
|
||||
// same trigger, different package id per build flavor). See PROGRESS.md
|
||||
// cont.48, and cont.78 above for why this is wrapped in queueEvent.
|
||||
private val trueDirectCarSelectTestReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context?, intent: Intent?) {
|
||||
nfs.mod.mpcore.MultiplayerCore.triggerTrueDirectCarSelectJump()
|
||||
gameGLSurfaceView.queueEvent { nfs.mod.mpcore.MultiplayerCore.triggerTrueDirectCarSelectJump() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +360,12 @@ class GameActivityMain : AppCompatActivity(), DrawFrameListener {
|
||||
mFrameLayout = FrameLayout(this)
|
||||
mFrameLayout.addView(gameGLSurfaceView)
|
||||
mFrameLayout.addView(buildCarSelectionOverlay())
|
||||
// Explicit debug-tooling gate (see DEBUG_MENU.md) - flip
|
||||
// DebugFeatures.debugMenuEnabled off and this view never gets built
|
||||
// or added, not just hidden.
|
||||
if (DebugFeatures.debugMenuEnabled) {
|
||||
mFrameLayout.addView(buildDebugMenuOverlay())
|
||||
}
|
||||
setContentView(mFrameLayout)
|
||||
// ARM64-only-device prototype (see /ARM64_TRANSLATION_LAYER.md):
|
||||
// fmodex/fmodevent/Nimble/app are all armeabi-v7a-only builds (no
|
||||
@@ -847,6 +866,27 @@ class GameActivityMain : AppCompatActivity(), DrawFrameListener {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug-only menu (see DEBUG_MENU.md), same ComposeView-over-mFrameLayout
|
||||
// pattern as buildCarSelectionOverlay above. Bottom-end corner,
|
||||
// deliberately opposite the car-selection badge's top-start corner so
|
||||
// the two never overlap. Only ever called when
|
||||
// DebugFeatures.debugMenuEnabled is true (see onCreate).
|
||||
private fun buildDebugMenuOverlay(): ComposeView {
|
||||
registerDebugMenuListener()
|
||||
return ComposeView(this).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.BOTTOM or Gravity.END,
|
||||
).apply { setMargins(24, 24, 24, 24) }
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
DebugMenuButton(visible = DebugMenuVisibility.mapLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun setContentView(view: View) {
|
||||
d(TAG, "setContentView($view)")
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
#pragma once
|
||||
|
||||
// Subtask 2.4 — remove police from multiplayer races. See ANALYSIS.md §6hh
|
||||
// and ARCHITECTURE.md §3b for the RE writeup this is built on.
|
||||
//
|
||||
// cont.73/74 CORRECTION: the original hook only covered sub_F7E9C. Tracing
|
||||
// callers found the real shape: sub_F5EA4 is a per-tick dispatcher (itself
|
||||
// only called from sub_F5BB4, the CopManager's broader per-tick Update,
|
||||
// which also does unrelated bookkeeping - bust timers etc. via sub_F9D34/
|
||||
// FA4F8/F6640/F67A8/F6BCC - so that outer function is too broad to skip
|
||||
// wholesale). sub_F5EA4 branches on a live byte flag (`*(a1+4036)`):
|
||||
// true -> sub_F7E9C (cooldown-timer-based single-candidate scheduler)
|
||||
// false -> sub_F8078 (distance-sorted-list scheduler, multiple candidates
|
||||
// checked per tick via the same sub_F82B4 candidate-check +
|
||||
// sub_F85B8/SpawnCopCar call)
|
||||
// Both leaves call the same SpawnCopCar (sub_F85B8, own assert string
|
||||
// confirms the name) - hooking only sub_F7E9C left sub_F8078's branch
|
||||
// completely unblocked, which is almost certainly why cops were still seen
|
||||
// live in cont.73 with the old hook enabled and 0 log lines from it (all
|
||||
// spawn activity was going through the un-hooked branch). Hooking the
|
||||
// dispatcher itself (sub_F5EA4) covers both leaves with one hook and skips
|
||||
// only the spawn-decision logic, not sub_F5BB4's other per-tick cop
|
||||
// maintenance - same "skip a periodic check function" shape as the existing
|
||||
// CopSoundsTick hook (crash_workarounds.h), just at the correct call depth.
|
||||
//
|
||||
// Gated off by default (g_enableCopSpawnSkipTest) for the same reason as
|
||||
// opponent_substitution.h's blanket test: no session/lobby flag exists yet
|
||||
// to distinguish "a multiplayer race is active" from singleplayer, so
|
||||
// leaving this on unconditionally would remove cops from singleplayer too.
|
||||
// Re-enable deliberately only for an isolated live test.
|
||||
|
||||
#include <cstdint>
|
||||
#include "util/util.h"
|
||||
#include "util/hook_install.h"
|
||||
|
||||
extern void* libapp_base;
|
||||
|
||||
#define COP_SPAWN_DISPATCHER_OFFSET 0xF5EA4
|
||||
|
||||
typedef int (*CopSpawnDispatcherFn)(int a1);
|
||||
static CopSpawnDispatcherFn orig_CopSpawnDispatcher = nullptr;
|
||||
|
||||
static volatile bool g_enableCopSpawnSkipTest = true;
|
||||
static volatile int g_copSpawnSkipLogBudget = 50;
|
||||
|
||||
extern "C" int Hook_CopSpawnDispatcher(int a1) {
|
||||
if (g_enableCopSpawnSkipTest) {
|
||||
if (g_copSpawnSkipLogBudget > 0) {
|
||||
g_copSpawnSkipLogBudget--;
|
||||
Log("DIAG CopSpawnDispatcher: skipped (g_enableCopSpawnSkipTest on) a1=%p", (void*)(uintptr_t)a1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return orig_CopSpawnDispatcher(a1);
|
||||
}
|
||||
|
||||
static bool InstallCopSpawnSchedulerSkipHook() {
|
||||
orig_CopSpawnDispatcher = (CopSpawnDispatcherFn)InstallArmTrampolineHook(
|
||||
libapp_base, COP_SPAWN_DISPATCHER_OFFSET, (void*)&Hook_CopSpawnDispatcher,
|
||||
"Cop spawn dispatcher skip hook");
|
||||
return orig_CopSpawnDispatcher != nullptr;
|
||||
}
|
||||
|
||||
// cont.70 CORRECTION: an earlier pass (ANALYSIS.md §6jj) wrongly concluded
|
||||
// TrafficCarCount needed a keyed reflective-lookup hook because sub_2A4D70
|
||||
// (RaceEvent's own deserializer) reads it via the same sub_4F99F0/sub_4F9A80
|
||||
// reflective helper used for every other field in that function. Re-reading
|
||||
// the full decompile: that reflective read happens ONCE, during
|
||||
// deserialization, exactly like RaceType/Location/ClassRestriction/etc. -
|
||||
// the resolved int is then stored at a plain, fixed offset on the live
|
||||
// RaceEvent object (`*(a1+116) = resolvedValue`), same shape as
|
||||
// ClassRestriction at +132. The original cont.69 plan (hook after
|
||||
// deserialization, overwrite the fixed offset) was correct all along.
|
||||
#define RACE_EVENT_DESERIALIZE_OFFSET 0x2A4D70
|
||||
#define RACE_EVENT_TRAFFIC_CAR_COUNT_OFFSET 116
|
||||
|
||||
typedef uint8_t* (*RaceEventDeserializeFn)(int a1, int a2, uint32_t* a3);
|
||||
static RaceEventDeserializeFn orig_RaceEventDeserialize = nullptr;
|
||||
|
||||
static volatile bool g_enableTrafficCarCountZeroTest = false;
|
||||
static volatile int g_trafficCarCountLogBudget = 50;
|
||||
|
||||
extern "C" uint8_t* Hook_RaceEventDeserialize(int a1, int a2, uint32_t* a3) {
|
||||
uint8_t* result = orig_RaceEventDeserialize(a1, a2, a3);
|
||||
if (g_enableTrafficCarCountZeroTest) {
|
||||
int before = *(int*)((uint8_t*)(uintptr_t)a1 + RACE_EVENT_TRAFFIC_CAR_COUNT_OFFSET);
|
||||
*(int*)((uint8_t*)(uintptr_t)a1 + RACE_EVENT_TRAFFIC_CAR_COUNT_OFFSET) = 0;
|
||||
if (g_trafficCarCountLogBudget > 0) {
|
||||
g_trafficCarCountLogBudget--;
|
||||
Log("DIAG RaceEvent::Deserialize: a1=%p TrafficCarCount %d -> 0", (void*)(uintptr_t)a1, before);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool InstallRaceEventTrafficCarCountHook() {
|
||||
orig_RaceEventDeserialize = (RaceEventDeserializeFn)InstallArmTrampolineHook(
|
||||
libapp_base, RACE_EVENT_DESERIALIZE_OFFSET, (void*)&Hook_RaceEventDeserialize,
|
||||
"RaceEvent deserialize (TrafficCarCount) hook");
|
||||
return orig_RaceEventDeserialize != nullptr;
|
||||
}
|
||||
|
||||
// cont.74 CORRECTION: live A/B testing (drive an actual race, not just check
|
||||
// logcat) showed civilian traffic (a red pickup, a blue sedan, etc.) still
|
||||
// present with the TrafficCarCount=0 hook active - that field write was
|
||||
// real (confirmed by logcat) but doesn't gate ambient traffic at all. Traced
|
||||
// the real spawner: sub_33D734 is TrafficCarSpawner's populate function (own
|
||||
// assert string confirms the class name "TrafficCarSpawner"), called twice
|
||||
// per race from sub_33C020 - once per traffic direction/lane-group (a3=0/1)
|
||||
// - with its max-count parameter (a5) read from `*(a1[3]+16)`/`*(a1[3]+20)`,
|
||||
// NOT from RaceEvent.TrafficCarCount (a completely different object/offset).
|
||||
// Whatever RaceEvent.TrafficCarCount actually feeds (not traced further -
|
||||
// not needed once the real gate was found) isn't the base ambient-traffic
|
||||
// density. Hooking sub_33D734 directly and forcing a5 to 0 chokes off both
|
||||
// calls (both directions/lane-groups) at the source, regardless of where
|
||||
// their real inputs come from.
|
||||
#define TRAFFIC_CAR_SPAWNER_POPULATE_OFFSET 0x33D734
|
||||
|
||||
typedef int* (*TrafficCarSpawnerPopulateFn)(int a1, int a2, int a3, int a4, int a5);
|
||||
static TrafficCarSpawnerPopulateFn orig_TrafficCarSpawnerPopulate = nullptr;
|
||||
|
||||
static volatile bool g_enableTrafficSpawnerZeroTest = false;
|
||||
static volatile int g_trafficSpawnerLogBudget = 50;
|
||||
|
||||
extern "C" int* Hook_TrafficCarSpawnerPopulate(int a1, int a2, int a3, int a4, int a5) {
|
||||
int effectiveMax = a5;
|
||||
if (g_enableTrafficSpawnerZeroTest) {
|
||||
effectiveMax = 0;
|
||||
}
|
||||
if (g_trafficSpawnerLogBudget > 0) {
|
||||
g_trafficSpawnerLogBudget--;
|
||||
Log("DIAG TrafficCarSpawner::Populate: mode=%d maxCount=%d -> %d", a3, a5, effectiveMax);
|
||||
}
|
||||
return orig_TrafficCarSpawnerPopulate(a1, a2, a3, a4, effectiveMax);
|
||||
}
|
||||
|
||||
static bool InstallTrafficCarSpawnerZeroHook() {
|
||||
orig_TrafficCarSpawnerPopulate = (TrafficCarSpawnerPopulateFn)InstallArmTrampolineHook(
|
||||
libapp_base, TRAFFIC_CAR_SPAWNER_POPULATE_OFFSET, (void*)&Hook_TrafficCarSpawnerPopulate,
|
||||
"TrafficCarSpawner::Populate zero hook");
|
||||
return orig_TrafficCarSpawnerPopulate != nullptr;
|
||||
}
|
||||
|
||||
// cont.75: the Populate do-while (above) can only reduce traffic to a
|
||||
// structural minimum (>=1 candidate per track waypoint), never zero - traced
|
||||
// one level further to find where a candidate actually becomes a visible,
|
||||
// positioned car on the road. sub_C26A0 is a generic "CarReset" utility (own
|
||||
// assert strings confirm "foundTrackInfo"/"Reset") - resolves a spline
|
||||
// distance + lateral offset to a world position (sub_2B2D18/sub_85660, same
|
||||
// TrackNavigator-family shape as PlaceCar/SpawnCopCar) and writes it via the
|
||||
// same sub_10B09C/sub_D5138 transform-write pair used by PlaceCar (subtask
|
||||
// 2.2) and SpawnCopCar (subtask 2.4) - i.e. this is the actual moment a
|
||||
// traffic car's position becomes real. It has two callers: sub_2A0470 (the
|
||||
// "ResetLine" event handler, via sub_31AAEC) and sub_C201C.
|
||||
//
|
||||
// First attempt hooked sub_2A0470 on the theory it handled both initial
|
||||
// placement and periodic recycling. Live-tested and DISPROVEN: across a full
|
||||
// race replay the hook's own diagnostic log (unconditional, not gated by the
|
||||
// test flag) never fired once, yet a traffic pickup truck was clearly
|
||||
// visible and moving the whole time - sub_2A0470 simply isn't on the path
|
||||
// that keeps an ordinary traffic car on the road during a normal-length
|
||||
// race. Left in place (harmless, possibly still relevant for very long
|
||||
// races) but is NOT the real fix.
|
||||
//
|
||||
// sub_C201C is: own second parameter carries a delta-time value (`*a2 *
|
||||
// -0.001` pattern), and it calls sub_C26A0 twice - once when an idle/wander
|
||||
// timer at `a1+88` counts down to zero (computing a fresh position via
|
||||
// sub_690A94), once to retry a previously-deferred reset stored at
|
||||
// `a1+112..128` (the exact same field layout sub_C26A0 itself writes on its
|
||||
// own "can't resolve position yet, queue it" fallback path) - i.e. this is
|
||||
// the traffic car's own per-tick controller, not a generic car utility.
|
||||
// Skipping it wholesale prevents that car from ever being (re)positioned on
|
||||
// the road at all.
|
||||
#define TRAFFIC_CAR_CONTROLLER_TICK_OFFSET 0xC201C
|
||||
|
||||
typedef int (*TrafficCarControllerTickFn)(int a1, int* a2);
|
||||
static TrafficCarControllerTickFn orig_TrafficCarControllerTick = nullptr;
|
||||
|
||||
// cont.77 CORRECTION: live-testing on a second track ("ПРОМЫШЛ. СОБЫТИЕ")
|
||||
// showed a traffic car sitting frozen right at the starting grid, causing a
|
||||
// spawn collision - unconfirmed on the first track (Reynolds Lane), where a
|
||||
// hook this thorough happened not to matter. Root cause: this wholesale skip
|
||||
// prevents sub_C201C's idle-timer branch from EVER calling sub_C26A0
|
||||
// (CarReset), which means the traffic car never receives ANY position -
|
||||
// not "no position" (invisible), but whatever its pooled/reused object's
|
||||
// stale transform already was, which can be anywhere, including right on
|
||||
// the grid. Skipping unconditionally forever is wrong; the fix is a
|
||||
// once-per-object CarReset hook below, which lets each traffic car receive
|
||||
// exactly one real, TrackNavigator-resolved position (same placement logic
|
||||
// used for opponents/cops - never intentionally at the grid) and then
|
||||
// freezes it there, rather than leaving it at an arbitrary stale transform.
|
||||
// This hook is now disabled by default in favor of Hook_CarReset.
|
||||
static volatile bool g_enableTrafficControllerSkipTest = false;
|
||||
static volatile int g_trafficControllerLogBudget = 50;
|
||||
|
||||
extern "C" int Hook_TrafficCarControllerTick(int a1, int* a2) {
|
||||
if (g_trafficControllerLogBudget > 0) {
|
||||
g_trafficControllerLogBudget--;
|
||||
Log("DIAG TrafficCarControllerTick: a1=%p enabled=%d", (void*)(uintptr_t)a1, (int)g_enableTrafficControllerSkipTest);
|
||||
}
|
||||
if (g_enableTrafficControllerSkipTest) {
|
||||
return 0;
|
||||
}
|
||||
return orig_TrafficCarControllerTick(a1, a2);
|
||||
}
|
||||
|
||||
static bool InstallTrafficCarControllerSkipHook() {
|
||||
orig_TrafficCarControllerTick = (TrafficCarControllerTickFn)InstallArmTrampolineHook(
|
||||
libapp_base, TRAFFIC_CAR_CONTROLLER_TICK_OFFSET, (void*)&Hook_TrafficCarControllerTick,
|
||||
"Traffic car controller tick skip hook");
|
||||
return orig_TrafficCarControllerTick != nullptr;
|
||||
}
|
||||
|
||||
// cont.77: the real fix. Hook CarReset (sub_C26A0) itself - the shared
|
||||
// position-write primitive both sub_C201C and sub_2A0470 call into (see
|
||||
// §6mm) - and let each distinct traffic-car "this" pointer through exactly
|
||||
// once (a real, TrackNavigator-resolved position, same system used for
|
||||
// opponents/cops, so it lands somewhere sane on the road, never at the
|
||||
// grid by construction), then no-op every subsequent call for that same
|
||||
// object. Net effect: traffic cars appear once, parked, never move again -
|
||||
// no continuously-cycling ambient traffic, no risk of a frozen car sitting
|
||||
// at an arbitrary stale (possibly grid-overlapping) transform.
|
||||
#define CAR_RESET_OFFSET 0xC26A0
|
||||
|
||||
typedef int (*CarResetFn)(int a1, uint32_t* a2, int a3, int a4);
|
||||
static CarResetFn orig_CarReset = nullptr;
|
||||
|
||||
static volatile bool g_enableTrafficOnceOnlyResetTest = true;
|
||||
static const int kSeenCarResetCapacity = 128;
|
||||
static int g_seenCarResetControllers[kSeenCarResetCapacity];
|
||||
static volatile int g_seenCarResetCount = 0;
|
||||
static volatile int g_carResetLogBudget = 50;
|
||||
|
||||
extern "C" int Hook_CarReset(int a1, uint32_t* a2, int a3, int a4) {
|
||||
if (!g_enableTrafficOnceOnlyResetTest) {
|
||||
return orig_CarReset(a1, a2, a3, a4);
|
||||
}
|
||||
for (int i = 0; i < g_seenCarResetCount; i++) {
|
||||
if (g_seenCarResetControllers[i] == a1) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (g_seenCarResetCount < kSeenCarResetCapacity) {
|
||||
g_seenCarResetControllers[g_seenCarResetCount++] = a1;
|
||||
}
|
||||
if (g_carResetLogBudget > 0) {
|
||||
g_carResetLogBudget--;
|
||||
Log("DIAG CarReset: first-and-only real placement for a1=%p (seen=%d)", (void*)(uintptr_t)a1, g_seenCarResetCount);
|
||||
}
|
||||
return orig_CarReset(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
static bool InstallCarResetOnceOnlyHook() {
|
||||
orig_CarReset = (CarResetFn)InstallArmTrampolineHook(
|
||||
libapp_base, CAR_RESET_OFFSET, (void*)&Hook_CarReset,
|
||||
"CarReset once-only (traffic) hook");
|
||||
return orig_CarReset != nullptr;
|
||||
}
|
||||
|
||||
#define TRAFFIC_RESET_LINE_HANDLER_OFFSET 0x2A0470
|
||||
|
||||
typedef int (*TrafficResetLineHandlerFn)(uint32_t* a1);
|
||||
static TrafficResetLineHandlerFn orig_TrafficResetLineHandler = nullptr;
|
||||
|
||||
static volatile bool g_enableTrafficResetLineSkipTest = false;
|
||||
static volatile int g_trafficResetLineLogBudget = 50;
|
||||
|
||||
extern "C" int Hook_TrafficResetLineHandler(uint32_t* a1) {
|
||||
if (g_trafficResetLineLogBudget > 0) {
|
||||
g_trafficResetLineLogBudget--;
|
||||
Log("DIAG TrafficResetLineHandler: a1=%p enabled=%d", (void*)a1, (int)g_enableTrafficResetLineSkipTest);
|
||||
}
|
||||
if (g_enableTrafficResetLineSkipTest) {
|
||||
return 0;
|
||||
}
|
||||
return orig_TrafficResetLineHandler(a1);
|
||||
}
|
||||
|
||||
static bool InstallTrafficResetLineSkipHook() {
|
||||
orig_TrafficResetLineHandler = (TrafficResetLineHandlerFn)InstallArmTrampolineHook(
|
||||
libapp_base, TRAFFIC_RESET_LINE_HANDLER_OFFSET, (void*)&Hook_TrafficResetLineHandler,
|
||||
"Traffic ResetLine handler skip hook");
|
||||
return orig_TrafficResetLineHandler != nullptr;
|
||||
}
|
||||
@@ -1277,6 +1277,60 @@ static bool InstallFireOutputDiagHook() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cont.78 DIAG: TriggerOpenCarSelectOnDemand's own FireEventOutput call
|
||||
// (sub_17A99C) reliably SIGSEGVs (fault addr 0x8) firing our synthetic key
|
||||
// 0xC0FFEE00, before Hook_FireOutputDiag's "output=..." line ever prints -
|
||||
// i.e. it never reaches the real FireOutput/FlowNode dispatch at all. The
|
||||
// crash is inside FireEventOutput's own diagnostic-label-building code
|
||||
// (builds a "Database of <Type> prefabs has no entry for component on
|
||||
// actor <id>" string when a key has no matching prefab-database entry -
|
||||
// own strings confirm this), independent of save state (reproduced on
|
||||
// both the 100%-save and the original small save) and independent of
|
||||
// threading (still crashes on GLThread after cont.78's queueEvent fix).
|
||||
// sub_240548 is the specific function that does the prefab lookup + builds
|
||||
// that message. Hooking its entry to log inputs before calling through -
|
||||
// purely diagnostic, always calls orig unconditionally, budget-capped.
|
||||
#define PREFAB_LOOKUP_DIAG_OFFSET 0x240548
|
||||
typedef char* (*PrefabLookupFn)(int a1, uint32_t* a2);
|
||||
static PrefabLookupFn orig_PrefabLookup = nullptr;
|
||||
static volatile int g_prefabLookupLogBudget = 20;
|
||||
|
||||
extern "C" char* Hook_PrefabLookupDiag(int a1, uint32_t* a2) {
|
||||
if (g_prefabLookupLogBudget > 0) {
|
||||
g_prefabLookupLogBudget--;
|
||||
uint32_t a2_0 = a2 ? a2[0] : 0;
|
||||
uint32_t a2_1 = a2 ? a2[1] : 0;
|
||||
uint32_t a2_2 = a2 ? a2[2] : 0;
|
||||
Log("DIAG PrefabLookup: a1=%p a2=%p a2[0]=0x%x a2[1]=0x%x a2[2]=0x%x",
|
||||
(void*)(uintptr_t)a1, (void*)a2, a2_0, a2_1, a2_2);
|
||||
}
|
||||
// cont.78 live finding: a2 is an eastl-style {begin,end,capacity} string
|
||||
// range (the "EVENT ID <n>" debug label FireEventOutput builds for its
|
||||
// prefab-database lookup) - captured live with end==0 while begin was a
|
||||
// valid heap pointer, a malformed/corrupt range that crashes downstream
|
||||
// (fault addr 0x8) inside this lookup. Our synthetic event keys
|
||||
// (0xC0FFEE00 family) are all negative as signed int32 (top bit set);
|
||||
// the int-to-string conversion feeding this label plausibly mishandles
|
||||
// that, producing this corrupt range. Defensive repair: if end is
|
||||
// before begin, collapse to an empty (but well-formed) string instead
|
||||
// of letting the corrupt range reach the lookup/message-building code -
|
||||
// matches this binary's own many existing "detect bad state, substitute
|
||||
// a safe default" guards rather than trying to patch the real root
|
||||
// cause (the number-formatting call) blind.
|
||||
if (a2 && a2[0] != 0 && a2[1] < a2[0]) {
|
||||
Log("DIAG PrefabLookup: repairing malformed range a2[1] (0x%x) < a2[0] (0x%x) -> collapsing to empty string",
|
||||
a2[1], a2[0]);
|
||||
a2[1] = a2[0];
|
||||
}
|
||||
return orig_PrefabLookup(a1, a2);
|
||||
}
|
||||
|
||||
static bool InstallPrefabLookupDiagHook() {
|
||||
orig_PrefabLookup = (PrefabLookupFn)InstallArmTrampolineHook(
|
||||
libapp_base, PREFAB_LOOKUP_DIAG_OFFSET, (void*)&Hook_PrefabLookupDiag, "Prefab lookup diag hook");
|
||||
return orig_PrefabLookup != nullptr;
|
||||
}
|
||||
|
||||
// Three one-shot diagnostic hooks used to have their install functions
|
||||
// here (cont.43/54/55: a "who fires this BACK cascade" tracer hooking
|
||||
// sub_1A7920, a field-value dumper on sub_170EC8, and an attempt to
|
||||
|
||||
@@ -56,6 +56,21 @@
|
||||
// out-parameter calls at all, so they ARE fully ported and are the real
|
||||
// end-to-end proof this mechanism works.
|
||||
// #include "lan_event_injection.h"
|
||||
//
|
||||
// Branch merge (2026-09-22): opponent_substitution.h and cop_traffic_disable.h
|
||||
// landed from the native32/lan-event-injection-poc branch, where they were
|
||||
// built and live-confirmed against a real dlopen'd libapp.so - proven
|
||||
// correct, but every one of their hooks resolves and calls raw
|
||||
// "libapp_base + OFFSET" function pointers directly (this file's own
|
||||
// InstallArmTrampolineHook pattern), the exact same shape as
|
||||
// lan_event_injection.h's un-ported call sites above. On THIS build there is
|
||||
// no real ARM32 code at that address to jump to - only Unicorn-backed guest
|
||||
// memory - so including these unmodified would not just misbehave, it would
|
||||
// jump the host CPU into data and crash immediately. Left as source
|
||||
// (untouched, not included) for the same follow-up porting session as
|
||||
// lan_event_injection.h, not wired in here.
|
||||
// #include "opponent_substitution.h"
|
||||
// #include "cop_traffic_disable.h"
|
||||
|
||||
void* libapp_base = NULL;
|
||||
|
||||
@@ -513,6 +528,15 @@ Java_com_ea_ironmonkey_GameActivityMain_nativeInstallCrashHandler(JNIEnv* env, j
|
||||
if (b) env->ReleaseStringUTFChars(buildStamp, b);
|
||||
}
|
||||
|
||||
// cont.69/70: subtask 2.1 diagnostic pass - logs OpponentCollection's built
|
||||
// Opponent vector and StreetRaceStartingGrid's own placement-vector argument
|
||||
// side by side, to confirm live whether they're the same set before writing
|
||||
// the real substitution hook (see opponent_substitution.h's own comment and
|
||||
// ANALYSIS.md §6hh). Diagnostic-only for now - does not change any opponent
|
||||
// data yet, only logs. Gated independently so it can stay on for real-race
|
||||
// testing without pulling in unrelated hooks.
|
||||
static constexpr bool kEnableOpponentGridDiagnosticHooks = true;
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env = nullptr;
|
||||
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) {
|
||||
@@ -520,6 +544,21 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
} else {
|
||||
Log("JNI_OnLoad: GetEnv failed, GameEvents bridge not initialised");
|
||||
}
|
||||
// Branch merge (2026-09-22): the native32/lan-event-injection-poc
|
||||
// branch's own JNI_OnLoad installs ~20 hooks here (opponent
|
||||
// substitution, cop/traffic removal, the LAN event-injection state
|
||||
// machine, several crash workarounds) against a real dlopen'd
|
||||
// libapp.so, available at JNI_OnLoad time. On this build libapp.so
|
||||
// isn't loaded until Java calls loadEmulatedLibapp() well after
|
||||
// JNI_OnLoad (see LoadEmulatedLibapp's own comment - no
|
||||
// Context/AssetManager access this early) - `get_libapp_base()` would
|
||||
// always be null here regardless, and every one of those Install*Hook
|
||||
// calls resolves a raw "libapp_base + OFFSET" function pointer that
|
||||
// needs GuestFn/CallGuestFunction plumbing to be callable at all (see
|
||||
// this file's own comment above the commented-out includes). None of
|
||||
// it is wired in here - deliberately, not an oversight - pending the
|
||||
// same porting session as lan_event_injection.h/
|
||||
// opponent_substitution.h/cop_traffic_disable.h themselves.
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#pragma once
|
||||
|
||||
// Subtask 2.1 — substitute real lobby players' car+color into AI opponent
|
||||
// slots. See ANALYSIS.md §6hh and ARCHITECTURE.md §3b for the full RE
|
||||
// writeup this is built on.
|
||||
//
|
||||
// cont.70 CONFIRMED LIVE: OpponentCollection::PopulateFromProperties
|
||||
// (sub_2B649C) builds a vector of Opponent* (8-byte elements) at
|
||||
// this+12/+16/+20. StreetRaceStartingGrid's own placement method
|
||||
// (sub_2B88BC) separately iterates a DIFFERENT vector (12-byte elements,
|
||||
// different heap addresses entirely, confirmed by comparing live pointer
|
||||
// dumps from both hooks) - so the two are NOT the same objects, there's an
|
||||
// intermediate spawn/resolve step between them. Despite that, overwriting
|
||||
// Opponent.CarDescriptionName/ColourIndex (+20/+24 string, +36 int) at
|
||||
// Populate time (map-load) WAS confirmed live to reach the actual spawned,
|
||||
// rendered car at the starting grid - screenshotted a Ford Focus RS500
|
||||
// forced into an opponent slot that was a completely different car/color
|
||||
// before the hook, on a real (non-synthetic) "Перед вами FAIRHAVEN" replay.
|
||||
// Whatever the intermediate step is, it reads CarDescriptionName/ColourIndex
|
||||
// off these same Opponent objects (or a value-copy taken after Populate
|
||||
// already ran), not off some earlier, already-fixed snapshot. Mechanism is
|
||||
// proven; the intermediate step itself was not traced (not needed for the
|
||||
// question this was testing).
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include "util/util.h"
|
||||
#include "util/hook_install.h"
|
||||
|
||||
extern void* libapp_base;
|
||||
|
||||
#define OPPONENT_COLLECTION_POPULATE_OFFSET 0x2B649C
|
||||
#define STREET_RACE_GRID_PLACE_OFFSET 0x2B88BC
|
||||
|
||||
typedef int (*OpponentCollectionPopulateFn)(uint32_t* thisPtr, int a2, uint32_t* a3);
|
||||
static OpponentCollectionPopulateFn orig_OpponentCollectionPopulate = nullptr;
|
||||
|
||||
static volatile int g_opponentPopulateLogBudget = 2000;
|
||||
|
||||
// Blanket test substitution (every opponent slot of EVERY collection, any
|
||||
// race) that produced the cont.70 live confirmation above. Kept here, but
|
||||
// gated OFF by default (g_enableBlanketOpponentSubstitutionTest) - it has no
|
||||
// way to target a specific race or a specific real lobby player yet (no
|
||||
// lobby system exists to source that data from), so leaving it on would
|
||||
// silently affect every race in normal play. Re-enable deliberately only for
|
||||
// another isolated live test; the real, production substitution hook
|
||||
// (targeted, sourced from actual lobby player data) is a separate follow-up
|
||||
// once the lobby UI/data model exists to drive it.
|
||||
//
|
||||
// cont.72: extended from a single fixed test car (cont.70) to a small roster
|
||||
// of distinct cars/colors cycling by slot index, to emulate what a real
|
||||
// multi-player lobby would look like (each opponent slot = "a different
|
||||
// player's own car"), per direct request.
|
||||
struct TestSubstituteCar {
|
||||
const char* carId;
|
||||
int colourIndex;
|
||||
};
|
||||
// cont.76: user-requested specific demo roster (car IDs sourced from
|
||||
// unpacked game data files, not binary strings, per user's own correction).
|
||||
static const TestSubstituteCar kTestSubstituteRoster[] = {
|
||||
{"marussia_b2_2011_desc", 0},
|
||||
{"lamborghini_gallardo_lp570_4_superleggera_2011_desc", 0},
|
||||
{"srt_viper_2013_desc", 0},
|
||||
{"lamborghini_aventador_lp700_4_2011_desc", 0},
|
||||
};
|
||||
static const int kTestSubstituteRosterLength =
|
||||
(int)(sizeof(kTestSubstituteRoster) / sizeof(kTestSubstituteRoster[0]));
|
||||
static volatile bool g_enableBlanketOpponentSubstitutionTest = true;
|
||||
|
||||
extern "C" int Hook_OpponentCollectionPopulate(uint32_t* thisPtr, int a2, uint32_t* a3) {
|
||||
int result = orig_OpponentCollectionPopulate(thisPtr, a2, a3);
|
||||
uint32_t* begin = (uint32_t*)thisPtr[3];
|
||||
uint32_t* end = (uint32_t*)thisPtr[4];
|
||||
int count = begin ? (int)(end - begin) / 2 : 0;
|
||||
|
||||
if (count > 0 && g_enableBlanketOpponentSubstitutionTest) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint32_t* opp = (uint32_t*)begin[i * 2];
|
||||
if (!opp) continue;
|
||||
const TestSubstituteCar& car = kTestSubstituteRoster[i % kTestSubstituteRosterLength];
|
||||
size_t len = strlen(car.carId);
|
||||
*(const char**)((uint8_t*)opp + 20) = car.carId;
|
||||
*(const char**)((uint8_t*)opp + 24) = car.carId + len;
|
||||
*(int*)((uint8_t*)opp + 36) = car.colourIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_opponentPopulateLogBudget > 0) {
|
||||
g_opponentPopulateLogBudget--;
|
||||
Log("DIAG OpponentCollection::Populate: this=%p vec=[%p..%p) count=%d", thisPtr, begin, end, count);
|
||||
for (int i = 0; i < count && i < 8; i++) {
|
||||
uint32_t* opp = (uint32_t*)begin[i * 2];
|
||||
if (!opp) {
|
||||
Log(" [%d] Opponent*=NULL", i);
|
||||
continue;
|
||||
}
|
||||
const char* nameBegin = *(const char**)((uint8_t*)opp + 20);
|
||||
const char* nameEnd = *(const char**)((uint8_t*)opp + 24);
|
||||
int nameLen = (nameEnd && nameBegin && nameEnd > nameBegin) ? (int)(nameEnd - nameBegin) : 0;
|
||||
char buf[96];
|
||||
if (nameLen > 0 && nameLen < (int)sizeof(buf)) {
|
||||
memcpy(buf, nameBegin, nameLen);
|
||||
buf[nameLen] = 0;
|
||||
} else {
|
||||
buf[0] = 0;
|
||||
}
|
||||
int colourIndex = *(int*)((uint8_t*)opp + 36);
|
||||
Log(" [%d] Opponent*=%p CarDescriptionName=\"%s\" ColourIndex=%d (AFTER any substitution)", i, opp, buf, colourIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool InstallOpponentCollectionPopulateHook() {
|
||||
orig_OpponentCollectionPopulate = (OpponentCollectionPopulateFn)InstallArmTrampolineHook(
|
||||
libapp_base, OPPONENT_COLLECTION_POPULATE_OFFSET, (void*)&Hook_OpponentCollectionPopulate,
|
||||
"OpponentCollection::Populate diag hook");
|
||||
return orig_OpponentCollectionPopulate != nullptr;
|
||||
}
|
||||
|
||||
typedef int (*StreetRaceGridPlaceFn)(int a1, int* a2, int a3, uint32_t* a4);
|
||||
static StreetRaceGridPlaceFn orig_StreetRaceGridPlace = nullptr;
|
||||
|
||||
static volatile int g_gridPlaceLogBudget = 20;
|
||||
|
||||
// cont.72: random player grid position. sub_2B88BC's own algorithm (see
|
||||
// ANALYSIS.md §6hh Q2) is why the player is always last - it loops over
|
||||
// every opponent first (accumulating a randomized inter-car distance each
|
||||
// time via sub_291BA4/PlaceOpponent), THEN places the player once, at
|
||||
// whatever distance the loop finished on. There's no field to flip for
|
||||
// this - the "always last" behavior is baked into the call ORDER, so
|
||||
// randomizing it means reimplementing that order, not just tweaking data
|
||||
// beforehand/afterward like every other hook in this project. This calls
|
||||
// the same two real placement primitives orig uses (sub_291BA4 for an
|
||||
// opponent - keeps its own lateral zigzag math untouched; sub_2914DC/
|
||||
// PlaceCar directly for the player, lateral=0, matching orig's own player
|
||||
// call) in a randomly reordered sequence: count+1 total slots (N opponents
|
||||
// + 1 player), one is picked at random for the player, the rest go to
|
||||
// opponents in their original order. Distance accumulation matches orig's
|
||||
// own shape (place, then advance by a random offset in
|
||||
// [MinDistanceBetweenRacers, MaxDistanceBetweenRacers]) but uses plain
|
||||
// rand() instead of replicating sub_75680/sub_61C9F8's own RNG chain
|
||||
// (which is seeded from the just-placed car's own return value in a way
|
||||
// not fully understood - not worth the risk of misusing it for a test
|
||||
// hook). Falls back to the real, untouched orig_StreetRaceGridPlace
|
||||
// whenever this test is disabled - zero behavior change for normal play.
|
||||
typedef int (*PlaceOpponentFn)(int a1, int a2, int a3, float a4, float a5, int a6);
|
||||
typedef int (*PlaceCarFn)(int a1, uint32_t* a2, int a3, int a4, float a5);
|
||||
static PlaceOpponentFn PlaceOpponent = nullptr;
|
||||
static PlaceCarFn PlaceCar = nullptr;
|
||||
|
||||
static volatile bool g_enableRandomPlayerGridPositionTest = true;
|
||||
|
||||
extern "C" int Hook_StreetRaceGridPlace(int a1, int* a2, int a3, uint32_t* a4) {
|
||||
if (g_gridPlaceLogBudget > 0) {
|
||||
g_gridPlaceLogBudget--;
|
||||
uint32_t begin = a4[0];
|
||||
uint32_t end = a4[1];
|
||||
int count = (end > begin) ? (int)(end - begin) / 12 : 0;
|
||||
Log("DIAG StreetRaceGrid::Place: a1(grid)=%p a2(ctx)=%p a3(playerHandle)=0x%x vec=[0x%x..0x%x) count=%d",
|
||||
(void*)(uintptr_t)a1, (void*)a2, a3, begin, end, count);
|
||||
for (int i = 0; i < count && i < 8; i++) {
|
||||
uint32_t* elem = (uint32_t*)(uintptr_t)(begin + i * 12);
|
||||
Log(" [%d] word0=0x%x word1=0x%x word2=0x%x", i, elem[0], elem[1], elem[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_enableRandomPlayerGridPositionTest || !PlaceOpponent || !PlaceCar) {
|
||||
return orig_StreetRaceGridPlace(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
uint32_t begin = a4[0];
|
||||
uint32_t end = a4[1];
|
||||
int count = (end > begin) ? (int)(end - begin) / 12 : 0;
|
||||
if (count <= 0) {
|
||||
return orig_StreetRaceGridPlace(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
float minDist = *(float*)((uint8_t*)(uintptr_t)a1 + 12);
|
||||
float maxDist = *(float*)((uint8_t*)(uintptr_t)a1 + 16);
|
||||
float trackWidthFraction = *(float*)((uint8_t*)(uintptr_t)a1 + 20);
|
||||
float playerSpeed = *(float*)((uint8_t*)(uintptr_t)a1 + 24);
|
||||
float opponentSpeed = *(float*)((uint8_t*)(uintptr_t)a1 + 28);
|
||||
|
||||
int playerSlot = rand() % (count + 1); // 0..count inclusive - count+1 total physical slots
|
||||
int ctxDeref = *a2;
|
||||
float distance = 0.0f;
|
||||
int opponentIndex = 0;
|
||||
|
||||
Log("RANDOM GRID TEST: %d opponents, player placed at slot %d/%d", count, playerSlot, count);
|
||||
|
||||
// Opponent lane index a6 cycles ((a6+1)%3), and a6%3==0 lands exactly on
|
||||
// world-lateral 0 (center) - the SAME literal offset PlaceCar always uses
|
||||
// for the player. Vanilla is safe because the player is placed once, at
|
||||
// the very end, past the entire accumulated distance - never adjacent to
|
||||
// any specific opponent. Here the player can land next to an opponent
|
||||
// sharing that center lane, and a plain minDist..maxDist gap (sized for
|
||||
// adjacent DIFFERENT lanes) isn't enough separation for two cars in the
|
||||
// SAME lane - reproduced live as a spawn-inside-another-car launch bug.
|
||||
// Fix: widen the distance gap on both sides of the player's slot so
|
||||
// longitudinal separation alone guarantees no overlap, regardless of lane.
|
||||
for (int slot = 0; slot <= count; slot++) {
|
||||
if (slot == playerSlot) {
|
||||
PlaceCar(ctxDeref, (uint32_t*)(uintptr_t)a3, (int)distance, 0, playerSpeed);
|
||||
} else {
|
||||
uint32_t* elem = (uint32_t*)(uintptr_t)(begin + opponentIndex * 12);
|
||||
PlaceOpponent(ctxDeref, (int)(uintptr_t)elem, (int)distance, trackWidthFraction, opponentSpeed, opponentIndex);
|
||||
opponentIndex++;
|
||||
}
|
||||
float randomFraction = (float)rand() / (float)RAND_MAX;
|
||||
bool adjacentToPlayer = (slot == playerSlot) || (slot + 1 == playerSlot);
|
||||
float gapMin = adjacentToPlayer ? (minDist + maxDist) : minDist;
|
||||
float gapMax = adjacentToPlayer ? (minDist + maxDist) * 2.0f : maxDist;
|
||||
distance += gapMin + randomFraction * (gapMax - gapMin);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool InstallStreetRaceGridPlaceHook() {
|
||||
orig_StreetRaceGridPlace = (StreetRaceGridPlaceFn)InstallArmTrampolineHook(
|
||||
libapp_base, STREET_RACE_GRID_PLACE_OFFSET, (void*)&Hook_StreetRaceGridPlace,
|
||||
"StreetRaceGrid::Place diag hook");
|
||||
PlaceOpponent = (PlaceOpponentFn)((uintptr_t)libapp_base + 0x291BA4);
|
||||
PlaceCar = (PlaceCarFn)((uintptr_t)libapp_base + 0x2914DC);
|
||||
return orig_StreetRaceGridPlace != nullptr;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package nfs.mod.mpcore
|
||||
|
||||
/**
|
||||
* Explicit on/off switches for debug-only tooling (e.g. the on-map debug
|
||||
* menu). Flip a flag here to enable/disable a feature - this is the one
|
||||
* place that decides whether debug UI gets built at all, kept in mpcore so
|
||||
* it's not tangled up with any one Activity/module.
|
||||
*/
|
||||
object DebugFeatures {
|
||||
/** Shows the on-map debug menu button (money editor, etc). */
|
||||
var debugMenuEnabled: Boolean = true
|
||||
}
|
||||
Reference in New Issue
Block a user