#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Accumulate FUT_ID_SWEEP results into a real player database. READ-ONLY against the game (it drives card_identity_probe, which only reads /proc/PID/mem). It writes exactly one file: data/players.json in this repo. HOW THE ORACLE WORKS, AND ITS ONE FALSE ANSWER ---------------------------------------------- utas_server's FUT_ID_SWEEP serves a window of candidate playerids as a synthetic club. The client merges its OWN local players table into every item it parses, so after one club fetch the resolved identity is sitting in the CardsDb map. A candidate is REAL when the client gives it a name. The false answer to guard against is the DB's default row: ids with no entry come back named "Jamal Blackman", byte-identical every time (first "Jamal", last "Blackman"). Six of our own pool ids hit this, including one that was in VERIFIED_ASSET_IDS -- which is why the old "verified" list cannot be trusted and this tool exists. So DEFAULT_NAME below is a rejection filter, not a curiosity. If a genuine Jamal Blackman is ever needed, take him from his real id, not from this sweep. Usage: sweep_collect.py probe now, merge into data/players.json sweep_collect.py --show summarise the accumulated database """ import argparse import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import card_identity_probe as P # noqa: E402 import watch_club_model as W # noqa: E402 DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "players.json") DEFAULT_NAME = ("Jamal", "Blackman") # the DB's empty-row placeholder SENTINEL_RATING = 7 # what utas_server serves in a sweep def load(): try: with open(DB) as f: return {int(k): v for k, v in json.load(f).items()} except (IOError, ValueError): return {} def save(db): os.makedirs(os.path.dirname(DB), exist_ok=True) with open(DB, "w") as f: json.dump({str(k): v for k, v in sorted(db.items())}, f, indent=1, ensure_ascii=False) def probe(): pid = W.find_pid() if pid is None: print("FIFA17.exe is not running.") return None base = W.dll_base(pid) mem = W.Mem(pid) obj = mem.q(base + (W.G_CARDSDB - W.IMG_BASE)) if base else None if not obj: print("CardsDb is not loaded (no FUT session).") return None cards = [c for c in (P.read_card(mem, n) for n in P.nodes(mem, obj)) if c] print("read %d card record(s), failed reads=%d" % (len(cards), mem.fails)) return cards def merge(db, cards): added = skipped = placeholder = 0 for c in cards: first, last = c["first"], c["last"] if (first, last) == DEFAULT_NAME: placeholder += 1 continue if not (first or last or c["known"]): skipped += 1 continue pid = c["playerid"] row = { "first": first, "last": last, "known": c["known"], "teamid": c["teamid"], "nation": c["nation"], "league": c["league"], "position": c["position"], } # A sweep card carries our sentinel rating, so the DB never learns a rating # from it. Ratings from OWNED cards are ours too -- the merge never # overwrites a nonzero rating. Rating therefore stays out of this file # rather than being recorded as if the game had supplied it. if pid not in db: added += 1 db[pid] = row return added, skipped, placeholder def show(db): print("%d player(s) in %s" % (len(db), os.path.normpath(DB))) if not db: return ids = sorted(db) print("id range %d..%d" % (ids[0], ids[-1])) teams = len({r["teamid"] for r in db.values() if r["teamid"]}) nats = len({r["nation"] for r in db.values() if r["nation"]}) lgs = len({r["league"] for r in db.values() if r["league"]}) print("%d club(s), %d nation(s), %d league(s)" % (teams, nats, lgs)) for pid in ids[:10]: r = db[pid] print(" %-7s %-28s team=%-6s nat=%-4s league=%s" % (pid, ("%s %s" % (r["first"], r["last"])).strip()[:28], r["teamid"], r["nation"], r["league"])) def main(): ap = argparse.ArgumentParser() ap.add_argument("--show", action="store_true") a = ap.parse_args() db = load() if a.show: show(db) return 0 cards = probe() if cards is None: return 1 added, skipped, placeholder = merge(db, cards) save(db) print("added %d new, %d placeholder row(s) rejected, %d unnamed skipped" % (added, placeholder, skipped)) show(db) return 0 if __name__ == "__main__": sys.exit(main())