70a64e3709
Freeze the running offline FUT backend into version control as fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a fresh checkout: * OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is required for remote mode (compose and entrypoint fail without it) * docker-compose.yml reproducing the frozen baseline container exactly (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart) * .env.example / .env for site config - the LAN IP is never hardcoded in source * tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10, verified byte-identical to the running container at freeze time * client_arm.sh (the 105 client-side arming counterpart) * Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying * docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record, restore instructions and rebuild-equivalence procedure Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored. The live container is untouched pending the .105 launcher audit.
119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
#!/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())
|