From 40e53ed02cd56d94ab0a4f2d67fc0229f0948e1a Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 23 Aug 2026 18:51:34 +0000 Subject: [PATCH] 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. --- fifa17-recon/docs/CARD_SYSTEM.md | 61 ++++++++-- .../tools/club_record_residency_probe.py | 111 ++++++++++++++++++ 2 files changed, 165 insertions(+), 7 deletions(-) create mode 100755 fifa17-recon/tools/club_record_residency_probe.py diff --git a/fifa17-recon/docs/CARD_SYSTEM.md b/fifa17-recon/docs/CARD_SYSTEM.md index 998d1d7..2565e7c 100644 --- a/fifa17-recon/docs/CARD_SYSTEM.md +++ b/fifa17-recon/docs/CARD_SYSTEM.md @@ -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`, diff --git a/fifa17-recon/tools/club_record_residency_probe.py b/fifa17-recon/tools/club_record_residency_probe.py new file mode 100755 index 0000000..d6675fe --- /dev/null +++ b/fifa17-recon/tools/club_record_residency_probe.py @@ -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("