40e53ed02c
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.
112 lines
3.9 KiB
Python
Executable File
112 lines
3.9 KiB
Python
Executable File
#!/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()
|