22ba361578
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
144 lines
5.6 KiB
Python
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())
|