From d7641175be07e73dc005cbe84b4e1fbe2084b99d Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 23 Aug 2026 18:39:44 +0000 Subject: [PATCH] Revert: remove the engine-provider kit detours entirely Two client breakages in a row from detouring FUN_180033770 / sub_180033430 / FUN_1800d73d0: the first cut froze FIFA at the "are both teams ready" prompt, and the rate-limited rewrite CRASHED it at the same point. Rate limiting fixed the I/O problem and the crash still happened, so the fault is the detours themselves, not the logging. Most likely cause: sub_180033430 is an address Ghidra never functionised, and at least one of these is reached in a way a 14-byte inline patch cannot survive - an interior branch target, or a callee taking stack arguments that the 4-register wrapper silently drops when it tail-calls the original. What the aborted runs did establish, and it is worth keeping: - KIT_SCAN fires for cardtype 7 with subtype 9 selector 2 AND selector 3, so BOTH the home and away club scans do run. - The FUT club enumerate (teamId 130000) did NOT occur in the crashed run before the kit screen, and KIT_DESC never fired at all. - KITS_AVAILABLE remains 0. - The "ret" value logged by kit_scan is the same constant for every call and is not a usable item pointer, so that reading was wrong. Next attempt must NOT patch this code path. Read the state from outside the process instead - /proc/PID/mem plus objdump against the live client, which cannot crash the game because it never writes to it. --- openfut-hook/src/kit_trace.rs | 169 ---------------------------------- 1 file changed, 169 deletions(-) diff --git a/openfut-hook/src/kit_trace.rs b/openfut-hook/src/kit_trace.rs index 3405639..233273e 100644 --- a/openfut-hook/src/kit_trace.rs +++ b/openfut-hook/src/kit_trace.rs @@ -156,130 +156,6 @@ unsafe extern "system" fn kit_db_clone_wrapper( original(rcx, rdx, r8, r9) } -// ── ENGINE-PROVIDER path, RATE-LIMITED ─────────────────────────────────────── -// -// A first cut of these three froze the client (2026-08-24). `FUN_180033770` is -// POLLED — roughly 150 calls, alternating between the two real match team ids — -// and the wrapper did a synchronous `write_log` on every one. The game hung at -// the "are both teams ready" prompt. -// -// Everything below is therefore built so the hot path costs nothing: -// * the enumerate trace returns immediately unless the team is the FUT club; -// * every trace has its OWN small budget rather than sharing the global 256; -// * repeated identical observations are collapsed by a tiny dedupe set, so a -// poll logs once and then never again. -// No trace may log per-call on a polled function. That is the rule this file -// broke once already. - -/// The FUT custom club. `FUN_180033770` answers only for this team id. -const FUT_CLUB_TEAM_ID: usize = 130_000; - -/// Fixed-capacity "have I already logged this?" set. Lock-free and allocation -/// free so it is safe to consult from a polled game thread. -struct SeenSet { - slots: [AtomicUsize; 16], -} - -impl SeenSet { - const fn new() -> Self { - #[allow(clippy::declare_interior_mutable_const)] - const Z: AtomicUsize = AtomicUsize::new(usize::MAX); - SeenSet { slots: [Z; 16] } - } - - /// True the FIRST time `key` is offered, false every time after. A full set - /// stops reporting rather than evicting: the point is a bounded log. - fn first_time(&self, key: usize) -> bool { - for slot in &self.slots { - match slot.compare_exchange(usize::MAX, key, Ordering::AcqRel, Ordering::Acquire) { - Ok(_) => return true, - Err(existing) if existing == key => return false, - Err(_) => continue, - } - } - false - } -} - -// FUN_180033770 (0x33770) — enumerate kits for a team. -static KIT_ENUM_TRAMP: AtomicUsize = AtomicUsize::new(0); -static ENUM_SEEN: SeenSet = SeenSet::new(); -unsafe extern "system" fn kit_enum_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { - // The polled case: real match team ids. Do NOT log, do NOT format, just go. - let interesting = rdx == FUT_CLUB_TEAM_ID && ENUM_SEEN.first_time(rdx); - let t = KIT_ENUM_TRAMP.load(Ordering::Acquire); - if t == 0 { - return 0; - } - let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = - core::mem::transmute(t); - let r = original(rcx, rdx, r8, r9); - if interesting { - // r8 is the out-list; report how many kits were actually offered. - let count = match (rd_usize(r8), rd_usize(r8 + 8)) { - (Some(b), Some(e)) if e >= b => ((e - b) / 4) as i64, - _ => -1, - }; - write_log(&format!( - "KIT_ENUM: FUT club enumerate -> returned {r:#x}, offered_kits={count}\n" - )); - } - r -} - -// sub_180033430 (0x33430) — describe one kit. Dedupes on the packed id, so a -// carousel that re-describes the same kit logs it once. -static KIT_DESC_TRAMP: AtomicUsize = AtomicUsize::new(0); -static DESC_SEEN: SeenSet = SeenSet::new(); -unsafe extern "system" fn kit_desc_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { - let packed = rdx as u32; - let fresh = DESC_SEEN.first_time(packed as usize); - if fresh { - let teamid = packed >> 14; - let year_field = (packed >> 5) & 0x1ff; - let year = if year_field != 0 { - year_field + 1800 - } else { - 0 - }; - write_log(&format!( - "KIT_DESC: packed={packed:#x} -> teamid={teamid} year={year} slot={}\n", - packed & 0x1f - )); - } - let t = KIT_DESC_TRAMP.load(Ordering::Acquire); - if t == 0 { - return 0; - } - let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = - core::mem::transmute(t); - original(rcx, rdx, r8, r9) -} - -// FUN_1800d73d0 (0xd73d0) — the club scan behind getActiveKit. Live args are -// (mgr, cardtype, cardsubtypeid, selector): observed a2=7 with a3 in {9,10,11} -// and a4 in {0,2,3}. Only cardtype 7 matters here, and each (subtype, selector) -// pair is reported once. -static KIT_SCAN_TRAMP: AtomicUsize = AtomicUsize::new(0); -static SCAN_SEEN: SeenSet = SeenSet::new(); -unsafe extern "system" fn kit_scan_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { - let fresh = rdx == 7 && SCAN_SEEN.first_time((r8 << 8) | (r9 & 0xff)); - let t = KIT_SCAN_TRAMP.load(Ordering::Acquire); - if t == 0 { - return 0; - } - let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = - core::mem::transmute(t); - let r = original(rcx, rdx, r8, r9); - if fresh { - write_log(&format!( - "KIT_SCAN: cardtype=7 subtype={r8} selector={r9} -> ret={r:#x} \ - (subtype 9=kit 10=stadium 11=badge; selector 2=home 3=away)\n" - )); - } - r -} - unsafe fn worker() { let mut base = 0usize; for _ in 0..600u32 { @@ -348,51 +224,6 @@ unsafe fn worker() { kit_db_clone_wrapper as *const () as usize, &KIT_DB_CLONE_TRAMP, ); - - // ENGINE-PROVIDER path. Prologues dumped from the analysed Ghidra project - // (cardsdll.dll, base 0x180000000). All three copy lengths are - // instruction-aligned and none of these prologues is rip-relative, so the - // plain installer is correct — unlike FUN_1801c3480 above. - - // FUN_180033770: 48 8b c4 57 41 56 41 57 48 83 ec 60 48 c7 40 c0 fe ff ff ff (20). - install_detour( - base, - 0x33770, - "kitProviderEnumerate(0x33770)", - 20, - &[ - 0x48, 0x8b, 0xc4, 0x57, 0x41, 0x56, 0x41, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, - 0x40, 0xc0, 0xfe, 0xff, 0xff, 0xff, - ], - kit_enum_wrapper as *const () as usize, - &KIT_ENUM_TRAMP, - ); - // sub_180033430 (Ghidra leaves it undefined): - // 48 8b c4 55 41 54 41 55 41 56 41 57 48 8b ec (15). - install_detour( - base, - 0x33430, - "kitProviderDescribe(0x33430)", - 15, - &[ - 0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8b, - 0xec, - ], - kit_desc_wrapper as *const () as usize, - &KIT_DESC_TRAMP, - ); - // FUN_1800d73d0: 48 89 5c 24 18 56 45 8b d9 41 8b f0 8b da (14). - install_detour( - base, - 0xd73d0, - "clubScanForKit(0xd73d0)", - 14, - &[ - 0x48, 0x89, 0x5c, 0x24, 0x18, 0x56, 0x45, 0x8b, 0xd9, 0x41, 0x8b, 0xf0, 0x8b, 0xda, - ], - kit_scan_wrapper as *const () as usize, - &KIT_SCAN_TRAMP, - ); write_log("KIT_TRACE: all kit-selector traces armed\n"); }