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:
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user