Merge lan-event-injection-poc: opponent substitution, cop/traffic removal, debug menu (source only, not wired into the emulated build)
Brings opponent_substitution.h and cop_traffic_disable.h - the
native32/launcher branch's proven, live-confirmed multiplayer-mod hooks
(opponent car/color substitution, cop dispatcher removal with a live
before/after roadblock proof, full traffic elimination) - into this repo
as source, plus DebugFeatures.kt/DebugMenuOverlay.kt (debug-only money/menu
scaffold) and this session's GameActivityMain.kt thread-safety fix
(BroadcastReceiver.onReceive wrapping native calls in
gameGLSurfaceView.queueEvent{} instead of calling straight into JNI from
the main thread - a real cross-thread race, independently useful on both
branches).
Two real conflicts, both resolved to preserve architecture rather than
naively keep both sides:
- main.cpp: kept this branch's own structure. The new .h files, like
lan_event_injection.h before them, are NOT #include'd or wired into
JNI_OnLoad - every hook in both files resolves and calls raw
"libapp_base + OFFSET" function pointers, which on a real dlopen'd
libapp.so is a valid jump target but here is only Unicorn-backed guest
memory - including them unmodified would crash immediately, not just
misbehave. Left as source for the same GuestFn/CallGuestFunction porting
session already planned for lan_event_injection.h.
- GameActivityMain.kt: trivial, a code comment disagreeing on this build's
own package id (nfs13_arm here vs nfs13_mod on native32) - merged to
document both.
Verified after resolving: compileTranslatedDebugKotlin,
compileNative32DebugKotlin, and externalNativeBuildDebug (arm64-v8a +
x86_64, confirmed via a fresh main.cpp.o/mpcore.so rebuild, not a cache
hit) all succeed - this merge compiles clean on both Kotlin flavors and
the native side, not just resolves textually.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user