diff --git a/fifa17-recon/docs/ENDPOINT_MAP.md b/fifa17-recon/docs/ENDPOINT_MAP.md new file mode 100644 index 0000000..9c57ad5 --- /dev/null +++ b/fifa17-recon/docs/ENDPOINT_MAP.md @@ -0,0 +1,1338 @@ +# FIFA 17 FUT — Complete Endpoint Map (clean-room) + +Status: 2026-08-02. Derived **entirely** from binaries we own (`CardsDLL_Win64_retail.dll`, +image base `0x180000000`) plus our own running client. **No leaked EA source used.** + +This is the spec for rebuilding FIFA 17 Ultimate Team fully offline (and the Rosetta for +porting to FIFA 23). It maps the **complete FUT API surface** — ~100 `FutXServerResponse` +types the client can parse — with field-level detail for every endpoint, prioritizing the +core playable loop. Target: port into Rust `openfut-core` behind a FIFA-17 bridge. + +## How this was produced (methodology) + +- **Master atom dictionary**: dumped the name table at `0x1802d2760` straight from the DLL + → **907 atoms** (`atom int → JSON key name`). Every SAX deserializer dispatches on these. + Table extractor: `tools/atomdump.py` (regenerable); output `atoms.tsv`. +- **Deserializer locator (RECIPE)**: struct-name string → `VA = 0x1801e5000 + (fileoff − 0x1e4400)` + → `.text` `lea r8` xref → the response object's vtable (**deserializer at vtable slot +0x08**) + → walk the atom `cmp`/`sub`/`dec`+`je` ladder → translate atoms via `atoms.tsv` → classify + each field by the leaf getter it calls. +- **Parser internals** (shared by ALL responses): key → FNV-1a (`0x180180d00`, seed + `0x811c9dc5`) → atom → jump-table dispatch; unknown atom → value-SKIP `0x180135ff0` + (**extra/unknown keys are always safe**). Leaf getters: int/num `0x1801c79d0`, bool + `0x1801c7620`, string `0x1801c7aa0`. **Type fidelity is mandatory**: feeding a scalar + getter an object/array desyncs the SAX reader → infinite tokenizer spin (freeze at + `0x1801c7f1a`). Freeze-risk (must-be-object/array) fields are flagged per struct below. +- **Ladder caveat**: atom dispatch is a jump-ladder of *running-sum* `sub`/`dec` chains — the + real atom is the accumulated sum, not the raw immediate. Field schemas are HIGH confidence; + **HTTP verbs are often inferred** (the verb table isn't statically recoverable) — LOW + confidence unless corroborated by `/tmp/utas_server.log`. + +## Reversal coverage (7 feature groups, ~100 structs) + +| Group | Structs | Fully reversed | Section | +|---|---|---|---| +| Transfer Market / Auction House | 12 | 9 | [Transfer Market](#transfer-market--auction-house) | +| Squad Building Challenges (SBC) | 8 | 5 | [SBC](#squad-building-challenges-sbc) | +| FUT Draft | 7 | 5 | [Draft](#fut-draft) | +| Match / Seasons / Tournaments | 19 | 9 | [Seasons](#match--seasons--tournaments) | +| Club / Cards / Consumables | 16 | 15 | [Club](#club--cards--consumables) | +| Store / Packs / Purchases | 6 | 5 | [Store](#store--packs--purchases) | +| User / Hub / Settings / Objectives / LB | 32 | 7 deep + 11 partial | [User](#user--hub--settings--objectives--leaderboards) | + +"Fully reversed" = top-level field schema + types decoded HIGH-confidence. Nested card/squad +elements reuse the shared item (`0x18013fe00`) / squad (`0x18013d1f0`) parsers documented in +`CARD_SYSTEM.md`. Every struct in the sections has its deserializer VA, key list, and a +minimal known-good JSON — including ack-only (`{}`) responses. + +## Shared record parsers (reused across groups) + +| VA | Record | Used by | +|---|---|---| +| `0x18013fe00` | ITEM / card element (`itemData`) | club, squad, packs, purchased, market, draft, SBC | +| `0x18013d1f0` | full SQUAD object | squad, draft, SBC squad-challenge | +| `0x18013e410` | auction/trade record (`auctionInfo[]` element) | market search/watch/tradepile | +| `0x18013e7f0` | IS-list body `{auctionInfo,credits,total,duplicateItemIdList}` | market | +| `0x180135ff0` | value-SKIP (unknown keys) | all | + +## ★ Highest-value findings (actionable now) + +1. **Store "not available" — the `ut/v2/store` eligibility gate.** `FutStorePackQuantitiesServerResponse` + (deser `0x1801758c0`) reads exactly one key `result` (atom `0x288`) → must be + **`{"result":"SUCCESS"}`** (other enum values gate the store closed: + `TOO_MANY_TOURNAMENTS`/`LOCKED_PERMANENT`/`LOCKED_RETRY`/`LOCKED_TROPHIES`). Pair with the + Blaze purchase flags + the `>1024×768` `GetSystemMetrics` resolution check. See Store §. +2. **Match rewards live in `FutDestroyMatchServerResponse`** (`0x180121b60`), NOT `FutPlayGame` + (ack-only). Coin fields at struct offsets `0x28–0x48`: `allCoins`/`matchCoins`/`seasonCoins`/ + `tournamentCoins`/`coins`/`boostConis`(sic)/`participationAward`. See Seasons §. +3. **`GetClubInfo` returns `user` (club-user stat array), not `itemData`.** Rendered cards come + from **`FutViewCardsServerResponse` (`0x1801293d0`) on `ut/%s/item`** — explains why the + current `/club` `itemData` is SKIP'd yet cards still render. See Club §. +4. **SBC requirements = generic `{eligibilityKey,eligibilityOperation,eligibilityValue}` triples** + in an `elgReq` array — the key to modding challenges. See SBC §. +5. **Objectives = ManagerQuests**, client-driven with **no ServerResponse struct / no route**; + gated by `enableObjectives` in settings. See User §. + +## Catalog corrections to the current backend (`utas_server.py` / `fut_store.py`) + +- Store `extPrice` inner keys are **`amount`/`currency`**, not `mtx`; pack identity is + **`assetId`** (0x23), not `id` (harmless SKIPs otherwise). +- `ut/v2/store` must return `{"result":"SUCCESS"}` (currently unhandled → contributes to store error). +- `GetUserMassInfo` top-level wrapper key is **`user`** (calls userInfo/squad/settings sub-parsers); + keep `{}` until every nested shape is exact (freeze-sensitive). +- No-op deserializers (bare `ret`) where `{}` always suffices: ChangeClubName, ActivateCard, + SignLoanPlayer, and most ack responses. + +--- + +# Deep sections +## Transfer Market / Auction House + +Clean-room RE of the FIFA 17 CardsDLL (`cardsdll.dll`, image base `0x180000000`) +Internet-Shopping (IS = auction house / transfer market) response deserializers, +via the RECIPE.md method (name-string → `.text` `lea r8` xref → atom dispatch → +`atoms.tsv`). All VAs are static CardsDLL VAs. + +### Shared parsers (the spine of every IS response) + +| VA | Role | +|---|---| +| `0x18013e410` | **Core auction/trade RECORD deserializer** (one item in `auctionInfo[]`). Reversed deeply below. | +| `0x18013e7f0` | **Shared IS-list response body** — `{auctionInfo:[record…], credits, total, duplicateItemIdList}`. Search / WatchList / TradePile all tail-delegate to it. | +| `0x18013fe00` | Shared ITEM/card element deserializer (the `itemData` object; same one used by club/squad/pack). | +| `0x180135ff0` | value-SKIP (unknown atoms — safe to send extra keys). | +| Leaf getters | int/number `0x1801c79d0` · bool `0x1801c7620` · string `0x1801c7aa0`. | + +Scalar-convert helpers seen: `0x1800d7b30` (num→int32), `0x1800d7b50` (num→bool/byte), +`0x180166380` (str→bidState enum), `0x180166bd0` (str→tradeState enum), +`0x180008120`/`0x180008020` (bounded string copy). + +--- + +### ★ Core record: `auctionInfo[]` element — deserializer `0x18013e410` (confidence: HIGH) + +Key dispatch (atom in `edi`, FNV-1a via `0x180180d00`; ordered by atom value): + +| atom | key | JSON type | getter | notes | +|---|---|---|---|---| +| `0x57` | `bidState` | string (enum) | `0x1801c7aa0`→`0x180166380` | e.g. `none`/`highest`/`outbid`/`buyNow` | +| `0x65` | `buyNowPrice` | int | `0x1801c79d0`→`0x1800d7b30` | | +| `0xc1` | `currentBid` | int | `0x1801c79d0`→`0x1800d7b30` | current highest bid | +| `0x116` | `expires` | int | `0x1801c79d0` (QWORD) | **seconds remaining** (not epoch) | +| `0x16b` | `itemData` | **nested OBJECT** | `0x18013fe00` | the card. **FREEZE-RISK: must be an object**, feeding a scalar desyncs the SAX reader (spin at `0x1801c7f1a`) | +| `0x2b6` | `sellerEstablished` | int | `0x1801c79d0`→`0x1800d7b30` | | +| `0x2b7` | `sellerName` | string | `0x1801c7aa0`→`0x180008120` | bounded copy, max 0x1e=30 chars | +| `0x2e6` | `startingBid` | int | `0x1801c79d0`→`0x1800d7b30` | | +| `0x2f4` | `coinsProcessed` | int/bool | `0x1801c79d0`→`0x1800d7b50` | truncated to byte | +| `0x331` | `tradeId` | int (64-bit) | `0x1801c79d0` (QWORD) | | +| `0x335` | `tradeState` | string (enum) | `0x1801c7aa0`→`0x180166bd0` | `active`/`closed`/`expired` | +| `0x380` | `watched` | bool | `0x1801c7620` | true if on watch list | + +Not present at record level: `bid` (0x55), `seller` id, `offers`. Extra keys are +skipped safely. + +Minimal known-good record: +```json +{ + "tradeId": 100000001, + "itemData": { "id": 100000001, "resourceId": 1610612736, "assetId": 20801, + "itemType": "player", "rating": 94, "preferredPosition": "ST", + "untradeable": false, "itemState": "free" }, + "tradeState": "active", + "buyNowPrice": 3000, + "startingBid": 1500, + "currentBid": 0, + "bidState": "none", + "expires": 3600, + "sellerName": "OpenFUT", + "sellerEstablished": 1, + "watched": false, + "coinsProcessed": 0 +} +``` + +### Shared IS-list body — `0x18013e7f0` (confidence: HIGH) + +| atom | key | type | getter | +|---|---|---|---| +| `0x35` | `auctionInfo` | **array of records** (`0x18013e410` in a loop) | FREEZE-RISK: must be array | +| `0xc0` | `credits` | int | `0x1801c79d0`→`0x1800d7af0`→object setter | +| `0xec` | `duplicateItemIdList` | nested (array) `0x180138e10` | FREEZE-RISK: must be array/obj | +| `0x325` | `total` | int | `0x1801c79d0`→`0x1800d7b30` (→ `obj+0x60`) | + +--- + +### The 12 response structs + +All 12 are **GAPs** — `utas_server.py` currently has **no** `auctionhouse` / `trade` +/ `tradePile` / `watchList` / `marketdata` routes. The nested `itemData` card is the +one piece already served (by the existing `/club` + `item_def` machinery in +`utas_server.py`), so it can be reused verbatim inside these responses. + +Path template `%s = "game/fifa17"`. Methods inferred from struct verb + endpoint. + +| # | Struct | Deser VA | Method + Path (inferred) | Schema | Conf | +|---|---|---|---|---|---| +| 1 | **FutISSearchServerResponse** | `0x180163420` → `0x18013e7f0` | `GET ut/%s/auctionhouse?...` (market search) | `{auctionInfo:[record], credits, total, duplicateItemIdList}` | HIGH | +| 2 | **FutISStartServerResponse** | `0x180165d70` (dispatch `…e98`) | `POST ut/%s/auctionhouse` (list item for sale) | `{id:int}` (new tradeId) — only atom `0x15c`=`id` | HIGH | +| 3 | **FutISViewTradeServerResponse** | `0x1801644d0` (rec call `…461f`) | `GET ut/%s/trade/{id}` (view one auction) | `{auctionInfo:[record], credits}` — atoms `0x35`,`0xc0` | HIGH | +| 4 | **FutISWatchListServerResponse** | `0x180166130` → `0x18013e7f0` | `GET ut/%s/watchList` | `{auctionInfo:[record], credits, total}` | HIGH | +| 5 | **FutISWatchTradeServerResponse** | `0x180164cd0` | `PUT ut/%s/watchList` (add to watch list) | ack; parses per-item status enum ladder (`0x28/0x14/0x11/0x24`, `-1`), no atom-keyed body → `{}` known-good | MED (partial) | +| 6 | **FutISOfferTradeServerResponse** | `0x180165410` (rec call `…56cf`) | `POST ut/%s/trade/{id}/bid` (place bid) | `{auctionInfo:[record], credits}` (echoes updated auction) | HIGH | +| 7 | **FutISRemoveTradeServerResponse** | `0x1801648d0` | `DELETE ut/delete/%s/trade/{id}` (clear from trade pile) | ack, no atom-keyed body → `{}` | MED | +| 8 | **FutISRemoveWatchServerResponse** | `0x1801659f0` | `DELETE ut/delete/%s/watchList/{id}` | ack, no atom-keyed body → `{}` | MED | +| 9 | **FutGetTradePileServerResponse** | `0x180170810` → `0x18013e7f0` | `GET ut/%s/tradePile` | `{auctionInfo:[record], credits, total}` | HIGH | +| 10 | **FutRelistAllServerResponse** | `0x180164210` (req-ser `0x180164370`) | `PUT ut/%s/auctionhouse/relist` (relist all expired) | ack; response body minimal → `{}` (request-side serializer builds a tradeId list) | MED (partial) | +| 11 | **FutGetAuctionCountServerResponse** | `0x180163670` (dispatch `…83e`) | `GET ut/%s/auctionhouse` count (or `marketdata`) | `{count, maxAuctionsAllowed, offered, selling, sold}` — atoms `0xbc,0x1bf,0x1e5,0x2b8,0x2c9`, all int | HIGH | +| 12 | **FutGetSuggestedPricingServerResponse** | `0x180163bb0` (dispatch `…ffa`) | `GET ut/%s/marketdata?defId=…` (price bands) | `{defId, minPrice, maxPrice}` — atoms `0xcf,0x1c2,0x1ca`, all int | HIGH | + +### Minimal known-good JSON per struct + +```jsonc +// 1 FutISSearch (GET auctionhouse) +{ "auctionInfo": [ ], "credits": 100000, "total": 1, "duplicateItemIdList": [] } +// 2 FutISStart (POST auctionhouse) +{ "id": 100000001 } +// 3 FutISViewTrade (GET trade/{id}) +{ "auctionInfo": [ ], "credits": 100000 } +// 4 FutISWatchList (GET watchList) +{ "auctionInfo": [ ], "credits": 100000, "total": 1 } +// 5 FutISWatchTrade (PUT watchList) -> {} +// 6 FutISOfferTrade (POST trade/{id}/bid) +{ "auctionInfo": [ ], "credits": 99000 } +// 7 FutISRemoveTrade (DELETE trade) -> {} +// 8 FutISRemoveWatch (DELETE watchList) -> {} +// 9 FutGetTradePile (GET tradePile) +{ "auctionInfo": [ ], "credits": 100000, "total": 1 } +// 10 FutRelistAll (PUT auctionhouse/relist) -> {} +// 11 FutGetAuctionCount +{ "count": 0, "maxAuctionsAllowed": 100, "offered": 0, "selling": 0, "sold": 0 } +// 12 FutGetSuggestedPricing +{ "defId": 1610612736, "minPrice": 900, "maxPrice": 10000 } +``` + +### Freeze-risk summary (type fidelity is mandatory) +- `auctionInfo` → **array** (never object/scalar). +- `itemData` inside each record → **object** (the card; reuse `item_def`). +- `duplicateItemIdList` → **array**. +- `bidState`, `tradeState`, `sellerName` → **strings**. +- `credits`, `total`, `count`, `*Price`, `*Bid`, `expires`, `tradeId` → **numbers**. +- `watched` → **bool**. +Any scalar fed where an object/array is expected desyncs the tokenizer → hard +busy-loop freeze at `0x1801c7f1a` (same failure mode documented for +`userMassInfo`/squad in CARD_SYSTEM.md). + +### Implementation notes for utas_server.py +- Add routes (all currently missing): `GET /auctionhouse` (search + count), + `POST /auctionhouse` (start), `PUT /auctionhouse/relist`, `GET/POST /trade`, + `GET /tradePile`, `GET/PUT /watchList`, `GET /marketdata`, + `DELETE (ut/delete)/trade`, `DELETE (ut/delete)/watchList`. +- Every list response shares one builder: `{auctionInfo, credits, total}`. +- `.itemData` = exactly the object returned by `item_def(rid)`. +- Ack endpoints (5,7,8,10) can safely return `{}` (bodies are non-keyed/ack). +## Squad Building Challenges (SBC) + +Reversed from FIFA17 `cardsdll.dll` (base `0x180000000`) using the deserializer-reversal +recipe: struct name → `.rdata` VA → `.text` xref (`lea r8`) → deserializer function → +atom `cmp`/`sub`/`dec` ladder + jump-table → `atoms.tsv` key names → leaf getter type. +Getter fingerprints: `0x1801c7aa0`=string, `0x1801c79d0`=int/number, `0x1801c7620`=bool, +`0x180008120`=fixed-buffer string-copy (string), `0x180135ff0`=value-SKIP (safe unknown key), +peek+loop via `0x1801c7f10`/`0x1801369f0`/`0x18015a750`=nested object/array. + +**Endpoint family:** `ut/%s/sbs` (`%s` = `game/fifa17`), base string @ fileoff `0x21d908`. +The baseline `utas_server.py` has **no `sbs*` routes at all** → every endpoint below is a **GAP**. + +### Path ↔ response bindings (proven via path-template `lea r8` adjacent to the response-struct `lea r8` in each request-builder) + +| Response struct | Method* | Path (under `ut/game/fifa17/`) | Builder site | +|---|---|---|---| +| FutSBCTagSetsServerResponse | POST/PUT | `sbs/sets/tag` | `0x180153fd1`→`0x18015405d` | +| FutLoadSetTypesServerResponse | GET | `sbs/challenge/%d/squad` | `0x1801545b9`→`0x18015469d` | +| FutSBCStartChallengeResponse | POST | `sbs/challenge/%d` | `0x1801552a9`→`0x18015531d` | +| FutSBCSubmitChallengeServerResponse | POST/PUT | `sbs/challenge/%d` (submit) | `0x1801618ff`→`0x18016196d` | +| FutSBCSetDataServerResponse | GET | `sbs/sets` (set-list / by category) | builder `0x18016fa7d` (no distinct path lea) | +| FutSBCLoadCategoryDetailsServerResponse | GET | `sbs/sets` | `0x18017a9a5`→`0x18017aa3d` | +| FutLoadSetChallengesResponse | GET | `sbs/setId/%d/challenges` | `0x18017b979`→`0x18017b9ed` | +| FutSBCSaveSquadChallengeServerResponse | PUT | `sbs/challenge/%d/squad` | `0x18017ce6f`→`0x18017cedd` | + +\*Method column is inferred from FUT16/17 REST conventions (builder method-enum not decoded); path bindings themselves are byte-proven. + +--- + +### 1. FutLoadSetChallengesResponse — CONFIDENCE: HIGH ✅ fully reversed +Deserializer dispatch @ **`0x18017bbbb`** (key-iter `call 0x180141ee0`); wrapper/ctor `0x18017b9ed`. +Parses an **array of flat SET+CHALLENGE records** (each record carries both set-level and +challenge-level fields). Low atoms via `cmp r8d`/`sub` ladder; high atoms (`0x280–0x354`) +via byte+dword jump table at `0x18017c334`/`0x18017c310` (decoded from binary). + +Method+path: **GET `ut/game/fifa17/sbs/setId/{setId}/challenges`** + +| key | atom | type | notes / struct offset | +|---|---|---|---| +| challengeId | 0x074 | int | `[rdi+0x34]` | +| categoryId | 0x073 | int | `[rdi+0x38]` | +| index | 0x163 | int | `[rdi+0x3c]` | +| setId | 0x2bc | int | `[rdi+0x3c]`-cluster | +| type | 0x2c4 | **string→enum** | string-compared: `"OPEN_CHALLENGE"`→0, `"BRICK_CHALLENGE"`→2 → `[rdi+0x40]` | +| name | 0x1d0 | string | `[rdi+0x44]`, max 0x7f | +| description | 0x0d1 | string | `[rdi+0xc3]`, max 0xff | +| challengeImageId | 0x075 | string | `[rdi+0x1c8]`, max 0x64 | +| formation | 0x12b | **string** | mapped via `0x180166590` → `[rdi+0x22c]`. **FREEZE-RISK: must be a JSON string, not int** | +| endTime | 0x106 | int (epoch s) | `[rdi+0x240]`, day-scaled (cmp 0x16d=365) | +| repeatable | 0x280 | bool | `[rdi+0x239]` | +| trophyId | 0x2ee | int | `[rdi+0x1c4]` | +| status | 0x30e | string | hash-mapped state | +| timesCompleted | 0x322 | int | | +| squadId | 0x2dc | int | | +| tutorial | 0x33e | (nested) | | +| **awards** | 0x047 | **nested ARRAY** | reward objects (see shared records). **FREEZE-RISK: must be array** | +| **elgReq** | 0x0f7 | **nested ARRAY** | the SBC requirement/constraint list (see shared records). **FREEZE-RISK: must be array** | + +All other high atoms (205 of them) route to the value-SKIP default → extra keys are safe. + +Minimal known-good: +```json +[{"challengeId":1,"setId":1,"categoryId":0,"index":0,"type":"OPEN_CHALLENGE", + "name":"League Basics","description":"Submit 11 players.","challengeImageId":"sbc_challenge_image_1", + "formation":"f442","endTime":0,"repeatable":false,"trophyId":0,"status":"OPEN", + "timesCompleted":0,"awards":[],"elgReq":[]}] +``` + +### 2. FutSBCSubmitChallengeServerResponse — CONFIDENCE: HIGH ✅ fully reversed +Dispatch @ **`0x180161bda`**; ctor `0x18016196d`. Method+path: **POST `sbs/challenge/{challengeId}`** (submit). + +| key | atom | type | +|---|---|---| +| challengeId | 0x074 | int | +| setId | 0x2bc | int | +| credits | 0x0c0 | int | +| preOrderPacks | 0x24b | int | +| recoveredPacks | 0x27b | int | +| grantedChallengeAwards | 0x14a | **nested array** (freeze-risk) | +| grantedSetAwards | 0x14b | **nested array** (freeze-risk) | + +```json +{"challengeId":1,"setId":1,"credits":500,"preOrderPacks":0,"recoveredPacks":0, + "grantedChallengeAwards":[],"grantedSetAwards":[]} +``` + +### 3. FutSBCStartChallengeResponse / squadChallenge record — CONFIDENCE: HIGH ✅ fully reversed +Dispatch @ **`0x180155949`**; ctor `0x18015531d`. Method+path: **POST `sbs/challenge/{challengeId}`**. +Returns the **squadChallenge** record (the working squad for a challenge). This same record +parser is shared by FutLoadSetTypesServerResponse. + +| key | atom | type | +|---|---|---| +| challengeId | 0x074 | int/nested | +| index | 0x163 | int | +| playerType | 0x23d | string | +| playerRequirements | 0x237 | **nested array** (per-slot constraint list; freeze-risk) | +| squad | 0x2cd | **nested array** of slot objects (freeze-risk) | + +Slot object = `{index:int, playerType:string, playerRequirements:[...]}`. + +```json +{"challengeId":1,"squad":[{"index":0,"playerType":"","playerRequirements":[]}]} +``` + +### 4. FutSBCLoadCategoryDetailsServerResponse — CONFIDENCE: HIGH ✅ fully reversed +Dispatch @ **`0x18017ac08`**; ctor `0x18017aa3d`. Method+path: **GET `sbs/sets`**. + +| key | atom | type | +|---|---|---| +| categoryId | 0x073 | int | +| name | 0x1d0 | string | +| priority | 0x250 | int | +| sets | 0x2be | **nested array** of set records (freeze-risk) | + +```json +{"categoryId":0,"name":"Challenges","priority":0,"sets":[]} +``` + +### 5. FutSBCSaveSquadChallengeServerResponse — CONFIDENCE: MEDIUM-HIGH ✅ fully reversed (minimal) +Dispatch @ **`0x18017d08a`**; ctor `0x18017cedd`. Method+path: **PUT `sbs/challenge/{challengeId}/squad`**. +Only one scalar field parsed: + +| key | atom | type | +|---|---|---| +| id | 0x15c | int (saved squad id) | + +```json +{"id":1} +``` + +### 6. FutLoadSetTypesServerResponse — CONFIDENCE: MEDIUM ⚠️ partial +Dispatch @ **`0x180154d69`**; ctor `0x18015469d`. Method+path: **GET `sbs/challenge/{challengeId}/squad`**. +Parses an **array of squadChallenge templates** ("set types" = squad-building slot templates), +same key set as the squadChallenge record: `{challengeId(0x074), index(0x163), playerType(0x23d str), +playerRequirements(0x237 nested), squad(0x2cd nested)}`. Outer top-level wrapper (array framing at +`0x180154a2d`) is callback-driven; exact top-level envelope key not resolved. + +```json +[{"challengeId":1,"index":0,"playerType":"","playerRequirements":[],"squad":[]}] +``` + +### 7. FutSBCSetDataServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial +Dispatch @ **`0x18016ff2a`**; ctor `0x18016fa7d`. Method+path: **GET `sbs/sets`** (set-data / by category). +Top-level is a **callback array-parser** (`lea r8,[rsi+0x50]` vector-append at `0x18016fb5e`) +that appends **set records** parsed by the shared flat set-record deserializer; only one +top-level scalar was resolved: + +| key | atom | type | +|---|---|---| +| reset | 0x283 | bool | +| (set records) | — | **nested array**; each element has the SET-level fields (setId, name, description, awards, repeatable, endTime, challenges, starRating, setImageId, timesCompleted, sortPriority) — reuse struct #1's field vocabulary | + +```json +{"reset":false,"sets":[{"setId":1,"name":"League Basics","description":"", + "repeatable":false,"endTime":0,"awards":[],"challenges":[]}]} +``` + +### 8. FutSBCTagSetsServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial +Ctor `0x18015405d`; deserializer is a **callback-based array parser** (installs per-element +handler `0x180154280`, vector-append) rather than an atom `cmp` ladder — no scalar keys resolved +from the dispatch. Semantically it acknowledges a set-tag operation and returns the updated +tagged sets. Method+path: **POST/PUT `sbs/sets/tag`**. + +```json +{"sets":[]} +``` + +--- + +### Shared record shapes + +**elgReq — the SBC requirement/constraint list** (the tricky part; parsed in struct #1 @ `0x18017bd9d`, +vector at `[rdi+0x358]`). Array of constraint objects, inner dispatch @ `0x18017bde0`: + +| key | atom | type | +|---|---|---| +| eligibilityKey | 0x0f2 | int (constraint selector) | +| eligibilitySlot | 0x0f4 | int/bool | +| eligibilityValue | 0x0f5 | int (target value) | +| eligibilityOperation | 0x0f3 | int (comparator; skip-handled here) | +| eligibilities | 0x0f1 | (container atom) | + +Chemistry/rating/nation/league-count constraints (`teamChemistry 0x307`, `starRating 0x2e2`, +`sameNationCount 0x297`, `sameLeagueCount 0x296`, `sameClubCount 0x295`, `nationCount 0x1d4`, +`leagueCount 0x18c`, `clubCount 0x8b`, `playerCount 0x22f`, `chemistry 0x81`) are encoded +generically as `{eligibilityKey, eligibilityOperation, eligibilityValue}` triples, **not** as +named scalar fields on the record. **FREEZE-RISK: elgReq must be a JSON array of objects.** + +**awards / grantedAwards** — nested array of reward objects (atoms: `rewardType 0x28e`, +`rewardValue 0x28f`, `rewardQuantity 0x28d`, `rewardMultiplier 0x28c`, `awardCount 0x40`, +`awardSet 0x45`, `awardSetId 0x46`, `prizeSet 0x253`). **FREEZE-RISK: must be array.** + +**SET-level field vocabulary** (available atoms for a full set record): +`setId 0x2bc`, `name 0x1d0`, `description 0x0d1`, `challenges 0x76`, `challengesCount 0x78`, +`challengesCompletedCount 0x77`, `repeatable 0x280`, `endTime 0x106`, `awardSet 0x45`, +`setImageId 0x2bd`, `starRating 0x2e2`, `sortPriority 0x2cb`, `timesCompleted 0x322`, +`categoryId 0x73`, `priority 0x250`. + +### Freeze-risk summary (type fidelity mandatory) +- `formation` = **string** (e.g. `"f442"`), never int. +- `awards`, `elgReq`, `squad`, `playerRequirements`, `grantedChallengeAwards`, + `grantedSetAwards`, `sets`, `challenges` = **arrays/objects**, never scalar. + Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer spin + (freeze at `0x1801c7f1a`). +- Unknown/extra keys are safe (value-SKIP handler `0x180135ff0`). +## FUT Draft + +Clean-room reverse of the FIFA 17 CardsDLL (base `0x180000000`) FUT **Draft** response +deserializers. Method: located each `RS4:Fut*DraftServerResponse` name string, computed its +`.rdata` VA, found the `.text` factory (`lea r8,[name]`), then read the adjacent JSON +deserializer's atom dispatch (FNV key → atom → getter). Atoms translated via `atoms.tsv`. +**All 7 endpoints are a total GAP** — `tools/utas_server.py` has zero `draft` routes today. + +Endpoint family: `ut/%s/draft/mode` (+ sub-paths below), `%s = "game/fifa17"`. + +Shared sub-deserializers used by Draft: +- `0x18013fe00` — ITEM/card element deser (a full player card, same schema as club/squad `itemData`; see CARD_SYSTEM.md). **Must be a JSON object.** +- `0x18013d1f0` — SQUAD deser (the whole squad model: `formation`, `players[].itemData`, `manager`, `custom`, `kicktakers`…; identical to `GET /squad/0` LoadActiveSquad). **Must be a JSON object.** + +Leaf getters: int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`; unknown-key SKIP `0x180135ff0` (extra keys are safe). Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer freeze — so every field flagged **[freeze-risk]** below MUST be emitted as the right container type (or omitted entirely). + +--- + +### 1. FutGetDraftChoicesServerResponse — CONFIDENCE: HIGH (fully reversed, incl. deep choice record) +- Struct name `.rdata` `0x180225ea0`; factory `0x18014f250`; **deserializer `0x18014f2d0`**. +- Method/Path: **GET** `ut/game/fifa17/{champId}/draft` (path fragment `"/%d/draft"` @0x2252c8). Returns the choices offered for the current pick. +- Top-level keys (ordered): + | key | atom | type | notes | + |---|---|---|---| + | `choices` | 0x83 | **array** [freeze-risk] | array of choice records (below) → vec@+0x30 | + | `positionid` | 0x244 | int | @+0x28 | + | `tier` | 0x315 | int | @+0x2c | +- **Choice record** (each element of `choices`, element size 0x20; sub-dispatch @0x18014f44c): + | key | atom | type | notes | + |---|---|---|---| + | `formation` | 0x12b | **string** | formation-choice rounds (e.g. `"f442"`); parsed str→id | + | `index` | 0x163 | int (byte) | choice slot index 0..n → byte@+0x28 | + | `itemData` | 0x16b | **object** [freeze-risk] | the offered player card, parsed by ITEM deser `0x18013fe00` | + So a pick round is either a **formation** round (choices carry `formation`) or a **player** round (choices carry `itemData` + `index`), with the round's slot given by top-level `positionid` and `tier`. +- MINIMAL known-good (player round, 5 choices for one position): +```json +{"positionid":0,"tier":1, + "choices":[ + {"index":0,"itemData":{"id":1000001,"resourceId":20801,"assetId":20801,"itemType":"player","rating":94,"preferredPosition":"ST","nation":38,"teamid":243,"leagueId":53,"rareflag":1,"untradeable":true,"attributeList":[{"index":0,"value":90}],"itemState":"free","owners":1,"contract":7,"fitness":99}} + ]} +``` + Formation round: `{"positionid":0,"tier":1,"choices":[{"index":0,"formation":"f442"},{"index":1,"formation":"f433"}]}`. + +--- + +### 2. FutGetDraftCurrentStateServerResponse — CONFIDENCE: HIGH (fully reversed, incl. roundsInfo element + squad delegation) +- Struct name `0x180224200`; factory `0x180146cc0`; **deserializer `0x180147070`** (begin-obj @0x1801470f2, dispatch @0x18014715c). +- Method/Path: **GET** `ut/game/fifa17/draft/state?mode=ONLINE` | `?mode=SINGLE_PLAYER` (fragments @0x223630/0x223650). +- Top-level keys: + | key | atom | type | notes | + |---|---|---|---| + | `squad` | 0x2cd | **object** [freeze-risk] | drafted squad, SQUAD deser `0x18013d1f0` | + | `entranceCriteria` | 0x108 | **object** [freeze-risk] | nested object (safe to omit) | + | `gamesWonCurrentMatch` | 0x13b | int | @+0x60 | + | `roundsInfo` | 0x293 | **array** [freeze-risk] | array of round records (below), elem deser `0x180146eb0`, elem size 0x20 → vec@+0x80 | + | `squadState` | 0x2d5 | string | e.g. `"DRAFTSQUAD_ON"` | + | `stateParam1` | 0x2ee | string→enum | @+0x5c (default 5) | + | `stateParam2` | 0x2ef | string→int | @+0x64 | +- **Round record** (`roundsInfo[]`, all scalar; deser `0x180146eb0`): + `round`(0x290,int@+0xc), `score`(0x29a,int@+0x18), `opponentScore`(0x200,int@+0x14), `penaltyScore`(0x217,int@+0x10), `opponentPenaltyScore`(0x1fd,int@+0x1c), `opponentId`(0x1fc,int/long@+0x0), `difficulty`(0xd4,string enum@+0x8). +- MINIMAL known-good (fresh single-player draft, round 0, empty squad shell): +```json +{"squadState":"DRAFTSQUAD_ON","stateParam1":"","stateParam2":"0","gamesWonCurrentMatch":0, + "roundsInfo":[], + "squad":{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]}} +``` + (Populate `squad.players[].itemData` with cards from the pick choices as the draft is built; `roundsInfo` grows one record per completed match. `entranceCriteria` omitted = safe.) + +--- + +### 3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk) +- Struct name `0x180226540`; factory `0x18015076d`; **deserializer `0x1801508c0`** (begin-obj @0x18015093b, dispatch @0x1801509bc). +- Method/Path: **GET** `ut/game/fifa17/draft/mode` (GetDraftStats RPC; historical/aggregate draft stats). +- Keys (all int, except `draftChampion` bool): + `gamesWon`(0x13a), `gamesLost`(0x138), `scoredGoals`(0x29b), `concededGoals`(0xa1), `bestBuilderScore`(0x52), `draftChampion`(0xe1, bool), `draftsCompleted`(0xe2), `passAccuracyTotal`(0x212), `possessionPercentage`(0x247), `possessionTotal`(0x249). +- MINIMAL known-good: +```json +{"gamesWon":0,"gamesLost":0,"scoredGoals":0,"concededGoals":0,"bestBuilderScore":0,"draftChampion":false,"draftsCompleted":0,"passAccuracyTotal":0,"possessionPercentage":0,"possessionTotal":0} +``` + +--- + +### 4. FutGetDraftAwardServerResponse — CONFIDENCE: HIGH (fully reversed) +- Struct name `0x1802266f8`; factory `0x18015106c`; **deserializer `0x1801510c0`** (begin-obj @0x180151129, dispatch @0x1801511ac). +- Method/Path: **GET/POST** `ut/game/fifa17/draft/mode` (GetDraftAward — claim/return the draft prize). +- Keys: + | key | atom | type | notes | + |---|---|---|---| + | `item` | 0x16a | **array** [freeze-risk] | awarded item cards, ITEM deser `0x18013fe00` (loops) | + | `halId` | 0x150 | int | @ (prize/hal id) | + | `type` | 0x354 | int | prize type code | + | `value` | 0x377 | int | prize value (e.g. coins) | +- MINIMAL known-good (coins-only prize, no items): +```json +{"type":1,"value":15000,"halId":0,"item":[]} +``` + With an item prize: `"item":[{ …full card object as in itemData… }]`. + +--- + +### 5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred) +- Struct name `0x180226078`; factory `0x18014fba0`. The factory installs the **generic base ServerResponse vtable `0x18022cb58`** (shared by dozens of structs) — i.e. **no struct-specific deserializer / no parsed fields**. The neighboring `0x18014fcb0` is the *request* serializer, not a response reader. +- Method/Path: **PUT/POST** `ut/game/fifa17/{champId}/draft/choose` and `ut/game/fifa17/draft/choose/difficulty` (fragments @0x2254c0 / 0x2254a0). Commits one pick (or the difficulty choice). +- Body: an empty ack — the client re-reads state via GetDraftCurrentState / GetDraftChoices afterward. +- MINIMAL known-good: `{}` + +--- + +### 6. FutPickDraftAutoChoiceServerResponse — CONFIDENCE: HIGH (delegates to squad deser) +- Struct name `0x180226208`; factory `0x18014fd80`; **deserializer `0x18014fdf0`**. The body is parsed by a single call to the SQUAD deser `0x18013d1f0` (@0x18014fe82) into the struct's squad member. +- Method/Path: **POST** `ut/game/fifa17/{champId}/draft/autocomplete` (fragment @0x225638). Auto-fills the remaining picks and returns the completed squad. +- Body: a **squad object** [freeze-risk] (same schema as `GET /squad/0`). +- MINIMAL known-good: +```json +{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]} +``` + (Emit a fully-populated `players[].itemData` for a real auto-drafted XI.) + +--- + +### 7. FutPurchaseDraftModeServerResponse — CONFIDENCE: MEDIUM-HIGH (field set fully reversed; response-variant ambiguity) +- Struct name `0x180224fb0`; factory `0x18014c0bd`; **deserializer `0x18014c260`** (begin-obj @0x18014c2d5, dispatch @0x18014c3bb). +- Method/Path: **POST** `ut/game/fifa17/purchase/mode/{price}/draft` (fragment @0x2257a8). Buys entry into draft mode; returns the fresh draft session summary. +- Keys (all scalar int): + `championEventId`(0x7b), `expectedTierLevel`(0x115), `gamesPlayed`(0x139), `gamesRemaining`(0x13c), `rank`(0x26d), `score`(0x29a), `tierLevel`(0x317). +- Note: a **second, larger struct (size 0x38, deser @0x180150379, factory @0x18015028d)** also references this name string. It carries no visible scalar field ladder and is likely an alternate/summary envelope; the `0x18014c260` field-parser above is treated as authoritative. Confirming which body the live client reads is the remaining gap. +- MINIMAL known-good: +```json +{"championEventId":0,"expectedTierLevel":1,"gamesPlayed":0,"gamesRemaining":4,"rank":0,"score":0,"tierLevel":1} +``` + +--- + +### Implementation notes for utas_server.py +- Add routes under `G + r"/draft"`: `GET …/draft/state`, `GET …/{id}/draft`, `POST …/{id}/draft/choose`, `POST …/draft/choose/difficulty`, `POST …/{id}/draft/autocomplete`, `POST …/purchase/mode/{n}/draft`, plus GetDraftStats / GetDraftAward on `…/draft/mode`. +- Reuse the existing squad/item JSON builders (`fut_seed.player_item`, `_base_squad`) for `squad`, `itemData`, `item[]` — those objects are already known-good through deser `0x18013d1f0` / `0x18013fe00`. +- Freeze-risk containers to never send as scalars: `choices`, `choices[].itemData`, `squad`, `entranceCriteria`, `roundsInfo`, `item`, and the PickAutoChoice squad body. +## Match / Seasons / Tournaments + +Reversed from `cardsdll.dll` (base `0x180000000`) per RECIPE.md. CardsDLL base `0x180000000`. +Parser shared internals: each JSON key → FNV-1a → atom int; deserializer dispatches on atom via +`cmp/sub/dec + je` binary tree. **Unknown atoms are routed to the container-aware SKIP handler +`0x180135ff0` — extra keys are SAFE.** Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`, +str `0x1801c7aa0`. Nested object/array = `call` to a sub-deserializer or array loop. Feeding a +scalar getter an object/array desyncs the SAX reader → infinite spin freeze at `0x1801c7f1a`. + +**SAX token-type constants seen in every loop (NOT keys, ignore):** atom 6 (`=` end-object token), +atom 10 (end-array/container token). Atom values > 906 in a raw dump are jump-table offsets, not atoms. + +**Key-spelling caveat:** the wire key is FNV-hashed, so JSON keys must match EA's *exact* spelling +incl. typos — notably `boostConis` (atom 96, not "boostCoins") and `matchCoinMultipliers`. + +### Two response families found +1. **Bespoke deserializer** (custom atom switch) — most Season/Match/Tournament *load/list* responses. +2. **Ack-only / base-response** — `FutMatchReady`, `FutPlayGame`, `FutUpdateSeason`, + `FutUpdateFriendlySeason`, `FutGetStoryModeReward`. Their factory allocates a 0x28-byte object and + calls **only the base ServerResponse constructor `0x18011f850`** (sets vtable `0x18022cb58`, + timeout `0x7530`, status `-1`). They parse **NO** body fields → the client accepts any body incl. + `{}`. **The match reward/coins are NOT in FutPlayGame — they are carried by FutDestroyMatch.** + +### HTTP method/path note +Endpoint path strings live in an `.rdata` string-pool (pointer table at fileoff `0x21d480`), not +lea-referenced from code, and the request-descriptor table carries the verb in a parallel array that +is not statically recoverable here. **Methods below are inferred from REST/UTAS semantics — confidence +LOW on method, HIGH on struct fields.** Path templates (verified in binary, `%s`="game/fifa17"): +`ut/%s/match`, `ut/%s/season`, `ut/%s/season/user`, `ut/%s/season/%s/user`, `ut/%s/season/%s/reset`, +`ut/%s/season/friendly`, `ut/%s/tournament`, `ut/%s/tournament/user`, `ut/delete/%s/tournament/user`, +`/season/user/history`. + +### Baseline (utas_server.py) status +All of these are currently **GAP / stubbed**: `/season` → `(200, {})`, `/match/keepalive` → `(204)`. +No match/season/tournament body is currently reversed in the baseline. + +--- + +## HIGHEST VALUE — MATCH RESULT / REWARD RECORD + +### FutDestroyMatchServerResponse — the post-match coin/credit reward record ★CONFIDENCE: HIGH +- **Deserializer VA:** `0x180121b60` (factory `0x180121700`, name-lea `0x18012170c`) +- **Method+path (inferred):** `DELETE ut/game/fifa17/match/{matchId}` — closes the match and returns + the credited rewards. This is where a completed match reports its coin/XP award. **Score/win-loss are + NOT here — the client SENDS the result in the request; the server RESPONDS with the coins.** +- **Ordered reward fields (all int unless noted; offsets in the parsed struct):** + + | atom | key | type | offset | notes | + |---|---|---|---|---| + | 20 | `allCoins` | int | 0x28 | new total coin balance | + | 436 | `matchCoins` | int | 0x2c | coins awarded for this match | + | 809 | `tournamentCoins` | int | 0x30 | | + | 776 | `teamOfTournamentWinner` | bool | 0x34 | | + | 670 | `seasonCoins` | int | 0x38 | | + | 149 | `coins` | int | 0x3c | reward amount (this txn) | + | 529 | `participationAward` | int | 0x44 | | + | 96 | `boostConis` | int | 0x48 | (EA typo — exact key) | + | 617 | `qualifiedChampionEventId` | int | 0xb0 | | + | 310 | `gameModeAward` | nested | — | object, SKIP-safe | + | 437 | `matchCoinMultipliers` | nested | — | array/object, SKIP-safe | + | 619/805/852/887 | `quantity`/`total`/`type`/`value` | nested item | — | reward-item sub-object fields (prize list element; `type` is str) | + | 877 | `userData` | nested | via `0x180142470` | user snapshot object (FREEZE-RISK: must be object) | + +- **Coin fields cluster tightly at 0x28–0x48**, confirming a coherent credits struct. All top-level + coin fields are scalar int → safe. +- **MINIMAL known-good JSON** (scalars only, zero freeze risk): +```json +{ + "coins": 400, + "allCoins": 15400, + "matchCoins": 400, + "seasonCoins": 0, + "tournamentCoins": 0, + "boostConis": 0, + "participationAward": 0, + "qualifiedChampionEventId": 0, + "teamOfTournamentWinner": false +} +``` +- **GAP** (baseline has no match reward body). + +--- + +## SINGLE-PLAYER PLAYABLE LOOP + +### FutCreateMatchServerResponse ★CONFIDENCE: HIGH +- **Deserializer VA:** `0x180120380` (name-lea `0x18011ffbd`) +- **Method+path (inferred):** `POST ut/game/fifa17/match` — creates the match, returns match/squad info. +- **Fields:** + | atom | key | type | offset | notes | + |---|---|---|---|---| + | 740 | `startDateTime` | int | 0x28 | epoch | + | 641 | `reportIdEnabled`| bool | — | | + | 717 | `squad` | nested (`0x18011a830`) | — | squad/ITEM array — FREEZE-RISK (must be array) | +- **MINIMAL JSON:** `{"startDateTime": 1580000000, "reportIdEnabled": false}` (omit `squad` — SKIP-safe) +- **GAP.** + +### FutMatchReadyServerResponse ★CONFIDENCE: HIGH (ack-only) +- **Deserializer:** none — factory `0x180120810` → base ctor `0x18011f850` only (name `0x18021d380`). +- **Method+path (inferred):** `PUT ut/game/fifa17/match/{matchId}` (mark ready). +- Parses **no** fields. **MINIMAL JSON:** `{}`. **GAP.** + +### FutPlayGameServerResponse ★CONFIDENCE: HIGH (ack-only) +- **Deserializer:** none — factory `0x180162170` → base ctor `0x18011f850` only (name `0x180228050`). +- **Method+path (inferred):** `POST ut/game/fifa17/match/{matchId}` (submit game result; body carries + score/stats client→server). Response body is ignored by the client. **Rewards arrive via DestroyMatch.** +- **MINIMAL JSON:** `{}`. **GAP.** + +### FutResetMatchServerResponse ★CONFIDENCE: HIGH +- **Deserializer VA:** `0x18016fd10` (name-lea `0x18016fccd`) +- **Method+path (inferred):** `POST ut/game/fifa17/season/{seasonId}/reset`. +- **Fields:** atom 643 `reset` = **bool** @0x28. +- **MINIMAL JSON:** `{"reset": true}`. **GAP.** + +### FutSeasonListServerResponse ★CONFIDENCE: HIGH +- **Deserializer VA:** `0x180167740` (name-lea `0x18016754d`) +- **Method+path (inferred):** `GET ut/game/fifa17/season` — list of available seasons/divisions + (array of season-descriptor objects; root container via `0x1800d84e0`). +- **Element fields:** + | atom | key | type | notes | + |---|---|---|---| + | 348 | `id` | int | season/division id | + | 220 | `divisionId` | int | | + | 242 | `eligibilityKey` | int | | + | 244 | `eligibilitySlot` | int | | + | 245 | `eligibilityValue` | int | | + | 246 | `elgOperation` | str | | + | 247 | `elgReq` | nested | SKIP-safe | + | 595 | `prizeSet` | nested | array — FREEZE-RISK | +- **MINIMAL JSON** (list root; per element scalars): +```json +[{"id":1,"divisionId":10,"eligibilityKey":0,"eligibilitySlot":0,"eligibilityValue":0,"elgOperation":""}] +``` +- **GAP.** (Note: root JSON shape is an array/object wrapper — verify container before shipping.) + +### FutSeasonLoadDataServerResponse ★CONFIDENCE: HIGH (fully traced switch) +- **Deserializer VA:** `0x180131450` (name-lea `0x18013141c`) +- **Method+path (inferred):** `GET ut/game/fifa17/season/user` — load the user's current season state. +- **Fields (verified from switch at `0x18013153c`):** + | atom | key | type | offset | notes | + |---|---|---|---|---| + | 674 | `seasonId` | int | 0x5c | | + | 220 | `divisionId` | int | 0x58 | (as int here, not str) | + | 656 | `round` | int | 0x60 | current round | + | 881 | `userPoints` | int | 0x64 | points/progress | + | 202 | `dataVersion` | str | 0x68 (enum byte) | | + | 201 | `data` | str | interned via `0x1801c7d40` | large opaque season-state blob string | + | — | root/`friendlySeasonHistory` | nested | via `0x180136880` | container — FREEZE-RISK | +- Unknown atoms → SKIP `0x180135ff0` (safe). +- **MINIMAL JSON:** +```json +{"seasonId":1,"divisionId":10,"round":1,"userPoints":0,"dataVersion":"1","data":""} +``` +- **GAP.** + +### FutUpdateSeasonServerResponse ★CONFIDENCE: HIGH (ack-only) +- **Deserializer:** none — factory `0x180168b10` → base ctor `0x18011f850` only (name `0x18022a748`). +- **Method+path (inferred):** `PUT/POST ut/game/fifa17/season/user` (submit season match result). +- **MINIMAL JSON:** `{}`. **GAP.** + +### FutSeasonQuitServerResponse ★CONFIDENCE: MEDIUM +- **Deserializer VA:** `0x180131a30` (name-lea `0x18013197d`) +- **Method+path (inferred):** `DELETE ut/delete/game/fifa17/season/user` (quit current season). +- **Fields:** atom 490 `offlineDivision` = nested (`0x18011a830`) — division-descriptor object. +- **MINIMAL JSON:** `{}` (offlineDivision optional/SKIP-safe; if included must be an object). **GAP.** + +--- + +## SECONDARY (best-effort catalog) + +### FutUpdateFriendlySeasonServerResponse ★CONFIDENCE: HIGH (ack-only) +- Factory → base ctor `0x18011f850` only (name `0x18022a840`). Method (inferred): `PUT ut/game/fifa17/season/friendly`. + Parses no fields. **MINIMAL JSON:** `{}`. **GAP.** + +### FutGetFriendlyHistoryDataServerResponse ★CONFIDENCE: MEDIUM +- **Deserializer VA:** `0x18014d570` (name-lea `0x18014d36d`). Path: `GET ut/game/fifa17/season/friendly` history. +- Fields: atom 675 `seasonGamesDraw` int @0x24; atom 151 `coinsEarned` int; (siblings `seasonGamesWon/Lost` + in same offset cluster likely present via SKIP). **MINIMAL JSON:** `{"seasonGamesDraw":0,"coinsEarned":0}`. **GAP.** + +### FutTournamentListServerResponse ★CONFIDENCE: MEDIUM +- **Deserializer VA:** `0x180169ef0` (name-lea `0x180169d7d`). Path (inferred): `GET ut/game/fifa17/tournament`. +- Clean scalar fields: atom 348 `id` int, 212 `difficulty` int, 149 `coins` int, 652 `rewardMultiplier` int, + 19 `aigroup` int, 37 `assetName` str, 243 `eligibilityOperation` str. Many nested (rounds, prizeSet, staff, + stadiumid, kit atoms 13/16 — treat as FREEZE-RISK objects, SKIP-safe if omitted). +- **MINIMAL JSON (array root):** +```json +[{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}] +``` +- **GAP.** + +### FutTournamentLoadDataServerResponse ★CONFIDENCE: MEDIUM +- **Deserializer VA:** `0x180147cb0` (name-lea `0x180147c7c`). Path (inferred): `GET ut/game/fifa17/tournament/user`. +- Fields: atom 202 `dataVersion` str, atom 656 `round` int, atom 810 `tournamentData` (str/opaque blob, + same interned-getter `0x1801c7d40` as season `data`), atom 92 `bonus` nested. Mirrors SeasonLoadData shape. +- **MINIMAL JSON:** `{"round":1,"dataVersion":"1","tournamentData":""}`. **GAP.** + +### FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse ★CONFIDENCE: LOW +- Both resolve to deserializer `0x18016bcf0` (names `0x18022af10` / `0x18022b068`) — near-empty switch; + only nested/opaque atoms seen (teamId, activeBadge). Likely thin/ack-ish. Paths (inferred): + Quit = `DELETE ut/delete/game/fifa17/tournament/user`; Teams = `GET ut/game/fifa17/tournament/user`. + **MINIMAL JSON:** `{}` (or `[]` for Teams if array-rooted). **GAP.** + +### FutUpdateTournamentServerResponse ★CONFIDENCE: LOW +- **Deserializer VA:** `0x1801758c0` (name-lea `0x18017580d`). Only enum/opaque atoms + (LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES/SUCCESS) — a status-enum response. + Path (inferred): `PUT/POST ut/game/fifa17/tournament/user`. **MINIMAL JSON:** `{}`. **GAP.** + +### FutGetActiveTournamentsServerResponse ★CONFIDENCE: LOW +- **Deserializer VA:** `0x18016b660` (name-lea `0x18016b55d`). Atoms `tournamentId`(811), `awardCount`(64) + seen but resolve nested/opaque. Path (inferred): `GET ut/game/fifa17/tournament`. Likely array of active + tournament ids. **MINIMAL JSON:** `[]` or `{}`. **GAP.** + +### FutGetHistoricalServerResponse ★CONFIDENCE: MEDIUM +- **Deserializer VA:** `0x180172930` (name-lea `0x18017279c`). Path (inferred): `GET ut/game/fifa17/season/user/history`. +- Fields: atom 336 `halId` int @0xc; atom 363 `itemData` = **ITEM** (shared card/item deser `0x18013fe00`) + → FREEZE-RISK (array of item cards); atom 653 `rewardQuantity` int @0x8; atom 654 `rewardType` int; + atom 655 `rewardValue` int; atom 656 `round` nested. +- **MINIMAL JSON:** `{"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0}` (omit itemData or send `[]`). **GAP.** + +### FutGetStoryModeRewardServerResponse ★CONFIDENCE: HIGH (ack-only) +- Factory → base ctor `0x18011f850` only (name `0x18022bc30`). Parses no fields. **MINIMAL JSON:** `{}`. **GAP.** + +--- + +### Endpoint → response cross-reference (summary) +| Method(inf) | Path | Response struct | Reversal | +|---|---|---|---| +| POST | ut/%s/match | FutCreateMatchServerResponse | FULL | +| PUT | ut/%s/match/{id} | FutMatchReadyServerResponse | FULL (ack) | +| POST | ut/%s/match/{id} | FutPlayGameServerResponse | FULL (ack) | +| DELETE | ut/%s/match/{id} | **FutDestroyMatchServerResponse (rewards)** | FULL | +| POST | ut/%s/season/{id}/reset | FutResetMatchServerResponse | FULL | +| GET | ut/%s/season | FutSeasonListServerResponse | FULL | +| GET | ut/%s/season/user | FutSeasonLoadDataServerResponse | FULL | +| PUT | ut/%s/season/user | FutUpdateSeasonServerResponse | FULL (ack) | +| DELETE | ut/delete/%s/season/user | FutSeasonQuitServerResponse | PARTIAL | +| PUT | ut/%s/season/friendly | FutUpdateFriendlySeasonServerResponse | FULL (ack) | +| GET | ut/%s/season/friendly (hist) | FutGetFriendlyHistoryDataServerResponse | PARTIAL | +| GET | ut/%s/season/user/history | FutGetHistoricalServerResponse | PARTIAL | +| GET | ut/%s/tournament | FutTournamentListServerResponse | PARTIAL | +| GET | ut/%s/tournament/user | FutTournamentLoadDataServerResponse | PARTIAL | +| PUT | ut/%s/tournament/user | FutUpdateTournamentServerResponse | PARTIAL | +| GET | ut/%s/tournament | FutGetActiveTournamentsServerResponse | PARTIAL | +| GET | ut/%s/tournament/user | FutGetTournamentTeamsServerResponse | PARTIAL | +| DELETE | ut/delete/%s/tournament/user | FutTournamentQuitServerResponse | PARTIAL | +| — | (story reward grant) | FutGetStoryModeRewardServerResponse | FULL (ack) | +## Club / Cards / Consumables + +Clean-room RE of the FIFA17 CardsDLL (base `0x180000000`). Method: each `FutXServerResponse` +class registers a vtable; **vtable slot `+0x08` is the JSON deserializer**. Located via +struct-name string → `VA = 0x1801e5000 + (fileoff - 0x1e4400)` → factory `lea r8` xref → +vtable install → slot `+0x08`. Every deserializer shares the same skeleton: + +``` +scratch init (0x1801c63e0/0x180008130/0x1801c8270) → NextToken×2 → +get target model (0x18011a830) → key-loop { KEYRD 0x180141ee0 → atom → dispatch } → +unknown atom → SKIP 0x180135ff0 (container-aware, extra keys are SAFE) +``` + +**KEYRD `0x180141ee0`** = read next key, FNV-hash (`0x180180d00`) → atom int, advance to value. +Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`. +Shared **ITEM/card element** deserializer = **`0x18013fe00`** (see `CARD_SYSTEM.md`; produces the +0x100-byte resolved-card record). **TYPE FIDELITY IS MANDATORY**: an array/object key fed to a +scalar getter desyncs the SAX reader → infinite tokenizer spin (freeze at `0x1801c7f1a`). Arrays +are flagged **[FREEZE-RISK]** below — they must be emitted as JSON arrays, never scalars. + +### Key structural finding (contradicts the initial brief) +`FutGetClubInfoServerResponse` does **NOT** return `itemData`/card items. Its only recognized +top-level key is **`user`** (atom `0x36c`), an array of **club-user summary records** (element +parser `0x18012c990`, which never calls the card element `0x18013fe00`). The card list that +actually renders in the club UI is **`FutViewCardsServerResponse`** (`itemData`, via +`0x18013fe00`) served on `ut/%s/item`. So "club-wide item search → cards" is ViewCards on `/item`, +while GetClubInfo/GetClubUsers carry club-user stat records. `data/` mental model of GetClubInfo = +{itemData,count,actives} is wrong for this binary. Actives (homekit/awaykit/badge/etc.) appear as +**fields inside each user record**, not as top-level keys. + +--- + +### Per-struct table + +Legend: method/path from the `ut/%s/...` templates in the binary (`%s="game/fifa17"`). +GAP = not (correctly) served by `utas_server.py`. All unknown keys are SKIP-safe, so `{}` never +freezes any of these — GAPs are "feature missing", not "crash". + +| # | Struct | Deser VA | Method + Path | Top-level keys (atom → type) | Status | Conf | +|---|--------|----------|---------------|------------------------------|--------|------| +| 1 | FutGetClubInfo | `0x18012d280` | GET `ut/%s/club` | `user`(0x36c) → **array[user-record]** [FREEZE-RISK] | GAP (utas serves `itemData`, which is SKIP'd here → empty user list) | deser HIGH / element PARTIAL | +| 2 | FutGetClubUsers | `0x180145c00` | GET `ut/%s/clubUser` | `user`(0x36c) → **array[user-record]** (elem `0x180145480`) [FREEZE-RISK] | GAP (utas `/clubUser` → `{}`) | deser HIGH / element PARTIAL | +| 3 | FutChangeClubName | `0x1801642c0` | PUT `ut/%s/club` (changeClubName) | **none** (deser is immediate `ret`) | ack — any parseable JSON works; `{}` fine | HIGH | +| 4 | FutViewCards | `0x1801293d0` | GET `ut/%s/item` | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | HANDLED (utas `/item` `defs_route` serves `itemData`) | HIGH | +| 5 | FutActivateCard | `0x1801642c0` | PUT `ut/%s/item` (FUT_CLUB_ACTIVATE_ITEM_DP) | **none** (immediate `ret`) | ack — `{}` fine | HIGH | +| 6 | FutApplyCard | `0x18012a710` | PUT `ut/%s/item` (apply by itemId) | `itemData`(0x16b) → **array[updated card-item]** via `0x18013fe00` [FREEZE-RISK] | GAP | HIGH | +| 7 | FutApplyCardByRes | `0x18012ad10` | PUT `ut/%s/item` (apply by resourceId) | `itemData`(0x16b) → **array[updated card-item]** [FREEZE-RISK] | GAP | HIGH | +| 8 | FutDiscardCard | `0x180127300` | DELETE `ut/delete/%s/item` (CardsDiscardCard) | `items`(0x171) → **array[int ids]** [FREEZE-RISK]; `totalCredits`(0x326) → int; `id`(0x15c) → int | GAP | HIGH | +| 9 | FutDiscardCardByRes | `0x1801279c0` | DELETE `ut/delete/%s/item` (by res) | `totalCredits`(0x326) → int | GAP | HIGH | +| 10 | FutMoveCard | `0x180128600` | PUT `ut/%s/item` (move) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool | GAP | HIGH | +| 11 | FutMoveCardByRes | `0x180128e30` | PUT `ut/%s/item` (move by res) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool (+ 2 str/1 int minor) | GAP | HIGH / extra-fields MED | +| 12 | FutConsumablesSearch | `0x180130d10` | GET `ut/%s/item?type=…` (GetFilteredConsumableSearchResults) | `itemData`(0x16b) → **array[consumable-item]** via `0x18013fe00` [FREEZE-RISK]; `displayGroupUseDefaultImage`(0xdb) → int + count scalars | GAP | deser HIGH / scalars MED | +| 13 | FutStaffBonus | `0x18012b730` | GET `ut/%s/…` (CardsGetStaffBonuses) | `bonus`(0x5c) → **nested** (branch sets bool @rbp+0x51) [FREEZE-RISK]; `assetId`(0x23) → int | GAP | MED | +| 14 | FutGetAvailableLoanPlayers | `0x18014e030` → sub `0x18013a1c0` | GET `ut/%s/item` (FUT_AVAILABLE_LOAN_PLAYERS_DP) | `loans`(0x19b) → **array** [FREEZE-RISK]; `itemData`(0x16b) → **array[card-item]** [FREEZE-RISK]; `default`(0xcd) → int | GAP | deser HIGH / fields MED | +| 15 | FutSignLoanPlayer | `0x1801642c0` | PUT `ut/%s/item` (sign loan) | **none** (immediate `ret`) | ack — `{}` fine | HIGH | +| 16 | FutStickerBookSearch | `0x18012eff0` | GET `ut/%s/…` (stickerbook search) | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | GAP | HIGH | + +Notes: +- **`0x1801642c0`** is a shared no-op deserializer (function body = `ret`). Three responses + (ChangeClubName, ActivateCard, SignLoanPlayer) use it → their HTTP body is fully ignored; only a + 200 + valid JSON (`{}`) is required. This is why "change name / activate / sign loan" succeed + with a bare stub. +- **user-record** element (`0x18012c990`, GetClubInfo) is a jump-table parser; caught fields + include `established`(0x110,str), `homekit`(0x159), `awaykit`(0x3f), `cleansheets`(0x84,int), + `attributeList`(0x31), `passing`(0x214), `awardType`(0x44), `categoryId`(0x73), + `controls`(0xb9), `changed`(0x7e), `currentChampionEvent`(0xc2). Full field map is PARTIAL + (jump table at `[0x1412c4]` not fully decoded); all fields optional/SKIP-safe. +- **card-item** element = `0x18013fe00`, already reversed in `CARD_SYSTEM.md` + (rating/position/nation/teamid/attributeList/name/resourceId…). Not re-derived here. +- Atom→key mapping from `atoms.tsv` (FNV-indexed sequential IDs 0x0–0x38a). + +--- + +### Minimal known-good JSON + +```jsonc +// 1 GetClubInfo — GET ut/game/fifa17/club (club-user summary list) +{ "user": [ { "established": "2026", "cleansheets": 0, "homekit": 0, "awaykit": 0 } ] } +// or safe empty: { "user": [] } + +// 2 GetClubUsers — GET ut/game/fifa17/clubUser +{ "user": [] } + +// 3 ChangeClubName — PUT ut/game/fifa17/club +{} + +// 4 ViewCards — GET ut/game/fifa17/item (THE card list; renders real cards) +{ "itemData": [ /* card items, deser 0x18013fe00 shape */ ] } + +// 5 ActivateCard — PUT ut/game/fifa17/item +{} + +// 6 ApplyCard / 7 ApplyCardByRes — PUT ut/game/fifa17/item +{ "itemData": [ /* the single updated card item */ ] } + +// 8 DiscardCard — DELETE ut/delete/game/fifa17/item +{ "items": [ 123456789 ], "totalCredits": 15000, "id": 123456789 } + +// 9 DiscardCardByRes — DELETE ut/delete/game/fifa17/item +{ "totalCredits": 15000 } + +// 10 MoveCard / 11 MoveCardByRes — PUT ut/game/fifa17/item +{ "itemData": [ /* moved item */ ], "chemistry": true } + +// 12 ConsumablesSearch — GET ut/game/fifa17/item?type= +{ "itemData": [ /* consumable items */ ], "displayGroupUseDefaultImage": 0 } + +// 13 StaffBonus — GET ut/game/fifa17/... (staff bonuses) +{ "bonus": [], "assetId": 0 } + +// 14 GetAvailableLoanPlayers — GET ut/game/fifa17/item (loans) +{ "loans": [], "itemData": [ /* loan card items */ ], "default": 0 } + +// 15 SignLoanPlayer — PUT ut/game/fifa17/item +{} + +// 16 StickerBookSearch — GET ut/game/fifa17/... (sticker book) +{ "itemData": [] } +``` + +### utas_server.py integration status +- `/item` (`defs_route`) already serves `{"itemData":[…]}` → satisfies **ViewCards, ApplyCard, + ApplyCardByRes, MoveCard, ConsumablesSearch, StickerBook, loan itemData** shape (though action + semantics — updated item / chemistry / credits — are not modelled). +- `/club` serves `{"itemData":…}` but GetClubInfo wants **`user`** → the itemData is SKIP'd; club + card render still works because it goes through `/item` ViewCards, not `/club`. To populate the + club-user summary, serve `{"user":[…]}` on `/club`. +- `/clubUser` → `{}` (GAP: should be `{"user":[…]}`; `{}` is non-freezing). +- No routes for discard-credits, move-chemistry, staff-bonus, loan `loans[]`, stickerbook → all + fall to catch-all `{}` (safe, feature-inert). +``` +## Store / Packs / Purchases + +Reversed from CardsDLL (base `0x180000000`) via the RECIPE deserializer method. All +struct-name VAs computed as `0x1801e5000 + (fileoff - 0x1e4400)`; deserializers located +from the `.text` `lea r8,[name]` xref; atoms translated through `atoms.tsv`. + +Parser primitives: INT `0x1801c79d0` · BOOL `0x1801c7620` · STR `0x1801c7aa0` · +value-SKIP (unknown atom, safe) `0x180135ff0` · next-token `0x1801c7f10` · +begin-object `0x1801c8270` · key→atom FNV `0x180180d00` · shared ITEM element deser +`0x18013fe00`. **Type fidelity is mandatory** — feeding a scalar getter an object/array +desyncs the SAX reader → tokenizer freeze at `0x1801c7f1a`. + +### Endpoint → struct map (paths use %s = "game/fifa17") + +| Method | Path | Request→Response struct | Blaze cmd token | +|---|---|---|---| +| GET | `ut/%s/store` (`store/purchasegroup/...`) | FutStoreGetPackTypesServerResponse | STOREPACKTYPES | +| GET | `ut/v2/%s/store` | FutStorePackQuantitiesServerResponse | STOREPACKQUANTITIES / V2STORE | +| POST/PUT | `store/transaction` | FutCreatePackServerResponse (buy=create) / FutPurchaseItemsServerResponse | CREATEPACK / PURCHASEITEMS | +| GET | `ut/%s/purchased` | FutGetPurchasedItemsServerResponse | PURCHASEDITEMS | +| — | credits refresh (embedded) | FutUpdateCreditsServerResponse | UPDATECREDITS | + +--- + +### 1. FutStoreGetPackTypesServerResponse — confidence: HIGH +- **name VA** `0x18021de20` · **deserializer** `0x1801234e0` · **pack element deser** `0x18013af30` +- **Root keys** (deser `0x1801234e0`): + - `purchase` (atom **0x260**) → **ARRAY** of pack objects (each → `0x18013af30`) *(freeze-risk: must be array)* + - `timestamp` (atom **0x31b**) → INT scalar → `[rdi+0x5c]` +- **Pack object fields** (deser `0x18013af30`, all optional; unknown keys skipped): + + | key | atom | type | notes | + |---|---|---|---| + | `assetId` | 0x23 | INT | **real pack identity** → `[rbp-0x3c]` | + | `actionType` | 0x08 | INT | | + | `bonus` | 0x5c | INT | | + | `dealType` | 0xcc | STR | | + | `description` | 0xd1 | STR | display name | + | `displayGroup` | 0xd9 | **ARRAY** | nested (freeze-risk) | + | `displayGroupAssetId` | 0xda | INT | `[rbp-0x80]` | + | `displayGroupUseDefaultImage` | 0xdb | BOOL | | + | `currencies` | 0xc5 | **ARRAY** | coin price: `[{name,funds,finalFunds}]` (freeze-risk) | + | `extPrice` | 0x119 | **OBJECT** | → `finalPrice`(0x125,obj `0x180139070`) + `originalPrice`(0x205,obj `0x18013aae0`); inner uses `amount`(0x1b)/`currency`(0xc4) (freeze-risk) | + | `packContentInfo` | 0x20c | **OBJECT** | → `bronzeQuantity`(0x63), `silverQuantity`(0x2c6), `goldQuantity`(0x149), `rareQuantity`(0x273), `itemQuantity`(0x170), `start`(0x2e3), `unopened`(0x35d,bool) (freeze-risk) | + | `sortPriority` | 0x2cb | INT | | + +- **Status: already handled (renders in-game), but with CORRECTIONS** + - `store_catalog()` currently emits `id, packType, quantity, purchaseLimit, purchaseCount, isPremium, saleType` — **none of these atoms exist in the pack deser** (`id`=0x15c, `quantity`=0x26b, `saleType`=0x298, `packType`=0x20f, `isPremium`=0x176 are all routed to SKIP `0x180135ff0`). They are harmless no-ops but do nothing. + - The **real identity field is `assetId` (0x23)**, which the current handler does NOT send. Recommend adding `assetId` per pack (packs currently work off `currencies`+`extPrice`+`packContentInfo` presence + the transaction-body `packId`, but `assetId` is the field the client actually deserializes). + - `extPrice.finalPrice/originalPrice` inner keys are NOT `mtx` (no such atom) — real inner atoms are `amount`/`currency`; current `{"mtx":N}` is skipped, so extPrice objects are effectively empty-but-present (enough to pass validation; FIFA-Points price shown comes from elsewhere). +- **Minimal known-good** (corrected): +```json +{"purchase":[{"assetId":101,"description":"Gold Pack","sortPriority":1, + "currencies":[{"name":"coins","funds":5000,"finalFunds":5000}], + "extPrice":{"finalPrice":{"amount":100,"currency":"fifapoints"},"originalPrice":{"amount":100,"currency":"fifapoints"}}, + "packContentInfo":{"bronzeQuantity":0,"silverQuantity":0,"goldQuantity":7,"rareQuantity":1,"itemQuantity":7}}], + "timestamp":1596326400} +``` + +--- + +### 2. FutStorePackQuantitiesServerResponse — confidence: HIGH ⟵ GAP (UNBUILT) +- **name VA** `0x18022d440` · **deserializer** `0x1801758c0` (token loop `0x180175920`) +- **This is the `ut/v2/%s/store` response.** It is NOT a per-pack quantity list — it is a single + **eligibility/result gate**. The deser reads exactly ONE key: + - `result` (atom **0x288**) → STRING; the string VALUE is FNV-hashed (`0x180180d00`) and mapped to an enum stored at `[rdi+0x28]`: + + | string value | atom hit | enum | + |---|---|---| + | `SUCCESS` | 0x2fb | 0 | + | `TOO_MANY_TOURNAMENTS` | 0x324 | 1 | + | `LOCKED_PERMANENT` | 0x1a1 | 2 | + | `LOCKED_RETRY` | 0x1a2 | 3 | + | `LOCKED_TROPHIES` | 0x1a3 | 4 | + + All other keys are skipped. Constructor default of `[rdi+0x28]` is 0 (SUCCESS), so an empty + `{}` also parses as SUCCESS, but send `result` explicitly. +- **Status: GAP** — no route builds this. Add a handler for `ut/v2/%s/store`. +- **Minimal known-good**: `{"result":"SUCCESS"}` + +--- + +### 3. FutCreatePackServerResponse — confidence: HIGH +- **name VA** `0x180228318` · **deserializer** `0x180162880` +- Wrapper key `createPackResponse` (atom **0xbe**) → OBJECT with: + + | key | atom | type | store | + |---|---|---|---| + | `itemList` | 0x16e | **ARRAY** of items (element deser `0x18013fe00`) | freeze-risk | + | `numberItems` | 0x1dd | INT | `[rsi+0x28]` | + | `purchasedPackId` | 0x264 | INT | `[rsi+0x70]` | + | `duplicateItemIdList` | 0xec | **ARRAY** (int list) | freeze-risk | + +- **Status: already handled — VERIFIED byte-exact** against `store_buy()`. +- **Minimal known-good**: +```json +{"createPackResponse":{"itemList":[],"numberItems":1,"purchasedPackId":101,"duplicateItemIdList":[]}} +``` + +--- + +### 4. FutPurchaseItemsServerResponse — confidence: MEDIUM-HIGH +- **name VA** `0x1802203a0` · **deserializer** `0x180126a04` (token loop `0x180126a63`) +- Purchase/transaction confirmation. Fields (dispatch is a cumulative sub-ladder off atom in `r8d`): + + | key | atom | type | store | + |---|---|---|---| + | `transactionId` | 0x33a | INT | `[rdi+0x28]` | + | `firstPartyStoreId` | 0x127 | INT | `[rdi+0x9c]` | + | `packId` | 0x20b | INT | | + | `purchasePackType` | 0x266 | STR | `[rdi+0x48]` | + | `state` | 0x2eb | STR | | + | `useAuth` | 0x367 | INT/BOOL | credit-block fields at `[rdi+0xa0/0xa4/0xa8]` | + +- **Status: GAP (optional).** The current buy flow uses CreatePack (`createPackResponse`), not this. + Only needed if a capture shows FIFA expecting a PURCHASEITEMS response on `store/transaction`. + Fields above are the confirmed lower branch; a few upper-branch credit fields are approximate. +- **Minimal known-good**: `{"transactionId":1,"packId":101,"purchasePackType":"GOLD","state":"SUCCESS"}` + +--- + +### 5. FutGetPurchasedItemsServerResponse — confidence: HIGH +- **name VA** `0x18021fca8` · **deserializer** `0x180124ed0` (body sub-parser `0x18013bd40`) +- Single root key `itemData` (atom **0x16b**) → **ARRAY** of item objects (element deser `0x18013fe00`). + (also tolerates `duplicateItemIdList` 0xec.) *(freeze-risk: itemData must be array)* +- **Status: already handled — VERIFIED** against `purchased_items()` → `{"itemData":[...]}`. +- **Minimal known-good**: `{"itemData":[]}` (or `[...]`) + +--- + +### 6. FutUpdateCreditsServerResponse — confidence: MEDIUM +- **name VA** `0x18022cc10` · **deserializer** `0x1801738b2` (delegates whole body to shared object parser `0x180139610`, `rdx=[rdi+0x10]`) +- Carries the credits/currencies balance object. The verified coin-binding path is the shared + currencies parser (deser `0x180122c50`, atom `currencies` 0xc5 → `[].funds`), already served by + `credits_route()` as `{"credits":N,"currencies":[{name,funds,finalFunds}]}`. Exact per-offset + field map of `0x180139610` not fully traced (it is a large shared parser); no correction needed + since the coin counter already binds correctly. +- **Status: effectively handled** via `credits_route`. Keep `{"credits":N,"currencies":[...]}`. + +--- + +## Definitive store-availability config flag list (for Blaze client-config) + +The store "not available" screen is gated by (a) a **resolution check** — `GetSystemMetrics` +must be > 1024×768 — and (b) the following Blaze **client-config** flags. `FUT_STORE_DISABLED` +is a UI **message** string, not a gate. Confirmed present in `cardsdll.strings`: + +**Blaze client-config booleans (must be "1"/true):** +- `IS_STORE_ENABLED` +- `IS_COIN_PURCHASABLE` +- `IS_FIFAPOINT_AVAILABLE` +- `IS_FIFAPOINT_PURCHASABLE` ← additional (found adjacent) +- `IS_EASTORE_SERVICE_READY` ← additional (found adjacent) +- `COINS_PURCHASE_ENABLED` +- `POINTS_PURCHASE_ENABLED` +- `MONEY_PURCHASE_ENABLED` ← additional (found adjacent) + +**FUT data/config flags (lowercase, JP-region variants exist):** +- `cardPackStoreEnabled` / `cardPackStoreEnabled_JP` +- `coinEnabled` / `coinEnabled_JP` + +Set all Blaze booleans to enabled and ensure the client renders above 1024×768. +# Section: User / Hub / Settings / Objectives / Leaderboards / Champions + +Clean-room RE of the FIFA 17 FUT boot/hub API from `cardsdll.dll` disassembly +(base `0x180000000`). Method: RECIPE.md — struct-name string → `lea r8` xref → +deserializer field-loop → FNV atom immediates (cumulative sub/cmp jump-ladders) +→ `atoms.tsv`. `%s = "game/fifa17"`. + +**Shared token-type noise** (excluded from field lists below): after every +`NextToken` (`0x1801c7f10`) the parser does `cmp eax,0xa` / `cmp eax,0x6` / +`cmp eax,0xd` — these are JSON token-TYPE checks, NOT field atoms. + +**Leaf getters:** int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`. +**Value-SKIP** (unknown atom, safe extra keys): `0x180135ff0`. **Card/item** +sub-deser: `0x18013fe00`. Feeding a scalar getter an object/array desyncs the SAX +reader → infinite spin at `0x1801c7f1a` (the hub freeze). + +--- + +## BOOT / HUB path (gates reaching the FUT hub — highest priority) + +### FutCreateUserServerResponse — CONFIDENCE: HIGH ✅ HANDLED +- **Deser:** `0x18014cc60` (lea r8 @ `0x18014cc2c`) +- **HTTP:** `POST ut/%s/user` +- **Fields (exact, matches util baseline):** + - `login` (0x1a5) — bool + - `userData` (0x36d) — nested → userInfo record (deser `0x18013ec10`) + - `squad` (0x2cd) — nested object (→ squad deser `0x18013d1f0`) + - `starterPack` (0x2e5) — nested object + - `bonusPacks` (0x5d) — array +- **Handled:** `utas_server.USER_POST`. Min JSON: + ```json + {"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]} + ``` + +### FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) ✅ HANDLED +- **Wrapper parser:** `0x180146970` — does `Parse` + TWO `NextToken` before + deserializing, so the body MUST be wrapped in exactly one member (member NAME + not compared, nesting level required). +- **userInfo record deser:** `0x18013ec10` (freeze-critical — shared with massinfo). +- **HTTP:** `GET ut/%s/user` (NOT called at boot; only reachable via massinfo). +- **userInfo fields — confirmed atoms (cumulative-ladder):** + - `personaId` (0x21b) — int + - `sessionCoinsBankBalance` (0x2bb) — int + - `trophies` (0x340) — int + - `won` (0x387) — int + - `purchased` (0x262) — bool + - `feature` (0x11c) — **nested object** (e.g. `{trade:bool}`) ⚠ freeze-risk + - `fifaPointsFromLastYear` (0x121) — int + - `squadList` (0x2d4) — **array** ⚠ freeze-risk + - `unopenedPacks` (0x35e) — **nested object** ⚠ freeze-risk; children + `preOrderPacks`(0x24b), `recoveredPacks`(0x27b), `count`(0xbc) + - nested notification sub-obj: `notification`(0x1da), `outbid`(0x206), `winning`(0x384) +- **From validated baseline (util) — also parse (unknown→SKIP):** `clubName`, + `clubAbbr`, `established`, `clubNameChangeAllowed`, `currencies`[] (array of + `{name,value}` ⚠ array), `won/draw/loss`, `divisionOffline/divisionOnline`, + `reliability`{reliability,matchUnfinishedTime}, `bidTokens`{count,updateTime}, + `actives`[] (array). +- **Handled:** `utas_server.USER_GET = {"userInfo": user_info()}`. + +### FutGetUserMassInfoServerResponse — CONFIDENCE: HIGH (freeze behavior) ⚠ MUST BE {} — GAP for populated +- **Deser:** `0x180174630` (freeze-sensitive per CARD_SYSTEM.md). +- **HTTP:** `GET ut/%s/userMassInfo` +- **Top-level keys (this deser dispatches):** + - `user` (0x36c) — nested → calls userInfo deser `0x18013ec10` + *(NOTE: the wrapping key is `user`, not `userInfo` — relevant if ever populated)* + - `clubUser` (0x91) — nested + - `settings` → calls settings deser `0x18013c6d0` + - squad → calls LoadActiveSquad deser `0x18013d1f0` + - `transaction` (0x339), `errors` (0x10c, array), `loanPlayers` (0x19a, array), + `pileSizeClientData` (0x227), `key`(0x177)/`value`(0x377) pairs +- **CRITICAL:** any content (userInfo AND/OR squad) desyncs the massinfo parser → + infinite tokenizer spin (`0x1801c7f1a`). **Return `{}`** — proven hub-reaching. + The exact squad/squadList shape + the userInfo nested-object typing (feature / + unopenedPacks / currencies) is the open desync GAP; deliver club/squad via their + own endpoints instead. +- **Handled:** `utas_server` `FUT_MASSINFO=empty` → `{}`. + +### FutGetSettingsServerResponse — CONFIDENCE: HIGH ✅ HANDLED +- **Deser:** `0x18013c6d0` +- **HTTP:** `GET ut/%s/settings` +- **Fields:** single wrapper key `configs` (0xa2) → array of config entries + `{ type (0x354), value (0x377) }`. +- **Handled:** `utas_server.SETTINGS = {"configs": []}`. Min JSON: `{"configs":[]}`. + +### FutGetHubDataServerResponse — CONFIDENCE: LOW (full schema) / HIGH (served {} works) — GAP +- **Wrapper:** `0x1801736ad` → inner `0x180173a50` / `0x180173b10` / `0x180173c00`. +- **HTTP:** `GET ut/%s/hub` +- **Note:** uses **C++ reflection / vtable dispatch** (`call [rax+0x10]`, + `call [rdx+0x1f8]`), NOT an inline atom ladder — no static field ladder to + read. It aggregates sub-objects (userInfo, settings, messages, etc.), each with + its own deser. Empty `{}` is tolerated (fields default). +- **Handled:** `utas_server` serves `{}` (validated hub-reaching). Deep populate = GAP. + +### FutUserDataServerResponse — CONFIDENCE: MEDIUM +- **Deser:** `0x18016dd50` (lea r8 @ `0x18016d98d`) +- **Fields:** `actives` (0xb, array), `key` (0x177), `value` (0x377) — key/value + user-data entries. Part of create/user path. Served inside `userData`. + +--- + +## THEN — user lifecycle structs + +| Struct | Deser VA | HTTP | Fields (atoms) | Status | +|---|---|---|---|---| +| FutGamerSetInfoServerResponse | 0x18016d85d | POST `ut/%s/user` (set gamer info) | none (empty ack) | GAP-trivial | +| FutKeepAliveServerResponse | 0x18016232d | `ut/%s/match/keepalive` | none | ✅ HANDLED (204) | +| FutLogoutServerResponse | 0x18017006d | logout | none (empty ack) | GAP-trivial | +| FutResetUserServerResponse | 0x18017548d | `DELETE ut/delete/%s/user` | none | GAP-trivial | +| FutGetUserActionServerResponse | 0x1801781ad | GET user actions | `actions`(0x7,array), `actionType`(0x8) | partial | +| FutUpdateUserActionServerResponse | 0x18012378d | PUT user action | none (empty ack) | GAP-trivial | +| FutSetFavFeatureServerResponse | 0x18016f2cd | set fav feature | none | GAP-trivial | +| FutLiveMessageUpdateServerResponse | 0x180153a2d | `ut/%s/livemessage` | none (empty ack) | GAP-trivial | +| FutGetTrustedConsoleListServerResponse | 0x18012a01d | `ut/%s/phishing/trusteddevice` | `changed`(0x7e,bool), `exists`(0x117,bool), `locked`(0x19e,bool), `trusted`(0x351,bool) | ✅ HANDLED (`trusted:true` skips security Q) | + +Min JSON for the empty-ack structs: `{}` (200) or `204` — all validated safe. +`activeMessage` (`ut/%s/activeMessage`), `clientdata` (`ut/%s/clientdata`), +`livemessage` — no dedicated field-ladder deser; served generically ({} / 200). + +--- + +## OBJECTIVES / CHALLENGES + +FUT 17 objectives = **ManagerQuests** (viewmodel `futmanagerquestsviewmodel`; +data providers `FUT_MQ_QUESTS_DATA_DP`, `FUT_SQUAD_QUESTS_DP`, +`FUT_PLAYER_IDENTITY_QUESTS_DP`). Enable flags live in **settings**: +`enableObjectives` (0xfd), `enableObjectivesAsManagerTasks` (0xfe). +Relevant atoms: `objectives`(0x1e2), `objectivesForCurrentUser`(0x1e3), +`allObjectivesForCurrentGameSpaceId`(0x15), `challenges`(0x76), +`challengesCount`(0x78), `challengesCompletedCount`(0x77), `challengeId`(0x74), +`challengeImageId`(0x75), `grantedChallengeAwards`(0x14a), `squadChallenge`(0x2d1). + +- **FutManagerQuestGetRewardServerCall** — deser `0x1801516cd` — reward-claim call; + 0 inline atoms (response reuses shared item/award desers). No dedicated + `...ServerResponse` struct exists — the quests UI is client-driven via the + viewmodel + generic item/award payloads. **GAP** (no route yet). +- **FutGetTowChallengeServerResponse** — deser `0x18016dbcd` — challenge progress as + `key`(0x177)/`value`(0x377) pairs, `actives`(0xb). GET. **GAP**. Min: `{}`. +- **FutSetTowChallengeServerResponse** — deser `0x18016ef6d` — none (empty ack). PUT. GAP-trivial. +- **FutLoadSetChallengesResponse** — deser `0x18017b9ed` — SBC set-challenge defs (adjacent): + `awards`(0x47), `categoryId`(0x73), `elgReq`(0xf7), `endTime`(0x106), + `formation`(0x12b), `eligibilityKey`(0xf2), `eligibilitySlot`(0xf4). partial/GAP. + +--- + +## LEADERBOARDS + +### FutGetLBEntriesServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x180144c8d` +- **HTTP:** `GET ut/%s/leaderboards` +- **Entry fields (array):** `clubName`(0x8e), `badge`(0x49), `est`(0x10f, + established), `score`(0x29a), `seasonOnlineDraws`(0x2a6), `TalkRating`(0x303), + `insetUrl`(0x166), `inset`(0x165). +- **Min JSON:** `{"entries":[]}` (empty list safe). + +### FutGetLBOptionsServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x18014351c` +- **HTTP:** `GET ut/%s/leaderboards/options` +- **Fields:** `category`(0x70), `id`(0x15c), `period`(0x218), `view`(0x37a), `url`(0x366). + +--- + +## CHAMPIONS + +### FutChampionsRegistrationServerResponse — CONFIDENCE: MEDIUM — GAP-trivial +- **Deser:** `0x18014980d` — no inline atoms (empty/status ack). `POST ut/%s/champion`. Min `{}`. + +### FutGetChampionsFriendsServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x18014b7ad`. `GET ut/%s/champion` (friends). +- **Fields:** `stats`(0x2ec, nested), `gamesPlayed`(0x139), `persona`(0x21a). Min `{}`. + +### FutGetChampionsTopXServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x18014a09d`. `GET ut/%s/champion` (topX). +- **Fields:** `entries`(0x109, array) of `{ clubName(0x8e), badge(0x49), est(0x10f) }`. Min `{"entries":[]}`. + +--- + +## CAPTCHA / PHISHING / TFA + +### FutGetCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x18014e78d`. `GET ut/%s/captcha`. +- **Fields:** `encodedImg`(0x101, string base64), `sequence`(0x2ba, int), + `sizeBeforeEncode`(0x2c7, int). + +### FutValidateCaptchaServerResponse — `0x18014ed5d` — none (status ack). `POST ut/%s/captcha`. GAP-trivial. Min `{}`. + +### FutExchangeCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP +- **Deser:** `0x180177d5d`. Fields: `token`(0x321, string). Min `{"token":"..."}`. + +### FutGetPhishingQuestionServerResponse — CONFIDENCE: HIGH ✅ HANDLED +- **Deser:** `0x18012980d`. **HTTP:** `GET ut/%s/phishing/question?deviceId=%s`. +- **Fields:** `question`(0x26c, int id), `answer`, `attempts`(0x28, int), + `recoverAttempts`(0x27a, int). +- **Handled:** util `{"question":0,"answer":"","attempts":5}`. + +### FutSetPhishingAnswerServerResponse — `0x180129b9d` — none. `POST /question?deviceId=%s&question=%d&answer=%s`. ✅ HANDLED. + +### FutValidatePhishingAnswerServerResponse — CONFIDENCE: HIGH ✅ HANDLED +- **Deser:** `0x180129ddd`. Returns a trust `token`. **HTTP:** `ut/%s/phishing/validate`. +- **Handled:** util `{"token":"OPENFUT-TRUST-..."}`. + +### TFA — `ut/%s/tfa` — endpoint present; no dedicated field-ladder deser (served generically). GAP-untriggered. + +--- + +## Summary of atom / path cross-checks +- Path templates confirmed in binary strings: `ut/%s/{user,user/list,hub,clientdata, + activeMessage,livemessage,leaderboards,leaderboards/options,champion,phishing, + captcha,tfa}`, plus `/question?deviceId=%s(&question=%d&answer=%s)`. +- Freeze-risk nested fields (must be object/array, never scalar): userInfo.`feature`, + userInfo.`unopenedPacks`, userInfo.`squadList`, userInfo.`currencies`, + userInfo.`actives`; and the whole userMassInfo body (keep `{}`). + + +--- + +## Regenerating the analysis inputs + +``` +cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll +objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm +strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings +python3 tools/atomdump.py > /tmp/fut/atoms.tsv # 907 atom->key rows +``` +Recipe: docs/OPENCODE_ENDPOINT_PROMPT.md · atom table VA 0x1802d2760 · deser locator in header above. diff --git a/fifa17-recon/docs/OPENCODE_ENDPOINT_PROMPT.md b/fifa17-recon/docs/OPENCODE_ENDPOINT_PROMPT.md new file mode 100644 index 0000000..21fe0d6 --- /dev/null +++ b/fifa17-recon/docs/OPENCODE_ENDPOINT_PROMPT.md @@ -0,0 +1,170 @@ +# opencode task — map the remaining FIFA 17 FUT endpoints for the offline rebuild + +## Context +OpenFUT runs FIFA 17 Ultimate Team fully offline (clean-room; EA servers are dead). +A working Python backend already exists at `~/Documents/OpenFUT/fifa17-recon/tools/` and +FIFA talks to it (`/etc/hosts` → `easw.easports.com` → `127.0.0.1:8099`). Many endpoints +are already reversed and served. Your job is **RESEARCH ONLY**: produce a complete map of +the FUT endpoints that are LEFT to build, using the tools/logs/disassembly that already +exist. Do NOT rewrite the backend — output `~/Documents/OpenFUT/fifa17-recon/docs/ENDPOINT_MAP.md`. + +## CLEAN-ROOM RULE (HARD) +Use ONLY files we own + our own client's traffic. NEVER use leaked EA source of any kind. + +## Use the ALREADY-BUILT tools — do not rebuild these +1. **`tools/utas_server.py`** — the live backend. Its `ROUTES` table is the AUTHORITATIVE + list of endpoints already handled: `auth`, `delete/auth`, `phishing/{trusteddevice, + validate,question}`, `user/credits`, `user/list`, `user/accountinfo`, `user`, `squad` + (+`/list`), `hub`, `userMassInfo`, `season`, `club`, `item(/resource)/defid`, + `store/purchasegroup`, `store/transaction`, `purchased`. Read it to see what's DONE and + the exact response shapes used. Its `_handle()` logs `!! UNMAPPED PATH -> catch-all 200 {}` + for any endpoint FIFA hits that ISN'T handled yet — those are your gaps. +2. **`/tmp/utas_server.log`** — GROUND TRUTH of every request FIFA makes (method, path, + headers, body) and our response. Start here: + ``` + grep 'UNMAPPED PATH' -B1 /tmp/utas_server.log # endpoints we stub {} + grep -oE '(GET|POST|PUT) /ut/(v2/)?game/fifa17/[^ ?]*' /tmp/utas_server.log | sort -u + ``` + This is the real, ordered list of what the client requests per FUT screen. + (Caveat: the log is temp — cleared on reboot — and only reflects FUT screens actually + visited. For richer traffic, run the harness and navigate more FUT menus first.) +3. **`tools/memtool.py`** — live FIFA17 `/proc/mem` reader/patcher (base `0x140000000`), + for inspecting parsed structs live if a format is ambiguous. +4. **`tools/fut_store.py` / `tools/fut_seed.py`** — the data models (club items, squad, + packs) the backend already uses; extend these conceptually, don't reinvent. +5. **`tools/openfut-fut.sh`** — starts the whole harness (lsx/blaze/roster/utas) if you + need it running to capture more traffic. `tools/root_arm.sh` arms host state (needs sudo). +6. **`docs/CARD_SYSTEM.md`** — the reversed card/parser system + method (READ FIRST). It + already documents the SAX-parser internals so you don't re-derive them. + +## Disassembly (regenerate once; same method `docs/CARD_SYSTEM.md` uses) +``` +mkdir -p /tmp/fut && cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll +objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm +strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings +``` +Shared-parser CHEATSHEET (reuse, don't re-derive): endpoint paths are strings `"ut/%s/..."` +(`%s` = `"game/fifa17"`); response structs are `"RS4:FutServerResponse"`; each has a SAX +deserializer that hashes each key via FNV-1a fn `0x180180d00` (seed `0x811c9dc5`) to an ATOM +int, looks the name up in the table at `0x1802d2760` (`table[atom]=char*`), and dispatches +via a jump-table; unknown keys hit the value-SKIP handler `0x180135ff0` (container-aware, +safe). Field TYPE must match its handler (scalar getters `0x1801c79d0`/`0x1801c7620`/ +`0x1801c7aa0` vs nested object/array handlers) — a scalar handler fed an object/array +**DESYNCS the parser and hard-freezes the game**, so type fidelity is mandatory. +Endpoint resolve table: `RS4::ServerSettings::resolve 0x180124270`. + +## Method (per endpoint) +For each gap endpoint (from the log's UNMAPPED list + the `"ut/%s/..."` strings not yet in +`ROUTES`): grep the strings for its path → find its `FutServerResponse` struct → find its +deserializer → list the atoms it dispatches, map atom→key via `0x1802d2760`, note each +field's JSON type + required-vs-skip → write the minimal known-good response JSON. + +## WHERE TO START +1. Read `docs/CARD_SYSTEM.md` and skim `tools/utas_server.py` `ROUTES`. +2. Run the two log greps above → the definitive list of endpoints FIFA calls but we only + stub `{}`. Rank them by FUT feature. +3. List ALL `"ut/%s/..."` path strings and subtract the ones already in `ROUTES` → the + endpoints not yet even discovered in traffic. +4. Reverse the response format for each gap, prioritizing the core loop first: + **transfermarket** (search/bid/buy/list/watchlist), **tradepile**, **SBC** + (challenges/submit), **objectives**, **draft**, **seasons/single-player**, **match** + (squad-battles/kickoff result), then club stats / concept squads / loans. + +## Deliverable +`docs/ENDPOINT_MAP.md`, one section per feature: for each endpoint — method, path, whether +already handled or a gap, request body shape, response shape (exact keys+types, required vs +optional), deserializer VA, and a minimal known-good example JSON. This is the spec for +finishing the FUT backend (target: port into the Rust `openfut-core` behind a FIFA-17 bridge). + +--- + +## APPENDIX — known atoms + verified response templates (reuse these; don't re-derive) + +### Parser internals (already reversed) +- key → atom: FNV-1a fn `0x180180d00` (seed `0x811c9dc5`) → `table[atom]=char*` at `0x1802d2760` +- dispatch: range jump-tables; unknown key → value-SKIP `0x180135ff0` (container-aware, safe) +- scalar getters (leaf, no descend): int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0` +- endpoint resolve: `RS4::ServerSettings::resolve 0x180124270`; path `"ut/%s/.."`, `%s="game/fifa17"` +- shared ITEM/card deserializer: `0x18013fe00` (used by club, squad slots, pack itemList, purchased) + +### Common shared keys (atom in hex | JSON type) +``` +itemData 0x16b (obj/array-elem) | id 0x15c (int) | resourceId 0x287 (int) | assetId 0x23 (int) +index 0x163 (int) | kitNumber 0x17a (int) | rating 0x274 (int) | preferredPosition 0x24a (str) +cardsubtypeid 0x6c (int) | attributeList 0x31 (array) | currencies 0xc5 (array) + currencies element: name 0x1d0 (str) | funds 0x134 (int) | finalFunds 0x124 (int) + currency-name literals are CASE-SENSITIVE strings: "coins", "points", "DRAFT_TOKEN" +configs 0xa2 (array) [settings response, key = "configs"] +``` + +### FutUserCreditsServerResponse — GET user/credits (deser 0x180122c50) [VERIFIED WORKS] +```json +{"currencies":[{"name":"coins","funds":15000,"finalFunds":15000}, + {"name":"points","funds":0,"finalFunds":0}], + "unopenedPacks":{"preOrderPacks":0,"recoveredPacks":0}} +``` +Coins read from `currencies[name=="coins"].funds`. A bare `{"credits":n}` is SKIPPED → 0. + +### FutStoreGetPackTypesServerResponse — GET store/purchasegroup/all (deser 0x1801234e0) +Root key MUST be `"purchase"` `0x260` (array); optional `"timestamp"` `0x31b`. Per-pack +(parser `0x18013af30`): `id 0x15c`(int16, the pack identity) | `packType 0x20f`(str) | +`description 0xd1`(str) | `currencies 0xc5`(array {name,funds,finalFunds} = coins price) | +`extPrice 0x119`(obj {`finalPrice 0x125`, `originalPrice 0x205`}, each a currency→amount map +incl `"mtx"`=FIFA-Points) | `packContentInfo 0x20c`(obj: `bronzeQuantity 0x63`, +`silverQuantity 0x2c6`, `goldQuantity 0x149`, `rareQuantity 0x273`, `itemQuantity 0x170`) | +`quantity 0x26b`(int, 0=unlimited) | `isPremium 0x176` | `saleType 0x298` | `state 0x2eb` | +`visible 0x37d`(sets flag, DON'T send the value—desyncs). + +**⚠ The parser is NOT the store gate (CONFIRMED by disassembly).** Per-pack parser epilogue +`0x18013badc` pushes every parsed pack unconditionally — no valid/drop predicate exists in +the parse path. The `"not available"` error (`FUT_CatalogNotAvailable`, msg-id `0x7550`) +comes from TWO downstream gates, neither JSON-schema-related: +1. **Resolution gate `0x18001756e`**: calls `GetSystemMetrics` — if the display is + **≤ 1024×768**, the store is declared unavailable regardless of any JSON. Run FIFA at + **> 1024×768** (1280×720 passes). *Cheapest cause to eliminate — check this first.* +2. **Store-data-model load status** (`0x180013cf0`): `model+0x30` must become `1` and the + screen's cached status `[rsi+0x250]` must not stay `-1`; else post `0x7550` at + `0x180013d6c`. Fed by two entitlement checks — `0x18001749d` (`vtable+0x138`) and + `0x1800175a2` (`vtable+0x280`) — that read the **Blaze client-config purchase flags**: + `IS_STORE_ENABLED`, `IS_COIN_PURCHASABLE`, `IS_FIFAPOINT_AVAILABLE`, + `COINS_PURCHASE_ENABLED`, `POINTS_PURCHASE_ENABLED`, `MONEY_PURCHASE_ENABLED`. These are + SEPARATE from `storeEnabled`/`cardPackStoreEnabled` and must ALSO be set in the Blaze config. +Recognized per-pack enum tokens (send these exact strings to avoid enum-reject): +`saleType` → `"promo"`/`"deal"`; a limit-type field → `NONE`/`QUANTITY`/`TIME`/`TIME_QUANTITY`; +pack `state` → `"active"`. Currency token is lowercase `"coins"`/`"mtx"` (NOT uppercase). + +### FutCreatePackServerResponse — PUT store/transaction (pack reveal, deser 0x180162880) +Wrapper key `"createPackResponse"` `0xbe`: +```json +{"createPackResponse":{"itemList":[/*cards*/],"numberItems":7, + "purchasedPackId":102,"duplicateItemIdList":[]}} +``` +(atoms: itemList `0x16e`, numberItems `0x1dd`, purchasedPackId `0x264`, duplicateItemIdList `0xec`.) +BUY signal = transaction body has `"packId"` `0x20b` AND `state 0x2eb != "TRANSACTIONCANCEL"`. +state enum strings: `TRANSACTIONCREATED`(carries packId=the buy), `PURCHASECOMPLETE`, +`TRANSACTIONCOMPLETE`, `TRANSACTIONCANCEL`. +`FutGetPurchasedItemsServerResponse` — GET purchased: `{"itemData":[/*cards*/]}` (itemData `0x16b`). + +### GetUserMassInfo — GET userMassInfo (deser 0x180174630) [FREEZE-SENSITIVE] +Top-level: `userInfo 0x370`, `squad 0x2cd`, `settings`, `userData`. MUST serve `{}` unless +every field type-matches, else the parser hard-freezes. `userInfo` deser `0x18013ec10`; +SAFE minimal that carries coins: +```json +{"userInfo":{"currencies":[{"name":"coins","funds":15000}],"sessionCoinsBankBalance":15000}} +``` +(`sessionCoinsBankBalance 0x2bb`.) DANGER: `squadList` must be an OBJECT `{"squad":[...]}` +NOT an array (array → desync → freeze). Container fields `feature`/`reliability`/ +`unopenedPacks`/`bidTokens`/`actives` must match exact shape or be omitted. + +### LoadActiveSquad — GET squad/0 (deser 0x18013d1f0) +Keys: `id 0x15c` | `personaId 0x21b` | `squadName 0x2d3` | `formation 0x12b`(str) | +`squadType 0x2d6` | `chemistry 0x81` | `starRating 0x2e2` | `captain 0x69` | `manager 0x1a8`(array) +| `actives 0xb`(array) | `custom 0xc6`(STRING of 33 ints) | `players 0x238`(array of +{index,itemData,kitNumber}) | `kicktakers 0x178`(array). Squad PUT stores slots as +`itemData={id:}` references (re-embed full items on GET, see `reconstruct_squad`). + +> Atoms are index-into-`0x1802d2760`; a few above came from mixed-confidence passes — when a +> value doesn't take, verify the atom by locating the key string in `cardsdll.strings` and +> re-hashing. Field TYPE fidelity is mandatory (scalar-vs-container mismatch = freeze). +> High-confidence/verified: `user/credits`, `createPackResponse`, the store gate analysis, +> and the `userMassInfo` safe-shape. diff --git a/fifa17-recon/docs/fut_atoms.tsv b/fifa17-recon/docs/fut_atoms.tsv new file mode 100644 index 0000000..6bb428c --- /dev/null +++ b/fifa17-recon/docs/fut_atoms.tsv @@ -0,0 +1,907 @@ +0 0x0 LIST_START +1 0x1 0 +2 0x2 1 +3 0x3 2 +4 0x4 3 +5 0x5 4 +6 0x6 accountCreatedPlatformName +7 0x7 actions +8 0x8 actionType +9 0x9 activateSlotNumber +10 0xa active +11 0xb actives +12 0xc activeAwayKit +13 0xd activeBadge +14 0xe activeBall +15 0xf activeChampionLeagues +16 0x10 activeHomeKit +17 0x11 activeMessage +18 0x12 activeStadium +19 0x13 aigroup +20 0x14 allCoins +21 0x15 allObjectivesForCurrentGameSpaceId +22 0x16 allofflinetrophy +23 0x17 allonlinetrophy +24 0x18 allowGracePeriodForSquadBuildingSets +25 0x19 allowUntradeableForSquadBuildingSets +26 0x1a AMATEUR +27 0x1b amount +28 0x1c AND +29 0x1d answer +30 0x1e any +31 0x1f apply +32 0x20 applyTo +33 0x21 areas +34 0x22 areaSubType +35 0x23 assetId +36 0x24 AssetId +37 0x25 assetName +38 0x26 assetType +39 0x27 assists +40 0x28 attempts +41 0x29 attrib1 +42 0x2a attrib6 +43 0x2b Attribute1 +44 0x2c Attribute2 +45 0x2d Attribute3 +46 0x2e Attribute4 +47 0x2f Attribute5 +48 0x30 Attribute6 +49 0x31 attributeList +50 0x32 auctionBid +51 0x33 auctionCount +52 0x34 auctionExpired +53 0x35 auctionInfo +54 0x36 auctionLostBidRejected +55 0x37 auctionLostOutbid +56 0x38 auctionLostOutbidSelf +57 0x39 auctionSoldBid +58 0x3a auctionSoldBuyNow +59 0x3b auctionWonBid +60 0x3c auctionWonBuyNow +61 0x3d authToken +62 0x3e available +63 0x3f awaykit +64 0x40 awardCount +65 0x41 awardedPrizes +66 0x42 awardItemData +67 0x43 awardMappings +68 0x44 awardType +69 0x45 awardSet +70 0x46 awardSetId +71 0x47 awards +72 0x48 awardValue +73 0x49 badge +74 0x4a badgeDBid +75 0x4b badges +76 0x4c Badge +77 0x4d ball +78 0x4e Ball +79 0x4f balls +80 0x50 base +81 0x51 BEGINNER +82 0x52 bestBuilderScore +83 0x53 bestPointsSeasonId +84 0x54 bestPointsSeasonValue +85 0x55 bid +86 0x56 bidPrices +87 0x57 bidState +88 0x58 bidToken +89 0x59 bidTokens +90 0x5a bio +91 0x5b biodescription +92 0x5c bonus +93 0x5d bonusPacks +94 0x5e Boost +95 0x5f boost +96 0x60 boostConis +97 0x61 boostCountLeft +98 0x62 bronze +99 0x63 bronzeQuantity +100 0x64 builder +101 0x65 buyNowPrice +102 0x66 buyoutPrices +103 0x67 Cap +104 0x68 capacity +105 0x69 captain +106 0x6a CAPTAIN_DRAFT +107 0x6b cardassetid +108 0x6c cardsubtypeid +109 0x6d cardPackStoreEnabled +110 0x6e cardPackStoreEnabled_JP +111 0x6f categories +112 0x70 category +113 0x71 Category +114 0x72 categoryCount +115 0x73 categoryId +116 0x74 challengeId +117 0x75 challengeImageId +118 0x76 challenges +119 0x77 challengesCompletedCount +120 0x78 challengesCount +121 0x79 CHAMPIONSHIP +122 0x7a championEvent +123 0x7b championEventId +124 0x7c championEventType +125 0x7d champion_qualifier +126 0x7e changed +127 0x7f checkPointsReached +128 0x80 checkServerDbVersion +129 0x81 chemistry +130 0x82 choiceIndex +131 0x83 choices +132 0x84 cleansheets +133 0x85 clientId +134 0x86 clientKeepAliveResetTimeoutSec +135 0x87 club +136 0x88 clubId +137 0x89 ClubId +138 0x8a clubInfo +139 0x8b clubCount +140 0x8c clubCreateThreshold +141 0x8d clubAbbr +142 0x8e clubName +143 0x8f clubNameChangeAllowed +144 0x90 clubPlayers +145 0x91 clubUser +146 0x92 code +147 0x93 codeType +148 0x94 coin +149 0x95 coins +150 0x96 COINS +151 0x97 coinsEarned +152 0x98 coinEnabled +153 0x99 coinEnabled_JP +154 0x9a collector +155 0x9b CommonName +156 0x9c COMPLETED_DRAFT +157 0x9d competitor +158 0x9e competitionId +159 0x9f competitionCountryCode +160 0xa0 competitionRegion +161 0xa1 concededGoals +162 0xa2 configs +163 0xa3 constrainGracePeriod +164 0xa4 consume +165 0xa5 consumables +166 0xa6 consumablesContract +167 0xa7 consumablesTraining +168 0xa8 consumablesFitness +169 0xa9 consumablesContractPlayer +170 0xaa consumablesContractManager +171 0xab consumablesFitnessPlayer +172 0xac consumablesFitnessTeam +173 0xad consumablesFormationManager +174 0xae consumablesTrainingManagerLeagueModifier +175 0xaf consumablesHealing +176 0xb0 consumablesTrainingPlayerPlayStyle +177 0xb1 consumablesTrainingGkPlayStyle +178 0xb2 consumablesPosition +179 0xb3 consumablesTrainingPlayer +180 0xb4 consumablesTrainingManager +181 0xb5 consumablesTrainingGk +182 0xb6 contextId +183 0xb7 contextValue +184 0xb8 contract +185 0xb9 controls +186 0xba corners +187 0xbb couchPlayEnabled +188 0xbc count +189 0xbd country +190 0xbe createPackResponse +191 0xbf creationTime +192 0xc0 credits +193 0xc1 currentBid +194 0xc2 currentChampionEvent +195 0xc3 currentTime +196 0xc4 currency +197 0xc5 currencies +198 0xc6 custom +199 0xc7 customData +200 0xc8 customData1 +201 0xc9 data +202 0xca dataVersion +203 0xcb debug +204 0xcc dealType +205 0xcd default +206 0xce defending +207 0xcf defId +208 0xd0 desc +209 0xd1 description +210 0xd2 detaildescription +211 0xd3 development +212 0xd4 difficulty +213 0xd5 difficultyName +214 0xd6 dimeId +215 0xd7 discardValue +216 0xd8 display +217 0xd9 displayGroup +218 0xda displayGroupAssetId +219 0xdb displayGroupUseDefaultImage +220 0xdc divisionId +221 0xdd divisionOffline +222 0xde divisionOnline +223 0xdf DRAFT_TOKEN +224 0xe0 draft_token +225 0xe1 draftChampion +226 0xe2 draftsCompleted +227 0xe3 draftState +228 0xe4 draftSummary +229 0xe5 draftToken +230 0xe6 draw +231 0xe7 dream +232 0xe8 dreamSquad +233 0xe9 dreamSquads +234 0xea dribbling +235 0xeb duplicateItemId +236 0xec duplicateItemIdList +237 0xed duplicateItemLoans +238 0xee duration +239 0xef durationInSec +240 0xf0 elegibilityId +241 0xf1 eligibilities +242 0xf2 eligibilityKey +243 0xf3 eligibilityOperation +244 0xf4 eligibilitySlot +245 0xf5 eligibilityValue +246 0xf6 elgOperation +247 0xf7 elgReq +248 0xf8 email +249 0xf9 enableDraftMode +250 0xfa enableOfflineDraftMode +251 0xfb enableLiveMessaging +252 0xfc enableLoyaltyBonusForConceptPlayers +253 0xfd enableObjectives +254 0xfe enableObjectivesAsManagerTasks +255 0xff enableSinglePlayerDraftMode +256 0x100 enableSquadBuildingSetsFeature +257 0x101 encodedImg +258 0x102 end +259 0x103 endDateTime +260 0x104 endReason +261 0x105 endtime +262 0x106 endTime +263 0x107 entitlementId +264 0x108 entranceCriteria +265 0x109 entries +266 0x10a equippables +267 0x10b errorMessage +268 0x10c errors +269 0x10d errorState +270 0x10e errorType +271 0x10f est +272 0x110 established +273 0x111 event +274 0x112 eventId +275 0x113 eventType +276 0x114 exact +277 0x115 expectedTierLevel +278 0x116 expires +279 0x117 exists +280 0x118 extendGameSessionTimerSec +281 0x119 extPrice +282 0x11a externalPriceId +283 0x11b false +284 0x11c feature +285 0x11d featuredofflinetrophy +286 0x11e featuredonlinetrophy +287 0x11f fifaPointsEnabled +288 0x120 fifaPointsEnabled_JP +289 0x121 fifaPointsFromLastYear +290 0x122 fifaPointsTransferredStatus +291 0x123 filter +292 0x124 finalFunds +293 0x125 finalPrice +294 0x126 FirstName +295 0x127 firstPartyStoreId +296 0x128 fitness +297 0x129 fitnesscoach +298 0x12a fitnessCoach +299 0x12b formation +300 0x12c FORMATION_DRAFT +301 0x12d fouls +302 0x12e free +303 0x12f friend +304 0x130 friendMessages +305 0x131 friendlySeason +306 0x132 friendlySeasonHistory +307 0x133 friendlySeasonsEnabled +308 0x134 funds +309 0x135 gameMode +310 0x136 gameModeAward +311 0x137 gamesDraw +312 0x138 gamesLost +313 0x139 gamesPlayed +314 0x13a gamesWon +315 0x13b gamesWonCurrentMatch +316 0x13c gamesRemaining +317 0x13d getOperationTimeoutSec +318 0x13e gkDiving +319 0x13f gkcoach +320 0x140 gkCoach +321 0x141 gkKicking +322 0x142 gkHandling +323 0x143 gkOneOnOne +324 0x144 gkPositioning +325 0x145 gkReflexes +326 0x146 goals +327 0x147 goalsScored +328 0x148 gold +329 0x149 goldQuantity +330 0x14a grantedChallengeAwards +331 0x14b grantedSetAwards +332 0x14c grantsGameModePrizes +333 0x14d group +334 0x14e groupName +335 0x14f halid +336 0x150 halId +337 0x151 halfLength +338 0x152 header +339 0x153 headcoach +340 0x154 headCoach +341 0x155 heading +342 0x156 healing +343 0x157 health +344 0x158 hidden +345 0x159 homekit +346 0x15a hub +347 0x15b icon +348 0x15c id +349 0x15d idList +350 0x15e image +351 0x15f imageFormat +352 0x160 imageId +353 0x161 immediateRecoveryAttempt +354 0x162 immediateRecoveryAttemptDelay +355 0x163 index +356 0x164 inGame +357 0x165 inset +358 0x166 insetUrl +359 0x167 injuryGames +360 0x168 injuryType +361 0x169 INVALID +362 0x16a item +363 0x16b itemData +364 0x16c itemDbVersion +365 0x16d itemId +366 0x16e itemList +367 0x16f itemLoans +368 0x170 itemQuantity +369 0x171 items +370 0x172 itemState +371 0x173 itemType +372 0x174 ItemType +373 0x175 isReturningUser +374 0x176 isPremium +375 0x177 key +376 0x178 kicktakers +377 0x179 kit +378 0x17a kitNumber +379 0x17b Kit +380 0x17c kits +381 0x17d kitsHome +382 0x17e kitsAway +383 0x17f knockout +384 0x180 knockout_group +385 0x181 label +386 0x182 lang +387 0x183 lastMatchUnfinished +388 0x184 LastName +389 0x185 lastSalePrice +390 0x186 leaderboard +391 0x187 LEGENDARY +392 0x188 legendCount +393 0x189 league +394 0x18a leagueId +395 0x18b LeagueId +396 0x18c leagueCount +397 0x18d leaguelogos +398 0x18e leagueLogos +399 0x18f link +400 0x190 liveMessagesAvailable +401 0x191 level +402 0x192 lifetimeAssists +403 0x193 lifetimeCleansheets +404 0x194 lifetimeStats +405 0x195 live_offline +406 0x196 live_online +407 0x197 loan +408 0x198 loanId +409 0x199 loanPlayerClientData +410 0x19a loanPlayers +411 0x19b loans +412 0x19c localizedName +413 0x19d lock +414 0x19e locked +415 0x19f LOCKED_ATTEMPTS_PERM +416 0x1a0 LOCKED_ATTEMPTS_TEMP +417 0x1a1 LOCKED_PERMANENT +418 0x1a2 LOCKED_RETRY +419 0x1a3 LOCKED_TROPHIES +420 0x1a4 locString +421 0x1a5 login +422 0x1a6 loss +423 0x1a7 MAINTENANCE +424 0x1a8 manager +425 0x1a9 Manager +426 0x1aa MANAGER +427 0x1ab managerTalk +428 0x1ac MANAGER_DRAFT +429 0x1ad manOfTheMatch +430 0x1ae manufacturer +431 0x1af marketData +432 0x1b0 marketDataMaxPrice +433 0x1b1 marketDataMinPrice +434 0x1b2 marketPriceLimitValues +435 0x1b3 maskDefId +436 0x1b4 matchCoins +437 0x1b5 matchCoinMultipliers +438 0x1b6 matchCoinPartials +439 0x1b7 matchDifficulty +440 0x1b8 matches +441 0x1b9 matchId +442 0x1ba matchlength +443 0x1bb matchLengthMin +444 0x1bc matchParamsKeyValues +445 0x1bd matchReportId +446 0x1be matchUnfinishedTime +447 0x1bf maxAuctionsAllowed +448 0x1c0 maximumTradePileSize +449 0x1c1 maxMatches +450 0x1c2 maxPrice +451 0x1c3 maxSize +452 0x1c4 maxWins +453 0x1c5 message +454 0x1c6 messagesAvailable +455 0x1c7 messageList +456 0x1c8 messagesRead +457 0x1c9 minMatchesToRank +458 0x1ca minPrice +459 0x1cb misc +460 0x1cc morale +461 0x1cd mtxEnabled +462 0x1ce mtxEnabled_JP +463 0x1cf myRating +464 0x1d0 name +465 0x1d1 nation +466 0x1d2 nationId +467 0x1d3 NationId +468 0x1d4 nationCount +469 0x1d5 negMods +470 0x1d6 Negotiation +471 0x1d7 newcards +472 0x1d8 nextReset +473 0x1d9 none +474 0x1da notification +475 0x1db NPTicket +476 0x1dc NPTicketLength +477 0x1dd numberItems +478 0x1de numEndMatchRetriesAllowed +479 0x1df numMatches +480 0x1e0 numRounds +481 0x1e1 numTeams +482 0x1e2 objectives +483 0x1e3 objectivesForCurrentUser +484 0x1e4 offer +485 0x1e5 offered +486 0x1e6 offers +487 0x1e7 offerState +488 0x1e8 offline +489 0x1e9 OFFLINE +490 0x1ea offlineDivision +491 0x1eb offlinetrophy +492 0x1ec offlineSeason +493 0x1ed offlineTournyProgress +494 0x1ee offset +495 0x1ef offsides +496 0x1f0 online +497 0x1f1 ONLINE +498 0x1f2 onlineELORating +499 0x1f3 onlineRatedUser +500 0x1f4 accountResetCount +501 0x1f5 onlinetrophy +502 0x1f6 onlineSeason +503 0x1f7 onlineTournyProgress +504 0x1f8 onSale +505 0x1f9 opponent +506 0x1fa opponentBadgeId +507 0x1fb opponentGoals +508 0x1fc opponentId +509 0x1fd opponentPenaltyScore +510 0x1fe opponentPersonaId +511 0x1ff opponentRating +512 0x200 opponentScore +513 0x201 opponentTeamId +514 0x202 opponentUserPoints +515 0x203 options +516 0x204 OR +517 0x205 originalPrice +518 0x206 outbid +519 0x207 owners +520 0x208 ownGoals +521 0x209 pace +522 0x20a pack +523 0x20b packId +524 0x20c packContentInfo +525 0x20d packList +526 0x20e packOpeningAnimationEnabled +527 0x20f packType +528 0x210 parent +529 0x211 participationAward +530 0x212 passAccuracyTotal +531 0x213 passesCompleted +532 0x214 passing +533 0x215 passingPercentage +534 0x216 penaltyGoals +535 0x217 penaltyScore +536 0x218 period +537 0x219 permutations +538 0x21a persona +539 0x21b personaId +540 0x21c phoneNumber +541 0x21d physio +542 0x21e physioArm +543 0x21f physioBack +544 0x220 physioFoot +545 0x221 physioHead +546 0x222 physioHip +547 0x223 physioLeg +548 0x224 physioShoudler +549 0x225 PICK_DIFFICULTY +550 0x226 pile +551 0x227 pileSizeClientData +552 0x228 pileType +553 0x229 PlayAFriendPractice +554 0x22a platform +555 0x22b player +556 0x22c Player +557 0x22d PLAYER +558 0x22e playerAttrBoostLevel +559 0x22f playerCount +560 0x230 playerdefender +561 0x231 playerforward +562 0x232 playerLevel +563 0x233 playermidfielder +564 0x234 playerOne +565 0x235 playerQuality +566 0x236 playerRarity +567 0x237 playerRequirements +568 0x238 players +569 0x239 playersBronze +570 0x23a playersGold +571 0x23b playersSilver +572 0x23c playerTwo +573 0x23d playerType +574 0x23e PLAYER_DRAFT +575 0x23f playStyle +576 0x240 points +577 0x241 POINTS +578 0x242 pointsPackStoreEnabled +579 0x243 position +580 0x244 positionid +581 0x245 positionId +582 0x246 posMods +583 0x247 possessionPercentage +584 0x248 possesionTotal +585 0x249 possessionTotal +586 0x24a preferredPosition +587 0x24b preOrderPacks +588 0x24c previousChampionEvents +589 0x24d price +590 0x24e primaryBadgeId +591 0x24f primaryPersonaId +592 0x250 priority +593 0x251 prize +594 0x252 prizeLevel +595 0x253 prizeSet +596 0x254 prizesInError +597 0x255 prizeTiers +598 0x256 PRO +599 0x257 processingStateEnabled +600 0x258 productId +601 0x259 PROFESSIONAL +602 0x25a progressdata +603 0x25b progressData +604 0x25c progressDataVersion +605 0x25d PROMOTION +606 0x25e promoUpdate +607 0x25f public +608 0x260 purchase +609 0x261 purchaseCount +610 0x262 purchased +611 0x263 purchasedItems +612 0x264 purchasedPackId +613 0x265 purchaseLimit +614 0x266 purchasePackType +615 0x267 qualified +616 0x268 qualifiedChampionLeagueIds +617 0x269 qualifiedChampionEventId +618 0x26a qualifierTournaments +619 0x26b quantity +620 0x26c question +621 0x26d rank +622 0x26e ranking +623 0x26f Rare +624 0x270 rare +625 0x271 rareflag +626 0x272 rarePlayers +627 0x273 rareQuantity +628 0x274 rating +629 0x275 Rating +630 0x276 read +631 0x277 READY_FOR_MATCH +632 0x278 READY_FOR_REWARDS +633 0x279 reason +634 0x27a recoverAttempts +635 0x27b recoveredPacks +636 0x27c redCards +637 0x27d RELEGATION +638 0x27e reliability +639 0x27f remainingMatches +640 0x280 repeatable +641 0x281 reportIdEnabled +642 0x282 requestBody +643 0x283 reset +644 0x284 resetBonus +645 0x285 responseBody +646 0x286 responseHeader +647 0x287 resourceId +648 0x288 result +649 0x289 returningUserRewards +650 0x28a returningUserRewardsScreenEnabled +651 0x28b rewardMult +652 0x28c rewardMultiplier +653 0x28d rewardQuantity +654 0x28e rewardType +655 0x28f rewardValue +656 0x290 round +657 0x291 roundId +658 0x292 rounds +659 0x293 roundsInfo +660 0x294 rule +661 0x295 sameClubCount +662 0x296 sameLeagueCount +663 0x297 sameNationCount +664 0x298 saleType +665 0x299 scope +666 0x29a score +667 0x29b scoredGoals +668 0x29c scrollDelay +669 0x29d SINGLE_PLAYER +670 0x29e seasonCoins +671 0x29f seasonCompleted +672 0x2a0 seasonData +673 0x2a1 seasonEndResult +674 0x2a2 seasonId +675 0x2a3 seasonGamesDraw +676 0x2a4 seasonGamesLost +677 0x2a5 seasonGamesWon +678 0x2a6 seasonOnlineDraws +679 0x2a7 seasonOnlineLosses +680 0x2a8 seasonOnlineWins +681 0x2a9 seasonsPassAccuracyTotal +682 0x2aa seasonsPossesionTotal +683 0x2ab seasonPromotions +684 0x2ac seasonRelegations +685 0x2ad seasons +686 0x2ae seasonTitlesWon +687 0x2af seasonConcededGoals +688 0x2b0 seasonsScoredGoals +689 0x2b1 seasonWins +690 0x2b2 secondsPlayed +691 0x2b3 secondsUntilEnd +692 0x2b4 secondsUntilStart +693 0x2b5 selection +694 0x2b6 sellerEstablished +695 0x2b7 sellerName +696 0x2b8 selling +697 0x2b9 SEMIPRO +698 0x2ba sequence +699 0x2bb sessionCoinsBankBalance +700 0x2bc setId +701 0x2bd setImageId +702 0x2be sets +703 0x2bf settings +704 0x2c0 shooting +705 0x2c1 shots +706 0x2c2 shotsOnTarget +707 0x2c3 silhouetteName +708 0x2c4 silName +709 0x2c5 silver +710 0x2c6 silverQuantity +711 0x2c7 sizeBeforeEncode +712 0x2c8 slotIndex +713 0x2c9 sold +714 0x2ca sort +715 0x2cb sortPriority +716 0x2cc source +717 0x2cd squad +718 0x2ce squadActives +719 0x2cf squadBuildingSetsClientData +720 0x2d0 squadBuildingSetsGracePeriodMinutes +721 0x2d1 squadChallenge +722 0x2d2 squadId +723 0x2d3 squadName +724 0x2d4 squadList +725 0x2d5 squadState +726 0x2d6 squadType +727 0x2d7 stadia +728 0x2d8 stadium +729 0x2d9 Stadium +730 0x2da StadiumId +731 0x2db stadiumid +732 0x2dc staff +733 0x2dd staffManager +734 0x2de staffHeadCoach +735 0x2df staffFitnessCoach +736 0x2e0 staffGKCoach +737 0x2e1 staffPhysio +738 0x2e2 starRating +739 0x2e3 start +740 0x2e4 startDateTime +741 0x2e5 starterPack +742 0x2e6 startingBid +743 0x2e7 starttime +744 0x2e8 startTime +745 0x2e9 stat +746 0x2ea statBonus +747 0x2eb state +748 0x2ec stats +749 0x2ed statsList +750 0x2ee stateParam1 +751 0x2ef stateParam2 +752 0x2f0 status +753 0x2f1 storeEnabled +754 0x2f2 storeEnabled_JP +755 0x2f3 storyModeRewardEnabled +756 0x2f4 coinsProcessed +757 0x2f5 championsScheduleViewPeriodInMinutes +758 0x2f6 string +759 0x2f7 style +760 0x2f8 styleAttribMods +761 0x2f9 subtype +762 0x2fa success +763 0x2fb SUCCESS +764 0x2fc successfulTackles +765 0x2fd suspension +766 0x2fe swap +767 0x2ff swapPlayerDefIds +768 0x300 tagged +769 0x301 taggedByProduction +770 0x302 taggedByUser +771 0x303 TalkRating +772 0x304 team +773 0x305 teamId +774 0x306 teamid +775 0x307 teamChemistry +776 0x308 teamOfTournamentWinner +777 0x309 teamRating +778 0x30a teamRating1To100 +779 0x30b text +780 0x30c tfaData +781 0x30d tFAEnabled +782 0x30e tFAResendIntervalSecs +783 0x30f enableFloatPointSquadRating +784 0x310 enableLegacyYearInfoInItemResourceId +785 0x311 tfaState +786 0x312 thresholdPoint +787 0x313 tiebreak +788 0x314 tiebreaker +789 0x315 tier +790 0x316 tierEnd +791 0x317 tierLevel +792 0x318 tierStart +793 0x319 tierType +794 0x31a timesCompleted +795 0x31b timestamp +796 0x31c timesWon +797 0x31d timeUntilEnd +798 0x31e timeUntilStart +799 0x31f titleHolderPersonaId +800 0x320 tokenRedemptionEnabled +801 0x321 token +802 0x322 tokens +803 0x323 TOO_MANY_SEASONS +804 0x324 TOO_MANY_TOURNAMENTS +805 0x325 total +806 0x326 totalCredits +807 0x327 totalGames +808 0x328 tournament +809 0x329 tournamentCoins +810 0x32a tournamentData +811 0x32b tournamentId +812 0x32c tournamentProgress +813 0x32d tournamentQuitEnabled +814 0x32e tournamentTrophyRound +815 0x32f tournamentType +816 0x330 trade +817 0x331 tradeId +818 0x332 tradepile +819 0x333 tradePile +820 0x334 trader +821 0x335 tradeState +822 0x336 tradingEnabled +823 0x337 training +824 0x338 trainingItem +825 0x339 transaction +826 0x33a transactionId +827 0x33b transferValue +828 0x33c treeType +829 0x33d triesMax +830 0x33e triesPeriod +831 0x33f triesRemaining +832 0x340 trophies +833 0x341 trophiesFeaturedOffline +834 0x342 trophiesFeaturedOnline +835 0x343 trophiesOffline +836 0x344 trophiesOnline +837 0x345 trophiesSeasonOffline +838 0x346 trophiesSeasonOnline +839 0x347 trophy +840 0x348 trophyId +841 0x349 trophyResourceId +842 0x34a trophyUseCount +843 0x34b trophyUserCount +844 0x34c TROPHY_FEATURED_OFFLINE +845 0x34d TROPHY_FEATURED_ONLINE +846 0x34e TROPHY_OFFLINE +847 0x34f TROPHY_ONLINE +848 0x350 true +849 0x351 trusted +850 0x352 tutorial +851 0x353 tutorialClientData +852 0x354 type +853 0x355 typeValue +854 0x356 ULTIMATE +855 0x357 unclaimedPrizesChampionEvents +856 0x358 uniqueId +857 0x359 unlock +858 0x35a UNLOCKED +859 0x35b unlockreq +860 0x35c unlocks +861 0x35d unopened +862 0x35e unopenedPacks +863 0x35f untilEndSeconds +864 0x360 untilStartSeconds +865 0x361 untradeable +866 0x362 untradeableCount +867 0x363 updateTime +868 0x364 upcomingChampionEvents +869 0x365 uri +870 0x366 url +871 0x367 useAuth +872 0x368 useCount +873 0x369 useCredits +874 0x36a useDefaultImage +875 0x36b usePreOrder +876 0x36c user +877 0x36d userData +878 0x36e userHubClientData +879 0x36f userId +880 0x370 userInfo +881 0x371 userPoints +882 0x372 userRegistration +883 0x373 userStats +884 0x374 userTierLevel +885 0x375 useTime +886 0x376 valid +887 0x377 value +888 0x378 Value +889 0x379 values +890 0x37a view +891 0x37b visEnd +892 0x37c visEndDays +893 0x37d visible +894 0x37e visStart +895 0x37f visStartDays +896 0x380 watched +897 0x381 watchlist +898 0x382 win +899 0x383 winForm +900 0x384 winning +901 0x385 winsRemaining +902 0x386 WORLDCLASS +903 0x387 won +904 0x388 XTicket +905 0x389 year +906 0x38a yellowCards diff --git a/fifa17-recon/tools/atomdump.py b/fifa17-recon/tools/atomdump.py new file mode 100644 index 0000000..2a9211e --- /dev/null +++ b/fifa17-recon/tools/atomdump.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# Dump the FUT atom name table (atom index -> key string) from CardsDLL. +# Table at VA 0x1802d2760 is an array of char* pointers into .rdata. +import struct, sys + +DLL = "/tmp/fut/cardsdll.dll" +data = open(DLL, "rb").read() + +# (VA_start, size, file_off) from objdump -h +SECTIONS = [ + (0x180001000, 0x1e3f62, 0x400), # .text + (0x1801e5000, 0xa4094, 0x1e4400), # .rdata + (0x18028a000, 0x54000, 0x288600), # .data +] + +def va_to_off(va): + for start, size, off in SECTIONS: + if start <= va < start + size: + return off + (va - start) + return None + +def read_cstr(va, maxlen=128): + off = va_to_off(va) + if off is None: + return None + end = data.find(b"\x00", off, off + maxlen) + if end < 0: + return None + try: + return data[off:end].decode("ascii") + except UnicodeDecodeError: + return None + +TABLE_VA = 0x1802d2760 +off = va_to_off(TABLE_VA) +atoms = {} +for i in range(0, 1200): + ptr = struct.unpack_from(" 40 and all(struct.unpack_from(" 1024x768, client-side.) + + [(k, "1") for k in ( + "storeEnabled", "cardPackStoreEnabled", "pointsPackStoreEnabled", + "cardPackStoreEnabled_JP", "coinEnabled", "coinEnabled_JP", + "IS_STORE_ENABLED", "IS_COIN_PURCHASABLE", "IS_FIFAPOINT_AVAILABLE", + "IS_FIFAPOINT_PURCHASABLE", "IS_EASTORE_SERVICE_READY", + "COINS_PURCHASE_ENABLED", "POINTS_PURCHASE_ENABLED", "MONEY_PURCHASE_ENABLED", + )] # NOTE: do NOT advertise itemDbVersion/checkServerDbVersion here or in any # response -- proven inert (wf_96b6c0c5): they are JSON field names that route # to the value-SKIP handler 0x180135ff0, never compared. See docs/CARD_SYSTEM.md. diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index dc038bf..25cf9b0 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -154,6 +154,11 @@ ROUTES = [ # ---- store / packs (match regardless of /ut/game vs /ut/v2/game prefix) ---- (re.compile(r"/store/purchasegroup"), lambda m, h: store_catalog(h)), (re.compile(r"/store/transaction"), lambda m, h: store_buy(h)), + # ut/v2/game/fifa17/store = FutStorePackQuantities ELIGIBILITY GATE, not a + # quantity list. deser 0x1801758c0 reads one key "result" (atom 0x288); the + # store screen shows "not available" unless this is SUCCESS. (ENDPOINT_MAP + # store §2.) Bare /store only -- purchasegroup/transaction matched above. + (re.compile(r"/store(\?|$)"), lambda m, h: (200, {"result": "SUCCESS"})), (re.compile(r"/purchased"), lambda m, h: purchased_items(h)), (re.compile(r"^/ut/auth"), lambda m, h: (200, auth_body())), (re.compile(r"^/ut/delete/auth"), lambda m, h: (200, {})), @@ -214,10 +219,19 @@ def store_catalog(h): # {name,funds,finalFunds}; display name is "description". (wf a245577b — # {purchaseGroups:...} + packId/price were all unknown atoms => empty => "not # available".) quantity:0 => unlimited. + # Parse format is verified correct ("purchase" array, per-pack 0x18013af30). + # The store still rejected minimal packs -> a pack must be COMPLETE to count as + # valid: content info + BOTH a coins price (currencies) and a FIFA-Points price + # (extPrice {finalPrice,originalPrice} -> {"mtx":N}). packs = [] for p in PACK_CATALOG: gold = p["gold"] + mtx = max(1, p["price"] // 100) packs.append({ + # assetId (atom 0x23) is the REAL pack identity the deser 0x18013af30 + # reads (ENDPOINT_MAP store §). id/packType/quantity/saleType/isPremium + # are all unknown atoms -> SKIP (harmless no-ops, kept for readability). + "assetId": p["id"], "id": p["id"], "packType": "GOLD" if gold else "BRONZE", "description": p["name"], @@ -228,11 +242,14 @@ def store_catalog(h): "saleType": "PERMANENT", "sortPriority": p["id"] - 100, "currencies": [{"name": "coins", "funds": p["price"], "finalFunds": p["price"]}], + # extPrice inner keys are amount(0x1b)/currency(0xc4), NOT mtx (skipped). + "extPrice": {"finalPrice": {"amount": mtx, "currency": "fifapoints"}, + "originalPrice": {"amount": mtx, "currency": "fifapoints"}}, "packContentInfo": { "bronzeQuantity": 0 if gold else p["count"], "silverQuantity": 0, "goldQuantity": p["count"] if gold else 0, - "rareQuantity": p["count"] if gold else 0, + "rareQuantity": 1 if gold else 0, "itemQuantity": p["count"], }, })