#!/usr/bin/env python3
"""Watch for a (re)launched FIFA17.exe and auto-apply both ProtoSSL cert patches
the moment its unpacked code is mapped. Idempotent; keeps watching across relaunches."""
import glob, time, struct, sys

# Watch for a (re)launched FIFA17.exe and auto-apply ProtoSSL cert + FUT store patches
import glob, time, os

GATE2=0x1461361b0; GATE2_ORIG=bytes.fromhex("48895c"); GATE2_PATCH=bytes.fromhex("31c0c3")
GATE1=0x146132548; GATE1_ORIG=bytes.fromhex("0f8576010000"); GATE1_PATCH=bytes.fromhex("90"*6)

RET_TRUE = bytes.fromhex("b801000000c3")
NOP2 = bytes.fromhex("9090")
IMG_BASE = 0x180000000

STORE_PATCHES = {
    0x1800f7fb0: RET_TRUE,
    0x1800fb850: RET_TRUE,
    0x180100500: RET_TRUE,
    0x180013cf0: RET_TRUE,
    0x180017543: bytes.fromhex("eb3f"),
    0x180017487: NOP2,
    0x180017490: NOP2,
    0x1800175aa: NOP2,
}

# Store resolver crash-guard for the empty "My Packs" case (bug 6c; PROVEN R1 on the
# tested FIFA 17 build -- see docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART IV and
# docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md).
#
# When no `mypacks` group exists, FIFA's Store resolver receives category id -1. CardsDLL
# FUN_1800147f0 @ 0x180014858 is `JNZ 0x14869` (75 0f): the original treats every non-zero
# category (including -1) as resolvable, calls FUN_180014420, gets NULL, and crashes at the
# [NULL+0x48] deref in FUN_1800147f0 (0x180014882). Changing JNZ->JG (7f 0f) preserves
# positive-category resolution (EDI>0 branch) while routing zero/negative categories through
# the existing Browse/list-all path -> no NULL lookup, no crash, Store opens on Browse Packs.
#
# CAVEAT: this guards the category SIGN only. It does NOT protect a stale *positive* invalid
# ordinal produced by changing the Store group topology (sentinel-present <-> sentinel-absent)
# DURING one running FIFA process -- that reproduced the same crash in the confounded run F3.
# The empty-My-Packs representation MUST stay stable for a FIFA session (see the SESSION-STABLE
# invariant in the client-fix plan).
#
# Orig-verified / fail-closed: applied only when the live bytes are the known original (75 0f);
# already-patched (7f 0f) is a no-op; anything else is logged and SKIPPED (never blindly
# overwritten), so an unrecognised CardsDLL build is not patched.
STORE_PATCHES_GUARDED = {
    0x180014858: (bytes.fromhex("750f"), bytes.fromhex("7f0f")),  # JNZ 0x14869 -> JG 0x14869
}

# Capability advertised to the launcher/backend once the resolver guard is VERIFIED
# live in a specific FIFA process (docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md #3/#4).
EMPTY_MYPACKS_RESOLVER_CAPABILITY = "fifa17.empty_mypacks_resolver"
EMPTY_MYPACKS_RESOLVER_VERSION = 1

# The guarded site whose verified enforcement backs the capability above.
RESOLVER_GUARD_VA = 0x180014858

# Per-FIFA-pid guard status (fail-closed; FIFA17_PATCHED_CLIENT_CAPABILITY.md #4).
GUARD_NOT_ATTEMPTED = "NOT_ATTEMPTED"          # CardsDLL not mapped / guard not yet evaluated
GUARD_VERIFIED = "VERIFIED"                     # live bytes == patch after enforcement (patch or noop)
GUARD_UNSUPPORTED_BUILD = "UNSUPPORTED_BUILD"   # neither original nor patched (guarded_action -> skip)
GUARD_WRITE_FAILED = "WRITE_FAILED"            # /proc/<pid>/mem write raised
GUARD_VERIFY_FAILED = "VERIFY_FAILED"          # post-write re-read != patch

LOG=os.environ.get("OPENFUT_AUTOPATCH_LOG", f"/tmp/openfut-autopatch-{os.getuid()}.log")

def log(m):
    line=f"[{time.strftime('%H:%M:%S')}] {m}"
    print(line,flush=True); open(LOG,"a").write(line+"\n")

def find_pids():
    out=[]
    for d in glob.glob('/proc/[0-9]*'):
        try:
            if open(d+'/comm').read().strip()=='FIFA17.exe': out.append(int(d.split('/')[-1]))
        except: pass
    return out

def cardsdll_base(pid):
    try:
        for line in open(f'/proc/{pid}/maps'):
            if 'CardsDLL' in line: return int(line.split('-')[0], 16)
    except: pass
    return None

def rd(pid,va,n):
    with open(f'/proc/{pid}/mem','rb') as f:
        f.seek(va); return f.read(n)
def wr(pid,va,b):
    with open(f'/proc/{pid}/mem','r+b') as f:
        f.seek(va); f.write(b)

