diff --git a/fifa17-recon/tools/strip_dead_cards.py b/fifa17-recon/tools/strip_dead_cards.py new file mode 100755 index 0000000..eaa1174 --- /dev/null +++ b/fifa17-recon/tools/strip_dead_cards.py @@ -0,0 +1,88 @@ +#!/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")))