"""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 reconstruct_squad(self, squad): """FIFA's updateActiveSquad PUT stores each slot as itemData={id:} (a reference). Re-embed the FULL club item by id so the squad reloads with real players instead of empty slots ('active squad resets').""" by_id = {it["id"]: it for it in self.items()} out = dict(squad) players = [] for pl in squad.get("players", []): iid = (pl.get("itemData") or {}).get("id", 0) if iid and iid in by_id: players.append({**pl, "itemData": by_id[iid]}) else: players.append(pl) out["players"] = players return out # ---- transfer-market listings (user's own sale pile) ------------------- def list_for_sale(self, item_id, start, buynow): with _LOCK: p = self.load() p.setdefault("listings", []) p["listings"] = [l for l in p["listings"] if l.get("itemId") != item_id] tid = 900500000 + p.get("nextListingSeq", 0) p["nextListingSeq"] = p.get("nextListingSeq", 0) + 1 p["listings"].append({"tradeId": tid, "itemId": item_id, "startingBid": start, "buyNowPrice": buynow}) self._save() return tid def listings(self): return self.load().get("listings", []) def remove_listing(self, tid): with _LOCK: p = self.load() p["listings"] = [l for l in p.get("listings", []) if l.get("tradeId") != tid] self._save() def new_item_id(self): with _LOCK: p = self.load(); i = p["nextItemId"]; p["nextItemId"] += 1; self._save() return i def open_pack(self, price, count, gold=True): """Deduct `price` coins, generate `count` player items from the pool, add them to the club, return them. Returns None if not enough coins.""" import random if not self.spend(price): return None pool = [p for p in PACK_POOL if (p[1] >= 75) == gold] or PACK_POOL picks = [random.choice(pool) for _ in range(count)] items = [_item(self.new_item_id(), a, r, p, n, lg, tm, at) for (a, r, p, n, lg, tm, at) in picks] self.add_items(items) with _LOCK: self.load()["packsOpened"] += 1 self._last_pack = items self._save() return items def last_pack(self): return getattr(self, "_last_pack", []) # Card pool for packs. TODO: replace with a full dbdata.dll extract (~18k players); # for now a curated set of real FIFA17 assetIds so packs hand out real cards. PACK_POOL = STARTER_PLAYERS + [ (167495, 90, "GK", 27, 19, 22, [86, 88, 52, 88, 22, 88]), # Neuer (192985, 88, "RM", 21, 19, 22, [80, 82, 85, 85, 63, 68]), # De Bruyne (188545, 89, "ST", 37, 16, 240, [77, 88, 75, 82, 42, 82]), # Lewandowski (169193, 87, "CDM",54, 16, 240, [70, 66, 80, 78, 82, 84]), # Alonso(X) (202126, 86, "ST", 18, 13, 5, [79, 84, 74, 82, 45, 79]), # Kane (177003, 88, "CM", 14, 13, 5, [65, 78, 88, 79, 71, 66]), # Modric(X) (190871, 87, "LW", 54, 16, 240, [90, 78, 80, 88, 36, 61]), # Neymar(X) (184941, 85, "CB", 14, 4, 5, [72, 40, 55, 60, 86, 85]), # (X) ] # 3 store packs (price in coins, card count, gold-only). Ids are stable. PACK_CATALOG = [ {"id": 101, "name": "Bronze Pack", "price": 400, "count": 5, "gold": False}, {"id": 102, "name": "Gold Pack", "price": 5000, "count": 7, "gold": True}, {"id": 103, "name": "Premium Gold", "price": 15000, "count": 11, "gold": True}, ] def pack_by_id(pid): for p in PACK_CATALOG: if p["id"] == pid: return p return None STORE = Store()