Files
OpenFUT/fifa17-recon/tools/repair_club.py
T
funman300 22ba361578 fifa17-recon: repair_club keeps dead cards unless asked, plus the 2026-08-05 plan
Deleting cards from someone's club is their call, not the tool's. The nine
unrepairable blanks are now KEPT unless --delete-dead is passed. A blank card is ugly,
not harmful, and the 175 stale cards were never the deletion candidates anyway: they
are real players wearing old invented numbers and they get repaired in place.

Also records the build round's synthesis as docs/plan-2026-08-05-families.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-05 10:16:13 -07:00

144 lines
5.6 KiB
Python

#!/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). There is nothing to repair, because there is no player to repair it
to. They are LEFT ALONE unless --delete-dead is passed: removing cards from
someone's club is their call, not the tool's, and a blank card is ugly
rather than harmful.
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 repair stale cards, keep dead ones
repair_club.py --fire --delete-dead also remove the unrepairable blanks
"""
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")
ap.add_argument("--delete-dead", action="store_true",
help="also remove cards whose playerid is not a real player")
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))
if a.delete_dead:
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]
else:
keep_dead, drop_dead = dead, []
print("club %d: %d already correct, %d to repair, %d dead to remove"
% (len(items), untouched, len(repaired), len(drop_dead)))
if keep_dead:
why = ("a squad references them" if a.delete_dead
else "--delete-dead was not passed")
print(" %d dead card(s) KEPT (%s)" % (len(keep_dead), why))
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())