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
|
**`+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
|
right. The pre-match kit selector gates on `+0x60 == 4` at `0x1801c34f2`, and no
|
||||||
gates on `+0x60 == 4` at `0x1801c34f2`, and **nothing anywhere stores 4 into that
|
instruction in CardsDLL stores that constant immediately (29 stores, constants
|
||||||
field** — not in CardsDLL (29 immediate stores, constants `{-2,0,1,908,0x3f800000}`),
|
`{-2,0,1,908,0x3f800000}`), nor does FIFA17.exe across 79 MB.
|
||||||
not in FIFA17.exe (zero across 79 MB), and no resident record has ever held it
|
Tool: `fifa17-recon/tools/kit_gate_probe.py`.
|
||||||
(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
|
**CORRECTED 2026-08-23 (live, pid 8793, read-only `/proc/PID/mem`).** The
|
||||||
wire field. Tool: `fifa17-recon/tools/kit_gate_probe.py`.
|
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
|
**`definitionId is NOT AN ATOM`, confirmed a fourth way 2026-08-21.** Every real
|
||||||
atom name appears exactly once in CardsDLL's `.rdata` — `resourceId`,
|
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