//! Passive, behavior-preserving diagnostic traces for FIFA 17's FUT pre-match //! KIT SELECTOR data flow. //! //! RE (2026-08-20, Ghidra on CardsDLL_Win64_retail.dll) established that the //! pre-match kit selector is fed ENTIRELY client-side (NOT by POW/EASFC): //! //! * `FUT_GET_MATCH_KITS_DP` (id 0x7565) builder `FUN_1800be6a0` (rva 0xbe6a0) //! reads a boolean gate `ctx+0x152` (`KITS_AVAILABLE`); when false, or when //! the two available-kit vectors are empty, the selector renders blank/white. //! * The available home/away kit-id lists live on `FutSquadServiceImpl` //! (`this+0xe08` home, `this+0xe38` away) and are written by the setter //! `FUN_180196760` (rva 0x96760, vtable slot 0x1d0): args (this, srcVec, side). //! * A club KIT ITEM is turned into an available kit by `FUN_1801c3480` //! (rva 0x1c3480): it reads item fields (`+0x4c==7`, `+0x60==4`, //! `+0x5c`∈{101 home,102 away}, `+0x94` source teamid, `+0xba` //! teamkittypetechid) and calls `FUN_1801c44b0` (rva 0x1c44b0) to clone that //! team's kit rows from the CLIENT-LOCAL `teamkits` DB into the FUT club //! (teamtechid 130000). //! //! These traces answer, in one operator-driven match, exactly WHERE the empty //! selector originates: do kit club items reach the client (kit_item_clone), does //! the clone into the FUT club happen (kit_db_clone), does the available list get //! set non-empty (set_available_kits), and what does the selector finally read //! (get_match_kits: KITS_AVAILABLE + count). Every trace is read-only: it logs, //! then tail-calls the original through a trampoline. Copied prologues are whole, //! position-independent instructions (the one rip-relative prologue uses the //! relocating installer). use core::sync::atomic::{AtomicUsize, Ordering}; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA; use crate::sbc_trace::{readable_range, validate_cards_build}; use crate::season_trace::{install_detour, install_detour_reloc, rd_i32, rd_u8}; use crate::write_log; static REPORTS: AtomicUsize = AtomicUsize::new(0); fn budget() -> bool { REPORTS.fetch_add(1, Ordering::Relaxed) < 256 } unsafe fn rd_usize(addr: usize) -> Option { readable_range(addr, 8).then(|| core::ptr::read_volatile(addr as *const usize)) } // FUT_GET_MATCH_KITS_DP builder FUN_1800be6a0 (0xbe6a0). rcx = DP model ctx. // ctx+0x152 is the KITS_AVAILABLE bool that gates the whole selector list. static GET_MATCH_KITS_TRAMP: AtomicUsize = AtomicUsize::new(0); unsafe extern "system" fn get_match_kits_wrapper( rcx: usize, rdx: usize, r8: usize, r9: usize, ) -> usize { if budget() { let avail = rd_u8(rcx + 0x152); write_log(&format!( "KIT_GET: FUT_GET_MATCH_KITS_DP ctx={rcx:#x} KITS_AVAILABLE={avail:?}\n" )); } let t = GET_MATCH_KITS_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) } // setAvailableKits FUN_180196760 (0x96760): (this, srcVec, side). srcVec is an // int vector {begin@+0, end@+8}; count = (end-begin)/4. side 0=home, 1=away. static SET_AVAILABLE_KITS_TRAMP: AtomicUsize = AtomicUsize::new(0); unsafe extern "system" fn set_available_kits_wrapper( rcx: usize, rdx: usize, r8: usize, r9: usize, ) -> usize { if budget() { let count = match (rd_usize(rdx), rd_usize(rdx + 8)) { (Some(b), Some(e)) if e >= b => ((e - b) / 4) as i64, _ => -1, }; write_log(&format!( "KIT_SET: setAvailableKits this={rcx:#x} side={r8} count={count}\n" )); } let t = SET_AVAILABLE_KITS_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) } // Kit-item clone driver FUN_1801c3480 (0x1c3480): rdx = param_2, the club-item // event; the item struct is at *(param_2+0x10). Logs the fields the function // branches on so we can see whether a kit club item reaches the client and its // home/away designator + source teamid. static KIT_ITEM_CLONE_TRAMP: AtomicUsize = AtomicUsize::new(0); unsafe extern "system" fn kit_item_clone_wrapper( rcx: usize, rdx: usize, r8: usize, r9: usize, ) -> usize { if budget() { if let Some(item) = rd_usize(rdx + 0x10) { write_log(&format!( "KIT_ITEM: clone-driver item={item:#x} type[+0x4c]={:?} subid[+0x5c]={:?} \ cat[+0x60]={:?} teamid[+0x94]={:?} kittype[+0xba]={:?}\n", rd_i32(item + 0x4c), rd_i32(item + 0x5c), rd_i32(item + 0x60), rd_i32(item + 0x94), rd_i32(item + 0xba), )); } else { write_log(&format!( "KIT_ITEM: clone-driver param_2={rdx:#x} (item ptr unreadable)\n" )); } } let t = KIT_ITEM_CLONE_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) } // Kit DB clone FUN_1801c44b0 (0x1c44b0): (clubmgr, side, teamtechid, kittype). // Fires only when the driver decided the item is a home(101)/away(102) kit, so // this is the proof the FUT-club (teamtechid 130000) kit rows get synthesized. static KIT_DB_CLONE_TRAMP: AtomicUsize = AtomicUsize::new(0); unsafe extern "system" fn kit_db_clone_wrapper( rcx: usize, rdx: usize, r8: usize, r9: usize, ) -> usize { if budget() { write_log(&format!( "KIT_DBCLONE: clone team kit side={rdx} src_teamtechid={r8} kittype={r9}\n" )); } let t = KIT_DB_CLONE_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) } unsafe fn worker() { let mut base = 0usize; for _ in 0..600u32 { base = GetModuleHandleA(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) as usize; if base != 0 { break; } std::thread::sleep(std::time::Duration::from_millis(500)); } if base == 0 || !validate_cards_build(base) { write_log("KIT_TRACE: CardsDLL unavailable/invalid; kit trace inactive\n"); return; } // FUN_1800be6a0: 48 8b c4 55 41 54 41 55 41 56 41 57 48 8d 68 a1 (copy_len 16). install_detour( base, 0xbe6a0, "GetMatchKits_DP(0xbe6a0)", 16, &[ 0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d, 0x68, 0xa1, ], get_match_kits_wrapper as *const () as usize, &GET_MATCH_KITS_TRAMP, ); // FUN_180196760: 48 89 54 24 10 53 48 83 ec 30 48 c7 44 24 20 fe ff ff ff (copy_len 19). install_detour( base, 0x96760, "setAvailableKits(0x96760)", 19, &[ 0x48, 0x89, 0x54, 0x24, 0x10, 0x53, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff, 0xff, ], set_available_kits_wrapper as *const () as usize, &SET_AVAILABLE_KITS_TRAMP, ); // FUN_1801c3480: 48 89 5c 24 08 57 48 83 ec 60 <48 8b 05 disp32> (rip-relative // MOV RAX,[rip+..] at copied offset 10; disp32 at 13, insn end 17; copy_len 17). install_detour_reloc( base, 0x1c3480, "kitItemClone(0x1c3480)", 17, &[ 0x48, 0x89, 0x5c, 0x24, 0x08, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0x8b, 0x05, 0x4f, 0x82, 0x11, 0x00, ], 13, 17, kit_item_clone_wrapper as *const () as usize, &KIT_ITEM_CLONE_TRAMP, ); // FUN_1801c44b0: 48 8b c4 55 41 54 41 55 41 56 41 57 48 8d 68 c8 (copy_len 16). install_detour( base, 0x1c44b0, "kitDbClone(0x1c44b0)", 16, &[ 0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d, 0x68, 0xc8, ], kit_db_clone_wrapper as *const () as usize, &KIT_DB_CLONE_TRAMP, ); write_log("KIT_TRACE: all kit-selector traces armed\n"); } /// Arm the passive kit-selector diagnostics on a deferred thread (CardsDLL is not /// yet loaded at DllMain time). Read-only: never changes game behavior. pub(crate) fn install() { write_log("KIT_TRACE: requested; deferred signature validation starting\n"); std::thread::spawn(|| unsafe { worker() }); }