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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
funman300
2026-08-01 20:56:07 -07:00
parent bebe573d05
commit 5c43dbe39e
2 changed files with 87 additions and 1 deletions
+38 -1
View File
@@ -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"