kits: recover the selector data path from CardsDLL; residency tracks the ROUTE, not itemType

RETRACTION FIRST. The previous commit added itemType to club items on the theory
that it gated ingestion, because player/staff sent it and were resident while
kit/badge/stadium omitted it and were not. Relaunched the client with itemType on
all three: ?type=kit answered total=2 emitted=2, and still no cardtype-7 record.

The correlation was an artefact of the control. Measured read-only over
/proc/PID/mem with full coverage (3605 MiB, nothing skipped): the "resident"
players and staff were all SQUAD members, which arrive via userMassInfo. Players
that appear in /club?type=player but NOT in userMassInfo are not resident either -
0 records for 6 of 6 sampled, 5 with no byte match at all, out of 1966 served.
Residency tracks the ROUTE. /club?type= responses never enter the persistent card
collection, and no value of itemType changes that. itemType is kept as wire
fidelity (every real EA item carries it) and relabelled; its doc no longer claims
to fix anything. The diagnostic KIT_PROBE is removed - it could only have tested
shape hypotheses that this result makes moot.

RECOVERED from the unpacked CardsDLL, no archive extraction, no instrumentation:

  Packed kit id, both directions present and agreeing:
    id = (teamid << 14) | (year ? (year-1800) << 5 : 0) | kittype
  so a kit is addressed by the triple (teamid, year, kittype).

  FUN_180033770 answers ONLY for team 130000 - 0x1800d8ab0 is literally
  `mov $0x1fbd0,%eax ; ret`. Every other team id falls through to the engine's
  catalogue kits, which are the lockable ones.

  sub_180033430 writes the tile: NAME = "HOME_SIDE"/"AWAY_SIDE", TYPE = the
  localised Kit_type_0 / Kit_type_1 / Kit_type_historical, and LOCKED (always
  value 0, never 1). If the queried triple matches NEITHER active triple it
  writes NOTHING - which is exactly why one tile rendered "undefined". A missing
  write, not a bad string. There is no Kit_type_2.

  FUN_1800d73d0 selector 2/3 does `setne dil ; add $0x65,%edi` then compares
  itemState: active home = 101, active away = 102, derived arithmetically and
  independent of the enum table. year at +0xba is movzbl - a byte INDEX.

  Above all of it: FUT_GET_MATCH_KITS_DP (0x7565) handler FUN_1800be6a0 gates on
  `cmpb $0x1,0x152(%r14)` and returns early otherwise. KITS_AVAILABLE IS
  ctx+0x152. Constructor zeroes it; the only setter is case index 6 (message
  0x757a) of the jump table at 0x1800c00d4. Live value is 0, so no kit list is
  ever built. 0x757a has no name in CardsDLL and that is bounded, not sloppy: the
  registration run ends at 0x7575 with the epilogue immediately after, and 70
  other ids resolve from the same table as the positive control.

Tables (audit_fifa17_kits.py, full-table counts): category 2/3/5 -> engine kit
type 0/1/2 with 0 counterexamples against 54/166/145 discriminating keys; the id
band is NOT home/away (band 63 holds 740 home AND 88 third).

Vault: "Kit Selector Data Path.md". cargo test 429 passed 0 failed across the two
crates; clippy -D warnings clean; fmt clean.
This commit is contained in:
funman300
2026-08-23 20:08:08 +00:00
parent eefa98c961
commit 6baa673252
3 changed files with 104 additions and 110 deletions
+26 -32
View File
@@ -371,38 +371,35 @@ pub fn shape_item(
/// Wire `itemType` (atom 0x173) for a cardtype-7 club item.
///
/// MEASURED 2026-08-23, live client pid 8793 parked on the pre-match kit
/// selector, read-only `/proc/PID/mem`. Whether a served club item becomes a
/// resident item record correlates perfectly with whether we send `itemType`:
/// Sent for WIRE FIDELITY only. Every real EA item in the capture corpus carries
/// `itemType`, and the two families OpenFUT already shaped (`player`, `staff`)
/// carry it, so omitting it on the club families was an inconsistency. Tokens
/// come from the `?type=` vocabulary decoded from the `FUN_18012ec50` jump table
/// (`kit` 12, `stadium` 13, `badge` 11).
///
/// ```text
/// family itemType sent resident record?
/// player "player" yes
/// staff "staff" yes
/// kit (absent) NO
/// badge (absent) NO
/// stadium (absent) NO
/// ```
/// It does NOT fix the pre-match kit selector, and the reasoning that first
/// introduced it was WRONG. That reasoning was: kit/badge/stadium omitted
/// `itemType` and were not resident as item records, while player and staff sent
/// it and were, so `itemType` must gate ingestion. Adding it changed nothing —
/// the client was relaunched, `?type=kit` answered `total=2 emitted=2` with
/// `itemType` present, and still no cardtype-7 record was resident.
///
/// Two of two families that carry it are ingested; none of the three that omit
/// it is. With no cardtype-7 record resident, the club scan `FUN_1800d73d0`
/// (`+0x4c == 7 && +0x50 == 9 && +0x5c in {101,102}`) matches nothing, the FUT
/// match-kit DataProvider is built empty (traced: `KITS_AVAILABLE = 0`), and the
/// selector falls back to catalogue-gated engine kits — which is the observed
/// "This kit is currently locked" dialog.
/// The correlation was an artefact of the CONTROL, not the field. Measured
/// 2026-08-23 read-only over `/proc/PID/mem`: the "resident" players and staff
/// were all SQUAD members, which arrive via `userMassInfo`. Testing players that
/// appear in `/club?type=player` but NOT in `userMassInfo` shows they are not
/// resident either — 0 records for 6 of 6 sampled, 5 with no byte match at all,
/// out of 1966 served. So residency tracks the ROUTE, not this field:
/// `/club?type=` responses do not enter the persistent card collection, and no
/// value of `itemType` changes that.
///
/// `CARD_SYSTEM.md` records that `itemType` "is parsed into a heap string and
/// never stored". That remains true of the RECORD; it does not follow that the
/// string is unused, and the correlation above is evidence that it is consulted
/// before the record is retained.
/// `CARD_SYSTEM.md`'s "parsed into a heap string and never stored" therefore
/// stands unchallenged; the earlier note here that it was "evidence the string is
/// consulted" is withdrawn.
///
/// The tokens are the `?type=` vocabulary decoded from the `FUN_18012ec50` jump
/// table (`kit` 12, `stadium` 13, `badge` 11), which is the same vocabulary the
/// two working families already use (`player` 1, `staff` 10).
///
/// INFERRED, not proven: no capture of a real EA club item exists anywhere in
/// the corpus, so the exact token for these three families is taken from the
/// atom vocabulary rather than observed on the wire.
/// INFERRED, not proven: no capture of a real EA club item exists anywhere in the
/// corpus, so the exact token for these three families is taken from the atom
/// vocabulary rather than observed on the wire.
fn club_item_type(subtype: i64) -> &'static str {
match subtype {
KIT_SUBTYPE => "kit",
@@ -1099,10 +1096,7 @@ mod tests {
year: 0,
};
let type_of = |subtype| {
shape_club_item(
Fifa17KitIdentity { subtype, ..base },
item_state::FREE,
)["itemType"]
shape_club_item(Fifa17KitIdentity { subtype, ..base }, item_state::FREE)["itemType"]
.as_str()
.expect("itemType is always emitted")
.to_string()