#!/usr/bin/env python3
"""Recover FIFA's on-screen error text from the live client, read-only.

Diagnosing this squad error from the server side has failed repeatedly: the host
answers 200/ok for every request, the squad round-trips exactly, and its shape now
matches production's known-good squad field for field. So the message the client is
actually showing is the missing evidence.

Scans readable regions of /proc/<pid>/mem for candidate substrings in both ASCII and
UTF-16LE (FIFA UI strings are typically wide), and prints the surrounding text so the
full sentence and any error code come out. Opens the memory O_RDONLY and only ever
pread()s -- it cannot perturb the process.
"""
import os
import re
import sys

NEEDLES = [b"quad Update", b"quad update", b"QUAD_UPDATE", b"quad_update",
           b"pdate your squad", b"pdating squad", b"quad Management",
           b"nable to update", b"FUT_ERR", b"SQUAD_ERR"]
MAX_REGION = 96 * 1024 * 1024   # skip absurd regions; the UI heap is not that big
CONTEXT = 140


def pid_of(name="FIFA17.exe"):
    for d in os.listdir("/proc"):
        if not d.isdigit():
            continue
        try:
            if open(f"/proc/{d}/comm").read().strip() == name:
                return int(d)
        except OSError:
            continue
    return None


def wide(b):
    """UTF-16LE form of an ASCII needle."""
    return b"".join(bytes([c, 0]) for c in b)


def regions(pid):
    out = []
    for line in open(f"/proc/{pid}/maps"):
        parts = line.split()
        if len(parts) < 2 or "r" not in parts[1]:
            continue
        lo, _, hi = parts[0].partition("-")
        lo, hi = int(lo, 16), int(hi, 16)
        size = hi - lo
        if 0 < size <= MAX_REGION:
            out.append((lo, size, parts[-1] if len(parts) > 5 else ""))
    return out


def render(buf, pos, is_wide):
    lo = max(0, pos - CONTEXT)
    hi = min(len(buf), pos + CONTEXT)
    chunk = buf[lo:hi]
    if is_wide:
        try:
            txt = chunk.decode("utf-16le", errors="replace")
        except Exception:
            txt = repr(chunk)
    else:
        txt = chunk.decode("latin-1", errors="replace")
    txt = re.sub(r"[^\x20-\x7e]+", " ", txt)
    return re.sub(r"\s{2,}", "  ", txt).strip()


def main():
    pid = pid_of()
    if not pid:
        print("FIFA17.exe not running")
        return 1
    print(f"scanning pid {pid}")
    targets = [(n, False) for n in NEEDLES] + [(wide(n), True) for n in NEEDLES]
    hits, scanned = [], 0
    fd = os.open(f"/proc/{pid}/mem", os.O_RDONLY)
    try:
        for lo, size, path in regions(pid):
            try:
                buf = os.pread(fd, size, lo)
            except OSError:
                continue
            scanned += size
            for needle, is_wide in targets:
                start = 0
                while True:
                    p = buf.find(needle, start)
                    if p < 0:
                        break
                    hits.append((lo + p, is_wide, render(buf, p, is_wide), path))
                    start = p + 1
                    if len(hits) > 60:
                        break
    finally:
        os.close(fd)
    print(f"scanned {scanned // (1024*1024)} MiB, {len(hits)} hit(s)\n")
    seen = set()
    for addr, is_wide, txt, path in hits:
        key = txt[:110]
        if key in seen:
            continue
        seen.add(key)
        kind = "utf16" if is_wide else "ascii"
        print(f"0x{addr:x} [{kind}] {path}")
        print(f"    {txt}\n")
    return 0


if __name__ == "__main__":
    sys.exit(main())
