fifa17-python: commit working FUT backend deployment (client/server split)
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.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user