wip: checkpoint FIFA 17 hook diagnostics
This commit is contained in:
+247
-82
@@ -17,11 +17,11 @@
|
||||
//! r8/r9) and returns in rax. All targets here are SDK methods with few args.
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows_sys::Win32::System::Memory::{
|
||||
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, MEM_PRIVATE,
|
||||
PAGE_EXECUTE_READWRITE, PAGE_GUARD, PAGE_NOACCESS,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
||||
|
||||
/// Fault-safe pointer read: returns None unless `ptr` lands in a committed, readable
|
||||
/// page (checked via VirtualQuery). Avoids crashing FIFA when we sample pointers that
|
||||
@@ -31,7 +31,11 @@ unsafe fn read_ptr(ptr: usize) -> Option<usize> {
|
||||
return None;
|
||||
}
|
||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||
let n = VirtualQuery(ptr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
||||
let n = VirtualQuery(
|
||||
ptr as _,
|
||||
&mut mbi,
|
||||
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||
);
|
||||
if n == 0 || mbi.State != MEM_COMMIT {
|
||||
return None;
|
||||
}
|
||||
@@ -165,14 +169,18 @@ pub unsafe fn install_listener_probe() {
|
||||
// Arm the dial trigger from the env var, ONCE, at install (DLL-load) time. Default
|
||||
// disarmed: OPENFUT_DIAL_TRIGGER must be explicitly "1". Orthogonal to the pump/ctx
|
||||
// env vars.
|
||||
let armed = std::env::var("OPENFUT_DIAL_TRIGGER").map(|v| v == "1").unwrap_or(false);
|
||||
let armed = std::env::var("OPENFUT_DIAL_TRIGGER")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
DIAL_ARMED.store(armed, Ordering::Relaxed);
|
||||
crate::write_log(&format!(
|
||||
"DIAL_TRIGGER: {} (env OPENFUT_DIAL_TRIGGER)\n",
|
||||
if armed { "ARMED" } else { "disarmed" }
|
||||
));
|
||||
// Arm the (independent) connMgr enumeration from its own env var, once, at load.
|
||||
let enum_armed = std::env::var("OPENFUT_CONNMGR_ENUM").map(|v| v == "1").unwrap_or(false);
|
||||
let enum_armed = std::env::var("OPENFUT_CONNMGR_ENUM")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
CONNMGR_ENUM_ARMED.store(enum_armed, Ordering::Relaxed);
|
||||
crate::write_log(&format!(
|
||||
"CONNMGR_ENUM: {} (env OPENFUT_CONNMGR_ENUM)\n",
|
||||
@@ -180,16 +188,25 @@ pub unsafe fn install_listener_probe() {
|
||||
));
|
||||
// Arm the (independent) [element+0x40] container-writer watchpoint from its own
|
||||
// env var, once, at load. Orthogonal to DIAL_TRIGGER / CONNMGR_ENUM.
|
||||
let elem_watch_armed = std::env::var("OPENFUT_ELEM_WATCH").map(|v| v == "1").unwrap_or(false);
|
||||
let elem_watch_armed = std::env::var("OPENFUT_ELEM_WATCH")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
ELEM_WATCH_ARMED.store(elem_watch_armed, Ordering::Relaxed);
|
||||
crate::write_log(&format!(
|
||||
"ELEM_WATCH: {} (env OPENFUT_ELEM_WATCH)\n",
|
||||
if elem_watch_armed { "ARMED" } else { "disarmed" }
|
||||
if elem_watch_armed {
|
||||
"ARMED"
|
||||
} else {
|
||||
"disarmed"
|
||||
}
|
||||
));
|
||||
RESUME_ADDR = (base + 0x274d4e5) as u64;
|
||||
let target = (base + 0x274d4d7) as *mut u8;
|
||||
write_jmp(target, openfut_listener_stub as usize as u64);
|
||||
crate::write_log(&format!("PROBE listener: dispatch site patched @ {:#x}\n", target as usize));
|
||||
crate::write_log(&format!(
|
||||
"PROBE listener: dispatch site patched @ {:#x}\n",
|
||||
target as usize
|
||||
));
|
||||
}
|
||||
|
||||
// ─── dial trigger (sub-phase B) ──────────────────────────────────────────────────
|
||||
@@ -262,7 +279,9 @@ fn observe_completion() {
|
||||
let c = crate::dial_notification::completion_stub_call_count();
|
||||
let last = LAST_COMPLETION_COUNT.swap(c, Ordering::Relaxed);
|
||||
if c != last {
|
||||
crate::write_log(&format!("DIAL_TRIGGER: completion stub count changed {last} → {c}\n"));
|
||||
crate::write_log(&format!(
|
||||
"DIAL_TRIGGER: completion stub count changed {last} → {c}\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,11 +348,17 @@ unsafe fn dial_trigger_tick() {
|
||||
|
||||
// Step 5 — resolve connMgr (reuse the ctx-dump scan + tiebreaker).
|
||||
let Some(g) = read_ptr(base + 0xacd02c0).filter(|&x| x != 0) else {
|
||||
log_skip(3, "DIAL_TRIGGER: connMgr resolution failed (G null) — skipping\n");
|
||||
log_skip(
|
||||
3,
|
||||
"DIAL_TRIGGER: connMgr resolution failed (G null) — skipping\n",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(m) = read_ptr(g + 0x360).filter(|&x| x != 0) else {
|
||||
log_skip(3, "DIAL_TRIGGER: connMgr resolution failed (M null) — skipping\n");
|
||||
log_skip(
|
||||
3,
|
||||
"DIAL_TRIGGER: connMgr resolution failed (M null) — skipping\n",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let expected_vtable = base + 0x80200b8;
|
||||
@@ -367,7 +392,9 @@ unsafe fn dial_trigger_tick() {
|
||||
if ctx == 0 || !(0x140000000..0x161000000).contains(&ctx_vt) {
|
||||
log_skip(
|
||||
4,
|
||||
&format!("DIAL_TRIGGER: ctx pointer implausible (ctx={ctx:#x} vt={ctx_vt:#x}) — skipping\n"),
|
||||
&format!(
|
||||
"DIAL_TRIGGER: ctx pointer implausible (ctx={ctx:#x} vt={ctx_vt:#x}) — skipping\n"
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +584,11 @@ unsafe fn connmgr_enum_tick() {
|
||||
crate::write_log(&format!(
|
||||
"CONNMGR_ENUM: [{i}] P={p:#x} vt={vtable:#x} vt[0]={vtable0:#x} ({}) \
|
||||
[+0x18]={} [+0x20]={} [+0x30]={} [+0xc38]={}\n",
|
||||
if info.vtable0_in_text { "in .text" } else { "NOT .text" },
|
||||
if info.vtable0_in_text {
|
||||
"in .text"
|
||||
} else {
|
||||
"NOT .text"
|
||||
},
|
||||
h(info.field_18),
|
||||
h(info.field_20),
|
||||
h(info.field_30),
|
||||
@@ -658,7 +689,13 @@ unsafe fn read_u32(addr: usize) -> Option<u32> {
|
||||
fn fourcc4(v: u32) -> String {
|
||||
let b = v.to_le_bytes();
|
||||
b.iter()
|
||||
.map(|&c| if (0x20..0x7f).contains(&c) { c as char } else { '.' })
|
||||
.map(|&c| {
|
||||
if (0x20..0x7f).contains(&c) {
|
||||
c as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -668,7 +705,10 @@ fn fourcc4(v: u32) -> String {
|
||||
/// (Refactoring `hex_dump` to take a prefix would touch the stable ctx-dump/enum probes
|
||||
/// for no real gain; a ~10-line duplicate is the lower-risk choice.)
|
||||
fn elem_hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
||||
let mut out = format!("ELEM_WATCH {label} @{start_va:#x} ({} bytes):\n", data.len());
|
||||
let mut out = format!(
|
||||
"ELEM_WATCH {label} @{start_va:#x} ({} bytes):\n",
|
||||
data.len()
|
||||
);
|
||||
for (row, chunk) in data.chunks(16).enumerate() {
|
||||
let mut hex = String::new();
|
||||
let mut ascii = String::new();
|
||||
@@ -677,7 +717,11 @@ fn elem_hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
||||
if i == 7 {
|
||||
hex.push(' ');
|
||||
}
|
||||
ascii.push(if (0x20..0x7f).contains(&b) { b as char } else { '.' });
|
||||
ascii.push(if (0x20..0x7f).contains(&b) {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
});
|
||||
}
|
||||
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
||||
}
|
||||
@@ -752,7 +796,11 @@ unsafe fn elem_watch_tick() {
|
||||
// Step 6 — walk connMgr -> M -> ctx. (M here is re-read from [connMgr+8]; it should
|
||||
// equal the global M we scanned with.)
|
||||
let cm_m = read_ptr(conn_mgr + 8).unwrap_or(0);
|
||||
let ctx = if cm_m != 0 { read_ptr(cm_m + 0x778).unwrap_or(0) } else { 0 };
|
||||
let ctx = if cm_m != 0 {
|
||||
read_ptr(cm_m + 0x778).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if cm_m == 0 || ctx == 0 {
|
||||
crate::write_log(&format!(
|
||||
"ELEM_WATCH: chain broke (connMgr={conn_mgr:#x} M={cm_m:#x} ctx={ctx:#x}) — watchpoint not armed\n"
|
||||
@@ -768,11 +816,22 @@ unsafe fn elem_watch_tick() {
|
||||
// target_index = [[M+0x7b0]+0x650] (u32; the dial read this with `mov edx,...`)
|
||||
let array_base = read_ptr(ctx + 0x1a8).unwrap_or(0);
|
||||
let sub_object = read_ptr(ctx + 0x20).unwrap_or(0);
|
||||
let count = if sub_object != 0 { read_u32(sub_object + 0x51c) } else { None };
|
||||
let count = if sub_object != 0 {
|
||||
read_u32(sub_object + 0x51c)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let m7b0 = read_ptr(cm_m + 0x7b0).unwrap_or(0);
|
||||
let target_index = if m7b0 != 0 { read_u32(m7b0 + 0x650) } else { None };
|
||||
let target_index = if m7b0 != 0 {
|
||||
read_u32(m7b0 + 0x650)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let fmt_u = |o: Option<u32>| o.map(|v| v.to_string()).unwrap_or_else(|| "<unreadable>".to_string());
|
||||
let fmt_u = |o: Option<u32>| {
|
||||
o.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "<unreadable>".to_string())
|
||||
};
|
||||
crate::write_log(&format!(
|
||||
"ELEM_WATCH: SNAPSHOT ctx={ctx:#x} array_base={array_base:#x} sub_object={sub_object:#x} \
|
||||
count={} [M+0x7b0]={m7b0:#x} target_index={}\n",
|
||||
@@ -786,7 +845,9 @@ unsafe fn elem_watch_tick() {
|
||||
return;
|
||||
};
|
||||
if array_base == 0 {
|
||||
crate::write_log("ELEM_WATCH: array_base null — element array not allocated; watchpoint not armed\n");
|
||||
crate::write_log(
|
||||
"ELEM_WATCH: array_base null — element array not allocated; watchpoint not armed\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if idx >= count {
|
||||
@@ -811,13 +872,19 @@ unsafe fn elem_watch_tick() {
|
||||
let interp = match begin {
|
||||
None => "unreadable",
|
||||
Some(0) => "container null-init (default-constructed empty vector — begin==end==0)",
|
||||
Some(v) if v < 0x10000 => "container UNINITIALIZED (small non-pointer sentinel — this is the crash shape)",
|
||||
Some(v) if read_bytes(v, 8).is_some() => "container appears INITIALIZED (begin is a readable heap pointer)",
|
||||
Some(v) if v < 0x10000 => {
|
||||
"container UNINITIALIZED (small non-pointer sentinel — this is the crash shape)"
|
||||
}
|
||||
Some(v) if read_bytes(v, 8).is_some() => {
|
||||
"container appears INITIALIZED (begin is a readable heap pointer)"
|
||||
}
|
||||
Some(_) => "container has a non-null but UNREADABLE begin (dangling / mid-construction?)",
|
||||
};
|
||||
crate::write_log(&format!(
|
||||
"ELEM_WATCH: [elem+0x40]={} [elem+0x48]={end:#x} => {interp}\n",
|
||||
begin.map(|v| format!("{v:#x}")).unwrap_or_else(|| "<unreadable>".to_string()),
|
||||
begin
|
||||
.map(|v| format!("{v:#x}"))
|
||||
.unwrap_or_else(|| "<unreadable>".to_string()),
|
||||
));
|
||||
|
||||
// Step 10 — arm Phase 2: seed the baseline and spawn the poller. We watch regardless
|
||||
@@ -872,7 +939,9 @@ fn spawn_elem_watcher(base: usize, elem: usize) {
|
||||
elem_hex_dump("element (after change)", elem, &bytes);
|
||||
}
|
||||
} else if changes == 6 {
|
||||
crate::write_log("ELEM_WATCH: (further changes suppressed; still tracking baseline)\n");
|
||||
crate::write_log(
|
||||
"ELEM_WATCH: (further changes suppressed; still tracking baseline)\n",
|
||||
);
|
||||
}
|
||||
// Beyond 6, keep updating the baseline silently so distinct future changes
|
||||
// are still detected — we just stop spamming the log.
|
||||
@@ -890,11 +959,12 @@ fn spawn_elem_watcher(base: usize, elem: usize) {
|
||||
pub fn install_force_connect() {
|
||||
std::thread::spawn(|| unsafe {
|
||||
let base = GetModuleHandleA(core::ptr::null());
|
||||
if base.is_null() { return; }
|
||||
if base.is_null() {
|
||||
return;
|
||||
}
|
||||
let base = base as usize;
|
||||
let x_slot = base + 0xacd02c0;
|
||||
let rest: extern "system" fn() -> usize =
|
||||
core::mem::transmute(base + 0x2861910);
|
||||
let rest: extern "system" fn() -> usize = core::mem::transmute(base + 0x2861910);
|
||||
// Wait for the ctx chain to be valid (FIFA past bootstrap / online), up to ~5 min.
|
||||
let mut fired = 0;
|
||||
for i in 0..600u32 {
|
||||
@@ -905,17 +975,23 @@ pub fn install_force_connect() {
|
||||
.filter(|&m| m != 0)
|
||||
.and_then(|m| read_ptr(m + 0x778))
|
||||
.filter(|&c| c != 0);
|
||||
let Some(ctx) = ctx else { continue; };
|
||||
let Some(ctx) = ctx else {
|
||||
continue;
|
||||
};
|
||||
// Give the game ~15s settled (ctx valid) before poking, then re-fire a few
|
||||
// times spaced out (the FUT-tick pump needs a moment to reach state 2).
|
||||
if i < 30 { continue; }
|
||||
if i < 30 {
|
||||
continue;
|
||||
}
|
||||
crate::write_log(&format!(
|
||||
"FORCE: calling nucleusConnectREST() (ctx={ctx:#x}) attempt {fired}\n"
|
||||
));
|
||||
let r = rest();
|
||||
crate::write_log(&format!("FORCE: nucleusConnectREST returned {r:#x}\n"));
|
||||
fired += 1;
|
||||
if fired >= 6 { break; }
|
||||
if fired >= 6 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5000));
|
||||
}
|
||||
crate::write_log("FORCE: done\n");
|
||||
@@ -965,23 +1041,33 @@ pub fn install_force_netconn_pump() {
|
||||
// (an env var is fixed for the process lifetime). Unset or "0" => short-circuit:
|
||||
// log and return, so the pump is wired in but completely inert — a safe default
|
||||
// that can be flipped without a rebuild.
|
||||
let enabled = std::env::var("OPENFUT_NETCONN_PUMP").map(|v| v == "1").unwrap_or(false);
|
||||
let enabled = std::env::var("OPENFUT_NETCONN_PUMP")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
if !enabled {
|
||||
crate::write_log("NETCONN_PUMP: disabled (set OPENFUT_NETCONN_PUMP=1 to enable)\n");
|
||||
return;
|
||||
}
|
||||
let base = GetModuleHandleA(core::ptr::null());
|
||||
if base.is_null() { return; }
|
||||
if base.is_null() {
|
||||
return;
|
||||
}
|
||||
let base = base as usize;
|
||||
let netconn_slot = base + 0x9fe5e50; // VA 0x149fe5e50 -> NetConn global (X)
|
||||
// NetConnIdle core pump; takes no args (reads globals, sets its own rcx). Win64.
|
||||
// NetConnIdle core pump; takes no args (reads globals, sets its own rcx). Win64.
|
||||
let pump: extern "system" fn() = core::mem::transmute(base + 0xf16a50);
|
||||
|
||||
// Render a 4-char status code the way DirtySDK stores it (e.g. 0x2b6f6e6c="+onl").
|
||||
let fourcc = |v: u32| -> String {
|
||||
[(v >> 24) as u8, (v >> 16) as u8, (v >> 8) as u8, v as u8]
|
||||
.iter()
|
||||
.map(|&b| if (0x20..0x7f).contains(&b) { b as char } else { '.' })
|
||||
.map(|&b| {
|
||||
if (0x20..0x7f).contains(&b) {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -998,7 +1084,9 @@ pub fn install_force_netconn_pump() {
|
||||
let mut samples = 0u32;
|
||||
for _ in 0..18000u32 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
let Some(nc) = read_ptr(netconn_slot).filter(|&x| x != 0) else { continue; };
|
||||
let Some(nc) = read_ptr(netconn_slot).filter(|&x| x != 0) else {
|
||||
continue;
|
||||
};
|
||||
// Read the conn status dword at [nc+0x48] (8-aligned; read_ptr is guarded).
|
||||
let status = read_ptr(nc + 0x48).map(|w| w as u32).unwrap_or(0);
|
||||
// Drive the idle loop. Self-guards on 'open'; a no-op if not yet open.
|
||||
@@ -1067,7 +1155,9 @@ pub fn install_force_netconn_pump() {
|
||||
pub fn install_force_fut_tick() {
|
||||
std::thread::spawn(|| unsafe {
|
||||
let base = GetModuleHandleA(core::ptr::null());
|
||||
if base.is_null() { return; }
|
||||
if base.is_null() {
|
||||
return;
|
||||
}
|
||||
let base = base as usize;
|
||||
let mgr_slot = base + 0xa199608; // VA 0x14a199608 -> FUT online manager ptr
|
||||
let tick: extern "system" fn(usize, usize) -> usize =
|
||||
@@ -1077,9 +1167,13 @@ pub fn install_force_fut_tick() {
|
||||
// ~15 min at 250ms. The tick is heavy (locks + sub-updates); don't spin at 100ms.
|
||||
for _ in 0..3600u32 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
let Some(mgr) = read_ptr(mgr_slot).filter(|&m| m != 0) else { continue; };
|
||||
let Some(mgr) = read_ptr(mgr_slot).filter(|&m| m != 0) else {
|
||||
continue;
|
||||
};
|
||||
// state @+0x1bb8 (low32) + latch byte @+0x1bbc share one 8-aligned qword.
|
||||
let Some(w) = read_ptr(mgr + 0x1bb8) else { continue; };
|
||||
let Some(w) = read_ptr(mgr + 0x1bb8) else {
|
||||
continue;
|
||||
};
|
||||
let state = w as u32;
|
||||
let latch = ((w >> 32) & 0xff) as u8;
|
||||
// Post the go-online request ONLY at state 0 (mimics event-0 delivery) to
|
||||
@@ -1146,7 +1240,11 @@ unsafe fn read_bytes(addr: usize, len: usize) -> Option<Vec<u8>> {
|
||||
return None;
|
||||
}
|
||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||
let n = VirtualQuery(addr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
||||
let n = VirtualQuery(
|
||||
addr as _,
|
||||
&mut mbi,
|
||||
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||
);
|
||||
if n == 0 || mbi.State != MEM_COMMIT {
|
||||
return None;
|
||||
}
|
||||
@@ -1180,7 +1278,11 @@ fn hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
||||
}
|
||||
// Printable ASCII stays; everything else shows as '.' so pointer bytes
|
||||
// don't corrupt the log line.
|
||||
ascii.push(if (0x20..0x7f).contains(&b) { b as char } else { '.' });
|
||||
ascii.push(if (0x20..0x7f).contains(&b) {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
});
|
||||
}
|
||||
out.push_str(&format!(" +{:#05x} {:<50}|{ascii}|\n", row * 16, hex));
|
||||
}
|
||||
@@ -1203,16 +1305,16 @@ fn hex_dump(label: &str, start_va: usize, data: &[u8]) {
|
||||
///
|
||||
/// Returns every matching `P`. Read-only throughout. Also fills `stats` with
|
||||
/// (regions_scanned, bytes_scanned) so we can report the cost.
|
||||
unsafe fn scan_conn_mgr(
|
||||
m: usize,
|
||||
expected_vtable: usize,
|
||||
stats: &mut (u64, u64),
|
||||
) -> Vec<usize> {
|
||||
unsafe fn scan_conn_mgr(m: usize, expected_vtable: usize, stats: &mut (u64, u64)) -> Vec<usize> {
|
||||
let mut hits = Vec::new();
|
||||
let mut addr: usize = 0x10000; // user space starts here; skip the null-guard page
|
||||
loop {
|
||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||
let n = VirtualQuery(addr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
||||
let n = VirtualQuery(
|
||||
addr as _,
|
||||
&mut mbi,
|
||||
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
||||
);
|
||||
if n == 0 {
|
||||
break; // past the top of the user address space
|
||||
}
|
||||
@@ -1262,7 +1364,9 @@ unsafe fn scan_conn_mgr(
|
||||
/// env `OPENFUT_CTX_DUMP` — armed only when it equals "1" (unset/"0" = disabled).
|
||||
/// Read once at install time; if disarmed we don't even spawn the thread.
|
||||
pub fn install_ctx_dump() {
|
||||
let armed = std::env::var("OPENFUT_CTX_DUMP").map(|v| v == "1").unwrap_or(false);
|
||||
let armed = std::env::var("OPENFUT_CTX_DUMP")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
if !armed {
|
||||
crate::write_log("CTXDUMP: disabled (set OPENFUT_CTX_DUMP=1 to arm)\n");
|
||||
return;
|
||||
@@ -1436,14 +1540,54 @@ const TARGETS: &[Target] = &[
|
||||
// Run 4: settle "connect state entered-but-stalled" vs "never entered". If the ctor
|
||||
// fires but nothing else, the connect states are created at init but never used; if
|
||||
// GetByIdx / any vtable step fires, the online subsystem is iterating them.
|
||||
Target { module: b"\0", rva: 0x5078d20, label: "connectState.ctor", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x4f46570, label: "ctrl.GetConnState", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x507cd60, label: "connState.m_a8", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x507cf90, label: "connState.m_b0", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x507d660, label: "connState.tick_b8", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x507d760, label: "connState.m_c0", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x2861910, label: "nucleusConnectREST", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x278a4d0, label: "OnlineStatus.deser", main_exe: true },
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x5078d20,
|
||||
label: "connectState.ctor",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x4f46570,
|
||||
label: "ctrl.GetConnState",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x507cd60,
|
||||
label: "connState.m_a8",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x507cf90,
|
||||
label: "connState.m_b0",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x507d660,
|
||||
label: "connState.tick_b8",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x507d760,
|
||||
label: "connState.m_c0",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x2861910,
|
||||
label: "nucleusConnectREST",
|
||||
main_exe: true,
|
||||
},
|
||||
Target {
|
||||
module: b"\0",
|
||||
rva: 0x278a4d0,
|
||||
label: "OnlineStatus.deser",
|
||||
main_exe: true,
|
||||
},
|
||||
];
|
||||
|
||||
const N: usize = 8; // must equal TARGETS.len()
|
||||
@@ -1498,19 +1642,37 @@ unsafe fn generic(slot: usize, a: usize, b: usize, c: usize, d: usize) -> usize
|
||||
if log {
|
||||
crate::write_log(&format!("PROBE {label} #{n} ret={r:#x}\n"));
|
||||
} else if n == LOG_CAP {
|
||||
crate::write_log(&format!("PROBE {label} (capped; still firing past {LOG_CAP})\n"));
|
||||
crate::write_log(&format!(
|
||||
"PROBE {label} (capped; still firing past {LOG_CAP})\n"
|
||||
));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
unsafe extern "system" fn p0(a: usize, b: usize, c: usize, d: usize) -> usize { generic(0, a, b, c, d) }
|
||||
unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize { generic(1, a, b, c, d) }
|
||||
unsafe extern "system" fn p2(a: usize, b: usize, c: usize, d: usize) -> usize { generic(2, a, b, c, d) }
|
||||
unsafe extern "system" fn p3(a: usize, b: usize, c: usize, d: usize) -> usize { generic(3, a, b, c, d) }
|
||||
unsafe extern "system" fn p4(a: usize, b: usize, c: usize, d: usize) -> usize { generic(4, a, b, c, d) }
|
||||
unsafe extern "system" fn p5(a: usize, b: usize, c: usize, d: usize) -> usize { generic(5, a, b, c, d) }
|
||||
unsafe extern "system" fn p6(a: usize, b: usize, c: usize, d: usize) -> usize { generic(6, a, b, c, d) }
|
||||
unsafe extern "system" fn p7(a: usize, b: usize, c: usize, d: usize) -> usize { generic(7, a, b, c, d) }
|
||||
unsafe extern "system" fn p0(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(0, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(1, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p2(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(2, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p3(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(3, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p4(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(4, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p5(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(5, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p6(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(6, a, b, c, d)
|
||||
}
|
||||
unsafe extern "system" fn p7(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
generic(7, a, b, c, d)
|
||||
}
|
||||
|
||||
/// Spawn a background thread that waits for anadius64.dll to load, then installs
|
||||
/// all probes. anadius may not be present when our DllMain runs, so we defer
|
||||
@@ -1526,23 +1688,23 @@ pub fn install_probes_deferred() {
|
||||
install_probes();
|
||||
install_listener_probe();
|
||||
install_state_sampler();
|
||||
install_ctx_dump(); // ENABLED 2026-07-03: READ-ONLY menu-time ctx dump (env
|
||||
// OPENFUT_CTX_DUMP=1). Resolves connMgr and hex-dumps M + ctx. No game calls.
|
||||
// install_force_connect(); // DISABLED 2026-07-03: re-enabling it CRASHED FIFA at
|
||||
// ~15s (EXCEPTION_ACCESS_VIOLATION, RIP 0x15d5e8dd7 in FIFA's packed/anti-tamper
|
||||
// region, all registers garbage). Once state 2 makes the Nucleus ctx live,
|
||||
// nucleusConnectREST's `ctx->vtable[0x40]` send path runs into protected code that
|
||||
// does not tolerate being called from our background thread. GetAuthCode must be
|
||||
// triggered on the GAME thread (via a detour), not a bg-thread forcing call.
|
||||
install_force_netconn_pump(); // RE-ENABLED 2026-07-03 (sub-phase B prereq): pump
|
||||
// NetConn toward '+onl' and capture the pump thread id. Gated by env
|
||||
// OPENFUT_NETCONN_PUMP=1 — completely inert unless set. This is the known-good
|
||||
// pump path (never crashed); the FUT-tick pump below stays OFF (it crashes the VM).
|
||||
// install_force_fut_tick(); // DISABLED 2026-07-03 for the ctx-dump build: it
|
||||
// WRITES the go-online latch and drives FifaOnline toward state 2, which
|
||||
// deterministically CRASHES the anti-tamper VM before the menu — so leaving it on
|
||||
// would prevent this menu-time probe from ever observing. Re-enable only if we
|
||||
// deliberately want the (crash-prone) state-2 path.
|
||||
install_ctx_dump(); // ENABLED 2026-07-03: READ-ONLY menu-time ctx dump (env
|
||||
// OPENFUT_CTX_DUMP=1). Resolves connMgr and hex-dumps M + ctx. No game calls.
|
||||
// install_force_connect(); // DISABLED 2026-07-03: re-enabling it CRASHED FIFA at
|
||||
// ~15s (EXCEPTION_ACCESS_VIOLATION, RIP 0x15d5e8dd7 in FIFA's packed/anti-tamper
|
||||
// region, all registers garbage). Once state 2 makes the Nucleus ctx live,
|
||||
// nucleusConnectREST's `ctx->vtable[0x40]` send path runs into protected code that
|
||||
// does not tolerate being called from our background thread. GetAuthCode must be
|
||||
// triggered on the GAME thread (via a detour), not a bg-thread forcing call.
|
||||
install_force_netconn_pump(); // RE-ENABLED 2026-07-03 (sub-phase B prereq): pump
|
||||
// NetConn toward '+onl' and capture the pump thread id. Gated by env
|
||||
// OPENFUT_NETCONN_PUMP=1 — completely inert unless set. This is the known-good
|
||||
// pump path (never crashed); the FUT-tick pump below stays OFF (it crashes the VM).
|
||||
// install_force_fut_tick(); // DISABLED 2026-07-03 for the ctx-dump build: it
|
||||
// WRITES the go-online latch and drives FifaOnline toward state 2, which
|
||||
// deterministically CRASHES the anti-tamper VM before the menu — so leaving it on
|
||||
// would prevent this menu-time probe from ever observing. Re-enable only if we
|
||||
// deliberately want the (crash-prone) state-2 path.
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1563,7 +1725,10 @@ pub unsafe fn install_probes() {
|
||||
core::ptr::copy_nonoverlapping(addr, (&raw mut ORIG[i]) as *mut u8, 14);
|
||||
ADDRS[i].store(addr as usize, Ordering::Relaxed);
|
||||
write_jmp(addr, PROBE_FNS[i] as u64);
|
||||
crate::write_log(&format!("PROBE {} installed @ {:#x}\n", t.label, addr as usize));
|
||||
crate::write_log(&format!(
|
||||
"PROBE {} installed @ {:#x}\n",
|
||||
t.label, addr as usize
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user