#!/usr/bin/env python3
"""Read the FutDataManagerImpl UI gate bytes out of the LIVE FIFA 17 client.

Why this exists: on 2026-08-05 the /settings gate plan concluded that
IS_FRIENDLY_SEASON_ENABLED and IS_DRAFT_MODE_ENABLED had never been set true by
anything. Measured against the running client, both are 1, and have been all along.
The applier FUN_18011dc50 runs whether or not the configs array has content, and the
settings struct it is handed defaults these fields to 1. "Nothing populates the array"
is not "nothing writes the byte".

Read-only. Opens /proc/<pid>/mem O_RDONLY and preads. Nothing here can write.

Nothing is assumed:
  * the pid is resolved by exact /proc/*/comm match, never hardcoded
  * the CardsDLL base is read from /proc/<pid>/maps, never cached across launches
    (Wine copies the sections into anonymous memory, so only the 4 KiB PE header is
    file-backed and `grep CardsDLL maps` returns exactly ONE line, which is easy to
    misread as "barely mapped")
  * the slide is PROVEN against the FNV atom-hash prologue at 0x180180d00, read from
    the on-disk PE, before any other address is trusted
  * each gate byte displacement is DECODED from its accessor stub (0f b6 81 <disp32>,
    movzx eax, byte [rcx+disp32]) rather than taken from a table

Requires the client to have reached Ultimate Team, since CardsDLL loads only then.
Usage: python3 gate_byte_probe.py
"""
import os, struct, sys
pid=None
for d in os.listdir('/proc'):
    if d.isdigit():
        try:
            if open('/proc/%s/comm'%d).read().strip()=='FIFA17.exe': pid=int(d); break
        except Exception: pass
assert pid, "not running"
print("pid", pid)
base=None
for ln in open('/proc/%d/maps'%pid):
    if 'CardsDLL' in ln:
        base=int(ln.split('-')[0],16); print("cardsdll map line:", ln.strip())
assert base
slide = base - 0x180000000
print("base %#x slide %#x" % (base, slide))
fd=os.open('/proc/%d/mem'%pid, os.O_RDONLY)
def rd(va,n): return os.pread(fd, n, va)
# control: FNV prologue, bytes taken from the on-disk PE
pe=open('/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll','rb').read()
# .text rva 0x1000 rawptr 0x400
def f(va): return va-0x180000000-0x1000+0x400
ctl_disk=pe[f(0x180180d00):f(0x180180d00)+32]
ctl_live=rd(0x180180d00+slide,32)
print("CONTROL FNV", "MATCH" if ctl_disk==ctl_live else "MISMATCH", ctl_live.hex())
# model singleton
dat=0x1802e6398+slide
obj=struct.unpack('<Q', rd(dat,8))[0]
print("DAT_1802e6398 ->", hex(obj))
vt=struct.unpack('<Q', rd(obj,8))[0]
print("vtable live %#x  static %#x" % (vt, vt-slide))
for off,name in [(0x2b0,'friendlySeasons'),(0x2c8,'draftMode'),(0x2e0,'packOpeningAnimation')]:
    slot=struct.unpack('<Q', rd(vt+off,8))[0]
    stub=rd(slot,8)
    disp=struct.unpack('<I', stub[3:7])[0] if stub[:3]==b'\x0f\xb6\x81' else None
    val=rd(obj+disp,1)[0] if disp is not None else None
    print("  slot +%#x -> %#x stub=%s disp=%s value=%s" % (off, slot-slide, stub.hex(), hex(disp) if disp else None, val))
# unopenedPacks total
print("model+0x20950 =", struct.unpack('<I', rd(obj+0x20950,4))[0])
os.close(fd)
