From 5c43dbe39e84541082b119024674887c796b8110 Mon Sep 17 00:00:00 2001 From: funman300 Date: Sat, 1 Aug 2026 20:56:07 -0700 Subject: [PATCH] fifa17-recon: pack opening (store catalog + buy + award) Add a first-cut FUT store on top of the persistent profile: - fut_store: PACK_CATALOG (Bronze/Gold/Premium), a curated real-player PACK_POOL, and open_pack() (deduct coins -> generate items -> add to club -> persist). - utas_server routes: GET store/purchasegroup/all (catalog), PUT (v2) store/transaction (buy + open, returns awarded itemData + updated coins), GET purchased (last pack). Matches both /ut/game and /ut/v2/game prefixes. Verified via curl: buy Gold Pack -> 7 real players awarded, coins 15000->10000, club 10->17, persisted. Wire format is a best-guess grounded in the CardsDLL store keys (packId/price/itemData/coins); iterate against the in-game store next. Pool is curated for now -- replace with a full dbdata.dll extract later. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o --- fifa17-recon/tools/fut_store.py | 49 +++++++++++++++++++++++++++++++ fifa17-recon/tools/utas_server.py | 39 +++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/fifa17-recon/tools/fut_store.py b/fifa17-recon/tools/fut_store.py index f2aca81..71f3e1c 100644 --- a/fifa17-recon/tools/fut_store.py +++ b/fifa17-recon/tools/fut_store.py @@ -155,4 +155,53 @@ class Store: 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() diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 3dc55f1..e199c4c 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -15,7 +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) +from fut_store import STORE, PACK_CATALOG, pack_by_id # persistent profile + packs ADDR = ("127.0.0.1", 8099) LOG = "/tmp/utas_server.log" @@ -151,6 +151,10 @@ ROUTES = [ (re.compile(G + r"/item/resource"), lambda m, h: defs_route(h)), (re.compile(G + r"/defid"), lambda m, h: defs_route(h)), (re.compile(G + r"/item(\?|$)"), lambda m, h: defs_route(h)), + # ---- store / packs (match regardless of /ut/game vs /ut/v2/game prefix) ---- + (re.compile(r"/store/purchasegroup"), lambda m, h: store_catalog(h)), + (re.compile(r"/store/transaction"), lambda m, h: store_buy(h)), + (re.compile(r"/purchased"), lambda m, h: purchased_items(h)), (re.compile(r"^/ut/auth"), lambda m, h: (200, auth_body())), (re.compile(r"^/ut/delete/auth"), lambda m, h: (200, {})), (re.compile(G + r"/settings"), lambda m, h: (200, SETTINGS)), @@ -203,6 +207,39 @@ def squad_route(h): return 200, (saved if saved else SQUAD) +# ---- STORE / PACKS (first-cut; iterate against the log) --------------------- +def store_catalog(h): + # GET store/purchasegroup/all -> the pack catalog FIFA displays. + groups = [{ + "id": p["id"], "packId": p["id"], "productId": p["id"], "name": p["name"], + "price": {"coins": p["price"], "points": 0}, "coins": p["price"], + "itemCount": p["count"], "currency": "coins", + } for p in PACK_CATALOG] + return 200, {"purchaseGroups": groups, "packs": groups} + + +def store_buy(h): + # PUT (v2) store/transaction -> buy + open a pack; return the awarded items. + pid = None + try: + body = json.loads(h._body.decode()) if getattr(h, "_body", b"") else {} + for k in ("packId", "productId", "id", "pack"): + if isinstance(body.get(k), int): + pid = body[k]; break + except Exception: + body = {} + pack = pack_by_id(pid) or PACK_CATALOG[1] # default Gold Pack + items = STORE.open_pack(pack["price"], pack["count"], pack["gold"]) + if items is None: + return 461, {"reason": "insufficient_coins", "credits": STORE.coins()} + return 200, {"itemData": items, "coins": STORE.coins(), + "currencies": [{"name": "coins", "value": STORE.coins()}]} + + +def purchased_items(h): + return 200, {"itemData": STORE.last_pack()} + + class H(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1"