From 9876a6c8709cb4b248ac88ffa49453bf2395832d Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 5 Aug 2026 10:03:10 -0700 Subject: [PATCH] fifa17-recon: repair_club -- fix stale cards in place, remove only the dead ones Audit of the live club: 194 items, 10 already correct, 175 stale, 9 dead. STALE means a real FIFA 17 player carrying the old invented fields: attributes computed from the rating, and in some cases a wrong club or nation (Kroos was stored at Bayern and is at Real Madrid; Alaba was nation 40 and is Austria 4). Those are REPAIRED in place from data/pool.json rather than deleted. Deleting them would throw away 90 percent of the club for no reason: name, face and badge are already right and only the numbers are wrong, and the item id does not change so squad slots survive. DEAD means the playerid is not in the roster at all, so the client misses and stamps its generic blank. Nothing to repair, so those are removed. A dead card referenced by a saved squad is kept rather than breaking the slot. The nation and team mappings were spot-checked against the game's own nations and teams tables before trusting them across 175 cards: 4 Austria, 21 Germany, 38 Portugal, 60 Uruguay, 243 Real Madrid, 5 Chelsea. MUST RUN WITH utas_server STOPPED. The server keeps the profile in memory and rewrites it on its own schedule, so an edit underneath a running server is clobbered by the next save. That is why the nine dead cards removed on 2026-08-04 were back a few hours later: the removal was correct and the running server undid it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW --- fifa17-recon/tools/repair_club.py | 133 ++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 fifa17-recon/tools/repair_club.py 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())