diff --git a/fifa17-recon/tools/repair_club.py b/fifa17-recon/tools/repair_club.py new file mode 100644 index 0000000..ab1cb6c --- /dev/null +++ b/fifa17-recon/tools/repair_club.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Bring every club card into line with the game's own player data. + +Two different faults, two different remedies: + + STALE the card is a real FIFA 17 player but its stored fields are the old + invented ones (rating derived from nothing, synthetic position, guessed + club and nation, attributes computed from the rating). REPAIRED in place + from data/pool.json, which is measured from the game's own database. + Deleting these would throw away almost the whole club for no reason: the + name, face and badge are already right, only the numbers are wrong. + + DEAD the playerid does not exist in the roster at all. The client misses on it + and stamps its generic card (rating 50, teamid 1933, nation 14, blank + name). These are the blanks on screen and there is nothing to repair, so + they are REMOVED. + +RUN THIS WITH utas_server STOPPED. The server holds the profile in memory and +rewrites it on its own schedule, so an edit made underneath a running server gets +clobbered by the next save. That is exactly what happened on 2026-08-04: nine dead +cards were removed and reappeared a few hours later. + +Squad safety: a card referenced by a saved squad is never removed. Repair is safe +for squad members because the item id does not change. + + repair_club.py dry run (default) + repair_club.py --fire write, after taking a timestamped backup +""" +import argparse +import json +import os +import shutil +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from fut_store import STORE # noqa: E402 + +POOL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "pool.json") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--fire", action="store_true") + a = ap.parse_args() + + truth = {p["id"]: p for p in json.load(open(POOL))} + p = STORE.load() + items = p.get("items", []) + + squad_ids = set() + + def walk(o): + if isinstance(o, dict): + for k, v in o.items(): + if k in ("itemId", "id") and isinstance(v, int): + squad_ids.add(v) + walk(v) + elif isinstance(o, list): + for v in o: + walk(v) + + for key in ("squad", "squads", "squadList"): + walk(p.get(key)) + + repaired, dead, untouched = [], [], 0 + for it in items: + t = truth.get(it.get("assetId")) + if not t: + dead.append(it) + continue + changes = [] + if it.get("rating") != t["rating"]: + changes.append("rating %s->%s" % (it.get("rating"), t["rating"])) + if it.get("preferredPosition") != t["pos"]: + changes.append("pos %s->%s" % (it.get("preferredPosition"), t["pos"])) + if it.get("teamid") != t["team"]: + changes.append("team %s->%s" % (it.get("teamid"), t["team"])) + if it.get("nation") != t["nation"]: + changes.append("nation %s->%s" % (it.get("nation"), t["nation"])) + if it.get("leagueId") != t["league"]: + changes.append("league %s->%s" % (it.get("leagueId"), t["league"])) + if [x.get("value") for x in it.get("attributeList", [])] != t["attrs"]: + changes.append("attrs") + if not changes: + untouched += 1 + continue + repaired.append((it, changes, t)) + + keep_dead = [d for d in dead if d.get("id") in squad_ids] + drop_dead = [d for d in dead if d.get("id") not in squad_ids] + + print("club %d: %d already correct, %d to repair, %d dead to remove" + % (len(items), untouched, len(repaired), len(drop_dead))) + if keep_dead: + print(" %d dead card(s) KEPT because a squad references them" % len(keep_dead)) + for it, ch, _ in repaired[:8]: + print(" repair id=%-11s asset=%-7s %s" % (it.get("id"), it.get("assetId"), + "; ".join(ch)[:80])) + if len(repaired) > 8: + print(" ... and %d more" % (len(repaired) - 8)) + for it in drop_dead[:8]: + print(" remove id=%-11s asset=%s" % (it.get("id"), it.get("assetId"))) + + if not a.fire: + print("\ndry run. Re-run with --fire to write.") + return 0 + + bak = STORE.path + ".bak-repair-%d" % int(time.time()) + shutil.copy(STORE.path, bak) + print("\nbackup: %s" % bak) + + for it, _, t in repaired: + it["rating"] = t["rating"] + it["preferredPosition"] = t["pos"] + it["teamid"] = t["team"] + it["nation"] = t["nation"] + it["leagueId"] = t["league"] + it["attributeList"] = [{"index": i, "value": v} for i, v in enumerate(t["attrs"])] + drop = {d.get("id") for d in drop_dead} + p["items"] = [i for i in items if i.get("id") not in drop] + STORE._save() + + q = STORE.load() + print("club: %d -> %d coins=%s purchased=%d" + % (len(items), len(q.get("items", [])), q.get("coins"), + len(q.get("purchased", [])))) + return 0 + + +if __name__ == "__main__": + sys.exit(main())