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:
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user