diff --git a/openfut-hook/src/kit_trace.rs b/openfut-hook/src/kit_trace.rs index 233273e..a1deb70 100644 --- a/openfut-hook/src/kit_trace.rs +++ b/openfut-hook/src/kit_trace.rs @@ -156,6 +156,115 @@ unsafe extern "system" fn kit_db_clone_wrapper( original(rcx, rdx, r8, r9) } +// ── The ENGINE-PROVIDER path (2026-08-24) ──────────────────────────────────── +// +// The four traces above were written against the 2026-08-20 model, in which the +// selector was fed by DP command 0x7576 -> FutSquadServiceImpl::setAvailableKits. +// A live run refuted that: `KIT_SET` never fires, `KIT_GET` reports +// `KITS_AVAILABLE=0`, and `FUN_1801c3480` is entered 810 times without ever +// seeing a kit. The selector is actually fed by a provider CardsDLL registers +// INTO the FIFA engine — singleton FUN_1800338f0, vtable 0x1801f1d68, slots +// +0x08 enumerate and +0x10 describe — and nothing instrumented it. +// +// These three close that gap. Still strictly passive: log, then tail-call. + +// FUN_180033770 (0x33770) — enumerate kits for a team. It answers only for the +// FUT custom club (130000) and otherwise forwards to the engine default, so the +// teamId it is ASKED about is the first thing worth knowing: if the pre-match +// screen asks about a real team id, our club items were never in scope. +static KIT_ENUM_TRAMP: AtomicUsize = AtomicUsize::new(0); +unsafe extern "system" fn kit_enum_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { + let logged = budget(); + if logged { + write_log(&format!( + "KIT_ENUM: provider.enumerate this={rcx:#x} teamId={rdx} (130000 = FUT club) \ + out={r8:#x} arg4={r9:#x}\n" + )); + } + 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 logged { + write_log(&format!("KIT_ENUM: -> returned {r:#x}\n")); + } + r +} + +// sub_180033430 (0x33430) — describe one kit. Ghidra never functionised this +// address. It decodes a packed id into (teamid, year, slot) and writes NAME/TYPE +// only when that triple equals the club's active home or away triple; a +// non-match leaves the descriptor completely untouched, which is what makes the +// engine fall back to its own catalogue kit. +static KIT_DESC_TRAMP: AtomicUsize = AtomicUsize::new(0); +unsafe extern "system" fn kit_desc_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { + if budget() { + // rdx carries the packed kit id: teamid<<14 | year_field<<5 | slot. + let packed = rdx as u32; + let teamid = packed >> 14; + let year_field = (packed >> 5) & 0x1ff; + let year = if year_field != 0 { + year_field + 1800 + } else { + 0 + }; + let slot = packed & 0x1f; + write_log(&format!( + "KIT_DESC: provider.describe this={rcx:#x} packed={packed:#x} \ + -> teamid={teamid} year={year} slot={slot}\n" + )); + } + 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. Its prologue +// compares EDX against 2, so rdx is the home/away selector (2 home, 3 away), +// NOT the cardtype the earlier note assumed. This is the function that must find +// a `cardtype 7 + cardsubtypeid 9 + itemState 101/102` item; if it returns +// nothing, the active triple stays zero and every kit is unmatchable. +static KIT_SCAN_TRAMP: AtomicUsize = AtomicUsize::new(0); +unsafe extern "system" fn kit_scan_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize { + let logged = budget(); + 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 logged { + // A hit returns the matching item; report the fields the caller reads so + // a wrong-shaped kit is distinguishable from an absent one. + let detail = if r != 0 { + format!( + " HIT item={r:#x} type[+0x4c]={:?} subtype[+0x50]={:?} itemState[+0x5c]={:?} \ + teamid[+0x94]={:?} category[+0xb8]={:?} year[+0xba]={:?}", + rd_i32(r + 0x4c), + rd_i32(r + 0x50), + rd_i32(r + 0x5c), + rd_i32(r + 0x94), + rd_i32(r + 0xb8), + rd_i32(r + 0xba), + ) + } else { + " MISS (no kit item matched -> active triple stays zero)".to_string() + }; + write_log(&format!( + "KIT_SCAN: clubScan mgr={rcx:#x} sel={rdx} (2=home 3=away) a3={r8} a4={r9}{detail}\n" + )); + } + r +} + unsafe fn worker() { let mut base = 0usize; for _ in 0..600u32 { @@ -224,6 +333,51 @@ 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) on 2026-08-24; every copy length below + // is instruction-aligned and none of these prologues is rip-relative, so + // the plain installer is correct for all three. + + // 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 this one 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"); }