|
|
|
@@ -0,0 +1,184 @@
|
|
|
|
|
//! 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() });
|
|
|
|
|
}
|