diag(fifa17): port the passive kit-selector trace onto main
Cherry-pick of 4b1d5aa from wip/kit-selector-re, which forked before the TLS
work and cannot be rebased: that branch predates fifa17_tls.rs/patch_mem.rs and
carries a large unrelated lineage (probe/lsx/recv_hook). Only the kit_trace
commit's own contents are taken.
Traces the client-side FUT pre-match kit path in CardsDLL: the GetMatchKits_DP
gate (KITS_AVAILABLE), setAvailableKits (home/away list count), the kit-item
clone driver (item type/subid/teamid at FUN_1801c3480), and the local teamkits
DB clone. Read-only passive detours reusing season_trace's installers, which
this widens to pub(crate).
The one open unknown it answers: the selector requires item+0x60 == 4, a pile
value the server has never been observed to produce (/club emits 1,
/purchased 6).
Verified in the cross-built artifact that the roster TLS gate patch is intact
alongside the new trace (fifa17_tls markers present, KIT_* markers present).
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
//! 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<usize> {
|
||||
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() });
|
||||
}
|
||||
Reference in New Issue
Block a user