ENDPOINT_MAP said this class reads one key, `configs`, and that was true and useless. What it missed is what happens after each element closes: the client feeds the STRING VALUE of `type` back through the atom hasher and switches on the result, 42 arms wide. A flag is a row, not a key, and the client hashes our string itself. Followed it to the end. FUN_18011dc50 is the only writer of the IS_* UI gate bytes inside FutDataManagerImpl, every line is `byte = (field == 1)`, and the constructor never touches those bytes. So a flag nobody sends is a gate nobody opens. friendlySeasonsEnabled and enableDraftMode have never been sent by anything, which is a mechanism for Seasons refusing while making zero requests to any of the four servers. The store is the control that makes this readable: IS_STORE_ENABLED is the same kind of byte and its screen works, because storeEnabled and friends already ship through the Blaze config store. That list has no seasons or draft flag. Ship the gates behind FUT_SETTINGS (off/keep/gates, default gates), and re-assert the working store flags in the same array on purpose: once a populated array makes the applier run, it writes EVERY gate byte, so omitting them could switch off a screen that works today. maximumTradePileSize=100 rides along as a positive control, because a boolean that changes nothing cannot distinguish "the flag did not help" from "the array never reached the consumer". check_settings_flags.py asserts each shipped name against the atom table AND the recovered switch, since a misnamed flag is silently inert and looks exactly like a failed fix. enableSquadBuildingSetsFeature is the reason both checks are needed: a real atom with no arm here. Live: 439 contract checks pass, market unit suite passes. Not yet tested in game. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 KiB
FIFA 17 FUT — Complete Endpoint Map (clean-room)
Status: 2026-08-02. Derived entirely from binaries we own (CardsDLL_Win64_retail.dll,
image base 0x180000000) plus our own running client. No leaked EA source used.
This is the spec for rebuilding FIFA 17 Ultimate Team fully offline (and the Rosetta for
porting to FIFA 23). It maps the complete FUT API surface — ~100 FutXServerResponse
types the client can parse — with field-level detail for every endpoint, prioritizing the
core playable loop. Target: port into Rust openfut-core behind a FIFA-17 bridge.
How this was produced (methodology)
- Master atom dictionary: dumped the name table at
0x1802d2760straight from the DLL → 907 atoms (atom int → JSON key name). Every SAX deserializer dispatches on these. Table extractor:tools/atomdump.py(regenerable); outputatoms.tsv. - Deserializer locator (RECIPE): struct-name string →
VA = 0x1801e5000 + (fileoff − 0x1e4400)→.textlea r8xref → the response object's vtable (deserializer at vtable slot +0x08) → walk the atomcmp/sub/dec+jeladder → translate atoms viaatoms.tsv→ classify each field by the leaf getter it calls. - Parser internals (shared by ALL responses): key → FNV-1a (
0x180180d00, seed0x811c9dc5) → atom → jump-table dispatch; unknown atom → value-SKIP0x180135ff0(extra/unknown keys are always safe). Leaf getters: int/num0x1801c79d0, bool0x1801c7620, string0x1801c7aa0. Type fidelity is mandatory: feeding a scalar getter an object/array desyncs the SAX reader → infinite tokenizer spin (freeze at0x1801c7f1a). Freeze-risk (must-be-object/array) fields are flagged per struct below. - Ladder caveat: atom dispatch is a jump-ladder of running-sum
sub/decchains — the real atom is the accumulated sum, not the raw immediate. Field schemas are HIGH confidence; HTTP verbs are often inferred (the verb table isn't statically recoverable) — LOW confidence unless corroborated by/tmp/utas_server.log.
Reversal coverage (7 feature groups, ~100 structs)
| Group | Structs | Fully reversed | Section |
|---|---|---|---|
| Transfer Market / Auction House | 12 | 9 | Transfer Market |
| Squad Building Challenges (SBC) | 8 | 5 | SBC |
| FUT Draft | 7 | 5 | Draft |
| Match / Seasons / Tournaments | 19 | 9 | Seasons |
| Club / Cards / Consumables | 16 | 15 | Club |
| Store / Packs / Purchases | 6 | 5 | Store |
| User / Hub / Settings / Objectives / LB | 32 | 7 deep + 11 partial | User |
"Fully reversed" = top-level field schema + types decoded HIGH-confidence. Nested card/squad
elements reuse the shared item (0x18013fe00) / squad (0x18013d1f0) parsers documented in
CARD_SYSTEM.md. Every struct in the sections has its deserializer VA, key list, and a
minimal known-good JSON — including ack-only ({}) responses.
Shared record parsers (reused across groups)
| VA | Record | Used by |
|---|---|---|
0x18013fe00 |
ITEM / card element (itemData) |
club, squad, packs, purchased, market, draft, SBC |
0x18013d1f0 |
full SQUAD object | squad, draft, SBC squad-challenge |
0x18013e410 |
auction/trade record (auctionInfo[] element) |
market search/watch/tradepile |
0x18013e7f0 |
IS-list body {auctionInfo,credits,total,duplicateItemIdList} |
market |
0x180135ff0 |
value-SKIP (unknown keys) | all |
★ Highest-value findings (actionable now)
- Store "not available" — the
ut/v2/storeeligibility gate.FutStorePackQuantitiesServerResponse(deser0x1801758c0) reads exactly one keyresult(atom0x288) → must be{"result":"SUCCESS"}(other enum values gate the store closed:TOO_MANY_TOURNAMENTS/LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES). Pair with the Blaze purchase flags + the>1024×768GetSystemMetricsresolution check. See Store §. - Match rewards live in
FutDestroyMatchServerResponse(0x180121b60), NOTFutPlayGame(ack-only). Coin fields at struct offsets0x28–0x48:allCoins/matchCoins/seasonCoins/tournamentCoins/coins/boostConis(sic)/participationAward. See Seasons §. GetClubInforeturnsuser(club-user stat array), notitemData. Rendered cards come fromFutViewCardsServerResponse(0x1801293d0) onut/%s/item— explains why the current/clubitemDatais SKIP'd yet cards still render. See Club §.- SBC requirements = generic
{eligibilityKey,eligibilityOperation,eligibilityValue}triples in anelgReqarray — the key to modding challenges. See SBC §. - Objectives = ManagerQuests, client-driven with no ServerResponse struct / no route;
gated by
enableObjectivesin settings. See User §.
Catalog corrections to the current backend (utas_server.py / fut_store.py)
- Store
extPriceinner keys areamount/currency, notmtx; pack identity isassetId(0x23), notid(harmless SKIPs otherwise). ut/v2/storemust return{"result":"SUCCESS"}(currently unhandled → contributes to store error).— WRONG, corrected 2026-08-03. The body is FLAT:GetUserMassInfotop-level wrapper key isuseruserInfo(0x370)/squad(0x2cd)/settings(0x2bf)/userData(0x36d) are top-level keys.user(0x36c) only appears nested insideclubUser. Now served populated.- No-op deserializers (bare
ret) where{}always suffices: ChangeClubName, ActivateCard, SignLoanPlayer, and most ack responses.
Deep sections
Transfer Market / Auction House
Clean-room RE of the FIFA 17 CardsDLL (cardsdll.dll, image base 0x180000000)
Internet-Shopping (IS = auction house / transfer market) response deserializers,
via the RECIPE.md method (name-string → .text lea r8 xref → atom dispatch →
atoms.tsv). All VAs are static CardsDLL VAs.
Shared parsers (the spine of every IS response)
| VA | Role |
|---|---|
0x18013e410 |
Core auction/trade RECORD deserializer (one item in auctionInfo[]). Reversed deeply below. |
0x18013e7f0 |
Shared IS-list response body — {auctionInfo:[record…], credits, total, duplicateItemIdList}. Search / WatchList / TradePile all tail-delegate to it. |
0x18013fe00 |
Shared ITEM/card element deserializer (the itemData object; same one used by club/squad/pack). |
0x180135ff0 |
value-SKIP (unknown atoms — safe to send extra keys). |
| Leaf getters | int/number 0x1801c79d0 · bool 0x1801c7620 · string 0x1801c7aa0. |
Scalar-convert helpers seen: 0x1800d7b30 (num→int32), 0x1800d7b50 (num→bool/byte),
0x180166380 (str→bidState enum), 0x180166bd0 (str→tradeState enum),
0x180008120/0x180008020 (bounded string copy).
★ Core record: auctionInfo[] element — deserializer 0x18013e410 (confidence: HIGH)
Key dispatch (atom in edi, FNV-1a via 0x180180d00; ordered by atom value):
| atom | key | JSON type | getter | notes |
|---|---|---|---|---|
0x57 |
bidState |
string (enum) | 0x1801c7aa0→0x180166380 |
e.g. none/highest/outbid/buyNow |
0x65 |
buyNowPrice |
int | 0x1801c79d0→0x1800d7b30 |
|
0xc1 |
currentBid |
int | 0x1801c79d0→0x1800d7b30 |
current highest bid |
0x116 |
expires |
int | 0x1801c79d0 (QWORD) |
seconds remaining (not epoch) |
0x16b |
itemData |
nested OBJECT | 0x18013fe00 |
the card. FREEZE-RISK: must be an object, feeding a scalar desyncs the SAX reader (spin at 0x1801c7f1a) |
0x2b6 |
sellerEstablished |
int | 0x1801c79d0→0x1800d7b30 |
|
0x2b7 |
sellerName |
string | 0x1801c7aa0→0x180008120 |
bounded copy, max 0x1e=30 chars |
0x2e6 |
startingBid |
int | 0x1801c79d0→0x1800d7b30 |
|
0x2f4 |
coinsProcessed |
int/bool | 0x1801c79d0→0x1800d7b50 |
truncated to byte |
0x331 |
tradeId |
int (64-bit) | 0x1801c79d0 (QWORD) |
|
0x335 |
tradeState |
string (enum) | 0x1801c7aa0→0x180166bd0 |
active/closed/expired |
0x380 |
watched |
bool | 0x1801c7620 |
true if on watch list |
Not present at record level: bid (0x55), seller id, offers. Extra keys are
skipped safely.
Minimal known-good record:
{
"tradeId": 100000001,
"itemData": { "id": 100000001, "resourceId": 1610612736, "assetId": 20801,
"itemType": "player", "rating": 94, "preferredPosition": "ST",
"untradeable": false, "itemState": "free" },
"tradeState": "active",
"buyNowPrice": 3000,
"startingBid": 1500,
"currentBid": 0,
"bidState": "none",
"expires": 3600,
"sellerName": "OpenFUT",
"sellerEstablished": 1,
"watched": false,
"coinsProcessed": 0
}
Shared IS-list body — 0x18013e7f0 (confidence: HIGH)
| atom | key | type | getter |
|---|---|---|---|
0x35 |
auctionInfo |
array of records (0x18013e410 in a loop) |
FREEZE-RISK: must be array |
0xc0 |
credits |
int | 0x1801c79d0→0x1800d7af0→object setter |
0xec |
duplicateItemIdList |
nested (array) 0x180138e10 |
FREEZE-RISK: must be array/obj |
0x325 |
total |
int | 0x1801c79d0→0x1800d7b30 (→ obj+0x60) |
The 12 response structs
All 12 are GAPs — utas_server.py currently has no auctionhouse / trade
/ tradePile / watchList / marketdata routes. The nested itemData card is the
one piece already served (by the existing /club + item_def machinery in
utas_server.py), so it can be reused verbatim inside these responses.
Path template %s = "game/fifa17". Methods inferred from struct verb + endpoint.
| # | Struct | Deser VA | Method + Path (inferred) | Schema | Conf |
|---|---|---|---|---|---|
| 1 | FutISSearchServerResponse | 0x180163420 → 0x18013e7f0 |
GET ut/%s/auctionhouse?... (market search) |
{auctionInfo:[record], credits, total, duplicateItemIdList} |
HIGH |
| 2 | FutISStartServerResponse | 0x180165d70 (dispatch …e98) |
POST ut/%s/auctionhouse (list item for sale) |
{id:int} (new tradeId) — only atom 0x15c=id |
HIGH |
| 3 | FutISViewTradeServerResponse | 0x1801644d0 (rec call …461f) |
GET ut/%s/trade/{id} (view one auction) |
{auctionInfo:[record], credits} — atoms 0x35,0xc0 |
HIGH |
| 4 | FutISWatchListServerResponse | 0x180166130 → 0x18013e7f0 |
GET ut/%s/watchList |
{auctionInfo:[record], credits, total} |
HIGH |
| 5 | FutISWatchTradeServerResponse | 0x180164cd0 |
PUT ut/%s/watchList (add to watch list) |
ack; parses per-item status enum ladder (0x28/0x14/0x11/0x24, -1), no atom-keyed body → {} known-good |
MED (partial) |
| 6 | FutISOfferTradeServerResponse | 0x180165410 (rec call …56cf) |
POST ut/%s/trade/{id}/bid (place bid) |
{auctionInfo:[record], credits} (echoes updated auction) |
HIGH |
| 7 | FutISRemoveTradeServerResponse | 0x1801648d0 |
DELETE ut/delete/%s/trade/{id} (clear from trade pile) |
ack, no atom-keyed body → {} |
MED |
| 8 | FutISRemoveWatchServerResponse | 0x1801659f0 |
DELETE ut/delete/%s/watchList/{id} |
ack, no atom-keyed body → {} |
MED |
| 9 | FutGetTradePileServerResponse | 0x180170810 → 0x18013e7f0 |
GET ut/%s/tradePile |
{auctionInfo:[record], credits, total} |
HIGH |
| 10 | FutRelistAllServerResponse | 0x180164210 (req-ser 0x180164370) |
PUT ut/%s/auctionhouse/relist (relist all expired) |
ack; response body minimal → {} (request-side serializer builds a tradeId list) |
MED (partial) |
| 11 | FutGetAuctionCountServerResponse | 0x180163670 (dispatch …83e) |
GET ut/%s/auctionhouse count (or marketdata) |
{count, maxAuctionsAllowed, offered, selling, sold} — atoms 0xbc,0x1bf,0x1e5,0x2b8,0x2c9, all int |
HIGH |
| 12 | FutGetSuggestedPricingServerResponse | 0x180163bb0 (dispatch …ffa) |
GET ut/%s/marketdata?defId=… (price bands) |
{defId, minPrice, maxPrice} — atoms 0xcf,0x1c2,0x1ca, all int |
HIGH |
Minimal known-good JSON per struct
// 1 FutISSearch (GET auctionhouse)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1, "duplicateItemIdList": [] }
// 2 FutISStart (POST auctionhouse)
{ "id": 100000001 }
// 3 FutISViewTrade (GET trade/{id})
{ "auctionInfo": [ <record> ], "credits": 100000 }
// 4 FutISWatchList (GET watchList)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 5 FutISWatchTrade (PUT watchList) -> {}
// 6 FutISOfferTrade (POST trade/{id}/bid)
{ "auctionInfo": [ <record> ], "credits": 99000 }
// 7 FutISRemoveTrade (DELETE trade) -> {}
// 8 FutISRemoveWatch (DELETE watchList) -> {}
// 9 FutGetTradePile (GET tradePile)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 10 FutRelistAll (PUT auctionhouse/relist) -> {}
// 11 FutGetAuctionCount
{ "count": 0, "maxAuctionsAllowed": 100, "offered": 0, "selling": 0, "sold": 0 }
// 12 FutGetSuggestedPricing
{ "defId": 1610612736, "minPrice": 900, "maxPrice": 10000 }
Freeze-risk summary (type fidelity is mandatory)
auctionInfo→ array (never object/scalar).itemDatainside each record → object (the card; reuseitem_def).duplicateItemIdList→ array.bidState,tradeState,sellerName→ strings.credits,total,count,*Price,*Bid,expires,tradeId→ numbers.watched→ bool. Any scalar fed where an object/array is expected desyncs the tokenizer → hard busy-loop freeze at0x1801c7f1a(same failure mode documented foruserMassInfo/squad in CARD_SYSTEM.md).
Implementation notes for utas_server.py
- Add routes (all currently missing):
GET /auctionhouse(search + count),POST /auctionhouse(start),PUT /auctionhouse/relist,GET/POST /trade,GET /tradePile,GET/PUT /watchList,GET /marketdata,DELETE (ut/delete)/trade,DELETE (ut/delete)/watchList. - Every list response shares one builder:
{auctionInfo, credits, total}. <record>.itemData= exactly the object returned byitem_def(rid).- Ack endpoints (5,7,8,10) can safely return
{}(bodies are non-keyed/ack).
Squad Building Challenges (SBC)
Reversed from FIFA17 cardsdll.dll (base 0x180000000) using the deserializer-reversal
recipe: struct name → .rdata VA → .text xref (lea r8) → deserializer function →
atom cmp/sub/dec ladder + jump-table → atoms.tsv key names → leaf getter type.
Getter fingerprints: 0x1801c7aa0=string, 0x1801c79d0=int/number, 0x1801c7620=bool,
0x180008120=fixed-buffer string-copy (string), 0x180135ff0=value-SKIP (safe unknown key),
peek+loop via 0x1801c7f10/0x1801369f0/0x18015a750=nested object/array.
Endpoint family: ut/%s/sbs (%s = game/fifa17), base string @ fileoff 0x21d908.
The baseline utas_server.py has no sbs* routes at all → every endpoint below is a GAP.
Path ↔ response bindings (proven via path-template lea r8 adjacent to the response-struct lea r8 in each request-builder)
| Response struct | Method* | Path (under ut/game/fifa17/) |
Builder site |
|---|---|---|---|
| FutSBCTagSetsServerResponse | POST/PUT | sbs/sets/tag |
0x180153fd1→0x18015405d |
| FutLoadSetTypesServerResponse | GET | sbs/challenge/%d/squad |
0x1801545b9→0x18015469d |
| FutSBCStartChallengeResponse | POST | sbs/challenge/%d |
0x1801552a9→0x18015531d |
| FutSBCSubmitChallengeServerResponse | POST/PUT | sbs/challenge/%d (submit) |
0x1801618ff→0x18016196d |
| FutSBCSetDataServerResponse | GET | sbs/sets (set-list / by category) |
builder 0x18016fa7d (no distinct path lea) |
| FutSBCLoadCategoryDetailsServerResponse | GET | sbs/sets |
0x18017a9a5→0x18017aa3d |
| FutLoadSetChallengesResponse | GET | sbs/setId/%d/challenges |
0x18017b979→0x18017b9ed |
| FutSBCSaveSquadChallengeServerResponse | PUT | sbs/challenge/%d/squad |
0x18017ce6f→0x18017cedd |
*Method column is inferred from FUT16/17 REST conventions (builder method-enum not decoded); path bindings themselves are byte-proven.
1. FutLoadSetChallengesResponse — CONFIDENCE: HIGH ✅ fully reversed
Deserializer dispatch @ 0x18017bbbb (key-iter call 0x180141ee0); wrapper/ctor 0x18017b9ed.
Parses an array of flat SET+CHALLENGE records (each record carries both set-level and
challenge-level fields). Low atoms via cmp r8d/sub ladder; high atoms (0x280–0x354)
via byte+dword jump table at 0x18017c334/0x18017c310 (decoded from binary).
Method+path: GET ut/game/fifa17/sbs/setId/{setId}/challenges
| key | atom | type | notes / struct offset |
|---|---|---|---|
| challengeId | 0x074 | int | [rdi+0x34] |
| categoryId | 0x073 | int | [rdi+0x38] |
| index | 0x163 | int | [rdi+0x3c] |
| setId | 0x2bc | int | [rdi+0x3c]-cluster |
| type | 0x2c4 | string→enum | string-compared: "OPEN_CHALLENGE"→0, "BRICK_CHALLENGE"→2 → [rdi+0x40] |
| name | 0x1d0 | string | [rdi+0x44], max 0x7f |
| description | 0x0d1 | string | [rdi+0xc3], max 0xff |
| challengeImageId | 0x075 | string | [rdi+0x1c8], max 0x64 |
| formation | 0x12b | string | mapped via 0x180166590 → [rdi+0x22c]. FREEZE-RISK: must be a JSON string, not int |
| endTime | 0x106 | int (epoch s) | [rdi+0x240], day-scaled (cmp 0x16d=365) |
| repeatable | 0x280 | bool | [rdi+0x239] |
| trophyId | 0x2ee | int | [rdi+0x1c4] |
| status | 0x30e | string | hash-mapped state |
| timesCompleted | 0x322 | int | |
| squadId | 0x2dc | int | |
| tutorial | 0x33e | (nested) | |
| awards | 0x047 | nested ARRAY | reward objects (see shared records). FREEZE-RISK: must be array |
| elgReq | 0x0f7 | nested ARRAY | the SBC requirement/constraint list (see shared records). FREEZE-RISK: must be array |
All other high atoms (205 of them) route to the value-SKIP default → extra keys are safe.
Minimal known-good:
[{"challengeId":1,"setId":1,"categoryId":0,"index":0,"type":"OPEN_CHALLENGE",
"name":"League Basics","description":"Submit 11 players.","challengeImageId":"sbc_challenge_image_1",
"formation":"f442","endTime":0,"repeatable":false,"trophyId":0,"status":"OPEN",
"timesCompleted":0,"awards":[],"elgReq":[]}]
2. FutSBCSubmitChallengeServerResponse — CONFIDENCE: HIGH ✅ fully reversed
Dispatch @ 0x180161bda; ctor 0x18016196d. Method+path: POST sbs/challenge/{challengeId} (submit).
| key | atom | type |
|---|---|---|
| challengeId | 0x074 | int |
| setId | 0x2bc | int |
| credits | 0x0c0 | int |
| preOrderPacks | 0x24b | int |
| recoveredPacks | 0x27b | int |
| grantedChallengeAwards | 0x14a | nested array (freeze-risk) |
| grantedSetAwards | 0x14b | nested array (freeze-risk) |
{"challengeId":1,"setId":1,"credits":500,"preOrderPacks":0,"recoveredPacks":0,
"grantedChallengeAwards":[],"grantedSetAwards":[]}
3. FutSBCStartChallengeResponse / squadChallenge record — CONFIDENCE: HIGH ✅ fully reversed
Dispatch @ 0x180155949; ctor 0x18015531d. Method+path: POST sbs/challenge/{challengeId}.
Returns the squadChallenge record (the working squad for a challenge). This same record
parser is shared by FutLoadSetTypesServerResponse.
| key | atom | type |
|---|---|---|
| challengeId | 0x074 | int/nested |
| index | 0x163 | int |
| playerType | 0x23d | string |
| playerRequirements | 0x237 | nested array (per-slot constraint list; freeze-risk) |
| squad | 0x2cd | nested array of slot objects (freeze-risk) |
Slot object = {index:int, playerType:string, playerRequirements:[...]}.
{"challengeId":1,"squad":[{"index":0,"playerType":"","playerRequirements":[]}]}
4. FutSBCLoadCategoryDetailsServerResponse — CONFIDENCE: HIGH ✅ fully reversed
Dispatch @ 0x18017ac08; ctor 0x18017aa3d. Method+path: GET sbs/sets.
| key | atom | type |
|---|---|---|
| categoryId | 0x073 | int |
| name | 0x1d0 | string |
| priority | 0x250 | int |
| sets | 0x2be | nested array of set records (freeze-risk) |
{"categoryId":0,"name":"Challenges","priority":0,"sets":[]}
5. FutSBCSaveSquadChallengeServerResponse — CONFIDENCE: MEDIUM-HIGH ✅ fully reversed (minimal)
Dispatch @ 0x18017d08a; ctor 0x18017cedd. Method+path: PUT sbs/challenge/{challengeId}/squad.
Only one scalar field parsed:
| key | atom | type |
|---|---|---|
| id | 0x15c | int (saved squad id) |
{"id":1}
6. FutLoadSetTypesServerResponse — CONFIDENCE: MEDIUM ⚠️ partial
Dispatch @ 0x180154d69; ctor 0x18015469d. Method+path: GET sbs/challenge/{challengeId}/squad.
Parses an array of squadChallenge templates ("set types" = squad-building slot templates),
same key set as the squadChallenge record: {challengeId(0x074), index(0x163), playerType(0x23d str), playerRequirements(0x237 nested), squad(0x2cd nested)}. Outer top-level wrapper (array framing at
0x180154a2d) is callback-driven; exact top-level envelope key not resolved.
[{"challengeId":1,"index":0,"playerType":"","playerRequirements":[],"squad":[]}]
7. FutSBCSetDataServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial
Dispatch @ 0x18016ff2a; ctor 0x18016fa7d. Method+path: GET sbs/sets (set-data / by category).
Top-level is a callback array-parser (lea r8,[rsi+0x50] vector-append at 0x18016fb5e)
that appends set records parsed by the shared flat set-record deserializer; only one
top-level scalar was resolved:
| key | atom | type |
|---|---|---|
| reset | 0x283 | bool |
| (set records) | — | nested array; each element has the SET-level fields (setId, name, description, awards, repeatable, endTime, challenges, starRating, setImageId, timesCompleted, sortPriority) — reuse struct #1's field vocabulary |
{"reset":false,"sets":[{"setId":1,"name":"League Basics","description":"",
"repeatable":false,"endTime":0,"awards":[],"challenges":[]}]}
8. FutSBCTagSetsServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial
Ctor 0x18015405d; deserializer is a callback-based array parser (installs per-element
handler 0x180154280, vector-append) rather than an atom cmp ladder — no scalar keys resolved
from the dispatch. Semantically it acknowledges a set-tag operation and returns the updated
tagged sets. Method+path: POST/PUT sbs/sets/tag.
{"sets":[]}
Shared record shapes
elgReq — the SBC requirement/constraint list (the tricky part; parsed in struct #1 @ 0x18017bd9d,
vector at [rdi+0x358]). Array of constraint objects, inner dispatch @ 0x18017bde0:
| key | atom | type |
|---|---|---|
| eligibilityKey | 0x0f2 | int (constraint selector) |
| eligibilitySlot | 0x0f4 | int/bool |
| eligibilityValue | 0x0f5 | int (target value) |
| eligibilityOperation | 0x0f3 | int (comparator; skip-handled here) |
| eligibilities | 0x0f1 | (container atom) |
Chemistry/rating/nation/league-count constraints (teamChemistry 0x307, starRating 0x2e2,
sameNationCount 0x297, sameLeagueCount 0x296, sameClubCount 0x295, nationCount 0x1d4,
leagueCount 0x18c, clubCount 0x8b, playerCount 0x22f, chemistry 0x81) are encoded
generically as {eligibilityKey, eligibilityOperation, eligibilityValue} triples, not as
named scalar fields on the record. FREEZE-RISK: elgReq must be a JSON array of objects.
awards / grantedAwards — nested array of reward objects (atoms: rewardType 0x28e,
rewardValue 0x28f, rewardQuantity 0x28d, rewardMultiplier 0x28c, awardCount 0x40,
awardSet 0x45, awardSetId 0x46, prizeSet 0x253). FREEZE-RISK: must be array.
SET-level field vocabulary (available atoms for a full set record):
setId 0x2bc, name 0x1d0, description 0x0d1, challenges 0x76, challengesCount 0x78,
challengesCompletedCount 0x77, repeatable 0x280, endTime 0x106, awardSet 0x45,
setImageId 0x2bd, starRating 0x2e2, sortPriority 0x2cb, timesCompleted 0x322,
categoryId 0x73, priority 0x250.
Freeze-risk summary (type fidelity mandatory)
formation= string (e.g."f442"), never int.awards,elgReq,squad,playerRequirements,grantedChallengeAwards,grantedSetAwards,sets,challenges= arrays/objects, never scalar. Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer spin (freeze at0x1801c7f1a).- Unknown/extra keys are safe (value-SKIP handler
0x180135ff0).
FUT Draft
Clean-room reverse of the FIFA 17 CardsDLL (base 0x180000000) FUT Draft response
deserializers. Method: located each RS4:Fut*DraftServerResponse name string, computed its
.rdata VA, found the .text factory (lea r8,[name]), then read the adjacent JSON
deserializer's atom dispatch (FNV key → atom → getter). Atoms translated via atoms.tsv.
All 7 endpoints are a total GAP — tools/utas_server.py has zero draft routes today.
Endpoint family: ut/%s/draft/mode (+ sub-paths below), %s = "game/fifa17".
Shared sub-deserializers used by Draft:
0x18013fe00— ITEM/card element deser (a full player card, same schema as club/squaditemData; see CARD_SYSTEM.md). Must be a JSON object.0x18013d1f0— SQUAD deser (the whole squad model:formation,players[].itemData,manager,custom,kicktakers…; identical toGET /squad/0LoadActiveSquad). Must be a JSON object.
Leaf getters: int/num 0x1801c79d0, bool 0x1801c7620, string 0x1801c7aa0; unknown-key SKIP 0x180135ff0 (extra keys are safe). Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer freeze — so every field flagged [freeze-risk] below MUST be emitted as the right container type (or omitted entirely).
1. FutGetDraftChoicesServerResponse — CONFIDENCE: HIGH (fully reversed, incl. deep choice record)
- Struct name
.rdata0x180225ea0; factory0x18014f250; deserializer0x18014f2d0. - Method/Path: GET
ut/game/fifa17/{champId}/draft(path fragment"/%d/draft"@0x2252c8). Returns the choices offered for the current pick. - Top-level keys (ordered):
key atom type notes choices0x83 array [freeze-risk] array of choice records (below) → vec@+0x30 positionid0x244 int @+0x28 tier0x315 int @+0x2c - Choice record (each element of
choices, element size 0x20; sub-dispatch @0x18014f44c):key atom type notes formation0x12b string formation-choice rounds (e.g. "f442"); parsed str→idindex0x163 int (byte) choice slot index 0..n → byte@+0x28 itemData0x16b object [freeze-risk] the offered player card, parsed by ITEM deser 0x18013fe00So a pick round is either a formation round (choices carry formation) or a player round (choices carryitemData+index), with the round's slot given by top-levelpositionidandtier. - MINIMAL known-good (player round, 5 choices for one position):
{"positionid":0,"tier":1,
"choices":[
{"index":0,"itemData":{"id":1000001,"resourceId":20801,"assetId":20801,"itemType":"player","rating":94,"preferredPosition":"ST","nation":38,"teamid":243,"leagueId":53,"rareflag":1,"untradeable":true,"attributeList":[{"index":0,"value":90}],"itemState":"free","owners":1,"contract":7,"fitness":99}}
]}
Formation round: {"positionid":0,"tier":1,"choices":[{"index":0,"formation":"f442"},{"index":1,"formation":"f433"}]}.
2. FutGetDraftCurrentStateServerResponse — CONFIDENCE: HIGH (fully reversed, incl. roundsInfo element + squad delegation)
- Struct name
0x180224200; factory0x180146cc0; deserializer0x180147070(begin-obj @0x1801470f2, dispatch @0x18014715c). - Method/Path: GET
ut/game/fifa17/squad/mode/draft/state?mode=ONLINE|?mode=SINGLE_PLAYER. CORRECTED 2026-08-04 from a LIVE CAPTURE. The path above previously omitted thesquad/modesegment. The real URL isut/%s/squad/mode(template @0x18021e7f8) with/draft/stateappended, which is why it is invisible to the request-template table and why the generic/squadroute swallowed it. The suffix is appended to a caller-supplied buffer byFUN_180146ac0, which has no resolvable callers, so the full path is established by the capture inREBUILD_RESEARCH.md§15, NOT statically. - Top-level keys:
key atom type notes squad0x2cd object [freeze-risk] drafted squad, SQUAD deser 0x18013d1f0entranceCriteria0x108 object [freeze-risk] nested object (safe to omit) gamesWonCurrentMatch0x13b int @+0x60 roundsInfo0x293 array [freeze-risk] array of round records (below), elem deser 0x180146eb0, elem size 0x20 → vec@+0x80squadState0x2d5 string e.g. "DRAFTSQUAD_ON"stateParam10x2ee string→enum @+0x5c (default 5) stateParam20x2ef string→int @+0x64 - Round record (
roundsInfo[], all scalar; deser0x180146eb0):round(0x290,int@+0xc),score(0x29a,int@+0x10),opponentScore(0x200,int@+0x14),penaltyScore(0x217,int@+0x18),opponentPenaltyScore(0x1fd,int@+0x1c),opponentId(0x1fc,int/long@+0x0),difficulty(0xd4,string enum@+0x8). (scoreandpenaltyScoreoffsets were SWAPPED here until 2026-08-04.) - THE ROOT CONTAINER IS A JSON ARRAY.
0x180147070initialises, discards two tokens, then testsif (t3 != 0xd)around ado { ... } while (t != 0xd)element loop. Handed a top-level OBJECT it never reaches its exit condition and spins in the innerwhile (t != 10)loop while the tokenizer returns EOF forever: process alive, no crash dump, no dialog. That is the hang observed live on 2026-08-03. - The body previously printed here was a HANG RECIPE and is replaced below. It was
object-root, it used the spelling
DRAFTSQUAD_ONwhich is NOT an accepted value of thesquadStateenum, and it embedded a fullsquadobject. Anyone who served it would have reproduced the exact freeze this entry was supposed to help avoid. - MINIMAL known-good (verified: two agents independently walked this body through the deserializer token by token to a clean exit in 16 reads):
[{"squadState":"INVALID","stateParam1":"INVALID","stateParam2":"0","gamesWonCurrentMatch":0,"roundsInfo":[]}]
squad(0x2cd) and entranceCriteria(0x108) are OMITTED and omission is provably
inert: FUN_180135ff0 is called from both defaults, so unknown keys are skipped.
entranceCriteria's shape is now known anyway (an object of three int keys
COINS / DRAFT_TOKEN / POINTS) but knowing a shape is not a reason to send it.
stateParam2 may be a JSON number or a string; the string getter stringifies token
types 2/3/4, so "0" is correct but not mandatory.
Served today behind FUT_DRAFT_STATE (default on) in utas_server.py.
3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk)
- Struct name
0x180226540; factory0x18015076d; deserializer0x1801508c0(begin-obj @0x18015093b, dispatch @0x1801509bc). - Method/Path: GET
ut/game/fifa17/draft/mode(GetDraftStats RPC; historical/aggregate draft stats). - Keys (all int, except
draftChampionbool):gamesWon(0x13a),gamesLost(0x138),scoredGoals(0x29b),concededGoals(0xa1),bestBuilderScore(0x52),draftChampion(0xe1, bool),draftsCompleted(0xe2),passAccuracyTotal(0x212),possessionPercentage(0x247),possessionTotal(0x249). - MINIMAL known-good:
{"gamesWon":0,"gamesLost":0,"scoredGoals":0,"concededGoals":0,"bestBuilderScore":0,"draftChampion":false,"draftsCompleted":0,"passAccuracyTotal":0,"possessionPercentage":0,"possessionTotal":0}
4. FutGetDraftAwardServerResponse — CONFIDENCE: HIGH (fully reversed)
- Struct name
0x1802266f8; factory0x18015106c; deserializer0x1801510c0(begin-obj @0x180151129, dispatch @0x1801511ac). - Method/Path: GET/POST
ut/game/fifa17/draft/mode(GetDraftAward — claim/return the draft prize). - Keys:
key atom type notes item0x16a array [freeze-risk] awarded item cards, ITEM deser 0x18013fe00(loops)halId0x150 int @ (prize/hal id) type0x354 int prize type code value0x377 int prize value (e.g. coins) - ROOT CONTAINER: ARRAY, not object. CORRECTED 2026-08-04.
0x1801510c0has the same array-root prologue asFutGetDraftCurrentState. The object-root body that used to be printed here would hang the client identically, and it was sitting in this file labelled "known-good". - MINIMAL (root shape corrected; the key set itself is unchanged and was not re-verified
this pass, so
TODO/CONFIRMthe members before serving):
[{"type":1,"value":15000,"halId":0,"item":[]}]
With an item prize: "item":[{ …full card object as in itemData… }].
- Why both of these were wrong at once: a census claimed only three array-root readers existed in the DLL. It missed this one. A second census, run to check the first, was wrong in the opposite direction. Roughly 23 of 86 top-level readers remain unclassified. Do not serve any endpoint in this document until its root container has been classified by reading the actual prologue, not by regex.
5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred)
- Struct name
0x180226078; factory0x18014fba0. The factory installs the generic base ServerResponse vtable0x18022cb58(shared by dozens of structs) — i.e. no struct-specific deserializer / no parsed fields. The neighboring0x18014fcb0is the request serializer, not a response reader. - Method/Path: PUT/POST
ut/game/fifa17/{champId}/draft/chooseandut/game/fifa17/draft/choose/difficulty(fragments @0x2254c0 / 0x2254a0). Commits one pick (or the difficulty choice). - Body: an empty ack — the client re-reads state via GetDraftCurrentState / GetDraftChoices afterward.
- MINIMAL known-good:
{}
6. FutPickDraftAutoChoiceServerResponse — CONFIDENCE: HIGH (delegates to squad deser)
- Struct name
0x180226208; factory0x18014fd80; deserializer0x18014fdf0. The body is parsed by a single call to the SQUAD deser0x18013d1f0(@0x18014fe82) into the struct's squad member. - Method/Path: POST
ut/game/fifa17/{champId}/draft/autocomplete(fragment @0x225638). Auto-fills the remaining picks and returns the completed squad. - Body: a squad object [freeze-risk] (same schema as
GET /squad/0). - MINIMAL known-good:
{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]}
(Emit a fully-populated players[].itemData for a real auto-drafted XI.)
7. FutPurchaseDraftModeServerResponse — CONFIDENCE: MEDIUM-HIGH (field set fully reversed; response-variant ambiguity)
- Struct name
0x180224fb0; factory0x18014c0bd; deserializer0x18014c260(begin-obj @0x18014c2d5, dispatch @0x18014c3bb). - Method/Path: POST
ut/game/fifa17/purchase/mode/{price}/draft(fragment @0x2257a8). Buys entry into draft mode; returns the fresh draft session summary. - Keys (all scalar int):
championEventId(0x7b),expectedTierLevel(0x115),gamesPlayed(0x139),gamesRemaining(0x13c),rank(0x26d),score(0x29a),tierLevel(0x317). - Note: a second, larger struct (size 0x38, deser @0x180150379, factory @0x18015028d) also references this name string. It carries no visible scalar field ladder and is likely an alternate/summary envelope; the
0x18014c260field-parser above is treated as authoritative. Confirming which body the live client reads is the remaining gap. - MINIMAL known-good:
{"championEventId":0,"expectedTierLevel":1,"gamesPlayed":0,"gamesRemaining":4,"rank":0,"score":0,"tierLevel":1}
Implementation notes for utas_server.py
- Add routes under
G + r"/draft":GET …/draft/state,GET …/{id}/draft,POST …/{id}/draft/choose,POST …/draft/choose/difficulty,POST …/{id}/draft/autocomplete,POST …/purchase/mode/{n}/draft, plus GetDraftStats / GetDraftAward on…/draft/mode. - Reuse the existing squad/item JSON builders (
fut_seed.player_item,_base_squad) forsquad,itemData,item[]— those objects are already known-good through deser0x18013d1f0/0x18013fe00. - Freeze-risk containers to never send as scalars:
choices,choices[].itemData,squad,entranceCriteria,roundsInfo,item, and the PickAutoChoice squad body.
Match / Seasons / Tournaments
Reversed from cardsdll.dll (base 0x180000000) per RECIPE.md. CardsDLL base 0x180000000.
Parser shared internals: each JSON key → FNV-1a → atom int; deserializer dispatches on atom via
cmp/sub/dec + je binary tree. Unknown atoms are routed to the container-aware SKIP handler
0x180135ff0 — extra keys are SAFE. Leaf getters: int 0x1801c79d0, bool 0x1801c7620,
str 0x1801c7aa0. Nested object/array = call to a sub-deserializer or array loop. Feeding a
scalar getter an object/array desyncs the SAX reader → infinite spin freeze at 0x1801c7f1a.
SAX token-type constants seen in every loop (NOT keys, ignore): atom 6 (= end-object token),
atom 10 (end-array/container token). Atom values > 906 in a raw dump are jump-table offsets, not atoms.
Key-spelling caveat: the wire key is FNV-hashed, so JSON keys must match EA's exact spelling
incl. typos — notably boostConis (atom 96, not "boostCoins") and matchCoinMultipliers.
Two response families found
- Bespoke deserializer (custom atom switch) — most Season/Match/Tournament load/list responses.
- Ack-only / base-response —
FutMatchReady,FutPlayGame,FutUpdateSeason,FutUpdateFriendlySeason,FutGetStoryModeReward. Their factory allocates a 0x28-byte object and calls only the base ServerResponse constructor0x18011f850(sets vtable0x18022cb58, timeout0x7530, status-1). They parse NO body fields → the client accepts any body incl.{}. The match reward/coins are NOT in FutPlayGame — they are carried by FutDestroyMatch.
HTTP method/path note
Endpoint path strings live in an .rdata string-pool (pointer table at fileoff 0x21d480), not
lea-referenced from code, and the request-descriptor table carries the verb in a parallel array that
is not statically recoverable here. Methods below are inferred from REST/UTAS semantics — confidence
LOW on method, HIGH on struct fields. Path templates (verified in binary, %s="game/fifa17"):
ut/%s/match, ut/%s/season, ut/%s/season/user, ut/%s/season/%s/user, ut/%s/season/%s/reset,
ut/%s/season/friendly, ut/%s/tournament, ut/%s/tournament/user, ut/delete/%s/tournament/user,
/season/user/history.
Baseline (utas_server.py) status
All of these are currently GAP / stubbed: /season → (200, {}), /match/keepalive → (204).
No match/season/tournament body is currently reversed in the baseline.
HIGHEST VALUE — MATCH RESULT / REWARD RECORD
FutDestroyMatchServerResponse — the post-match coin/credit reward record ★CONFIDENCE: HIGH
-
Deserializer VA:
0x180121b60(factory0x180121700, name-lea0x18012170c) -
Method+path (inferred):
DELETE ut/game/fifa17/match/{matchId}— closes the match and returns the credited rewards. This is where a completed match reports its coin/XP award. Score/win-loss are NOT here — the client SENDS the result in the request; the server RESPONDS with the coins. -
Ordered reward fields (all int unless noted; offsets in the parsed struct):
atom key type offset notes 20 allCoinsint 0x28 new total coin balance 436 matchCoinsint 0x2c coins awarded for this match 809 tournamentCoinsint 0x30 776 teamOfTournamentWinnerbool 0x34 670 seasonCoinsint 0x38 149 coinsint 0x3c reward amount (this txn) 529 participationAwardint 0x44 96 boostConisint 0x48 (EA typo — exact key) 617 qualifiedChampionEventIdint 0xb0 310 gameModeAwardnested — object, SKIP-safe 437 matchCoinMultipliersnested — array/object, SKIP-safe 619/805/852/887 quantity/total/type/valuenested item — reward-item sub-object fields (prize list element; typeis str)877 userDatanested via 0x180142470user snapshot object (FREEZE-RISK: must be object) -
Coin fields cluster tightly at 0x28–0x48, confirming a coherent credits struct. All top-level coin fields are scalar int → safe.
-
MINIMAL known-good JSON (scalars only, zero freeze risk):
{
"coins": 400,
"allCoins": 15400,
"matchCoins": 400,
"seasonCoins": 0,
"tournamentCoins": 0,
"boostConis": 0,
"participationAward": 0,
"qualifiedChampionEventId": 0,
"teamOfTournamentWinner": false
}
- GAP (baseline has no match reward body).
SINGLE-PLAYER PLAYABLE LOOP
FutCreateMatchServerResponse ★CONFIDENCE: HIGH
- Deserializer VA:
0x180120380(name-lea0x18011ffbd) - Method+path (inferred):
POST ut/game/fifa17/match— creates the match, returns match/squad info. - Fields:
atom key type offset notes 740 startDateTimeint 0x28 epoch 641 reportIdEnabledbool — 717 squadnested ( 0x18011a830)— squad/ITEM array — FREEZE-RISK (must be array) - MINIMAL JSON:
{"startDateTime": 1580000000, "reportIdEnabled": false}(omitsquad— SKIP-safe) - GAP.
FutMatchReadyServerResponse ★CONFIDENCE: HIGH (ack-only)
- Deserializer: none — factory
0x180120810→ base ctor0x18011f850only (name0x18021d380). - Method+path (inferred):
PUT ut/game/fifa17/match/{matchId}(mark ready). - Parses no fields. MINIMAL JSON:
{}. GAP.
FutPlayGameServerResponse ★CONFIDENCE: HIGH (ack-only)
- Deserializer: none — factory
0x180162170→ base ctor0x18011f850only (name0x180228050). - Method+path (inferred):
POST ut/game/fifa17/match/{matchId}(submit game result; body carries score/stats client→server). Response body is ignored by the client. Rewards arrive via DestroyMatch. - MINIMAL JSON:
{}. GAP.
FutResetMatchServerResponse ★CONFIDENCE: HIGH
- Deserializer VA:
0x18016fd10(name-lea0x18016fccd) - Method+path (inferred):
POST ut/game/fifa17/season/{seasonId}/reset. - Fields: atom 643
reset= bool @0x28. - MINIMAL JSON:
{"reset": true}. GAP.
FutSeasonListServerResponse ★CONFIDENCE: HIGH (root shape CORRECTED 2026-08-04)
- Deserializer VA:
0x1801683f0. The previously documented0x180167740is the per-ELEMENT parser, not the response deserializer. - THE ROOT IS AN OBJECT WITH ONE KEY.
0x1801683f0runs a key loop and matches exactly one atom,seasons(0x2ad); the array opens only inside it. This entry previously described an array root, because someone read the element parser and documented its key set at the document level. A bare array populates nothing, andutas_server.pyserved one for months on the strength of this row. - Method+path:
GET ut/game/fifa17/season. - Element ordering matters:
typeMUST precededivisionId, because thedivisionIdbranch reads the already-parsed type field atelem+0x1b4. - Element stride is
0x318. (0x1f8, recorded elsewhere, is the offset of the compared short WITHIN an element.) The short the online path matches on is written fromdivisionIdas(0xb - divisionId), not fromid. eligibilityKey/eligibilitySlot/eligibilityValuebelow are inner members ofelgReqand are inert at element level, so the old minimal body was wrong twice.- Element fields:
atom key type notes 348 idint season/division id 220 divisionIdint 242 eligibilityKeyint 244 eligibilitySlotint 245 eligibilityValueint 246 elgOperationstr 247 elgReqnested SKIP-safe 595 prizeSetnested array — FREEZE-RISK - MINIMAL JSON (object root, single
seasonskey):
{"seasons":[{"type":"OFFLINE","id":1,"divisionId":10}]}
prizeSet(0x253), elgReq(0xf7) and matches(0x1b8) are all while (tok != 0xd)
ARRAY loops: a scalar in any of them is the 0x1801c7f1a spin. Omit all three.
Semantic hazard: omitting untilEndSeconds makes the season end timestamp equal now.
- Do not serve this yet. Across 486 real client requests (User-Agent
ProtoHttp, roughly 30 boots) the game has never requested/season. Every/seasonline in/tmp/utas_server.logis our owncurlorPython-urllib. Serving a body here changes nothing observable until something upstream makes the client ask. - GAP. (Note: root JSON shape is an array/object wrapper — verify container before shipping.)
FutSeasonLoadDataServerResponse ★CONFIDENCE: HIGH (fully traced switch)
- Deserializer VA:
0x180131450(name-lea0x18013141c) - Method+path (inferred):
GET ut/game/fifa17/season/user— load the user's current season state. - Fields (verified from switch at
0x18013153c):atom key type offset notes 674 seasonIdint 0x5c 220 divisionIdint 0x58 (as int here, not str) 656 roundint 0x60 current round 881 userPointsint 0x64 points/progress 202 dataVersionstr 0x68 (enum byte) 201 datastr interned via 0x1801c7d40large opaque season-state blob string — root/ friendlySeasonHistorynested via 0x180136880container — FREEZE-RISK - Unknown atoms → SKIP
0x180135ff0(safe). - MINIMAL JSON:
{"seasonId":1,"divisionId":10,"round":1,"userPoints":0,"dataVersion":"1","data":""}
- GAP.
FutUpdateSeasonServerResponse ★CONFIDENCE: HIGH (ack-only)
- Deserializer: none — factory
0x180168b10→ base ctor0x18011f850only (name0x18022a748). - Method+path (inferred):
PUT/POST ut/game/fifa17/season/user(submit season match result). - MINIMAL JSON:
{}. GAP.
FutSeasonQuitServerResponse ★CONFIDENCE: MEDIUM
- Deserializer VA:
0x180131a30(name-lea0x18013197d) - Method+path (inferred):
DELETE ut/delete/game/fifa17/season/user(quit current season). - Fields: atom 490
offlineDivision= nested (0x18011a830) — division-descriptor object. - MINIMAL JSON:
{}(offlineDivision optional/SKIP-safe; if included must be an object). GAP.
SECONDARY (best-effort catalog)
FutUpdateFriendlySeasonServerResponse ★CONFIDENCE: HIGH (ack-only)
- Factory → base ctor
0x18011f850only (name0x18022a840). Method (inferred):PUT ut/game/fifa17/season/friendly. Parses no fields. MINIMAL JSON:{}. GAP.
FutGetFriendlyHistoryDataServerResponse ★CONFIDENCE: MEDIUM
- Deserializer VA:
0x18014d570(name-lea0x18014d36d). Path:GET ut/game/fifa17/season/friendlyhistory. - Fields: atom 675
seasonGamesDrawint @0x24; atom 151coinsEarnedint; (siblingsseasonGamesWon/Lostin same offset cluster likely present via SKIP). MINIMAL JSON:{"seasonGamesDraw":0,"coinsEarned":0}. GAP.
FutTournamentListServerResponse ★CONFIDENCE: MEDIUM
- Deserializer VA:
0x180169ef0(name-lea0x180169d7d). Path (inferred):GET ut/game/fifa17/tournament. - Clean scalar fields: atom 348
idint, 212difficultyint, 149coinsint, 652rewardMultiplierint, 19aigroupint, 37assetNamestr, 243eligibilityOperationstr. Many nested (rounds, prizeSet, staff, stadiumid, kit atoms 13/16 — treat as FREEZE-RISK objects, SKIP-safe if omitted). - MINIMAL JSON (array root):
[{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}]
- GAP.
FutTournamentLoadDataServerResponse ★CONFIDENCE: MEDIUM
- Deserializer VA:
0x180147cb0(name-lea0x180147c7c). Path (inferred):GET ut/game/fifa17/tournament/user. - Fields: atom 202
dataVersionstr, atom 656roundint, atom 810tournamentData(str/opaque blob, same interned-getter0x1801c7d40as seasondata), atom 92bonusnested. Mirrors SeasonLoadData shape. - MINIMAL JSON:
{"round":1,"dataVersion":"1","tournamentData":""}. GAP.
FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse ★CONFIDENCE: LOW
- Both resolve to deserializer
0x18016bcf0(names0x18022af10/0x18022b068) — near-empty switch; only nested/opaque atoms seen (teamId, activeBadge). Likely thin/ack-ish. Paths (inferred): Quit =DELETE ut/delete/game/fifa17/tournament/user; Teams =GET ut/game/fifa17/tournament/user. MINIMAL JSON:{}(or[]for Teams if array-rooted). GAP.
FutUpdateTournamentServerResponse ★CONFIDENCE: LOW
- Deserializer VA:
0x1801758c0(name-lea0x18017580d). Only enum/opaque atoms (LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES/SUCCESS) — a status-enum response. Path (inferred):PUT/POST ut/game/fifa17/tournament/user. MINIMAL JSON:{}. GAP.
FutGetActiveTournamentsServerResponse ★CONFIDENCE: LOW
- Deserializer VA:
0x18016b660(name-lea0x18016b55d). AtomstournamentId(811),awardCount(64) seen but resolve nested/opaque. Path (inferred):GET ut/game/fifa17/tournament. Likely array of active tournament ids. MINIMAL JSON:[]or{}. GAP.
FutGetHistoricalServerResponse ★CONFIDENCE: MEDIUM
- Deserializer VA:
0x180172930(name-lea0x18017279c). Path (inferred):GET ut/game/fifa17/season/user/history. - Fields: atom 336
halIdint @0xc; atom 363itemData= ITEM (shared card/item deser0x18013fe00) → FREEZE-RISK (array of item cards); atom 653rewardQuantityint @0x8; atom 654rewardTypeint; atom 655rewardValueint; atom 656roundnested. - MINIMAL JSON:
{"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0}(omit itemData or send[]). GAP.
FutGetStoryModeRewardServerResponse ★CONFIDENCE: HIGH (ack-only)
- Factory → base ctor
0x18011f850only (name0x18022bc30). Parses no fields. MINIMAL JSON:{}. GAP.
Endpoint → response cross-reference (summary)
| Method(inf) | Path | Response struct | Reversal |
|---|---|---|---|
| POST | ut/%s/match | FutCreateMatchServerResponse | FULL |
| PUT | ut/%s/match/{id} | FutMatchReadyServerResponse | FULL (ack) |
| POST | ut/%s/match/{id} | FutPlayGameServerResponse | FULL (ack) |
| DELETE | ut/%s/match/{id} | FutDestroyMatchServerResponse (rewards) | FULL |
| POST | ut/%s/season/{id}/reset | FutResetMatchServerResponse | FULL |
| GET | ut/%s/season | FutSeasonListServerResponse | FULL |
| GET | ut/%s/season/user | FutSeasonLoadDataServerResponse | FULL |
| PUT | ut/%s/season/user | FutUpdateSeasonServerResponse | FULL (ack) |
| DELETE | ut/delete/%s/season/user | FutSeasonQuitServerResponse | PARTIAL |
| PUT | ut/%s/season/friendly | FutUpdateFriendlySeasonServerResponse | FULL (ack) |
| GET | ut/%s/season/friendly (hist) | FutGetFriendlyHistoryDataServerResponse | PARTIAL |
| GET | ut/%s/season/user/history | FutGetHistoricalServerResponse | PARTIAL |
| GET | ut/%s/tournament | FutTournamentListServerResponse | PARTIAL |
| GET | ut/%s/tournament/user | FutTournamentLoadDataServerResponse | PARTIAL |
| PUT | ut/%s/tournament/user | FutUpdateTournamentServerResponse | PARTIAL |
| GET | ut/%s/tournament | FutGetActiveTournamentsServerResponse | PARTIAL |
| GET | ut/%s/tournament/user | FutGetTournamentTeamsServerResponse | PARTIAL |
| DELETE | ut/delete/%s/tournament/user | FutTournamentQuitServerResponse | PARTIAL |
| — | (story reward grant) | FutGetStoryModeRewardServerResponse | FULL (ack) |
Club / Cards / Consumables
Clean-room RE of the FIFA17 CardsDLL (base 0x180000000). Method: each FutXServerResponse
class registers a vtable; vtable slot +0x08 is the JSON deserializer. Located via
struct-name string → VA = 0x1801e5000 + (fileoff - 0x1e4400) → factory lea r8 xref →
vtable install → slot +0x08. Every deserializer shares the same skeleton:
scratch init (0x1801c63e0/0x180008130/0x1801c8270) → NextToken×2 →
get target model (0x18011a830) → key-loop { KEYRD 0x180141ee0 → atom → dispatch } →
unknown atom → SKIP 0x180135ff0 (container-aware, extra keys are SAFE)
KEYRD 0x180141ee0 = read next key, FNV-hash (0x180180d00) → atom int, advance to value.
Leaf getters: int 0x1801c79d0, bool 0x1801c7620, string 0x1801c7aa0.
Shared ITEM/card element deserializer = 0x18013fe00 (see CARD_SYSTEM.md; produces the
0x100-byte resolved-card record). TYPE FIDELITY IS MANDATORY: an array/object key fed to a
scalar getter desyncs the SAX reader → infinite tokenizer spin (freeze at 0x1801c7f1a). Arrays
are flagged [FREEZE-RISK] below — they must be emitted as JSON arrays, never scalars.
Key structural finding (contradicts the initial brief)
FutGetClubInfoServerResponse does NOT return itemData/card items. Its only recognized
top-level key is user (atom 0x36c), an array of club-user summary records (element
parser 0x18012c990, which never calls the card element 0x18013fe00). The card list that
actually renders in the club UI is FutViewCardsServerResponse (itemData, via
0x18013fe00) served on ut/%s/item. So "club-wide item search → cards" is ViewCards on /item,
while GetClubInfo/GetClubUsers carry club-user stat records. data/ mental model of GetClubInfo =
{itemData,count,actives} is wrong for this binary. Actives (homekit/awaykit/badge/etc.) appear as
fields inside each user record, not as top-level keys.
Per-struct table
Legend: method/path from the ut/%s/... templates in the binary (%s="game/fifa17").
GAP = not (correctly) served by utas_server.py. All unknown keys are SKIP-safe, so {} never
freezes any of these — GAPs are "feature missing", not "crash".
| # | Struct | Deser VA | Method + Path | Top-level keys (atom → type) | Status | Conf |
|---|---|---|---|---|---|---|
| 1 | FutGetClubInfo | 0x18012d280 |
GET ut/%s/club |
user(0x36c) → array[user-record] [FREEZE-RISK] |
GAP (utas serves itemData, which is SKIP'd here → empty user list) |
deser HIGH / element PARTIAL |
| 2 | FutGetClubUsers | 0x180145c00 |
GET ut/%s/clubUser |
user(0x36c) → array[user-record] (elem 0x180145480) [FREEZE-RISK] |
GAP (utas /clubUser → {}) |
deser HIGH / element PARTIAL |
| 3 | FutChangeClubName | 0x1801642c0 |
PUT ut/%s/club (changeClubName) |
none (deser is immediate ret) |
ack — any parseable JSON works; {} fine |
HIGH |
| 4 | FutViewCards | 0x1801293d0 |
GET ut/%s/item |
itemData(0x16b) → array[card-item] via 0x18013fe00 [FREEZE-RISK] |
HANDLED (utas /item defs_route serves itemData) |
HIGH |
| 5 | FutActivateCard | 0x1801642c0 |
PUT ut/%s/item (FUT_CLUB_ACTIVATE_ITEM_DP) |
none (immediate ret) |
ack — {} fine |
HIGH |
| 6 | FutApplyCard | 0x18012a710 |
PUT ut/%s/item (apply by itemId) |
itemData(0x16b) → array[updated card-item] via 0x18013fe00 [FREEZE-RISK] |
GAP | HIGH |
| 7 | FutApplyCardByRes | 0x18012ad10 |
PUT ut/%s/item (apply by resourceId) |
itemData(0x16b) → array[updated card-item] [FREEZE-RISK] |
GAP | HIGH |
| 8 | FutDiscardCard | 0x180127300 |
DELETE ut/delete/%s/item (CardsDiscardCard) |
items(0x171) → array[int ids] [FREEZE-RISK]; totalCredits(0x326) → int; id(0x15c) → int |
GAP | HIGH |
| 9 | FutDiscardCardByRes | 0x1801279c0 |
DELETE ut/delete/%s/item (by res) |
totalCredits(0x326) → int |
GAP | HIGH |
| 10 | FutMoveCard | 0x180128600 |
PUT ut/%s/item (move) |
itemData(0x16b) → array [FREEZE-RISK]; chemistry(0x81) → bool |
GAP | HIGH |
| 11 | FutMoveCardByRes | 0x180128e30 |
PUT ut/%s/item (move by res) |
itemData(0x16b) → array [FREEZE-RISK]; chemistry(0x81) → bool (+ 2 str/1 int minor) |
GAP | HIGH / extra-fields MED |
| 12 | FutConsumablesSearch | 0x180130d10 |
GET ut/%s/item?type=… (GetFilteredConsumableSearchResults) |
itemData(0x16b) → array[consumable-item] via 0x18013fe00 [FREEZE-RISK]; displayGroupUseDefaultImage(0xdb) → int + count scalars |
GAP | deser HIGH / scalars MED |
| 13 | FutStaffBonus | 0x18012b730 |
GET ut/%s/… (CardsGetStaffBonuses) |
bonus(0x5c) → nested (branch sets bool @rbp+0x51) [FREEZE-RISK]; assetId(0x23) → int |
GAP | MED |
| 14 | FutGetAvailableLoanPlayers | 0x18014e030 → sub 0x18013a1c0 |
GET ut/%s/item (FUT_AVAILABLE_LOAN_PLAYERS_DP) |
loans(0x19b) → array [FREEZE-RISK]; itemData(0x16b) → array[card-item] [FREEZE-RISK]; default(0xcd) → int |
GAP | deser HIGH / fields MED |
| 15 | FutSignLoanPlayer | 0x1801642c0 |
PUT ut/%s/item (sign loan) |
none (immediate ret) |
ack — {} fine |
HIGH |
| 16 | FutStickerBookSearch | 0x18012eff0 |
GET ut/%s/… (stickerbook search) |
itemData(0x16b) → array[card-item] via 0x18013fe00 [FREEZE-RISK] |
GAP | HIGH |
Notes:
0x1801642c0is a shared no-op deserializer (function body =ret). Three responses (ChangeClubName, ActivateCard, SignLoanPlayer) use it → their HTTP body is fully ignored; only a 200 + valid JSON ({}) is required. This is why "change name / activate / sign loan" succeed with a bare stub.- user-record element (
0x18012c990, GetClubInfo) is a jump-table parser; caught fields includeestablished(0x110,str),homekit(0x159),awaykit(0x3f),cleansheets(0x84,int),attributeList(0x31),passing(0x214),awardType(0x44),categoryId(0x73),controls(0xb9),changed(0x7e),currentChampionEvent(0xc2). Full field map is PARTIAL (jump table at[0x1412c4]not fully decoded); all fields optional/SKIP-safe. - card-item element =
0x18013fe00, already reversed inCARD_SYSTEM.md(rating/position/nation/teamid/attributeList/name/resourceId…). Not re-derived here. - Atom→key mapping from
atoms.tsv(FNV-indexed sequential IDs 0x0–0x38a).
Minimal known-good JSON
// 1 GetClubInfo — GET ut/game/fifa17/club (club-user summary list)
{ "user": [ { "established": "2026", "cleansheets": 0, "homekit": 0, "awaykit": 0 } ] }
// or safe empty: { "user": [] }
// 2 GetClubUsers — GET ut/game/fifa17/clubUser
{ "user": [] }
// 3 ChangeClubName — PUT ut/game/fifa17/club
{}
// 4 ViewCards — GET ut/game/fifa17/item (THE card list; renders real cards)
{ "itemData": [ /* card items, deser 0x18013fe00 shape */ ] }
// 5 ActivateCard — PUT ut/game/fifa17/item
{}
// 6 ApplyCard / 7 ApplyCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* the single updated card item */ ] }
// 8 DiscardCard — DELETE ut/delete/game/fifa17/item
{ "items": [ 123456789 ], "totalCredits": 15000, "id": 123456789 }
// 9 DiscardCardByRes — DELETE ut/delete/game/fifa17/item
{ "totalCredits": 15000 }
// 10 MoveCard / 11 MoveCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* moved item */ ], "chemistry": true }
// 12 ConsumablesSearch — GET ut/game/fifa17/item?type=<consumable>
{ "itemData": [ /* consumable items */ ], "displayGroupUseDefaultImage": 0 }
// 13 StaffBonus — GET ut/game/fifa17/... (staff bonuses)
{ "bonus": [], "assetId": 0 }
// 14 GetAvailableLoanPlayers — GET ut/game/fifa17/item (loans)
{ "loans": [], "itemData": [ /* loan card items */ ], "default": 0 }
// 15 SignLoanPlayer — PUT ut/game/fifa17/item
{}
// 16 StickerBookSearch — GET ut/game/fifa17/... (sticker book)
{ "itemData": [] }
utas_server.py integration status
/item(defs_route) already serves{"itemData":[…]}→ satisfies ViewCards, ApplyCard, ApplyCardByRes, MoveCard, ConsumablesSearch, StickerBook, loan itemData shape (though action semantics — updated item / chemistry / credits — are not modelled)./clubserves{"itemData":…}but GetClubInfo wantsuser→ the itemData is SKIP'd; club card render still works because it goes through/itemViewCards, not/club. To populate the club-user summary, serve{"user":[…]}on/club./clubUser→{}(GAP: should be{"user":[…]};{}is non-freezing).- No routes for discard-credits, move-chemistry, staff-bonus, loan
loans[], stickerbook → all fall to catch-all{}(safe, feature-inert).
## Store / Packs / Purchases
Reversed from CardsDLL (base `0x180000000`) via the RECIPE deserializer method. All
struct-name VAs computed as `0x1801e5000 + (fileoff - 0x1e4400)`; deserializers located
from the `.text` `lea r8,[name]` xref; atoms translated through `atoms.tsv`.
Parser primitives: INT `0x1801c79d0` · BOOL `0x1801c7620` · STR `0x1801c7aa0` ·
value-SKIP (unknown atom, safe) `0x180135ff0` · next-token `0x1801c7f10` ·
begin-object `0x1801c8270` · key→atom FNV `0x180180d00` · shared ITEM element deser
`0x18013fe00`. **Type fidelity is mandatory** — feeding a scalar getter an object/array
desyncs the SAX reader → tokenizer freeze at `0x1801c7f1a`.
### Endpoint → struct map (paths use %s = "game/fifa17")
| Method | Path | Request→Response struct | Blaze cmd token |
|---|---|---|---|
| GET | `ut/%s/store` (`store/purchasegroup/...`) | FutStoreGetPackTypesServerResponse | STOREPACKTYPES |
| GET | `ut/v2/%s/store` | FutStorePackQuantitiesServerResponse | STOREPACKQUANTITIES / V2STORE |
| POST/PUT | `store/transaction` | FutCreatePackServerResponse (buy=create) / FutPurchaseItemsServerResponse | CREATEPACK / PURCHASEITEMS |
| GET | `ut/%s/purchased` | FutGetPurchasedItemsServerResponse | PURCHASEDITEMS |
| — | credits refresh (embedded) | FutUpdateCreditsServerResponse | UPDATECREDITS |
---
### 1. FutStoreGetPackTypesServerResponse — confidence: HIGH
- **name VA** `0x18021de20` · **deserializer** `0x1801234e0` · **pack element deser** `0x18013af30`
- **Root keys** (deser `0x1801234e0`):
- `purchase` (atom **0x260**) → **ARRAY** of pack objects (each → `0x18013af30`) *(freeze-risk: must be array)*
- `timestamp` (atom **0x31b**) → INT scalar → `[rdi+0x5c]`
- **Pack object fields** (deser `0x18013af30`, all optional; unknown keys skipped):
| key | atom | type | notes |
|---|---|---|---|
| `assetId` | 0x23 | INT | **real pack identity** → `[rbp-0x3c]` |
| `actionType` | 0x08 | INT | |
| `bonus` | 0x5c | INT | |
| `dealType` | 0xcc | STR | |
| `description` | 0xd1 | STR | display name |
| `displayGroup` | 0xd9 | **ARRAY** | nested (freeze-risk) |
| `displayGroupAssetId` | 0xda | INT | `[rbp-0x80]` |
| `displayGroupUseDefaultImage` | 0xdb | BOOL | |
| `currencies` | 0xc5 | **ARRAY** | coin price: `[{name,funds,finalFunds}]` (freeze-risk) |
| `extPrice` | 0x119 | **OBJECT** | → `finalPrice`(0x125,obj `0x180139070`) + `originalPrice`(0x205,obj `0x18013aae0`); inner uses `amount`(0x1b)/`currency`(0xc4) (freeze-risk) |
| `packContentInfo` | 0x20c | **OBJECT** | → `bronzeQuantity`(0x63), `silverQuantity`(0x2c6), `goldQuantity`(0x149), `rareQuantity`(0x273), `itemQuantity`(0x170), `start`(0x2e3), `unopened`(0x35d,bool) (freeze-risk) |
| `sortPriority` | 0x2cb | INT | |
- **Status: already handled (renders in-game), but with CORRECTIONS**
- `store_catalog()` currently emits `id, packType, quantity, purchaseLimit, purchaseCount, isPremium, saleType` — **none of these atoms exist in the pack deser** (`id`=0x15c, `quantity`=0x26b, `saleType`=0x298, `packType`=0x20f, `isPremium`=0x176 are all routed to SKIP `0x180135ff0`). They are harmless no-ops but do nothing.
- The **real identity field is `assetId` (0x23)**, which the current handler does NOT send. Recommend adding `assetId` per pack (packs currently work off `currencies`+`extPrice`+`packContentInfo` presence + the transaction-body `packId`, but `assetId` is the field the client actually deserializes).
- `extPrice.finalPrice/originalPrice` inner keys are NOT `mtx` (no such atom) — real inner atoms are `amount`/`currency`; current `{"mtx":N}` is skipped, so extPrice objects are effectively empty-but-present (enough to pass validation; FIFA-Points price shown comes from elsewhere).
- **Minimal known-good** (corrected):
```json
{"purchase":[{"assetId":101,"description":"Gold Pack","sortPriority":1,
"currencies":[{"name":"coins","funds":5000,"finalFunds":5000}],
"extPrice":{"finalPrice":{"amount":100,"currency":"fifapoints"},"originalPrice":{"amount":100,"currency":"fifapoints"}},
"packContentInfo":{"bronzeQuantity":0,"silverQuantity":0,"goldQuantity":7,"rareQuantity":1,"itemQuantity":7}}],
"timestamp":1596326400}
2. FutStorePackQuantitiesServerResponse — confidence: HIGH ⟵ GAP (UNBUILT)
-
name VA
0x18022d440· deserializer0x1801758c0(token loop0x180175920) -
This is the
ut/v2/%s/storeresponse. It is NOT a per-pack quantity list — it is a single eligibility/result gate. The deser reads exactly ONE key:result(atom 0x288) → STRING; the string VALUE is FNV-hashed (0x180180d00) and mapped to an enum stored at[rdi+0x28]:
string value atom hit enum SUCCESS0x2fb 0 TOO_MANY_TOURNAMENTS0x324 1 LOCKED_PERMANENT0x1a1 2 LOCKED_RETRY0x1a2 3 LOCKED_TROPHIES0x1a3 4 All other keys are skipped. Constructor default of
[rdi+0x28]is 0 (SUCCESS), so an empty{}also parses as SUCCESS, but sendresultexplicitly. -
Status: GAP — no route builds this. Add a handler for
ut/v2/%s/store. -
Minimal known-good:
{"result":"SUCCESS"}
3. FutCreatePackServerResponse — confidence: HIGH
-
name VA
0x180228318· deserializer0x180162880 -
Wrapper key
createPackResponse(atom 0xbe) → OBJECT with:key atom type store itemList0x16e ARRAY of items (element deser 0x18013fe00)freeze-risk numberItems0x1dd INT [rsi+0x28]purchasedPackId0x264 INT [rsi+0x70]duplicateItemIdList0xec ARRAY (int list) freeze-risk -
Status: already handled — VERIFIED byte-exact against
store_buy(). -
Minimal known-good:
{"createPackResponse":{"itemList":[<item>],"numberItems":1,"purchasedPackId":101,"duplicateItemIdList":[]}}
4. FutPurchaseItemsServerResponse — confidence: MEDIUM-HIGH
-
name VA
0x1802203a0· deserializer0x180126a04(token loop0x180126a63) -
Purchase/transaction confirmation. Fields (dispatch is a cumulative sub-ladder off atom in
r8d):key atom type store transactionId0x33a INT [rdi+0x28]firstPartyStoreId0x127 INT [rdi+0x9c]packId0x20b INT purchasePackType0x266 STR [rdi+0x48]state0x2eb STR useAuth0x367 INT/BOOL credit-block fields at [rdi+0xa0/0xa4/0xa8] -
Status: GAP (optional). The current buy flow uses CreatePack (
createPackResponse), not this. Only needed if a capture shows FIFA expecting a PURCHASEITEMS response onstore/transaction. Fields above are the confirmed lower branch; a few upper-branch credit fields are approximate. -
Minimal known-good:
{"transactionId":1,"packId":101,"purchasePackType":"GOLD","state":"SUCCESS"}
5. FutGetPurchasedItemsServerResponse — confidence: HIGH
- name VA
0x18021fca8· deserializer0x180124ed0(body sub-parser0x18013bd40) - Single root key
itemData(atom 0x16b) → ARRAY of item objects (element deser0x18013fe00). (also toleratesduplicateItemIdList0xec.) (freeze-risk: itemData must be array) - Status: already handled — VERIFIED against
purchased_items()→{"itemData":[...]}. - Minimal known-good:
{"itemData":[]}(or[<item>...])
6. FutUpdateCreditsServerResponse — confidence: MEDIUM
- name VA
0x18022cc10· deserializer0x1801738b2(delegates whole body to shared object parser0x180139610,rdx=[rdi+0x10]) - Carries the credits/currencies balance object. The verified coin-binding path is the shared
currencies parser (deser
0x180122c50, atomcurrencies0xc5 →[].funds), already served bycredits_route()as{"credits":N,"currencies":[{name,funds,finalFunds}]}. Exact per-offset field map of0x180139610not fully traced (it is a large shared parser); no correction needed since the coin counter already binds correctly. - Status: effectively handled via
credits_route. Keep{"credits":N,"currencies":[...]}.
Definitive store-availability config flag list (for Blaze client-config)
The store "not available" screen is gated by (a) a resolution check — GetSystemMetrics
must be > 1024×768 — and (b) the following Blaze client-config flags. FUT_STORE_DISABLED
is a UI message string, not a gate. Confirmed present in cardsdll.strings:
Blaze client-config booleans (must be "1"/true):
IS_STORE_ENABLEDIS_COIN_PURCHASABLEIS_FIFAPOINT_AVAILABLEIS_FIFAPOINT_PURCHASABLE← additional (found adjacent)IS_EASTORE_SERVICE_READY← additional (found adjacent)COINS_PURCHASE_ENABLEDPOINTS_PURCHASE_ENABLEDMONEY_PURCHASE_ENABLED← additional (found adjacent)
FUT data/config flags (lowercase, JP-region variants exist):
cardPackStoreEnabled/cardPackStoreEnabled_JPcoinEnabled/coinEnabled_JP
Set all Blaze booleans to enabled and ensure the client renders above 1024×768.
Section: User / Hub / Settings / Objectives / Leaderboards / Champions
Clean-room RE of the FIFA 17 FUT boot/hub API from cardsdll.dll disassembly
(base 0x180000000). Method: RECIPE.md — struct-name string → lea r8 xref →
deserializer field-loop → FNV atom immediates (cumulative sub/cmp jump-ladders)
→ atoms.tsv. %s = "game/fifa17".
Shared token-type noise (excluded from field lists below): after every
NextToken (0x1801c7f10) the parser does cmp eax,0xa / cmp eax,0x6 /
cmp eax,0xd — these are JSON token-TYPE checks, NOT field atoms.
Leaf getters: int/num 0x1801c79d0, bool 0x1801c7620, string 0x1801c7aa0.
Value-SKIP (unknown atom, safe extra keys): 0x180135ff0. Card/item
sub-deser: 0x18013fe00. Feeding a scalar getter an object/array desyncs the SAX
reader → infinite spin at 0x1801c7f1a (the hub freeze).
BOOT / HUB path (gates reaching the FUT hub — highest priority)
FutCreateUserServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- Deser:
0x18014cc60(lea r8 @0x18014cc2c) - HTTP:
POST ut/%s/user - Fields (exact, matches util baseline):
login(0x1a5) — booluserData(0x36d) — nested → userInfo record (deser0x18013ec10)squad(0x2cd) — nested object (→ squad deser0x18013d1f0)starterPack(0x2e5) — nested objectbonusPacks(0x5d) — array
- Handled:
utas_server.USER_POST. Min JSON:{"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]}
FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) ✅ HANDLED
- Wrapper parser:
0x180146970— doesParse+ TWONextTokenbefore deserializing, so the body MUST be wrapped in exactly one member (member NAME not compared, nesting level required). - userInfo record deser:
0x18013ec10(freeze-critical — shared with massinfo). - HTTP:
GET ut/%s/user(NOT called at boot; only reachable via massinfo). - userInfo fields — confirmed atoms (cumulative-ladder):
personaId(0x21b) — intsessionCoinsBankBalance(0x2bb) — inttrophies(0x340) — intwon(0x387) — intpurchased(0x262) — boolfeature(0x11c) — nested object (e.g.{trade:bool}) ⚠ freeze-riskfifaPointsFromLastYear(0x121) — intsquadList(0x2d4) — array ⚠ freeze-riskunopenedPacks(0x35e) — nested object ⚠ freeze-risk; childrenpreOrderPacks(0x24b),recoveredPacks(0x27b),count(0xbc)- nested notification sub-obj:
notification(0x1da),outbid(0x206),winning(0x384)
- From validated baseline (util) — also parse (unknown→SKIP):
clubName,clubAbbr,established,clubNameChangeAllowed,currencies[] (array of{name,value}⚠ array),won/draw/loss,divisionOffline/divisionOnline,reliability{reliability,matchUnfinishedTime},bidTokens{count,updateTime},actives[] (array). - Handled:
utas_server.USER_GET = {"userInfo": user_info()}.
FutGetUserMassInfoServerResponse — CONFIDENCE: HIGH (schema) ✅ SERVED POPULATED (live-verify)
- Deser:
0x180174630(full decompile:/tmp/ghidra_fut/massinfo.txt). - HTTP:
GET ut/%s/userMassInfo - Shape: FLAT object, NO wrapper. (Corrected 2026-08-03 — the earlier "wrapper key is
user" note was wrong;user(0x36c) occurs only nested insideclubUser. The prologue, 2×NextToken before the key loop, is identical to the proven-flat CreateUser parser0x18014cc60.) - Top-level keys this deser dispatches:
userInfo(0x370) → userInfo deser0x18013ec10squad(0x2cd) → LoadActiveSquad deser0x18013d1f0(loads the ACTIVE squad model)settings(0x2bf) → settings deser0x18013c6d0userData(0x36d) →0x180142470clubUser(0x91),errors(0x10c),loanPlayerClientData(0x199),loanPlayers(0x19a),pileSizeClientData(0x227)- everything else → SKIP (
0x180135ff0)
- Freeze history: the documented "any content desyncs" was recorded before the squad
schema was reversed; the prime suspect is the malformed
squadmember fed to0x18013d1f0. Everyuser_info()field type-checks against0x18013ec10. - Handled:
utas_server.massinfo()→{userInfo, squad, settings, userData};FUT_MASSINFO=full|squad|userinfo|settings|emptybisects it one member per relaunch.
FutGetSettingsServerResponse — CONFIDENCE: HIGH ✅ HANDLED (schema) / the 42 flags are RECOVERED, UNTESTED
- Deser:
0x18013c6d0(1982 bytes, 12061-char decompile, read end to end) - HTTP:
GET ut/%s/settings, and thesettings(0x2bf) member ofuserMassInfo(both callers of the deser:0x18014e590and0x180174630) - Fields: single wrapper key
configs(0xa2) → array of config entries{ type (0x354), value (0x377) }. The key ladder really does hold nothing else.
The mechanism the key ladder hides. A flag is not a JSON key. When an element
closes, the client feeds the STRING VALUE of type back through the atom hasher
(FUN_180180d00) and switches on the result, 42 arms wide:
{"configs": [{"type": "friendlySeasonsEnabled", "value": 1}]}
So the flag vocabulary is the same atom table everything else uses, and the client hashes our string itself — a flag cannot be misnamed silently, it simply falls through to the default arm and is ignored.
valueis type-forgiving. Its getter0x1801c79d0accepts int (token 2), float (3), bool (4) and string (5, viasscanf "%I64d"), coercing all four to int64.1,"1"andtrueare equivalent. This is one of the few scalar getters in the API with NO desync risk on scalars. An object or array is still a freeze.- The applier demands exactly 1.
FUN_18011dc50is the only writer of the gate bytes and every line isgate_byte = (field == 1). Not truthiness.2,-1and"yes"all read as OFF.
Flags that publish a UI gate key. FUN_18006cc60 publishes IS_* state keys by
reading single bytes inside FutDataManagerImpl (service id 0xed84b11, ctor
0x18010cdc0). Those bytes are written ONLY by the applier, and the ctor never
touches them (whole 16620-char ctor scanned):
flag type |
field | gate byte | UI key |
|---|---|---|---|
tradingEnabled |
[10] |
0x1fd2e |
IS_TRADING_ENABLED |
storeEnabled / _JP |
[0xb] / [0xc] |
0x1fd2f / 0x1fd30 |
IS_STORE_ENABLED (accessor 0x18011c600 picks _JP when region == 4) |
friendlySeasonsEnabled |
[0x16] |
0x1fd3a |
IS_FRIENDLY_SEASON_ENABLED |
tournamentQuitEnabled |
[0x20] |
0x1fd3b |
IS_TOURNAMENT_QUIT_ENABLED |
processingStateEnabled |
[0x21] |
0x1fd3c |
IS_PROCESSING_STATE_ENABLED |
enableDraftMode |
[0x17] |
0x1fd3d |
IS_DRAFT_MODE_ENABLED |
enableOfflineDraftMode = enableSinglePlayerDraftMode |
[0x18] |
0x1fd3e |
(shared arm, one field) |
storyModeRewardEnabled |
[0x1f] |
0x1fd3f |
IS_STORY_MODE_REWARD_ENABLED |
returningUserRewardsScreenEnabled |
[0x19] |
0x1fd40 |
IS_RETURNING_USER_REWARDS_SCREEN_ENABLED |
Why this is the standing suspect for Seasons and Draft. Both refuse while
making zero requests to any of the four servers, which no response shape can
explain. A UI key evaluated from a byte that nothing ever wrote does explain it.
The store is the control: IS_STORE_ENABLED reads the same kind of byte and its
screen works, because storeEnabled and friends are already shipped through the
Blaze client-config store (FUT_RS4_CONFIG in blaze_responder_v3b.py) —
and that list contains no seasons, draft or tournament flag. Same mechanism, one
population, one blank.
This is a hypothesis with a mechanism, not a confirmed cause. It predicts that sending the flags opens the screens; if they still refuse, the gate is upstream of the UI key and the whole settings line is dead.
Two arms that are not simple assignments:
enableObjectives(0xfd) andenableObjectivesAsManagerTasks(0xfe) share an arm that can only ever CLEAR[0x1c]:if (value == 0) field = 0. Sending 1 is a no-op. Objectives cannot be turned ON here, only off.clientKeepAliveResetTimeoutSec(0x86, vtable +0x68) andgetOperationTimeoutSec(0x13d, +0x58) do not store a field; they call a timer object withvalue * 1000. Sending a small number shortens client timeouts. Leave them alone.
maximumTradePileSize (0x1c0) is the positive control. It lands in [0] and
is passed to FUN_18011f380, and transfer-list capacity is visible in game. It
distinguishes "the flag did not help" from "the configs array never reached the
consumer at all", which no boolean flag can do on its own.
Not in the switch: enableSquadBuildingSetsFeature (0x100) is a real atom but
has NO arm here, so SBC is gated somewhere else. Scanned the full decompile;
this absence is asserted over the whole function, not a slice.
- Handled:
utas_server.SETTINGS,FUT_SETTINGS(defaultgates).offrestores the historical{"configs": []}.
FutGetHubDataServerResponse — CONFIDENCE: LOW (full schema) / HIGH (served {} works) — GAP
- Wrapper:
0x1801736ad→ inner0x180173a50/0x180173b10/0x180173c00. - HTTP:
GET ut/%s/hub - Note: uses C++ reflection / vtable dispatch (
call [rax+0x10],call [rdx+0x1f8]), NOT an inline atom ladder — no static field ladder to read. It aggregates sub-objects (userInfo, settings, messages, etc.), each with its own deser. Empty{}is tolerated (fields default). - Handled:
utas_serverserves{}(validated hub-reaching). Deep populate = GAP.
FutUserDataServerResponse — CONFIDENCE: MEDIUM
- Deser:
0x18016dd50(lea r8 @0x18016d98d) - Fields:
actives(0xb, array),key(0x177),value(0x377) — key/value user-data entries. Part of create/user path. Served insideuserData.
THEN — user lifecycle structs
| Struct | Deser VA | HTTP | Fields (atoms) | Status |
|---|---|---|---|---|
| FutGamerSetInfoServerResponse | 0x18016d85d | POST ut/%s/user (set gamer info) |
none (empty ack) | GAP-trivial |
| FutKeepAliveServerResponse | 0x18016232d | ut/%s/match/keepalive |
none | ✅ HANDLED (204) |
| FutLogoutServerResponse | 0x18017006d | logout | none (empty ack) | GAP-trivial |
| FutResetUserServerResponse | 0x18017548d | DELETE ut/delete/%s/user |
none | GAP-trivial |
| FutGetUserActionServerResponse | 0x1801781ad | GET user actions | actions(0x7,array), actionType(0x8) |
partial |
| FutUpdateUserActionServerResponse | 0x18012378d | PUT user action | none (empty ack) | GAP-trivial |
| FutSetFavFeatureServerResponse | 0x18016f2cd | set fav feature | none | GAP-trivial |
| FutLiveMessageUpdateServerResponse | 0x180153a2d | ut/%s/livemessage |
none (empty ack) | GAP-trivial |
| FutGetTrustedConsoleListServerResponse | 0x18012a01d | ut/%s/phishing/trusteddevice |
changed(0x7e,bool), exists(0x117,bool), locked(0x19e,bool), trusted(0x351,bool) |
✅ HANDLED (trusted:true skips security Q) |
Min JSON for the empty-ack structs: {} (200) or 204 — all validated safe.
activeMessage (ut/%s/activeMessage), clientdata (ut/%s/clientdata),
livemessage — no dedicated field-ladder deser; served generically ({} / 200).
OBJECTIVES / CHALLENGES
FUT 17 objectives = ManagerQuests (viewmodel futmanagerquestsviewmodel;
data providers FUT_MQ_QUESTS_DATA_DP, FUT_SQUAD_QUESTS_DP,
FUT_PLAYER_IDENTITY_QUESTS_DP). Enable flags live in settings:
enableObjectives (0xfd), enableObjectivesAsManagerTasks (0xfe).
Relevant atoms: objectives(0x1e2), objectivesForCurrentUser(0x1e3),
allObjectivesForCurrentGameSpaceId(0x15), challenges(0x76),
challengesCount(0x78), challengesCompletedCount(0x77), challengeId(0x74),
challengeImageId(0x75), grantedChallengeAwards(0x14a), squadChallenge(0x2d1).
- FutManagerQuestGetRewardServerCall — deser
0x1801516cd— reward-claim call; 0 inline atoms (response reuses shared item/award desers). No dedicated...ServerResponsestruct exists — the quests UI is client-driven via the viewmodel + generic item/award payloads. GAP (no route yet). - FutGetTowChallengeServerResponse — deser
0x18016dbcd— challenge progress askey(0x177)/value(0x377) pairs,actives(0xb). GET. GAP. Min:{}. - FutSetTowChallengeServerResponse — deser
0x18016ef6d— none (empty ack). PUT. GAP-trivial. - FutLoadSetChallengesResponse — deser
0x18017b9ed— SBC set-challenge defs (adjacent):awards(0x47),categoryId(0x73),elgReq(0xf7),endTime(0x106),formation(0x12b),eligibilityKey(0xf2),eligibilitySlot(0xf4). partial/GAP.
LEADERBOARDS
FutGetLBEntriesServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x180144c8d - HTTP:
GET ut/%s/leaderboards - Entry fields (array):
clubName(0x8e),badge(0x49),est(0x10f, established),score(0x29a),seasonOnlineDraws(0x2a6),TalkRating(0x303),insetUrl(0x166),inset(0x165). - Min JSON:
{"entries":[]}(empty list safe).
FutGetLBOptionsServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x18014351c - HTTP:
GET ut/%s/leaderboards/options - Fields:
category(0x70),id(0x15c),period(0x218),view(0x37a),url(0x366).
CHAMPIONS
FutChampionsRegistrationServerResponse — CONFIDENCE: MEDIUM — GAP-trivial
- Deser:
0x18014980d— no inline atoms (empty/status ack).POST ut/%s/champion. Min{}.
FutGetChampionsFriendsServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x18014b7ad.GET ut/%s/champion(friends). - Fields:
stats(0x2ec, nested),gamesPlayed(0x139),persona(0x21a). Min{}.
FutGetChampionsTopXServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x18014a09d.GET ut/%s/champion(topX). - Fields:
entries(0x109, array) of{ clubName(0x8e), badge(0x49), est(0x10f) }. Min{"entries":[]}.
CAPTCHA / PHISHING / TFA
FutGetCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x18014e78d.GET ut/%s/captcha. - Fields:
encodedImg(0x101, string base64),sequence(0x2ba, int),sizeBeforeEncode(0x2c7, int).
FutValidateCaptchaServerResponse — 0x18014ed5d — none (status ack). POST ut/%s/captcha. GAP-trivial. Min {}.
FutExchangeCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
- Deser:
0x180177d5d. Fields:token(0x321, string). Min{"token":"..."}.
FutGetPhishingQuestionServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- Deser:
0x18012980d. HTTP:GET ut/%s/phishing/question?deviceId=%s. - Fields:
question(0x26c, int id),answer,attempts(0x28, int),recoverAttempts(0x27a, int). - Handled: util
{"question":0,"answer":"","attempts":5}.
FutSetPhishingAnswerServerResponse — 0x180129b9d — none. POST /question?deviceId=%s&question=%d&answer=%s. ✅ HANDLED.
FutValidatePhishingAnswerServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- Deser:
0x180129ddd. Returns a trusttoken. HTTP:ut/%s/phishing/validate. - Handled: util
{"token":"OPENFUT-TRUST-..."}.
TFA — ut/%s/tfa — endpoint present; no dedicated field-ladder deser (served generically). GAP-untriggered.
Summary of atom / path cross-checks
- Path templates confirmed in binary strings:
ut/%s/{user,user/list,hub,clientdata, activeMessage,livemessage,leaderboards,leaderboards/options,champion,phishing, captcha,tfa}, plus/question?deviceId=%s(&question=%d&answer=%s). - Freeze-risk nested fields (must be object/array, never scalar): userInfo.
feature, userInfo.unopenedPacks, userInfo.squadList, userInfo.currencies, userInfo.actives; and the whole userMassInfo body (keep{}).
Regenerating the analysis inputs
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
python3 tools/atomdump.py > /tmp/fut/atoms.tsv # 907 atom->key rows
Recipe: docs/OPENCODE_ENDPOINT_PROMPT.md · atom table VA 0x1802d2760 · deser locator in header above.
Implementation status & leads (updated 2026-08-02, autonomous session)
- Store — FIXED (v2/store SUCCESS gate + 14 Blaze flags + catalog assetId/extPrice). Live-verified server-side.
- Transfer market — IMPLEMENTED read path:
auctionhousesearch serves 18 real-player listings (auction record 0x18013e410, itemData via proven 0x18013fe00); tradePile/watchList empty; validated freeze-safe offline by tools/test_fut_contract.py (311 checks). Toggle FUT_MARKET=empty. NEXT: buy/bid flow (stateful — deduct coins, grant card, echo updated auction) — needs live test. - SBC — LEADS (not yet enabled; need live test): feature gate flag
enableSquadBuildingSetsFeature;FUT/SBC_USE_STUBS(BRICK) may enable client-side stub SBCs with NO server content (safest path — try first); set-list deser 0x180154990 (vtable 0x180226fc0 slot+0x08) is a string-scanning/callback parser, not a clean atom ladder (envelope key not cleanly resolvable statically); requirements areSBC_ELG_KEY_*eligibility triples.SBC_TIMER_EXPIREDpresent. Recommended: set enableSquadBuildingSetsFeature + FUT/SBC_USE_STUBS in Blaze config and observe whether the SBC menu populates from client stubs. - Draft — deferred (stateful pick-progression state machine; broken state soft-locks — needs live test).
- Match rewards — reversed (FutDestroyMatch 0x180121b60 coin fields) but stateful — needs live test.