diff --git a/fifa17-recon/.gitignore b/fifa17-recon/.gitignore index fe43d08..55bf695 100644 --- a/fifa17-recon/.gitignore +++ b/fifa17-recon/.gitignore @@ -9,3 +9,4 @@ *.log __pycache__/ captures/ +tools/fifa17_profile.json diff --git a/fifa17-recon/tools/fut_store.py b/fifa17-recon/tools/fut_store.py new file mode 100644 index 0000000..f2aca81 --- /dev/null +++ b/fifa17-recon/tools/fut_store.py @@ -0,0 +1,158 @@ +"""Persistent FUT profile store for the FIFA 17 offline backend (OpenFUT). + +A single JSON file holds the user's save: coins/points, owned club items, squads, +and record. Loaded once, saved on every mutation. This is the "user + saved files" +layer; packs and the starter grant build on it. (Later this gets ported into +openfut-core's SQLite backend behind a FIFA-17 bridge; JSON keeps iteration fast +while we nail the wire format.) + +Item shape mirrors what /club and /squad already serve (fut_seed.player_item): +resourceId/assetId + attrs; identity (name/photo/club/nation) resolves locally in +FIFA from dbdata.dll on the club-search/add path (see docs/CARD_SYSTEM.md). +""" +import json, os, threading + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROFILE_PATH = os.environ.get("FUT_PROFILE", os.path.join(HERE, "fifa17_profile.json")) + +PERSONA_ID = 33068179 +PERSONA_NAME = "CAGE" +_LOCK = threading.Lock() + +# Starter squad granted on first run (real FIFA17 assetIds; identity resolves +# locally in-game). (assetId, rating, position, nation, leagueId, teamid, [6 attrs]) +STARTER_PLAYERS = [ + (20801, 94, "LW", 38, 53, 243, [90, 93, 82, 91, 33, 80]), # Ronaldo + (158023, 93, "RW", 52, 53, 241, [89, 90, 86, 96, 26, 61]), # Messi + (200389, 87, "GK", 44, 53, 240, [86, 88, 47, 88, 25, 86]), # Oblak + (183907, 90, "CB", 21, 19, 21, [80, 58, 70, 71, 89, 85]), # Boateng + (155862, 89, "CB", 45, 53, 243, [77, 62, 72, 74, 87, 84]), # Ramos + (197445, 87, "LB", 40, 19, 21, [86, 68, 79, 82, 82, 79]), # Alaba + (189332, 86, "LB", 45, 53, 241, [92, 66, 78, 84, 80, 71]), # Alba + (182521, 88, "CM", 21, 19, 21, [61, 79, 89, 82, 72, 74]), # Kroos + (183277, 88, "LM", 7, 13, 5, [89, 80, 82, 92, 37, 62]), # Hazard + (176580, 92, "ST", 60, 53, 241, [83, 90, 79, 87, 42, 80]), # Suarez +] + +ITEM_ID_BASE = 100000000 + + +def _item(item_id, asset, rating, pos, nation, league, team, attrs, version=0x00): + return { + "id": item_id, + "resourceId": (version << 24) | asset, + "assetId": asset, + "cardassetid": asset, + "definitionId": (version << 24) | asset, + "cardsubtypeid": 0, + "itemType": "player", + "rareflag": 1, + "rating": rating, + "preferredPosition": pos, + "nation": nation, + "teamid": team, + "leagueId": league, + "playStyle": 250, + "attributeList": [{"index": i, "value": v} for i, v in enumerate(attrs)], + "itemState": "free", + "owners": 1, + "untradeable": True, + "contract": 7, + "fitness": 99, + } + + +def _new_profile(): + """First-run grant: opening coins + the starter squad as owned items.""" + items = [_item(ITEM_ID_BASE + i + 1, a, r, p, n, lg, tm, at) + for i, (a, r, p, n, lg, tm, at) in enumerate(STARTER_PLAYERS)] + return { + "version": 1, + "personaId": PERSONA_ID, + "personaName": PERSONA_NAME, + "clubName": "OpenFUT", + "clubAbbr": "OFC", + "established": "2026", + "coins": 15000, + "points": 0, + "record": {"won": 0, "draw": 0, "loss": 0}, + "nextItemId": ITEM_ID_BASE + len(STARTER_PLAYERS) + 1, + "items": items, # owned club items + "squads": [], # saved squads (raw squad objects from PUT /squad) + "packsOpened": 0, + } + + +class Store: + def __init__(self, path=PROFILE_PATH): + self.path = path + self._p = None + + def load(self): + if self._p is not None: + return self._p + if os.path.exists(self.path): + with open(self.path) as f: + self._p = json.load(f) + else: + self._p = _new_profile() + self._save() + return self._p + + def _save(self): + tmp = self.path + ".tmp" + with open(tmp, "w") as f: + json.dump(self._p, f, indent=1) + os.replace(tmp, self.path) + + # ---- accessors used by utas_server ------------------------------------- + def profile(self): + return self.load() + + def coins(self): + return self.load()["coins"] + + def items(self): + return self.load()["items"] + + def add_items(self, new_items): + with _LOCK: + p = self.load() + for it in new_items: + it.setdefault("id", p["nextItemId"]); p["nextItemId"] += 1 + p["items"].append(it) + self._save() + return new_items + + def spend(self, amount): + with _LOCK: + p = self.load() + if p["coins"] < amount: + return False + p["coins"] -= amount + self._save() + return True + + def grant_coins(self, amount): + with _LOCK: + self.load()["coins"] += amount + self._save() + + def save_squad(self, squad): + with _LOCK: + p = self.load() + sid = squad.get("id", 0) + p["squads"] = [s for s in p["squads"] if s.get("id") != sid] + [squad] + self._save() + + def active_squad(self): + sq = self.load()["squads"] + return sq[0] if sq else None + + def new_item_id(self): + with _LOCK: + p = self.load(); i = p["nextItemId"]; p["nextItemId"] += 1; self._save() + return i + + +STORE = Store() diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 457eb24..3dc55f1 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -15,6 +15,7 @@ import datetime, json, os, re, sys, http.server sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from fut_seed import CLUB, SQUAD, USER_LIST # forged starter squad (clean-room) +from fut_store import STORE # persistent profile (coins/club/squads) ADDR = ("127.0.0.1", 8099) LOG = "/tmp/utas_server.log" @@ -159,7 +160,7 @@ ROUTES = [ (re.compile(G + r"/phishing/trusteddevice"), lambda m, h: (200, {"trusted": True})), (re.compile(G + r"/phishing/validate"), lambda m, h: (200, {"token": "OPENFUT-TRUST-0000000000000000"})), (re.compile(G + r"/phishing/question"), lambda m, h: (200, {"question": 0, "answer": "", "attempts": 5})), - (re.compile(G + r"/user/credits"), lambda m, h: (200, {"credits": 15000})), + (re.compile(G + r"/user/credits"), lambda m, h: (200, {"credits": STORE.coins()})), # ---- club/squad routes reverted to known-good {} stubs (2026-08-01) ---- # The forged squad in MASSINFO/SQUAD/CLUB HANGS CardsDLL's deserializer (hard # freeze at boot). Re-enable only after the exact shape is reversed. The forged @@ -173,7 +174,7 @@ ROUTES = [ # STEP 1 (wf_0bc80ab3): zero-resolve squad in MASSINFO; club stays {}. (re.compile(G + r"/userMassInfo"), lambda m, h: (200, MASSINFO)), (re.compile(G + r"/season"), lambda m, h: (200, {})), - (re.compile(G + r"/club"), lambda m, h: (200, CLUB)), + (re.compile(G + r"/club"), lambda m, h: (200, {"itemData": STORE.items()})), ] @@ -187,9 +188,19 @@ def user_route(h): def squad_route(h): - # GET = LoadActiveSquad, PUT = updateActiveSquad. Always echo the full canonical - # squad (never {} — an empty body resets the client's 23 slots, 0x18013d1f0). - return 200, SQUAD + # GET = LoadActiveSquad, PUT = updateActiveSquad. Persist the squad the user + # builds so it survives relaunches. Never return {} (empty body resets the 23 + # slots, 0x18013d1f0). + if h.command == "PUT": + try: + sq = json.loads(h._body.decode("utf-8")) if getattr(h, "_body", b"") else None + except Exception: + sq = None + if isinstance(sq, dict) and sq.get("players"): + STORE.save_squad(sq) + return 200, sq + saved = STORE.active_squad() + return 200, (saved if saved else SQUAD) class H(http.server.BaseHTTPRequestHandler): @@ -198,6 +209,7 @@ class H(http.server.BaseHTTPRequestHandler): def _handle(self): n = int(self.headers.get("Content-Length", 0) or 0) body = self.rfile.read(n) if n else b"" + self._body = body # route fns (squad PUT) read this log("%s %s" % (self.command, self.path)) for k, v in self.headers.items(): log(" %s: %s" % (k, v))