#!/usr/bin/env python3 """Is the squad manager REGISTERED (not merely parsed) in a live FIFA17 client? READ-ONLY. Opens /proc//mem for reading and scans. Writes nothing, sends no input to the game, and never opens 'r+b'. manager_coldproof.py [pid] [--manager-wire N] [--manager-resource N] [--control WIRE:RESOURCE ...] Defaults describe the staging profile used to close the manager milestone; pass the flags for any other profile. WHAT THIS DECIDES ----------------- FIFA17's squad parser (FUN_18013d1f0) reaches the item parser FUN_18013fe00 by two different routes: players : atom 568 -> per-element atoms 355 index / 363 itemData / 378 kitNumber; the 363 arm at 0x18013d8d9 calls the item parser on the NESTED itemData object. manager : atom 424 -> array loop at 0x18013da29 calls that same item parser DIRECTLY on the array ELEMENT, into squad+0xC0. No itemData step. So `squad.manager[]` elements must be BARE ITEM OBJECTS. When they were served as {id, itemData:{...}, dream} the parser read only the two keys that happen to be item atoms -- id and dream -- and left resourceId at 0. resourceId is the merge key, compared RAW against carddbid (fut_staff.py::manager_item, +0x18), so 0 resolves no manager: no name, no rating, no art, empty slot. Fixed in OpenFUT b91e707; see Vault "FIFA 17/Squad Manager Wire Shape.md". CONTROLS -------- manager wire id the instance id. Present even when BROKEN, because `id` is an item atom the parser reads at element level. Its presence proves the element was parsed and therefore proves nothing about registration -- do not use it as the verdict. manager resourceId THE VERDICT. Resident => the merge key survived the load. player wire id and positive controls. Players demonstrably render, so if their player resourceId resourceIds are absent the squad simply is not loaded yet and the run is INCONCLUSIVE, not a failure. RESIDENT-MANAGER HIT -------------------- A 4-byte-aligned little-endian i32 equal to the manager resourceId, anywhere in a readable private mapping. Corroborate with the record context printed below: a real item record carries resourceId eight words ahead of its wire id, which is the layout the player controls exhibit. Hits without that shape are usually id lists or unrelated integers -- the layout, not the raw count, is the proof. LAYOUT ASSUMPTION (the only one) -------------------------------- Item records place resourceId 0x20 bytes before the wire id. Measured, both sides: before b91e707 (pid 126936) -- manager parsed, merge key absent player @0xb85dbf48: 83906881 1 0 0 0 0 0 0 | 100002878 0 | 7 player @0xb85dbd68: 84053575 1 0 0 0 0 0 0 | 100003237 0 | 7 manager @0xb85dc1b8: 0 0 0 0 0 0 0 0 | 100004870 0 | 7 after b91e707 (pid 134118) -- same layout, key present player @0xb8740fd8: 84053575 1 0 0 0 0 0 0 | 100003237 0 | 7 0 player @0xb87411b8: 83906881 1 0 0 0 0 0 0 | 100002878 0 | 7 0 manager @0xb8741428: 1000509 2 0 0 0 0 0 0 | 100004870 0 | 7 0 Addresses shift every session and are recorded only as provenance; nothing here depends on them. The tool re-derives everything by scanning. EXIT CODES (fail-closed) ------------------------ 0 PASS manager resourceId resident, controls present 1 FAIL controls present, manager resourceId absent 2 NO PROCESS no FIFA17.exe, or /proc//mem unreadable 3 INCONCLUSIVE controls absent -- squad not loaded yet; re-run at the squad screen. Deliberately NOT 0: absent controls mean the probe proved nothing. """ import argparse import glob import os import re import struct import sys # Staging profile defaults (override on the command line). DEF_MANAGER_WIRE = 100004870 DEF_MANAGER_RESOURCE = 1000509 DEF_CONTROLS = [(100002878, 83906881), (100003237, 84053575)] # Item record layout: resourceId sits this far BEFORE the wire id. RESOURCE_BACK_OFF = 0x20 def find_pid(): """The Wine process whose comm is FIFA17.exe (same rule as memtool.py).""" for d in glob.glob("/proc/[0-9]*"): try: with open(os.path.join(d, "comm")) as fh: if fh.read().strip() == "FIFA17.exe": return int(os.path.basename(d)) except OSError: continue return None def regions(pid): """Readable private mappings worth scanning. Skips device/memfd mappings and anything over 512 MiB (the big reserved ranges are not where parsed records live and dominate the runtime). """ out = [] with open(f"/proc/{pid}/maps") as fh: for line in fh: m = re.match(r"([0-9a-f]+)-([0-9a-f]+)\s+(\S{4})\s+\S+\s+\S+\s+\S+\s*(.*)", line) if not m: continue lo, hi = int(m.group(1), 16), int(m.group(2), 16) perms, path = m.group(3), m.group(4) if perms[0] != "r" or path.startswith(("/dev", "/memfd")): continue if hi - lo > 512 * 1024 * 1024: continue out.append((lo, hi)) return out def scan(pid, needles, ctx_before=0x40, ctx_after=0x40): """4-byte-aligned little-endian i32 search; keeps a window around each hit.""" found = {n: [] for n in needles} pats = {n: struct.pack("= 0: if i % 4 == 0: found[n].append( (lo + i, buf[max(0, i - ctx_before): i + ctx_after], min(i, ctx_before)) ) i = buf.find(pat, i + 4) return found def words(blob, centre, before=8, after=4): cells = [] for k in range(-before, after): o = centre + k * 4 if 0 <= o <= len(blob) - 4: cells.append(str(struct.unpack_from(" len(blob) - 4: return False return struct.unpack_from("= 2: break shaped[tag] = marked ctl_keys = sum(len(res[r]) for _w, r in controls) mgr_keys = len(res[args.manager_resource]) print("\n ===== verdict =====") if ctl_keys == 0: print(" INCONCLUSIVE: no player resourceId control is resident, so the squad") print(" is not loaded. Reach the squad screen and re-run. (Nothing proven.)") return 3 if mgr_keys == 0: print(f" FAIL: manager resourceId {args.manager_resource} is absent while " f"{ctl_keys} player") print(" resourceId control hit(s) are resident -> PARSED_BUT_NOT_REGISTERED.") return 1 print(f" PASS: manager resourceId {args.manager_resource} is resident " f"({mgr_keys} hits, {shaped['manager']} in item-record layout).") print(" The merge key survived the load; the broken projection had 0.") return 0 if __name__ == "__main__": sys.exit(main())