#!/usr/bin/env python3 """OpenFUT save maintenance — inspect and repair fifa17_profile.json offline. Exists because the pack→club hand-off is not proven. Cards from an opened pack land in the PENDING pile (`profile["purchased"]`) and only reach the club when the client sends `PUT ut/%s/item` (FutMoveCard) from the reveal screen's "send to club". Across every logged session that request has fired **zero** times, while 12 cards sit pending — so either the flow was never exercised in-game, or the client does not issue it the way we assume. Same shape as the squad blocker: an assumed client request that never actually arrives. Until a live pack-open settles it, this is the manual path. SAFETY: the client desyncs fatally (logout) if a card exists in BOTH the pending pile and the club — see docs/CARD_SYSTEM.md and Store.move_items. `--flush-purchased` therefore MOVES (never copies): each card is removed from `purchased` in the same transaction that appends it to `items`. Run it with FIFA CLOSED so the client cannot be holding a stale view of either pile. Usage: fut_admin.py --show profile summary (default) fut_admin.py --flush-purchased move every pending card into the club fut_admin.py --flush-purchased -n dry run: show what would move fut_admin.py --backup timestamped copy of the profile """ import argparse import datetime import json import os import shutil import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from fut_store import STORE # noqa: E402 def _fmt(it): return "asset=%-7s rating=%-3s pos=%-4s id=%s" % ( it.get("assetId"), it.get("rating"), it.get("preferredPosition"), it.get("id")) def show(): p = STORE.profile() rec = p.get("record", {}) print("profile : %s" % STORE.path) print("club : %s (%s) est %s" % (p.get("clubName"), p.get("clubAbbr"), p.get("established"))) print("coins : %s points: %s" % (p.get("coins"), p.get("points"))) print("record : %s-%s-%s matches: %s" % (rec.get("won", 0), rec.get("draw", 0), rec.get("loss", 0), p.get("matchesPlayed", 0))) print("club items : %d" % len(p.get("items", []))) print("squads saved : %d" % len(p.get("squads", []))) print("packs opened : %s" % p.get("packsOpened", 0)) print("listings : %d" % len(p.get("listings", []))) print("clientdata : %s" % (sorted(p.get("clientdata", {})) or "none")) pend = p.get("purchased", []) print("PENDING pack items: %d%s" % (len(pend), " <-- not in the club; see --flush-purchased" if pend else "")) for it in pend[:20]: print(" %s" % _fmt(it)) if len(pend) > 20: print(" ... and %d more" % (len(pend) - 20)) def backup(): dst = "%s.%s.bak" % (STORE.path, datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) shutil.copy2(STORE.path, dst) print("backup -> %s" % dst) return dst def flush(dry_run): pend = list(STORE.profile().get("purchased", [])) if not pend: print("nothing pending — the club already has every pack card") return 0 print("%d pending card(s)%s:" % (len(pend), " (DRY RUN)" if dry_run else "")) for it in pend: print(" %s" % _fmt(it)) if dry_run: print("\ndry run — nothing written. Re-run without -n to move them.") return 0 backup() # Reuse the server's own move path so the pending/club invariant is enforced in # exactly one place: move_items() deletes from `purchased` in the same locked # transaction that appends to `items`. moved = STORE.move_items([{"id": it["id"], "pile": "club"} for it in pend]) p = STORE.profile() print("\nmoved %d card(s) into the club" % len(moved)) print("club items now: %d pending now: %d" % (len(p.get("items", [])), len(p.get("purchased", [])))) if p.get("purchased"): print("WARNING: %d card(s) did not move — ids missing from the pending pile" % len(p["purchased"])) return 0 def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--show", action="store_true", help="profile summary (default)") ap.add_argument("--flush-purchased", action="store_true", help="move pending pack cards into the club (run with FIFA closed)") ap.add_argument("-n", "--dry-run", action="store_true", help="with --flush-purchased") ap.add_argument("--backup", action="store_true", help="timestamped profile copy") a = ap.parse_args(argv) if a.backup: backup() if a.flush_purchased: return flush(a.dry_run) show() return 0 if __name__ == "__main__": sys.exit(main())