0a1c9dc2ce
Leftovers from the invented-id pool that data/roster.json replaced. The client misses on them and stamps its generic card (rating 50, teamid 1933, nation 14, position 2, attributes 1, blank name), which is what a blank card on screen IS. Removed 9 from the live club (5 distinct bad ids, 169193 four times over). 188 of 194 cards were already resolving; these were the whole remainder. Refuses to remove anything a saved squad references, backs up first, and is a dry run unless --fire. It touches only the club pile: a card present in both club and purchased is the known fatal desync, and deleting from one pile cannot create that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
89 lines
2.9 KiB
Python
Executable File
89 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Remove club cards whose playerid does not exist in FIFA 17's roster.
|
|
|
|
These are leftovers from the invented-id pool that data/roster.json replaced. The
|
|
client resolves them through its own players table, misses, and stamps the generic
|
|
card: rating 50, teamid 1933, nation 14, position 2, all attributes 1, blank name.
|
|
They are the blanks on screen.
|
|
|
|
SAFETY
|
|
* backs the profile up first, and prints the backup path;
|
|
* refuses to touch any card referenced by a saved squad, so a squad slot can
|
|
never end up pointing at a deleted item;
|
|
* removes from the club pile only, and never from `purchased` in the same pass:
|
|
a card present in BOTH piles is the known fatal desync, and the way to avoid
|
|
it is to keep every card in exactly one place, which deleting from one pile
|
|
preserves.
|
|
* dry run unless --fire is passed.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, "/home/alex/Documents/OpenFUT/fifa17-recon/tools")
|
|
from fut_store import STORE # noqa: E402
|
|
import fut_cards # noqa: E402
|
|
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--fire", action="store_true", help="actually write")
|
|
a = ap.parse_args()
|
|
|
|
REAL = {p[0] for p in fut_cards.POOL}
|
|
p = STORE.load()
|
|
items = p.get("items", [])
|
|
|
|
# Every item id any saved squad refers to. Squad shapes have varied, so walk the
|
|
# structure generically rather than assuming one layout.
|
|
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))
|
|
|
|
dead = [c for c in items
|
|
if c.get("assetId") not in REAL and c.get("id") not in squad_ids]
|
|
protected = [c for c in items
|
|
if c.get("assetId") not in REAL and c.get("id") in squad_ids]
|
|
|
|
print("club=%d roster=%d" % (len(items), len(REAL)))
|
|
print("dead cards to remove: %d" % len(dead))
|
|
for c in dead:
|
|
print(" id=%-11s asset=%-8s rating=%s" % (c.get("id"), c.get("assetId"), c.get("rating")))
|
|
if protected:
|
|
print("KEPT (referenced by a squad, removing them would break a slot): %d" % len(protected))
|
|
for c in protected:
|
|
print(" id=%-11s asset=%s" % (c.get("id"), c.get("assetId")))
|
|
|
|
if not a.fire:
|
|
print("\ndry run. Re-run with --fire to write.")
|
|
sys.exit(0)
|
|
if not dead:
|
|
print("\nnothing to do.")
|
|
sys.exit(0)
|
|
|
|
bak = STORE.path + ".bak-stripdead-%d" % int(time.time())
|
|
shutil.copy(STORE.path, bak)
|
|
print("\nbackup: %s" % bak)
|
|
|
|
drop = {c.get("id") for c in dead}
|
|
p["items"] = [c for c in items if c.get("id") not in drop]
|
|
STORE._save()
|
|
|
|
q = STORE.load()
|
|
print("club: %d -> %d purchased=%d coins=%s"
|
|
% (len(items), len(q.get("items", [])), len(q.get("purchased", [])), q.get("coins")))
|