def guarded_action(cur, orig, patch):
    """Fail-closed decision for a guarded byte patch (see STORE_PATCHES_GUARDED).

    Returns "noop" when the live bytes are already patched, "patch" when they are the
    known original (safe to apply), or "skip" for anything else -- an unrecognised
    CardsDLL build that must never be blindly overwritten.
    """
    if cur == patch:
        return "noop"
    if cur == orig:
        return "patch"
    return "skip"

def guard_state_after(cur_before, orig, patch, wrote_ok, cur_after):
    """Map a guarded-patch enforcement outcome to a per-pid guard STATE (pure).

    Mirrors guarded_action's decision, extended with post-write verification so the
    caller advertises the capability only on VERIFIED. No /proc access -- unit-testable.

    - cur_before == patch -> VERIFIED           (already patched; guarded_action "noop")
    - cur_before == orig  -> WRITE_FAILED if the write raised, else VERIFIED when the
                             re-read is patch, else VERIFY_FAILED (guarded_action "patch")
    - otherwise           -> UNSUPPORTED_BUILD  (guarded_action "skip")
    """
    if cur_before == patch:
        return GUARD_VERIFIED
    if cur_before == orig:
        if not wrote_ok:
            return GUARD_WRITE_FAILED
        if cur_after == patch:
            return GUARD_VERIFIED
        return GUARD_VERIFY_FAILED
    return GUARD_UNSUPPORTED_BUILD

patched=set()
store_patched=set()
guard_reported=set()

if __name__ == "__main__":
    launcher_pid = None
    if "--launcher-pid" in sys.argv:
        try: launcher_pid = int(sys.argv[sys.argv.index("--launcher-pid") + 1])
        except (ValueError, IndexError): raise SystemExit("invalid --launcher-pid")

    log("=== AUTOPATCH watching for FIFA17.exe ===")
    while True:
        if launcher_pid and not os.path.exists(f"/proc/{launcher_pid}"):
            log(f"launcher pid {launcher_pid} exited; stopping autopatch")
            break
        for pid in find_pids():
            if pid not in patched:
                try:
                    g2=rd(pid,GATE2,3); g1=rd(pid,GATE1,6)
                except Exception:
                    continue  # code not mapped yet
                if g2==GATE2_PATCH and g1==GATE1_PATCH:
                    log(f"pid {pid}: cert gates already patched"); patched.add(pid)
                elif g2==GATE2_ORIG and g1==GATE1_ORIG:
                    try:
                        wr(pid,GATE2,GATE2_PATCH); wr(pid,GATE1,GATE1_PATCH)
                        log(f"pid {pid}: PATCHED cert gates")
                        patched.add(pid)
                    except Exception as e:
                        log(f"pid {pid}: cert patch write failed: {e}")

            # Continuously enforce store patches every tick
            cbase = cardsdll_base(pid)
            if cbase is not None:
                try:
                    for va, data in STORE_PATCHES.items():
                        live = cbase + (va - IMG_BASE)
                        if rd(pid, live, len(data)) != data:
                            wr(pid, live, data)
                            log(f"pid {pid}: ENFORCED store patch @ {live:#x}")
                    for va, (orig, patch) in STORE_PATCHES_GUARDED.items():
                        live = cbase + (va - IMG_BASE)
                        cur = rd(pid, live, len(patch))
                        action = guarded_action(cur, orig, patch)
                        wrote_ok = True
                        cur_after = cur
                        if action == "patch":
                            try:
                                wr(pid, live, patch)
                                log(f"pid {pid}: ENFORCED guarded store patch @ {live:#x} (JNZ->JG, empty My Packs)")
                            except Exception as e:
                                wrote_ok = False
                                log(f"pid {pid}: guarded patch write failed @ {live:#x}: {e}")
                            if wrote_ok:
                                try:
                                    cur_after = rd(pid, live, len(patch))
                                except Exception:
                                    cur_after = b""
                        elif action == "skip":
                            log(f"pid {pid}: SKIP guarded patch @ {live:#x}: unexpected {cur.hex()} (build mismatch)")
                        # action == "noop": already patched; nothing to write.
                        if va == RESOLVER_GUARD_VA and pid not in guard_reported:
                            state = guard_state_after(cur, orig, patch, wrote_ok, cur_after)
                            if state == GUARD_VERIFIED:
                                log(f"[store-guard] verified capability {EMPTY_MYPACKS_RESOLVER_CAPABILITY}={EMPTY_MYPACKS_RESOLVER_VERSION} fifa_pid={pid}")
                            else:
                                log(f"[store-guard] guard status={state} fifa_pid={pid} (no capability advertised)")
                            guard_reported.add(pid)
                    if pid not in store_patched:
                        log(f"pid {pid}: PATCHED store gates in CardsDLL @ {cbase:#x}")
                        store_patched.add(pid)
                except Exception as e:
                    log(f"pid {pid}: store patch write failed: {e}")

        time.sleep(1)
