fifa17-recon: working FUT store + pack opening + coins format

Reverse-engineered the exact FIFA17 store/purchase/credits response
shapes (wf a245577b + wf_76fcf89b) and applied them:

- Store catalog: root key MUST be "purchase" (atom 608) not
  "purchaseGroups"; packs keyed by "id" (int16) not packId; price is a
  "currencies":[{name,funds,finalFunds}] array; name is "description".
  Our old {purchaseGroups:...} hashed to unknown atoms -> empty -> "store
  not available". Now the store displays.
- Pack buy: gate open_pack on a transaction with "packId" and state !=
  TRANSACTIONCANCEL (the TRANSACTIONCREATED create step) -- fixes the
  phantom-buy. Reveal response = {"createPackResponse":{itemList,
  numberItems,purchasedPackId,duplicateItemIdList}}
  (FutCreatePackServerResponse).
- Coins: /user/credits must return currencies[name=="coins"].funds, not
  {"credits":N} (the hub/store read currencies). squad_route also
  reconstructs the active squad from club item-id references.

Verified via curl: store shows 3 packs; cancel spends nothing; Bronze
buy awards 5 real players and deducts 400 coins. NOTE: the FUT HUB coin
counter reads from userMassInfo (not /user/credits) -- still blocked on
the userMassInfo-freeze wall (separate reverse in progress).

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-02 10:22:24 -07:00
parent 89dc9baa61
commit c7759a52c4
+63 -17
View File
@@ -164,7 +164,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": STORE.coins()})),
(re.compile(G + r"/user/credits"), lambda m, h: credits_route(h)),
# ---- 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
@@ -209,35 +209,81 @@ def squad_route(h):
# ---- 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}
# GET store/purchasegroup/all. Root key MUST be "purchase" (atom 608, array);
# pack identity is "id" (int16, NOT packId); price is a "currencies" array of
# {name,funds,finalFunds}; display name is "description". (wf a245577b —
# {purchaseGroups:...} + packId/price were all unknown atoms => empty => "not
# available".) quantity:0 => unlimited.
packs = []
for p in PACK_CATALOG:
gold = p["gold"]
packs.append({
"id": p["id"],
"packType": "GOLD" if gold else "BRONZE",
"description": p["name"],
"quantity": 0,
"purchaseLimit": 0,
"purchaseCount": 0,
"isPremium": False,
"saleType": "PERMANENT",
"sortPriority": p["id"] - 100,
"currencies": [{"name": "coins", "funds": p["price"], "finalFunds": p["price"]}],
"packContentInfo": {
"bronzeQuantity": 0 if gold else p["count"],
"silverQuantity": 0,
"goldQuantity": p["count"] if gold else 0,
"rareQuantity": p["count"] if gold else 0,
"itemQuantity": p["count"],
},
})
return 200, {"purchase": packs, "timestamp": 1596326400}
def store_buy(h):
# PUT (v2) store/transaction. SAFE NO-OP until the real purchase-CONFIRM signal
# is reversed. Observed bodies are NOT confirmed buys:
# {"state":"TRANSACTIONCANCEL"} = cancel/close the store
# {"packId":N} = fetch a pack's details when the store loads
# Opening a pack on either wrongly spent coins. Log every body so we can spot
# the real confirm body when the user makes a DELIBERATE purchase, then gate
# open_pack() on exactly that. (open_pack lives in fut_store, ready to wire.)
# PUT (v2) store/transaction. The BUY is the create step: body carries "packId"
# (atom 0x20b, TRANSACTIONCREATED) and state != TRANSACTIONCANCEL (wf a245577b /
# wf_76fcf89b). Only THEN open a pack. Cancel/other -> no-op {} (fixes the
# phantom-buy). Reveal = {"createPackResponse":{itemList,numberItems,
# purchasedPackId,duplicateItemIdList}} (FutCreatePackServerResponse 0x180162880).
try:
body = json.loads(h._body.decode()) if getattr(h, "_body", b"") else {}
except Exception:
body = {}
log(" STORE txn body=%r (no-op; buy-confirm flow not reversed yet)" % body)
return 200, {}
pid = body.get("packId")
if body.get("state") == "TRANSACTIONCANCEL" or not isinstance(pid, int):
return 200, {} # not a confirmed buy
pack = pack_by_id(pid)
if not pack:
return 200, {}
items = STORE.open_pack(pack["price"], pack["count"], pack["gold"])
if items is None:
return 461, {"reason": "insufficient_coins", "credits": STORE.coins()}
log(" STORE: opened pack %s -> %d items, coins=%d" % (pack["name"], len(items), STORE.coins()))
return 200, {"createPackResponse": {
"itemList": items,
"numberItems": len(items),
"purchasedPackId": pid,
"duplicateItemIdList": [],
}}
def purchased_items(h):
return 200, {"itemData": STORE.last_pack()}
def credits_route(h):
# The FUT hub coin counter binds to currencies[].funds (deser 0x180122c50,
# atom "currencies" 0xc5), NOT a "credits" key -- wf_76fcf89b.
c = STORE.coins()
return 200, {
"credits": c,
"currencies": [
{"name": "coins", "funds": c, "finalFunds": c},
{"name": "points", "funds": 0, "finalFunds": 0},
],
}
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"