#!/usr/bin/env python3
"""SBC cache populate/arm probe + poke (FIFA 17 CardsDLL).

READ-ONLY BY DEFAULT. The write path exists for the human's morning test but is
NEVER reached unless you pass --arm AND --i-mean-it. Running with no args only
READS /proc/<pid>/mem (O_RDONLY) and prints what a poke WOULD do.

Object graph (all static VAs, image base 0x180000000; add the live slide):
  A  = FUT root singleton  = *(0x1802e6398)          (getter 0x18011a830)
  B  = SBC TTL cache       = A + 0x1f9d8             (vtable 0x1801fae70)
       B+0x08 collection ptr, B+0x20 QPC deadline, B+0x28 ready byte
       isValid = 0x180065d40  (B.vtable[+0x08]); clear = 0x180065d20 (B.vtable[+0x10])
  HUB TTL cache            = A + 0x1fd70             (same class, armed online)
  SBC set-data req mgr     = A + 0x2a0               (vtable 0x18022be90, ctor 0x18016fac0)

Populate path (normal, online):
  fetch sbs/sets -> req mgr A+0x2a0 -> response obj (factory 0x18016fca0, vt 0x18022be80)
  -> SAX drive 0x18016c330 -> top deser 0x18017b2b0
       (which does service.[+0x9b0] to get the SBC manager, then)
       category deser 0x18017ab80 / set deser 0x18017ad60
  -> finalizers 0x180160e00 / 0x180160e50 / 0x180161020 build the SBC manager's
     category array (stride 0xf0) + set array (stride 0x3570); select/rebuild 0x180160b80
  -> generic cache commit copy-assigns a stack temp {collection, deadline, ready=1}
     into B (assign 0x1800c21a0), arming B+0x28 and pointing B+0x08 at the manager data.

The UI renders by polling isValid(B) each frame and iterating *(B+0x08). Arming
B+0x28 alone (see --arm-flag-only) opens the menu but draws EMPTY (collection NULL).
A populated render needs *(B+0x08) to point at a real set/category collection.
"""
import os, struct, sys

SLIDE_KNOWN = 0x6ffe7c140000  # informational; actual slide is read from maps
A_SINGLETON = 0x1802e6398
B_OFF       = 0x1f9d8
HUB_OFF     = 0x1fd70
REQMGR_OFF  = 0x2a0
CTRL_VA     = 0x180180d00

def find_pid():
    for d in os.listdir('/proc'):
        if d.isdigit():
            try:
                if open('/proc/%s/comm' % d).read().strip() == 'FIFA17.exe':
                    return int(d)
            except Exception:
                pass
    return None

def main():
    arm = '--arm' in sys.argv
    flag_only = '--arm-flag-only' in sys.argv
    confirm = '--i-mean-it' in sys.argv
    pid = find_pid()
    if not pid:
        print("FIFA17.exe not running -> nothing to read. (static analysis is authoritative)")
        return
    base = None
    for ln in open('/proc/%d/maps' % pid):
        if 'CardsDLL' in ln:
            base = int(ln.split('-')[0], 16); break
    if not base:
        print("CardsDLL not mapped yet (client not in Ultimate Team). Skip live step.")
        return
    slide = base - 0x180000000
    fdr = os.open('/proc/%d/mem' % pid, os.O_RDONLY)
    rd = lambda va, n: os.pread(fdr, n, va)
    q  = lambda va: struct.unpack('<Q', rd(va, 8))[0]
    # prove slide against on-disk FNV prologue
    pe = open('/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll', 'rb').read()
    f = lambda va: va - 0x180000000 - 0x1000 + 0x400
    ok = pe[f(CTRL_VA):f(CTRL_VA)+24] == rd(CTRL_VA + slide, 24)
    print("pid %d  base %#x  slide %#x  CONTROL %s" % (pid, base, slide, "OK" if ok else "MISMATCH-ABORT"))
    if not ok:
        os.close(fdr); return
    A = q(A_SINGLETON + slide)
    B = A + B_OFF
    HUB = A + HUB_OFF
    print("A(FUT root)=%#x  B(SBC cache)=%#x  HUB=%#x  reqmgr=%#x" % (A, B, HUB, A + REQMGR_OFF))
    def show(name, C):
        coll = q(C + 0x08); dl = q(C + 0x20); ready = rd(C + 0x28, 1)[0]
        vt = q(C) - slide
        print("  %-4s vt=%#x coll(+8)=%#x deadline(+0x20)=%#x ready(+0x28)=%d"
              % (name, vt, coll, dl, ready))
        return coll, dl, ready
    print("live cache state:")
    show("SBC", B); show("HUB", HUB)
    # what a poke WOULD do
    print("\n-- INTERVENTION PLAN (dry-run) --")
    print(" [flag-only]  write BYTE @ %#x = 1   (opens menu, EMPTY render)" % (B + 0x28))
    print(" [real fix]   preferred = force the client to issue sbs/sets so its own")
    print("              parser populates the SBC manager and commits B. The offline")
    print("              block is the FUT front-end refusing to call the native fetch;")
    print("              route the issued GET ut/game/fifa17/sbs/sets through the bridge.")
    if flag_only and arm and confirm:
        # guarded, explicit, single-byte only
        fdw = os.open('/proc/%d/mem' % pid, os.O_WRONLY)
        os.pwrite(fdw, b'\x01', B + 0x28)
        os.close(fdw)
        print("\n[WROTE] BYTE @ %#x = 1 (flag-only). Expect menu opens, likely empty." % (B + 0x28))
    elif arm:
        print("\n[SAFE] --arm given but not both --arm-flag-only and --i-mean-it; no write performed.")
    os.close(fdr)

if __name__ == '__main__':
    main()
