2 Commits

Author SHA1 Message Date
funman300 d7641175be 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.
2026-08-23 18:39:44 +00:00
funman300 89cf5df71f kit_trace: rate-limited engine-provider traces (the first cut froze the client)
The previous version of these three detours hung FIFA at the "are both teams
ready" prompt. FUN_180033770 is POLLED - about 150 calls alternating between the
two real match team ids - and the wrapper did a synchronous write_log on every
one. That is the whole cause; the detours themselves were sound.

Rebuilt so the hot path costs nothing:

  - kit_enum returns immediately unless the team is the FUT club (130000), so
    the polled case does no formatting and no I/O at all. When it is the FUT
    club it reports the out-list length, i.e. how many kits were actually
    offered - the number that decides whether the carousel has anything.
  - kit_desc dedupes on the packed kit id, so a carousel that re-describes the
    same kit logs it once. It decodes teamid/year/slot for comparison against
    the active triple.
  - kit_scan only reports cardtype 7 and dedupes on (subtype, selector).

Dedupe is a fixed 16-slot lock-free SeenSet - no allocation, no locks, safe to
consult from a polled game thread. A full set stops reporting rather than
evicting, because the point is a bounded log.

Rule this file broke once and must not break again: no trace may log per-call on
a polled function.

Motivation changed too. The kit selector is not cosmetic: it is what blocks
entering a match, which also explains why every match in the capture corpus is a
DNF with an unpopulated params object - entered and backed out of. A screenshot
shows the carousel with two tiles, one named HOME and one labelled "undefined",
both with untextured white shirts, which is exactly sub_180033430 writing NAME
on a match and nothing at all on a miss.
2026-08-23 18:36:11 +00:00
-154
View File
@@ -156,115 +156,6 @@ 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 {
@@ -333,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) 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");
}