#!/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//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())