# 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`**~~ — **WRONG, corrected 2026-08-03.** The body is FLAT: `userInfo`(0x370)/`squad`(0x2cd)/`settings`(0x2bf)/`userData`(0x36d) are top-level keys. `user`(0x36c) only appears nested inside `clubUser`. 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: ```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 of objects** (element deser `0x180138e10`: `itemId` 0x16d, `duplicateItemId` 0xeb, `itemLoans` 0x16f, `duplicateItemLoans` 0xed). Not an int list. `[]` is safe; a list of bare ints is a freeze. Control that this is not a misread: `dreamSquads` 0xe9 in FutMoveCard genuinely IS a bare int array, parsed by a `while (tok != 0xd)` loop calling the int getter with no inner object loop. - `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.** > **2026-08-19 — `eligibilityKey`/`eligibilityOperation` are LOCALIZATION ORDINALS, not the > atom hex ids above.** Reversed from the pinned CardsDLL (`4706a881…`). The client's sole > confirmed consumer of these fields is the requirement-display string builder at > `~0x1800ef900`: it loads the eligibility int fields (`0x148(rcx)`) and formats them through > *indexed localization keys* — `ELIGIBILITY_STRING%d` (`0x1802186b8`), `LOC_SBC_ELG_KEY_%d` > (`0x180226710`), `ELIGIBILITY_OPERATION` (`0x1802186e8`) — appending to a string builder via > vtable `*0x10`/`*0x20`. There is **no comparison/branch**: the client does not validate on > these ints, it renders `LOC_SBC_ELG_KEY_` (and an operation string) as > display text. Therefore `eligibilityKey` is a small ordinal that indexes the **packed FIFA17 > locale**, NOT `0x307`/`0x22f`/etc. (those hex values are the atom ids of the *named* fields > the encoding replaces, not the ordinal values). CONSEQUENCE: correct projection needs the > ordinal→locale-string map, which lives only in the packed locale (absent from CardsDLL and > every `fifa17-recon/data` file; a game-dir locale probe on the live client found none) or a > real EA `elgReq` capture (unavailable on a private server). Emitting a *guessed* ordinal > renders the WRONG requirement text to the player, so `elgReq` stays `[]` until the ordinal > map is recovered. This is a display-only gap: SBC submission is fully validated server-side > (Core), and an invalid squad's generic comms modal originates from the server 400, not from > the empty `elgReq`. **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/squad/mode/draft/state?mode=ONLINE` | `?mode=SINGLE_PLAYER`. **CORRECTED 2026-08-04 from a LIVE CAPTURE.** The path above previously omitted the `squad/mode` segment. The real URL is `ut/%s/squad/mode` (template @`0x18021e7f8`) with `/draft/state` appended, which is why it is invisible to the request-template table and why the generic `/squad` route swallowed it. The suffix is appended to a caller-supplied buffer by `FUN_180146ac0`, which has no resolvable callers, so the full path is established by the capture in `REBUILD_RESEARCH.md` §15, NOT statically. - 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@**+0x10**), `opponentScore`(0x200,int@+0x14), `penaltyScore`(0x217,int@**+0x18**), `opponentPenaltyScore`(0x1fd,int@+0x1c), `opponentId`(0x1fc,int/long@+0x0), `difficulty`(0xd4,string enum@+0x8). (`score` and `penaltyScore` offsets were SWAPPED here until 2026-08-04.) - **THE ROOT CONTAINER IS A JSON ARRAY.** `0x180147070` initialises, discards two tokens, then tests `if (t3 != 0xd)` around a `do { ... } while (t != 0xd)` element loop. Handed a top-level OBJECT it never reaches its exit condition and spins in the inner `while (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_ON` which is NOT an accepted value of the `squadState` enum, and it embedded a full `squad` object. 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): ```json [{"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`; 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) | - **ROOT CONTAINER: ARRAY, not object. CORRECTED 2026-08-04.** `0x1801510c0` has the same array-root prologue as `FutGetDraftCurrentState`. 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/CONFIRM` the members before serving): ```json [{"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`; 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 (root shape CORRECTED 2026-08-04) - **Deserializer VA:** `0x1801683f0`. The previously documented `0x180167740` is the per-ELEMENT parser, not the response deserializer. - **THE ROOT IS AN OBJECT WITH ONE KEY.** `0x1801683f0` runs 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, and `utas_server.py` served one for months on the strength of this row. - **Method+path:** `GET ut/game/fifa17/season`. - **Element ordering matters:** `type` MUST precede `divisionId`, because the `divisionId` branch reads the already-parsed type field at `elem+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 from `divisionId` as `(0xb - divisionId)`, not from `id`. - `eligibilityKey`/`eligibilitySlot`/`eligibilityValue` below are inner members of `elgReq` and are **inert at element level**, so the old minimal body was wrong twice. - **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** (object root, single `seasons` key): ```json {"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 `/season` line in `/tmp/utas_server.log` is our own `curl` or `Python-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-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/club/consumables/` (ConsumablesSearch) **[CORRECTED 2026-08-21]** | `itemData`(0x16b) → **array[consumable-stack]** via `0x18013fe00` [FREEZE-RISK]; `displayGroupUseDefaultImage`(0xdb) → int + count scalars | SERVED (Rust host) | deser HIGH / scalars MED | | 13 | FutStaffBonus | `0x18012b730` | GET `ut/%s/club/stats/staff` (StaffStats, thunk `0x18012b080`) **[CORRECTED 2026-08-21]** | `bonus`(0x5c) → **nested** (branch sets bool @rbp+0x51) [FREEZE-RISK]; `assetId`(0x23) → int | SERVED (`{}`, the oracle body) | 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/club?` (ClubSearch, `FUN_18012ddf0`) **[CORRECTED 2026-08-21]** | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | SERVED (Rust host) | HIGH | ### The four `ut/%s/club` routes are a TABLE, not an inference (2026-08-21) The URLs for rows 12, 13 and 16 above were previously guessed as `ut/%s/item?…` or left as `ut/%s/…`. The binding is exact: the 125-row action table at `0x1802caa20` indexes the 48-entry URL-base table at `0x18021df80` through column 1, and **base index 3 = `ut/%s/club` is carried by exactly four rows** — so the client can emit exactly four request families on that base and no others. ``` | ClubSearch | FUN_18012ddf0 | GET ut/%s/club? | FutStickerBookSearchServerResponse | | ClubStats | FUN_18012f4f0 | GET ut/%s/club/stats/[/] | FutStickerBookStats2ServerResponse | | StaffStats | thunk 0x18012b080 | GET ut/%s/club/stats/staff | FutStaffBonusServerResponse | | ConsumablesSearch | FUN_1801308c0 | GET ut/%s/club/consumables/ | FutConsumablesSearchServerResponse | ``` **Club query grammar**, complete and ordered: `?year=2017` (always, hardcoded), then `type`, `start` (omitted at 0), `count` (omitted at 100), `filter`, then EITHER the filter block (`position, formation, state, level, rare, nation, country, league, playStyle, team, sort`) OR a comma-joined `defId=` list, never both. Live control from the log: `GET /ut/game/fifa17/club?year=2017&type=equippables&count=11&level=any&sort=desc` matches the predicted order and every suppression rule. Sub-vocabularies: `filter` = available/base/exact/any; `level` = bronze/silver/gold/any; `sort` = asc/desc; `rare` = the literal string `SP`, not a boolean; `state` = the itemState names plus `any` — and note the REQUEST spells it `onSale` where the RESPONSE value is `forSale`. `?type=` has 30 values. Decoded 2026-08-21 from the jump table itself rather than from a case count: `FUN_18012ec50` is `cmp ecx,0x1d` + a 30-entry table at `0x18012ed9c`, and each case is `mov ecx,; jmp 0x180180cd0` (atom → string). Resolving those atoms against `fut_atoms.tsv` gives the vocabulary in table order: ``` 0 any 1 player 2 manager 3 headcoach 4 fitnesscoach 5 physio 6 development 7 custom 8 unlocks 9 gkcoach 10 staff 11 badge 12 kit 13 stadium 14 ball 15 equippables 16 leaguelogos 17 offlinetrophy 18 onlinetrophy 19 featuredofflinetrophy 20 featuredonlinetrophy 21 allofflinetrophy 22 allonlinetrophy 23 healing 24 contract 25 training 26 misc 27 playerdefender 28 playermidfielder 29 playerforward ``` Notes worth having: there is **no `playergoalkeeper`** — the client has only DEF/MID/FWD tabs, so goalkeepers belong to `playerdefender`, and a GK appearing there is correct rather than a filter bug. `healing`, `contract` and `training` exist here as `?type=` arms even though consumables have their own `club/consumables/` route. Six of the thirty are trophy arms. `openfut-utas-host`'s `club_type_filter` implements all 30 with no extras; a unit test pins the list so a missing arm (an empty real tab) or an invented one (dead code that looks like coverage) fails the build. **`/club/stats` has exactly seven forms**: `club`, `year`, `country/`, `league/`, `newcards`, `consumables`, and the separately-dispatched `staff`. **There is no `/club/stats/team/`** — verified twice (the switch has six cases with no such arm, and an exhaustive PE string scan finds no literal containing `stats/team`). Any handling of a `team` stats mode is dead code. **Two holes in the base table**, recorded so nobody re-derives them as findings: base index 43 = `ut/v2/%s/store` is carried by no action row and has zero references in `.text`, yet `ut/v2/store` is live-proven; base index 9 = `ut/%s/activeMessage` is a second hole of the same kind. So at least one route is composed OUTSIDE CardsDLL, most likely in the packed exe — every "the table bounds it" statement here is bounded to CardsDLL only. 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 // CORRECTED 2026-08-05: `items` is an array of OBJECTS and there is no top-level `id`. // The previous shape, { "items": [ 123456789 ], ..., "id": 123456789 }, was wrong twice // over, and feeding a bare int where the element parser expects an object is a tokenizer // desync, i.e. a hard freeze at 0x1801c7f1a, not a soft failure. { "items": [ { "id": 123456789 } ], "totalCredits": 15000 } // 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). **`finalFunds` is the number the tile RENDERS. CONFIRMED LIVE 2026-08-05** by serving `funds=15000, finalFunds=4321` on one pack and reading `4,321` off the store tile. `funds` is not displayed. | | `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 of OBJECTS** (element deser `0x180138e10`) | 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 (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 inside `clubUser`. The prologue, 2×NextToken before the key loop, is identical to the proven-flat CreateUser parser `0x18014cc60`.) - **Top-level keys this deser dispatches:** - `userInfo` (0x370) → userInfo deser `0x18013ec10` - `squad` (0x2cd) → LoadActiveSquad deser `0x18013d1f0` (loads the ACTIVE squad model) - `settings` (0x2bf) → settings deser `0x18013c6d0` - `userData` (0x36d) → `0x180142470` - `clubUser` (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 `squad` member fed to `0x18013d1f0`. Every `user_info()` field type-checks against `0x18013ec10`. - **Handled:** `utas_server.massinfo()` → `{userInfo, squad, settings, userData}`; `FUT_MASSINFO=full|squad|userinfo|settings|empty` bisects 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 the `settings` (0x2bf) member of `userMassInfo` (both callers of the deser: `0x18014e590` and `0x180174630`) - **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: ```json {"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. - **`value` is type-forgiving.** Its getter `0x1801c79d0` accepts int (token 2), float (3), bool (4) and string (5, via `sscanf "%I64d"`), coercing all four to int64. `1`, `"1"` and `true` are 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_18011dc50` is the only writer of the gate bytes and every line is `gate_byte = (field == 1)`. Not truthiness. `2`, `-1` and `"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) and `enableObjectivesAsManagerTasks` (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) and `getOperationTimeoutSec` (0x13d, +0x58) do not store a field; they call a timer object with `value * 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` (default `gates`). `off` restores the historical `{"configs": []}`. ### FutGetHubDataServerResponse — CONFIDENCE: HIGH (schema fully enumerated) — ✅ HANDLED (tiles populated) - **Deser:** `FUN_180139610` (root object parser). Wrapper `0x1801736ad`. - **HTTP:** `GET ut/%s/hub` - **CORRECTION (2026-08-06):** the earlier note here — "uses C++ reflection / vtable dispatch, NOT an inline atom ladder, no static field ladder to read, GAP" — was **WRONG**. `FUN_180139610` has an ordinary inline atom ladder: a running-sum `sub ecx,d / … / cmp ecx,d` dispatch plus a few direct `cmp esi,imm`. It reads **18 atoms**, all enumerated below straight from the on-disk CardsDLL via objdump (`fifa17-recon` scratchpad `hub_ladder.py`). The vtable calls are the per-sub-object dispatch one indirection deeper, not the field read itself. - **The 18 root atoms** (name ← `fut_atoms.tsv`): `allObjectivesForCurrentGameSpaceId`(0x15), `auctionCount`(0x33), `championEvent`(0x7a), `clubPlayers`(0x90), `draftSummary`(0xe4), `friendlySeason`(0x131), `leaderboard`(0x186), `liveMessagesAvailable`(0x190), `objectivesForCurrentUser`(0x1e3), `offlineSeason`(0x1ec), `ONLINE`(0x1f1), `onlineSeason`(0x1f6), `SINGLE_PLAYER`(0x29d), `squad`(0x2cd), `tournament`(0x328), `tournamentProgress`(0x32c), `tradePile`(0x333), `watchlist`(0x381). - **TILE MAP (which atom drives which hub tile):** - `clubPlayers`(0x90) int → MY CLUB tile "N players" (TILE_ID 0x210) - `auctionCount`(0x33) int → TRANSFER MARKET tile "N LIVE TRANSFERS" (TILE_ID 0x1b0) - `tradePile`(0x333) **nested object**, sub-deser `0x18013ead0` → TRANSFER LIST tile "N ITEMS / Selling / Sold". Sub-atoms: `count`(0xbc), `notification`(0x1da), `selling`(0x2b8), `sold`(0x2c9) — all scalar int via `0x1801c79d0` (5 int reads, one SKIP, object field loop; no array/nested object → no type-desync surface). Same atom scheme as `FutGetAuctionCount`. **All active listings are `selling`; `count == selling == len(listings)`, `sold == 0`.** - `watchlist`(0x381) nested object, sub-deser `0x18013f3b0` → WATCH LIST tile (not yet populated; empty watch list defaults to 0, which is correct today). - **LIVE SYMPTOM this fixed (2026-08-06):** a card was actively listed (`auctionCount` 1, Listed Items screen showed it) yet the TRANSFER LIST tile read "0 items / Selling 0". The tile reads `hub.tradePile`, which we were omitting; it does **not** re-poll `/tradePile/counts` (the standalone GetAuctionCount endpoint) once at the hub. Serving `hub.tradePile:{count,selling,sold}` corrected the tile. - **Handled:** `utas_server.hub_data()` serves `clubPlayers`, `auctionCount`, and `tradePile:{count,selling,sold}` (`FUT_HUBDATA=1`, default on). Remaining atoms (seasons/draft/tournament/objectives/leaderboard summaries) default to 0/absent, which is correct while those modes are unpopulated. ### 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. --- ## 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: `auctionhouse` search 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 are `SBC_ELG_KEY_*` eligibility triples. `SBC_TIMER_EXPIRED` present. 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.