Store "not available" root cause reversed from CardsDLL:
- ut/v2/game/fifa17/store is an ELIGIBILITY gate (FutStorePackQuantities
deser 0x1801758c0), not a quantity list. It reads one key "result"
(atom 0x288); the store screen refuses to open unless SUCCESS. Was
unhandled -> catch-all {} -> "not available". Now returns {"result":"SUCCESS"}.
- Store-screen entitlement checks (0x18001749d/0x1800175a2) read IS_*/
*_PURCHASE_ENABLED Blaze flags, separate from storeEnabled. Added the full
confirmed set (14 flags) to FUT_RS4_CONFIG.
- Catalog: assetId (0x23) is the real pack identity; extPrice inner keys are
amount/currency (not mtx). (Also gated client-side by GetSystemMetrics>1024x768.)
Full FUT API reversed (clean-room, CardsDLL only) into docs/ENDPOINT_MAP.md:
~100 FutXServerResponse types across 7 feature groups (market, SBC, draft,
seasons/match, club, store, user/hub), each with deserializer VA, atom-mapped
field schema + types, freeze-risk flags, and minimal known-good JSON.
Tooling kept: tools/atomdump.py (dumps the 907-atom key table at 0x1802d2760)
-> docs/fut_atoms.tsv. Research prompt: docs/OPENCODE_ENDPOINT_PROMPT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
11 KiB
opencode task — map the remaining FIFA 17 FUT endpoints for the offline rebuild
Context
OpenFUT runs FIFA 17 Ultimate Team fully offline (clean-room; EA servers are dead).
A working Python backend already exists at ~/Documents/OpenFUT/fifa17-recon/tools/ and
FIFA talks to it (/etc/hosts → easw.easports.com → 127.0.0.1:8099). Many endpoints
are already reversed and served. Your job is RESEARCH ONLY: produce a complete map of
the FUT endpoints that are LEFT to build, using the tools/logs/disassembly that already
exist. Do NOT rewrite the backend — output ~/Documents/OpenFUT/fifa17-recon/docs/ENDPOINT_MAP.md.
CLEAN-ROOM RULE (HARD)
Use ONLY files we own + our own client's traffic. NEVER use leaked EA source of any kind.
Use the ALREADY-BUILT tools — do not rebuild these
tools/utas_server.py— the live backend. ItsROUTEStable is the AUTHORITATIVE list of endpoints already handled:auth,delete/auth,phishing/{trusteddevice, validate,question},user/credits,user/list,user/accountinfo,user,squad(+/list),hub,userMassInfo,season,club,item(/resource)/defid,store/purchasegroup,store/transaction,purchased. Read it to see what's DONE and the exact response shapes used. Its_handle()logs!! UNMAPPED PATH -> catch-all 200 {}for any endpoint FIFA hits that ISN'T handled yet — those are your gaps./tmp/utas_server.log— GROUND TRUTH of every request FIFA makes (method, path, headers, body) and our response. Start here:This is the real, ordered list of what the client requests per FUT screen. (Caveat: the log is temp — cleared on reboot — and only reflects FUT screens actually visited. For richer traffic, run the harness and navigate more FUT menus first.)grep 'UNMAPPED PATH' -B1 /tmp/utas_server.log # endpoints we stub {} grep -oE '(GET|POST|PUT) /ut/(v2/)?game/fifa17/[^ ?]*' /tmp/utas_server.log | sort -utools/memtool.py— live FIFA17/proc/memreader/patcher (base0x140000000), for inspecting parsed structs live if a format is ambiguous.tools/fut_store.py/tools/fut_seed.py— the data models (club items, squad, packs) the backend already uses; extend these conceptually, don't reinvent.tools/openfut-fut.sh— starts the whole harness (lsx/blaze/roster/utas) if you need it running to capture more traffic.tools/root_arm.sharms host state (needs sudo).docs/CARD_SYSTEM.md— the reversed card/parser system + method (READ FIRST). It already documents the SAX-parser internals so you don't re-derive them.
Disassembly (regenerate once; same method docs/CARD_SYSTEM.md uses)
mkdir -p /tmp/fut && cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll
objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm
strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings
Shared-parser CHEATSHEET (reuse, don't re-derive): endpoint paths are strings "ut/%s/..."
(%s = "game/fifa17"); response structs are "RS4:Fut<X>ServerResponse"; each has a SAX
deserializer that hashes each key via FNV-1a fn 0x180180d00 (seed 0x811c9dc5) to an ATOM
int, looks the name up in the table at 0x1802d2760 (table[atom]=char*), and dispatches
via a jump-table; unknown keys hit the value-SKIP handler 0x180135ff0 (container-aware,
safe). Field TYPE must match its handler (scalar getters 0x1801c79d0/0x1801c7620/
0x1801c7aa0 vs nested object/array handlers) — a scalar handler fed an object/array
DESYNCS the parser and hard-freezes the game, so type fidelity is mandatory.
Endpoint resolve table: RS4::ServerSettings::resolve 0x180124270.
Method (per endpoint)
For each gap endpoint (from the log's UNMAPPED list + the "ut/%s/..." strings not yet in
ROUTES): grep the strings for its path → find its Fut<X>ServerResponse struct → find its
deserializer → list the atoms it dispatches, map atom→key via 0x1802d2760, note each
field's JSON type + required-vs-skip → write the minimal known-good response JSON.
WHERE TO START
- Read
docs/CARD_SYSTEM.mdand skimtools/utas_server.pyROUTES. - Run the two log greps above → the definitive list of endpoints FIFA calls but we only
stub
{}. Rank them by FUT feature. - List ALL
"ut/%s/..."path strings and subtract the ones already inROUTES→ the endpoints not yet even discovered in traffic. - Reverse the response format for each gap, prioritizing the core loop first: transfermarket (search/bid/buy/list/watchlist), tradepile, SBC (challenges/submit), objectives, draft, seasons/single-player, match (squad-battles/kickoff result), then club stats / concept squads / loans.
Deliverable
docs/ENDPOINT_MAP.md, one section per feature: for each endpoint — method, path, whether
already handled or a gap, request body shape, response shape (exact keys+types, required vs
optional), deserializer VA, and a minimal known-good example JSON. This is the spec for
finishing the FUT backend (target: port into the Rust openfut-core behind a FIFA-17 bridge).
APPENDIX — known atoms + verified response templates (reuse these; don't re-derive)
Parser internals (already reversed)
- key → atom: FNV-1a fn
0x180180d00(seed0x811c9dc5) →table[atom]=char*at0x1802d2760 - dispatch: range jump-tables; unknown key → value-SKIP
0x180135ff0(container-aware, safe) - scalar getters (leaf, no descend): int/num
0x1801c79d0, bool0x1801c7620, string0x1801c7aa0 - endpoint resolve:
RS4::ServerSettings::resolve 0x180124270; path"ut/%s/..",%s="game/fifa17" - shared ITEM/card deserializer:
0x18013fe00(used by club, squad slots, pack itemList, purchased)
Common shared keys (atom in hex | JSON type)
itemData 0x16b (obj/array-elem) | id 0x15c (int) | resourceId 0x287 (int) | assetId 0x23 (int)
index 0x163 (int) | kitNumber 0x17a (int) | rating 0x274 (int) | preferredPosition 0x24a (str)
cardsubtypeid 0x6c (int) | attributeList 0x31 (array) | currencies 0xc5 (array)
currencies element: name 0x1d0 (str) | funds 0x134 (int) | finalFunds 0x124 (int)
currency-name literals are CASE-SENSITIVE strings: "coins", "points", "DRAFT_TOKEN"
configs 0xa2 (array) [settings response, key = "configs"]
FutUserCreditsServerResponse — GET user/credits (deser 0x180122c50) [VERIFIED WORKS]
{"currencies":[{"name":"coins","funds":15000,"finalFunds":15000},
{"name":"points","funds":0,"finalFunds":0}],
"unopenedPacks":{"preOrderPacks":0,"recoveredPacks":0}}
Coins read from currencies[name=="coins"].funds. A bare {"credits":n} is SKIPPED → 0.
FutStoreGetPackTypesServerResponse — GET store/purchasegroup/all (deser 0x1801234e0)
Root key MUST be "purchase" 0x260 (array); optional "timestamp" 0x31b. Per-pack
(parser 0x18013af30): id 0x15c(int16, the pack identity) | packType 0x20f(str) |
description 0xd1(str) | currencies 0xc5(array {name,funds,finalFunds} = coins price) |
extPrice 0x119(obj {finalPrice 0x125, originalPrice 0x205}, each a currency→amount map
incl "mtx"=FIFA-Points) | packContentInfo 0x20c(obj: bronzeQuantity 0x63,
silverQuantity 0x2c6, goldQuantity 0x149, rareQuantity 0x273, itemQuantity 0x170) |
quantity 0x26b(int, 0=unlimited) | isPremium 0x176 | saleType 0x298 | state 0x2eb |
visible 0x37d(sets flag, DON'T send the value—desyncs).
⚠ The parser is NOT the store gate (CONFIRMED by disassembly). Per-pack parser epilogue
0x18013badc pushes every parsed pack unconditionally — no valid/drop predicate exists in
the parse path. The "not available" error (FUT_CatalogNotAvailable, msg-id 0x7550)
comes from TWO downstream gates, neither JSON-schema-related:
- Resolution gate
0x18001756e: callsGetSystemMetrics— if the display is ≤ 1024×768, the store is declared unavailable regardless of any JSON. Run FIFA at > 1024×768 (1280×720 passes). Cheapest cause to eliminate — check this first. - Store-data-model load status (
0x180013cf0):model+0x30must become1and the screen's cached status[rsi+0x250]must not stay-1; else post0x7550at0x180013d6c. Fed by two entitlement checks —0x18001749d(vtable+0x138) and0x1800175a2(vtable+0x280) — that read the Blaze client-config purchase flags:IS_STORE_ENABLED,IS_COIN_PURCHASABLE,IS_FIFAPOINT_AVAILABLE,COINS_PURCHASE_ENABLED,POINTS_PURCHASE_ENABLED,MONEY_PURCHASE_ENABLED. These are SEPARATE fromstoreEnabled/cardPackStoreEnabledand must ALSO be set in the Blaze config. Recognized per-pack enum tokens (send these exact strings to avoid enum-reject):saleType→"promo"/"deal"; a limit-type field →NONE/QUANTITY/TIME/TIME_QUANTITY; packstate→"active". Currency token is lowercase"coins"/"mtx"(NOT uppercase).
FutCreatePackServerResponse — PUT store/transaction (pack reveal, deser 0x180162880)
Wrapper key "createPackResponse" 0xbe:
{"createPackResponse":{"itemList":[/*cards*/],"numberItems":7,
"purchasedPackId":102,"duplicateItemIdList":[]}}
(atoms: itemList 0x16e, numberItems 0x1dd, purchasedPackId 0x264, duplicateItemIdList 0xec.)
BUY signal = transaction body has "packId" 0x20b AND state 0x2eb != "TRANSACTIONCANCEL".
state enum strings: TRANSACTIONCREATED(carries packId=the buy), PURCHASECOMPLETE,
TRANSACTIONCOMPLETE, TRANSACTIONCANCEL.
FutGetPurchasedItemsServerResponse — GET purchased: {"itemData":[/*cards*/]} (itemData 0x16b).
GetUserMassInfo — GET userMassInfo (deser 0x180174630) [FREEZE-SENSITIVE]
Top-level: userInfo 0x370, squad 0x2cd, settings, userData. MUST serve {} unless
every field type-matches, else the parser hard-freezes. userInfo deser 0x18013ec10;
SAFE minimal that carries coins:
{"userInfo":{"currencies":[{"name":"coins","funds":15000}],"sessionCoinsBankBalance":15000}}
(sessionCoinsBankBalance 0x2bb.) DANGER: squadList must be an OBJECT {"squad":[...]}
NOT an array (array → desync → freeze). Container fields feature/reliability/
unopenedPacks/bidTokens/actives must match exact shape or be omitted.
LoadActiveSquad — GET squad/0 (deser 0x18013d1f0)
Keys: id 0x15c | personaId 0x21b | squadName 0x2d3 | formation 0x12b(str) |
squadType 0x2d6 | chemistry 0x81 | starRating 0x2e2 | captain 0x69 | manager 0x1a8(array)
| actives 0xb(array) | custom 0xc6(STRING of 33 ints) | players 0x238(array of
{index,itemData,kitNumber}) | kicktakers 0x178(array). Squad PUT stores slots as
itemData={id:<clubItemId>} references (re-embed full items on GET, see reconstruct_squad).
Atoms are index-into-
0x1802d2760; a few above came from mixed-confidence passes — when a value doesn't take, verify the atom by locating the key string incardsdll.stringsand re-hashing. Field TYPE fidelity is mandatory (scalar-vs-container mismatch = freeze). High-confidence/verified:user/credits,createPackResponse, the store gate analysis, and theuserMassInfosafe-shape.