70a64e3709
Freeze the running offline FUT backend into version control as fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a fresh checkout: * OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is required for remote mode (compose and entrypoint fail without it) * docker-compose.yml reproducing the frozen baseline container exactly (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart) * .env.example / .env for site config - the LAN IP is never hardcoded in source * tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10, verified byte-identical to the running container at freeze time * client_arm.sh (the 105 client-side arming counterpart) * Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying * docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record, restore instructions and rebuild-equivalence procedure Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored. The live container is untouched pending the .105 launcher audit.
67 lines
3.0 KiB
Python
Executable File
67 lines
3.0 KiB
Python
Executable File
#!/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)
|