# 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 `0x1802d2760` straight from the DLL → **907 atoms** (`atom int → JSON key name`). Every SAX deserializer dispatches on these. Table extractor: `tools/atomdump.py` (regenerable); output `atoms.tsv`. - **Deserializer locator (RECIPE)**: struct-name string → `VA = 0x1801e5000 + (fileoff − 0x1e4400)` → `.text` `lea r8` xref → the response object's vtable (**deserializer at vtable slot +0x08**) → walk the atom `cmp`/`sub`/`dec`+`je` ladder → translate atoms via `atoms.tsv` → classify each field by the leaf getter it calls. - **Parser internals** (shared by ALL responses): key → FNV-1a (`0x180180d00`, seed `0x811c9dc5`) → atom → jump-table dispatch; unknown atom → value-SKIP `0x180135ff0` (**extra/unknown keys are always safe**). Leaf getters: int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`. **Type fidelity is mandatory**: feeding a scalar getter an object/array desyncs the SAX reader → infinite tokenizer spin (freeze at `0x1801c7f1a`). Freeze-risk (must-be-object/array) fields are flagged per struct below. - **Ladder caveat**: atom dispatch is a jump-ladder of *running-sum* `sub`/`dec` chains — 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](#transfer-market--auction-house) | | Squad Building Challenges (SBC) | 8 | 5 | [SBC](#squad-building-challenges-sbc) | | FUT Draft | 7 | 5 | [Draft](#fut-draft) | | Match / Seasons / Tournaments | 19 | 9 | [Seasons](#match--seasons--tournaments) | | Club / Cards / Consumables | 16 | 15 | [Club](#club--cards--consumables) | | Store / Packs / Purchases | 6 | 5 | [Store](#store--packs--purchases) | | User / Hub / Settings / Objectives / LB | 32 | 7 deep + 11 partial | [User](#user--hub--settings--objectives--leaderboards) | "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) 1. **Store "not available" — the `ut/v2/store` eligibility gate.** `FutStorePackQuantitiesServerResponse` (deser `0x1801758c0`) reads exactly one key `result` (atom `0x288`) → 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×768` `GetSystemMetrics` resolution check. See Store §. 2. **Match rewards live in `FutDestroyMatchServerResponse`** (`0x180121b60`), NOT `FutPlayGame` (ack-only). Coin fields at struct offsets `0x28–0x48`: `allCoins`/`matchCoins`/`seasonCoins`/ `tournamentCoins`/`coins`/`boostConis`(sic)/`participationAward`. See Seasons §. 3. **`GetClubInfo` returns `user` (club-user stat array), not `itemData`.** Rendered cards come from **`FutViewCardsServerResponse` (`0x1801293d0`) on `ut/%s/item`** — explains why the current `/club` `itemData` is SKIP'd yet cards still render. See Club §. 4. **SBC requirements = generic `{eligibilityKey,eligibilityOperation,eligibilityValue}` triples** in an `elgReq` array — the key to modding challenges. See SBC §. 5. **Objectives = ManagerQuests**, client-driven with **no ServerResponse struct / no route**; gated by `enableObjectives` in settings. See User §. ## Catalog corrections to the current backend (`utas_server.py` / `fut_store.py`) - Store `extPrice` inner keys are **`amount`/`currency`**, not `mtx`; pack identity is **`assetId`** (0x23), not `id` (harmless SKIPs otherwise). - `ut/v2/store` must return `{"result":"SUCCESS"}` (currently unhandled → contributes to store error). - `GetUserMassInfo` top-level wrapper key is **`user`** (calls userInfo/squad/settings sub-parsers); keep `{}` until every nested shape is exact (freeze-sensitive). - 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: ```json { "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 ```jsonc // 1 FutISSearch (GET auctionhouse) { "auctionInfo": [ ], "credits": 100000, "total": 1, "duplicateItemIdList": [] } // 2 FutISStart (POST auctionhouse) { "id": 100000001 } // 3 FutISViewTrade (GET trade/{id}) { "auctionInfo": [ ], "credits": 100000 } // 4 FutISWatchList (GET watchList) { "auctionInfo": [ ], "credits": 100000, "total": 1 } // 5 FutISWatchTrade (PUT watchList) -> {} // 6 FutISOfferTrade (POST trade/{id}/bid) { "auctionInfo": [ ], "credits": 99000 } // 7 FutISRemoveTrade (DELETE trade) -> {} // 8 FutISRemoveWatch (DELETE watchList) -> {} // 9 FutGetTradePile (GET tradePile) { "auctionInfo": [ ], "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). - `itemData` inside each record → **object** (the card; reuse `item_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 at `0x1801c7f1a` (same failure mode documented for `userMassInfo`/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}`. - `.itemData` = exactly the object returned by `item_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: ```json [{"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) | ```json {"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:[...]}`. ```json {"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) | ```json {"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) | ```json {"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. ```json [{"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 | ```json {"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`**. ```json {"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 at `0x1801c7f1a`). - 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/squad `itemData`; see CARD_SYSTEM.md). **Must be a JSON object.** - `0x18013d1f0` — SQUAD deser (the whole squad model: `formation`, `players[].itemData`, `manager`, `custom`, `kicktakers`…; identical to `GET /squad/0` LoadActiveSquad). **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 `.rdata` `0x180225ea0`; factory `0x18014f250`; **deserializer `0x18014f2d0`**. - 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 | |---|---|---|---| | `choices` | 0x83 | **array** [freeze-risk] | array of choice records (below) → vec@+0x30 | | `positionid` | 0x244 | int | @+0x28 | | `tier` | 0x315 | int | @+0x2c | - **Choice record** (each element of `choices`, element size 0x20; sub-dispatch @0x18014f44c): | key | atom | type | notes | |---|---|---|---| | `formation` | 0x12b | **string** | formation-choice rounds (e.g. `"f442"`); parsed str→id | | `index` | 0x163 | int (byte) | choice slot index 0..n → byte@+0x28 | | `itemData` | 0x16b | **object** [freeze-risk] | the offered player card, parsed by ITEM deser `0x18013fe00` | So a pick round is either a **formation** round (choices carry `formation`) or a **player** round (choices carry `itemData` + `index`), with the round's slot given by top-level `positionid` and `tier`. - MINIMAL known-good (player round, 5 choices for one position): ```json {"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`; factory `0x180146cc0`; **deserializer `0x180147070`** (begin-obj @0x1801470f2, dispatch @0x18014715c). - Method/Path: **GET** `ut/game/fifa17/draft/state?mode=ONLINE` | `?mode=SINGLE_PLAYER` (fragments @0x223630/0x223650). - Top-level keys: | key | atom | type | notes | |---|---|---|---| | `squad` | 0x2cd | **object** [freeze-risk] | drafted squad, SQUAD deser `0x18013d1f0` | | `entranceCriteria` | 0x108 | **object** [freeze-risk] | nested object (safe to omit) | | `gamesWonCurrentMatch` | 0x13b | int | @+0x60 | | `roundsInfo` | 0x293 | **array** [freeze-risk] | array of round records (below), elem deser `0x180146eb0`, elem size 0x20 → vec@+0x80 | | `squadState` | 0x2d5 | string | e.g. `"DRAFTSQUAD_ON"` | | `stateParam1` | 0x2ee | string→enum | @+0x5c (default 5) | | `stateParam2` | 0x2ef | string→int | @+0x64 | - **Round record** (`roundsInfo[]`, all scalar; deser `0x180146eb0`): `round`(0x290,int@+0xc), `score`(0x29a,int@+0x18), `opponentScore`(0x200,int@+0x14), `penaltyScore`(0x217,int@+0x10), `opponentPenaltyScore`(0x1fd,int@+0x1c), `opponentId`(0x1fc,int/long@+0x0), `difficulty`(0xd4,string enum@+0x8). - MINIMAL known-good (fresh single-player draft, round 0, empty squad shell): ```json {"squadState":"DRAFTSQUAD_ON","stateParam1":"","stateParam2":"0","gamesWonCurrentMatch":0, "roundsInfo":[], "squad":{"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":[]}} ``` (Populate `squad.players[].itemData` with cards from the pick choices as the draft is built; `roundsInfo` grows one record per completed match. `entranceCriteria` omitted = safe.) --- ### 3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk) - Struct name `0x180226540`; factory `0x18015076d`; **deserializer `0x1801508c0`** (begin-obj @0x18015093b, dispatch @0x1801509bc). - Method/Path: **GET** `ut/game/fifa17/draft/mode` (GetDraftStats RPC; historical/aggregate draft stats). - Keys (all int, except `draftChampion` bool): `gamesWon`(0x13a), `gamesLost`(0x138), `scoredGoals`(0x29b), `concededGoals`(0xa1), `bestBuilderScore`(0x52), `draftChampion`(0xe1, bool), `draftsCompleted`(0xe2), `passAccuracyTotal`(0x212), `possessionPercentage`(0x247), `possessionTotal`(0x249). - MINIMAL known-good: ```json {"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`; factory `0x18015106c`; **deserializer `0x1801510c0`** (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 | |---|---|---|---| | `item` | 0x16a | **array** [freeze-risk] | awarded item cards, ITEM deser `0x18013fe00` (loops) | | `halId` | 0x150 | int | @ (prize/hal id) | | `type` | 0x354 | int | prize type code | | `value` | 0x377 | int | prize value (e.g. coins) | - MINIMAL known-good (coins-only prize, no items): ```json {"type":1,"value":15000,"halId":0,"item":[]} ``` With an item prize: `"item":[{ …full card object as in itemData… }]`. --- ### 5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred) - Struct name `0x180226078`; factory `0x18014fba0`. The factory installs the **generic base ServerResponse vtable `0x18022cb58`** (shared by dozens of structs) — i.e. **no struct-specific deserializer / no parsed fields**. The neighboring `0x18014fcb0` is the *request* serializer, not a response reader. - Method/Path: **PUT/POST** `ut/game/fifa17/{champId}/draft/choose` and `ut/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`; factory `0x18014fd80`; **deserializer `0x18014fdf0`**. The body is parsed by a single call to the SQUAD deser `0x18013d1f0` (@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: ```json {"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`; factory `0x18014c0bd`; **deserializer `0x18014c260`** (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 `0x18014c260` field-parser above is treated as authoritative. Confirming which body the live client reads is the remaining gap. - MINIMAL known-good: ```json {"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`) for `squad`, `itemData`, `item[]` — those objects are already known-good through deser `0x18013d1f0` / `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 1. **Bespoke deserializer** (custom atom switch) — most Season/Match/Tournament *load/list* responses. 2. **Ack-only / base-response** — `FutMatchReady`, `FutPlayGame`, `FutUpdateSeason`, `FutUpdateFriendlySeason`, `FutGetStoryModeReward`. Their factory allocates a 0x28-byte object and calls **only the base ServerResponse constructor `0x18011f850`** (sets vtable `0x18022cb58`, timeout `0x7530`, 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` (factory `0x180121700`, name-lea `0x18012170c`) - **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 | `allCoins` | int | 0x28 | new total coin balance | | 436 | `matchCoins` | int | 0x2c | coins awarded for this match | | 809 | `tournamentCoins` | int | 0x30 | | | 776 | `teamOfTournamentWinner` | bool | 0x34 | | | 670 | `seasonCoins` | int | 0x38 | | | 149 | `coins` | int | 0x3c | reward amount (this txn) | | 529 | `participationAward` | int | 0x44 | | | 96 | `boostConis` | int | 0x48 | (EA typo — exact key) | | 617 | `qualifiedChampionEventId` | int | 0xb0 | | | 310 | `gameModeAward` | nested | — | object, SKIP-safe | | 437 | `matchCoinMultipliers` | nested | — | array/object, SKIP-safe | | 619/805/852/887 | `quantity`/`total`/`type`/`value` | nested item | — | reward-item sub-object fields (prize list element; `type` is str) | | 877 | `userData` | nested | via `0x180142470` | user 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): ```json { "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-lea `0x18011ffbd`) - **Method+path (inferred):** `POST ut/game/fifa17/match` — creates the match, returns match/squad info. - **Fields:** | atom | key | type | offset | notes | |---|---|---|---|---| | 740 | `startDateTime` | int | 0x28 | epoch | | 641 | `reportIdEnabled`| bool | — | | | 717 | `squad` | nested (`0x18011a830`) | — | squad/ITEM array — FREEZE-RISK (must be array) | - **MINIMAL JSON:** `{"startDateTime": 1580000000, "reportIdEnabled": false}` (omit `squad` — SKIP-safe) - **GAP.** ### FutMatchReadyServerResponse ★CONFIDENCE: HIGH (ack-only) - **Deserializer:** none — factory `0x180120810` → base ctor `0x18011f850` only (name `0x18021d380`). - **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 ctor `0x18011f850` only (name `0x180228050`). - **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-lea `0x18016fccd`) - **Method+path (inferred):** `POST ut/game/fifa17/season/{seasonId}/reset`. - **Fields:** atom 643 `reset` = **bool** @0x28. - **MINIMAL JSON:** `{"reset": true}`. **GAP.** ### FutSeasonListServerResponse ★CONFIDENCE: HIGH - **Deserializer VA:** `0x180167740` (name-lea `0x18016754d`) - **Method+path (inferred):** `GET ut/game/fifa17/season` — list of available seasons/divisions (array of season-descriptor objects; root container via `0x1800d84e0`). - **Element fields:** | atom | key | type | notes | |---|---|---|---| | 348 | `id` | int | season/division id | | 220 | `divisionId` | int | | | 242 | `eligibilityKey` | int | | | 244 | `eligibilitySlot` | int | | | 245 | `eligibilityValue` | int | | | 246 | `elgOperation` | str | | | 247 | `elgReq` | nested | SKIP-safe | | 595 | `prizeSet` | nested | array — FREEZE-RISK | - **MINIMAL JSON** (list root; per element scalars): ```json [{"id":1,"divisionId":10,"eligibilityKey":0,"eligibilitySlot":0,"eligibilityValue":0,"elgOperation":""}] ``` - **GAP.** (Note: root JSON shape is an array/object wrapper — verify container before shipping.) ### FutSeasonLoadDataServerResponse ★CONFIDENCE: HIGH (fully traced switch) - **Deserializer VA:** `0x180131450` (name-lea `0x18013141c`) - **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 | `seasonId` | int | 0x5c | | | 220 | `divisionId` | int | 0x58 | (as int here, not str) | | 656 | `round` | int | 0x60 | current round | | 881 | `userPoints` | int | 0x64 | points/progress | | 202 | `dataVersion` | str | 0x68 (enum byte) | | | 201 | `data` | str | interned via `0x1801c7d40` | large opaque season-state blob string | | — | root/`friendlySeasonHistory` | nested | via `0x180136880` | container — FREEZE-RISK | - Unknown atoms → SKIP `0x180135ff0` (safe). - **MINIMAL JSON:** ```json {"seasonId":1,"divisionId":10,"round":1,"userPoints":0,"dataVersion":"1","data":""} ``` - **GAP.** ### FutUpdateSeasonServerResponse ★CONFIDENCE: HIGH (ack-only) - **Deserializer:** none — factory `0x180168b10` → base ctor `0x18011f850` only (name `0x18022a748`). - **Method+path (inferred):** `PUT/POST ut/game/fifa17/season/user` (submit season match result). - **MINIMAL JSON:** `{}`. **GAP.** ### FutSeasonQuitServerResponse ★CONFIDENCE: MEDIUM - **Deserializer VA:** `0x180131a30` (name-lea `0x18013197d`) - **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 `0x18011f850` only (name `0x18022a840`). Method (inferred): `PUT ut/game/fifa17/season/friendly`. Parses no fields. **MINIMAL JSON:** `{}`. **GAP.** ### FutGetFriendlyHistoryDataServerResponse ★CONFIDENCE: MEDIUM - **Deserializer VA:** `0x18014d570` (name-lea `0x18014d36d`). Path: `GET ut/game/fifa17/season/friendly` history. - Fields: atom 675 `seasonGamesDraw` int @0x24; atom 151 `coinsEarned` int; (siblings `seasonGamesWon/Lost` in same offset cluster likely present via SKIP). **MINIMAL JSON:** `{"seasonGamesDraw":0,"coinsEarned":0}`. **GAP.** ### FutTournamentListServerResponse ★CONFIDENCE: MEDIUM - **Deserializer VA:** `0x180169ef0` (name-lea `0x180169d7d`). Path (inferred): `GET ut/game/fifa17/tournament`. - Clean scalar fields: atom 348 `id` int, 212 `difficulty` int, 149 `coins` int, 652 `rewardMultiplier` int, 19 `aigroup` int, 37 `assetName` str, 243 `eligibilityOperation` str. Many nested (rounds, prizeSet, staff, stadiumid, kit atoms 13/16 — treat as FREEZE-RISK objects, SKIP-safe if omitted). - **MINIMAL JSON (array root):** ```json [{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}] ``` - **GAP.** ### FutTournamentLoadDataServerResponse ★CONFIDENCE: MEDIUM - **Deserializer VA:** `0x180147cb0` (name-lea `0x180147c7c`). Path (inferred): `GET ut/game/fifa17/tournament/user`. - Fields: atom 202 `dataVersion` str, atom 656 `round` int, atom 810 `tournamentData` (str/opaque blob, same interned-getter `0x1801c7d40` as season `data`), atom 92 `bonus` nested. Mirrors SeasonLoadData shape. - **MINIMAL JSON:** `{"round":1,"dataVersion":"1","tournamentData":""}`. **GAP.** ### FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse ★CONFIDENCE: LOW - Both resolve to deserializer `0x18016bcf0` (names `0x18022af10` / `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-lea `0x18017580d`). 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-lea `0x18016b55d`). Atoms `tournamentId`(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-lea `0x18017279c`). Path (inferred): `GET ut/game/fifa17/season/user/history`. - Fields: atom 336 `halId` int @0xc; atom 363 `itemData` = **ITEM** (shared card/item deser `0x18013fe00`) → FREEZE-RISK (array of item cards); atom 653 `rewardQuantity` int @0x8; atom 654 `rewardType` int; atom 655 `rewardValue` int; atom 656 `round` nested. - **MINIMAL JSON:** `{"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0}` (omit itemData or send `[]`). **GAP.** ### FutGetStoryModeRewardServerResponse ★CONFIDENCE: HIGH (ack-only) - Factory → base ctor `0x18011f850` only (name `0x18022bc30`). 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: - **`0x1801642c0`** is 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 include `established`(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 in `CARD_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 ```jsonc // 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= { "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). - `/club` serves `{"itemData":…}` but GetClubInfo wants **`user`** → the itemData is SKIP'd; club card render still works because it goes through `/item` ViewCards, 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` · **deserializer** `0x1801758c0` (token loop `0x180175920`) - **This is the `ut/v2/%s/store` response.** 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 | |---|---|---| | `SUCCESS` | 0x2fb | 0 | | `TOO_MANY_TOURNAMENTS` | 0x324 | 1 | | `LOCKED_PERMANENT` | 0x1a1 | 2 | | `LOCKED_RETRY` | 0x1a2 | 3 | | `LOCKED_TROPHIES` | 0x1a3 | 4 | All other keys are skipped. Constructor default of `[rdi+0x28]` is 0 (SUCCESS), so an empty `{}` also parses as SUCCESS, but send `result` explicitly. - **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` · **deserializer** `0x180162880` - Wrapper key `createPackResponse` (atom **0xbe**) → OBJECT with: | key | atom | type | store | |---|---|---|---| | `itemList` | 0x16e | **ARRAY** of items (element deser `0x18013fe00`) | freeze-risk | | `numberItems` | 0x1dd | INT | `[rsi+0x28]` | | `purchasedPackId` | 0x264 | INT | `[rsi+0x70]` | | `duplicateItemIdList` | 0xec | **ARRAY** (int list) | freeze-risk | - **Status: already handled — VERIFIED byte-exact** against `store_buy()`. - **Minimal known-good**: ```json {"createPackResponse":{"itemList":[],"numberItems":1,"purchasedPackId":101,"duplicateItemIdList":[]}} ``` --- ### 4. FutPurchaseItemsServerResponse — confidence: MEDIUM-HIGH - **name VA** `0x1802203a0` · **deserializer** `0x180126a04` (token loop `0x180126a63`) - Purchase/transaction confirmation. Fields (dispatch is a cumulative sub-ladder off atom in `r8d`): | key | atom | type | store | |---|---|---|---| | `transactionId` | 0x33a | INT | `[rdi+0x28]` | | `firstPartyStoreId` | 0x127 | INT | `[rdi+0x9c]` | | `packId` | 0x20b | INT | | | `purchasePackType` | 0x266 | STR | `[rdi+0x48]` | | `state` | 0x2eb | STR | | | `useAuth` | 0x367 | 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 on `store/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` · **deserializer** `0x180124ed0` (body sub-parser `0x18013bd40`) - Single root key `itemData` (atom **0x16b**) → **ARRAY** of item objects (element deser `0x18013fe00`). (also tolerates `duplicateItemIdList` 0xec.) *(freeze-risk: itemData must be array)* - **Status: already handled — VERIFIED** against `purchased_items()` → `{"itemData":[...]}`. - **Minimal known-good**: `{"itemData":[]}` (or `[...]`) --- ### 6. FutUpdateCreditsServerResponse — confidence: MEDIUM - **name VA** `0x18022cc10` · **deserializer** `0x1801738b2` (delegates whole body to shared object parser `0x180139610`, `rdx=[rdi+0x10]`) - Carries the credits/currencies balance object. The verified coin-binding path is the shared currencies parser (deser `0x180122c50`, atom `currencies` 0xc5 → `[].funds`), already served by `credits_route()` as `{"credits":N,"currencies":[{name,funds,finalFunds}]}`. Exact per-offset field map of `0x180139610` not 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_ENABLED` - `IS_COIN_PURCHASABLE` - `IS_FIFAPOINT_AVAILABLE` - `IS_FIFAPOINT_PURCHASABLE` ← additional (found adjacent) - `IS_EASTORE_SERVICE_READY` ← additional (found adjacent) - `COINS_PURCHASE_ENABLED` - `POINTS_PURCHASE_ENABLED` - `MONEY_PURCHASE_ENABLED` ← additional (found adjacent) **FUT data/config flags (lowercase, JP-region variants exist):** - `cardPackStoreEnabled` / `cardPackStoreEnabled_JP` - `coinEnabled` / `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) — bool - `userData` (0x36d) — nested → userInfo record (deser `0x18013ec10`) - `squad` (0x2cd) — nested object (→ squad deser `0x18013d1f0`) - `starterPack` (0x2e5) — nested object - `bonusPacks` (0x5d) — array - **Handled:** `utas_server.USER_POST`. Min JSON: ```json {"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]} ``` ### FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) ✅ HANDLED - **Wrapper parser:** `0x180146970` — does `Parse` + TWO `NextToken` before 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) — int - `sessionCoinsBankBalance` (0x2bb) — int - `trophies` (0x340) — int - `won` (0x387) — int - `purchased` (0x262) — bool - `feature` (0x11c) — **nested object** (e.g. `{trade:bool}`) ⚠ freeze-risk - `fifaPointsFromLastYear` (0x121) — int - `squadList` (0x2d4) — **array** ⚠ freeze-risk - `unopenedPacks` (0x35e) — **nested object** ⚠ freeze-risk; children `preOrderPacks`(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 (freeze behavior) ⚠ MUST BE {} — GAP for populated - **Deser:** `0x180174630` (freeze-sensitive per CARD_SYSTEM.md). - **HTTP:** `GET ut/%s/userMassInfo` - **Top-level keys (this deser dispatches):** - `user` (0x36c) — nested → calls userInfo deser `0x18013ec10` *(NOTE: the wrapping key is `user`, not `userInfo` — relevant if ever populated)* - `clubUser` (0x91) — nested - `settings` → calls settings deser `0x18013c6d0` - squad → calls LoadActiveSquad deser `0x18013d1f0` - `transaction` (0x339), `errors` (0x10c, array), `loanPlayers` (0x19a, array), `pileSizeClientData` (0x227), `key`(0x177)/`value`(0x377) pairs - **CRITICAL:** any content (userInfo AND/OR squad) desyncs the massinfo parser → infinite tokenizer spin (`0x1801c7f1a`). **Return `{}`** — proven hub-reaching. The exact squad/squadList shape + the userInfo nested-object typing (feature / unopenedPacks / currencies) is the open desync GAP; deliver club/squad via their own endpoints instead. - **Handled:** `utas_server` `FUT_MASSINFO=empty` → `{}`. ### FutGetSettingsServerResponse — CONFIDENCE: HIGH ✅ HANDLED - **Deser:** `0x18013c6d0` - **HTTP:** `GET ut/%s/settings` - **Fields:** single wrapper key `configs` (0xa2) → array of config entries `{ type (0x354), value (0x377) }`. - **Handled:** `utas_server.SETTINGS = {"configs": []}`. Min JSON: `{"configs":[]}`. ### FutGetHubDataServerResponse — CONFIDENCE: LOW (full schema) / HIGH (served {} works) — GAP - **Wrapper:** `0x1801736ad` → inner `0x180173a50` / `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_server` serves `{}` (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 inside `userData`. --- ## 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 `...ServerResponse` struct exists — the quests UI is client-driven via the viewmodel + generic item/award payloads. **GAP** (no route yet). - **FutGetTowChallengeServerResponse** — deser `0x18016dbcd` — challenge progress as `key`(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 trust `token`. **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.