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.
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user