kit selector: withdraw the "client dead end" verdict, measure what is actually resident

The 2026-08-21 entry concluded the pre-match kit selector "is a client dead end,
not a missing wire field" because nothing stores 4 into item +0x60. Withdrawn.
It rested on two mistakes:

  1. +0x60 == 4 DOES occur live - a record with +0x4c == 2 and +0x60 == 4 reached
     the art-clone driver FUN_1801c3480. The immediate-store scan cannot see it,
     so "nothing can satisfy the gate" was never licensed by that evidence.
  2. It annotated `cmp [rdi+0x4c], 7` with "<- we produce this" without measuring
     it. Its own live half showed only {1: players, 0: staff}: zero cardtype-7
     records. That is the finding, and it was read as the opposite.

Measured now against the live client parked on the kit selector, read-only via
/proc/PID/mem over 3047 MiB, searching the exact u32 values the server sent:
players and staff are resident with sane fields; kit, badge and stadium are all
absent by resourceId AND by instance id. The host served ?type=kit total=2
emitted=2 at 17:50:09 this session and neither kit produced a record.

So the blocker sits upstream of the +0x60 gate: no cardtype-7 record is ever
created, so the club scan FUN_1800d73d0 has nothing to match, KIT_DESC never
fires and KITS_AVAILABLE reads 0. Cause is not yet settled - either our wire
shape (the cardtype-7 arm wants name/localizedName/description, which we do not
send) or cardtype-7 items being transient. Neither is recorded as fact.

Also: kit_gate_probe.py's live half is unreliable. On pid 8793 it reported
"CardsDb is empty" while a byte scan found 1966 resident players, so its
structural chain is stale and its record counts understate reality. Adds
club_record_residency_probe.py, which is read-only and cannot disturb the game.
This commit is contained in:
funman300
2026-08-23 18:51:34 +00:00
parent b55dd16a46
commit 40e53ed02c
2 changed files with 165 additions and 7 deletions
+54 -7
View File
@@ -518,13 +518,60 @@ CORRECTED 2026-08-06 (live diff + deserializer frame arithmetic, record_off = 0x
```
**`+0x60`, extended 2026-08-21.** "Assigned by the owning list, not parsed" is
right, and the stronger statement is now measured: the pre-match kit selector
gates on `+0x60 == 4` at `0x1801c34f2`, and **nothing anywhere stores 4 into that
field** — not in CardsDLL (29 immediate stores, constants `{-2,0,1,908,0x3f800000}`),
not in FIFA17.exe (zero across 79 MB), and no resident record has ever held it
(live: `{1: players, 0: staff}`). Every OTHER input to that gate is already
served. So the empty kit-selection screen is a client dead end, not a missing
wire field. Tool: `fifa17-recon/tools/kit_gate_probe.py`.
right. The pre-match kit selector gates on `+0x60 == 4` at `0x1801c34f2`, and no
instruction in CardsDLL stores that constant immediately (29 stores, constants
`{-2,0,1,908,0x3f800000}`), nor does FIFA17.exe across 79 MB.
Tool: `fifa17-recon/tools/kit_gate_probe.py`.
**CORRECTED 2026-08-23 (live, pid 8793, read-only `/proc/PID/mem`).** The
2026-08-21 entry went on to call the kit selector "a client dead end, not a
missing wire field", on the grounds that "every OTHER input to that gate is
already served". That conclusion is WITHDRAWN. It rested on two mistakes.
1. **`+0x60 == 4` does occur.** A live record reached the art-clone driver
`FUN_1801c3480` holding `+0x4c == 2`, `+0x60 == 4`. So the value arrives by
some path the immediate-store scan cannot see (register copy or computed),
and "nothing can ever satisfy the gate" is false. What the static scan
actually licenses is the narrower claim above.
2. **cardtype 7 was never verified to be produced at all.** The probe annotates
`cmp [rdi+0x4c], 7` with "<- we produce this". Nothing measured that. Its own
live half showed `{1: players, 0: staff}` -- i.e. zero cardtype-7 records --
and that was read as "the only thing missing is +0x60".
**What is actually measured now.** With the client parked on the kit selector,
scanning all 3047 MiB of readable process memory for the exact u32 values the
server sent:
```
resident (record-shaped, sane fields):
player resourceId 83906881 -> cardtype 1, itemState 1, teamid 243, +0x60 1
staff resourceId 9000081 -> cardtype 2
staff resourceId 3000083 -> cardtype 4, subtype 8
staff resourceId 1000509 -> cardtype 2, subtype 4, teamid 241
NOT resident, by resourceId AND by instance id, zero hits each:
kit 6300006 / 100004874 (cardsubtypeid 9)
kit 6400003 / 100004873 (cardsubtypeid 9)
badge 6000005 / 100004875 (cardsubtypeid 11)
stadium 6200000 / 100004876 (cardsubtypeid 10)
```
The client fetched `?type=kit` at 17:50:09 this session and the host logged
`total=2 emitted=2`. Both kits were delivered and NEITHER produced a record.
Every cardtype-7 family is absent while cardtype 1/2/4 are resident.
So the blocker is upstream of the `+0x60` gate: no cardtype-7 record is ever
created, therefore the club scan `FUN_1800d73d0` (`+0x4c==7 && +0x50==9 &&
`+0x5c in {101,102}`) has nothing to match, `KIT_DESC` never fires, and
`KITS_AVAILABLE` reads 0. Whether that is a bad wire shape (the cardtype-7 parse
arm wants `name`/`localizedName`/`description`, which OpenFUT does not send) or
cardtype-7 items being transient by design is NOT yet settled -- do not record
either as fact.
**Method note.** `kit_gate_probe.py`'s live half is unreliable as written: on
pid 8793 it printed "CardsDb is empty (no FUT session loaded)" while a byte scan
found 1966 resident players. Its structural chain is stale, so its record counts
(including the original "27 resident records") understate reality. Prefer the
value scan until the chain is re-derived.
**`definitionId is NOT AN ATOM`, confirmed a fourth way 2026-08-21.** Every real
atom name appears exactly once in CardsDLL's `.rdata` — `resourceId`,
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Read-only probe v3: discriminate "kits never ingested" from "ingested then freed".
Staff was refetched by the client at 18:40:38, four minutes before the scan, and
players are resident. If staff/badge/stadium records are resident but the two
kits are not, the kits are being dropped specifically.
"""
import re
import struct
import subprocess
import sys
NEEDLES = {
"PLAYER resourceId 83906881 (control, resident)": 83906881,
"STAFF resourceId 9000081 (headcoach-ish)": 9000081,
"STAFF resourceId 3000083 (x2)": 3000083,
"STAFF resourceId 1000509": 1000509,
"STAFF instance 100004870": 100004870,
"BADGE resourceId 6000005": 6000005,
"BADGE instance 100004875": 100004875,
"STADIUM resourceId 6200000": 6200000,
"STADIUM instance 100004876": 100004876,
"KIT resourceId 6300006 (home)": 6300006,
"KIT resourceId 6400003 (away)": 6400003,
"KIT instance 100004874 (home)": 100004874,
"KIT instance 100004873 (away)": 100004873,
"KIT cardassetid 35": 35,
}
def find_pid():
out = subprocess.run(["pgrep", "-f", "FIFA17.exe"], capture_output=True, text=True).stdout.split()
for p in out:
try:
with open(f"/proc/{p}/maps") as fh:
if "CardsDLL" in fh.read():
return int(p)
except OSError:
continue
return int(out[0]) if out else None
def main():
pid = find_pid()
if not pid:
sys.exit("FIFA17.exe not running")
print(f"pid={pid}")
regs = []
with open(f"/proc/{pid}/maps") as fh:
for line in fh:
m = re.match(r"([0-9a-f]+)-([0-9a-f]+) (\S{4}) \S+ \S+ \S+\s*(.*)", line)
if not m:
continue
lo, hi, perms, path = int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4)
if "r" in perms and not path.startswith("/dev/") and (hi - lo) <= (512 << 20):
regs.append((lo, hi))
hits = {k: [] for k in NEEDLES}
pats = {k: struct.pack("<I", v) for k, v in NEEDLES.items()}
mib = 0
with open(f"/proc/{pid}/mem", "rb", buffering=0) as mem:
for lo, hi in regs:
try:
mem.seek(lo)
buf = mem.read(hi - lo)
except (OSError, ValueError, OverflowError):
continue
if not buf:
continue
mib += len(buf)
for k, needle in pats.items():
start = 0
while len(hits[k]) < 5000:
i = buf.find(needle, start)
if i < 0:
break
hits[k].append(lo + i)
start = i + 4
print(f"read {mib/(1<<20):.0f} MiB\n" + "=" * 66)
def rd(base, off, size=4):
try:
mem.seek(base + off)
raw = mem.read(size)
return int.from_bytes(raw, "little") if len(raw) == size else None
except (OSError, ValueError, OverflowError):
return None
for k in NEEDLES:
addrs = hits[k]
# count how many look like real item records (plausible cardtype)
recs = []
for a in addrs[:3000]:
base = a - 0x18
ct = rd(base, 0x4C)
if ct in (1, 2, 3, 4, 5, 6, 7, 9):
recs.append((base, ct))
flag = "" if addrs else " <-- ZERO"
print(f" {len(addrs):6d} raw / {len(recs):4d} record-shaped {k}{flag}")
for base, ct in recs[:3]:
print(f" @{base:#x} cardtype={ct} subtype={rd(base,0x50)} "
f"itemState={rd(base,0x5c)} +0x60={rd(base,0x60)} "
f"teamid={rd(base,0x94)} cat={rd(base,0xb8)} year={rd(base,0xba,2)}")
print("=" * 66)
if __name__ == "__main__":
main()