274838cc2e
Decoded the vocabulary from the binary rather than trusting a case count: FUN_18012ec50 is `cmp ecx,0x1d` plus a 30-entry jump table at 0x18012ed9c, each case `mov ecx,<atom>; jmp <atom->string>`. Resolving those atoms against fut_atoms.tsv yields the exact token list, and it matches club_type_filter one-for-one — 30 implemented, none missing, none invented. That is worth a test rather than a note. A MISSING arm answers a real tab with unsupported_type and an empty screen; an INVENTED arm is worse, because it is dead code that looks like coverage. Mutation-checked: renaming the leaguelogos arm fails the test. Two facts fall out that were previously guesswork. There is no `playergoalkeeper` token — the client has only DEF/MID/FWD tabs — so a goalkeeper appearing under playerdefender is CORRECT and not a filter bug, which I had flagged as suspicious while sweeping. And `healing`/`contract`/`training` exist as ?type= arms even though consumables have their own route. Also completes the last unapplied item of plan section 7: the full vocabulary is now written into ENDPOINT_MAP.md with how it was derived.
1608 lines
96 KiB
Markdown
1608 lines
96 KiB
Markdown
# FIFA 17 FUT — Complete Endpoint Map (clean-room)
|
||
|
||
Status: 2026-08-02. Derived **entirely** from binaries we own (`CardsDLL_Win64_retail.dll`,
|
||
image base `0x180000000`) plus our own running client. **No leaked EA source used.**
|
||
|
||
This is the spec for rebuilding FIFA 17 Ultimate Team fully offline (and the Rosetta for
|
||
porting to FIFA 23). It maps the **complete FUT API surface** — ~100 `FutXServerResponse`
|
||
types the client can parse — with field-level detail for every endpoint, prioritizing the
|
||
core playable loop. Target: port into Rust `openfut-core` behind a FIFA-17 bridge.
|
||
|
||
## How this was produced (methodology)
|
||
|
||
- **Master atom dictionary**: dumped the name table at `0x1802d2760` straight from the DLL
|
||
→ **907 atoms** (`atom int → JSON key name`). Every SAX deserializer dispatches on these.
|
||
Table extractor: `tools/atomdump.py` (regenerable); output `atoms.tsv`.
|
||
- **Deserializer locator (RECIPE)**: struct-name string → `VA = 0x1801e5000 + (fileoff − 0x1e4400)`
|
||
→ `.text` `lea r8` xref → the response object's vtable (**deserializer at vtable slot +0x08**)
|
||
→ walk the atom `cmp`/`sub`/`dec`+`je` ladder → translate atoms via `atoms.tsv` → classify
|
||
each field by the leaf getter it calls.
|
||
- **Parser internals** (shared by ALL responses): key → FNV-1a (`0x180180d00`, seed
|
||
`0x811c9dc5`) → atom → jump-table dispatch; unknown atom → value-SKIP `0x180135ff0`
|
||
(**extra/unknown keys are always safe**). Leaf getters: int/num `0x1801c79d0`, bool
|
||
`0x1801c7620`, string `0x1801c7aa0`. **Type fidelity is mandatory**: feeding a scalar
|
||
getter an object/array desyncs the SAX reader → infinite tokenizer spin (freeze at
|
||
`0x1801c7f1a`). Freeze-risk (must-be-object/array) fields are flagged per struct below.
|
||
- **Ladder caveat**: atom dispatch is a jump-ladder of *running-sum* `sub`/`dec` chains — the
|
||
real atom is the accumulated sum, not the raw immediate. Field schemas are HIGH confidence;
|
||
**HTTP verbs are often inferred** (the verb table isn't statically recoverable) — LOW
|
||
confidence unless corroborated by `/tmp/utas_server.log`.
|
||
|
||
## Reversal coverage (7 feature groups, ~100 structs)
|
||
|
||
| Group | Structs | Fully reversed | Section |
|
||
|---|---|---|---|
|
||
| Transfer Market / Auction House | 12 | 9 | [Transfer Market](#transfer-market--auction-house) |
|
||
| Squad Building Challenges (SBC) | 8 | 5 | [SBC](#squad-building-challenges-sbc) |
|
||
| FUT Draft | 7 | 5 | [Draft](#fut-draft) |
|
||
| Match / Seasons / Tournaments | 19 | 9 | [Seasons](#match--seasons--tournaments) |
|
||
| Club / Cards / Consumables | 16 | 15 | [Club](#club--cards--consumables) |
|
||
| Store / Packs / Purchases | 6 | 5 | [Store](#store--packs--purchases) |
|
||
| User / Hub / Settings / Objectives / LB | 32 | 7 deep + 11 partial | [User](#user--hub--settings--objectives--leaderboards) |
|
||
|
||
"Fully reversed" = top-level field schema + types decoded HIGH-confidence. Nested card/squad
|
||
elements reuse the shared item (`0x18013fe00`) / squad (`0x18013d1f0`) parsers documented in
|
||
`CARD_SYSTEM.md`. Every struct in the sections has its deserializer VA, key list, and a
|
||
minimal known-good JSON — including ack-only (`{}`) responses.
|
||
|
||
## Shared record parsers (reused across groups)
|
||
|
||
| VA | Record | Used by |
|
||
|---|---|---|
|
||
| `0x18013fe00` | ITEM / card element (`itemData`) | club, squad, packs, purchased, market, draft, SBC |
|
||
| `0x18013d1f0` | full SQUAD object | squad, draft, SBC squad-challenge |
|
||
| `0x18013e410` | auction/trade record (`auctionInfo[]` element) | market search/watch/tradepile |
|
||
| `0x18013e7f0` | IS-list body `{auctionInfo,credits,total,duplicateItemIdList}` | market |
|
||
| `0x180135ff0` | value-SKIP (unknown keys) | all |
|
||
|
||
## ★ Highest-value findings (actionable now)
|
||
|
||
1. **Store "not available" — the `ut/v2/store` eligibility gate.** `FutStorePackQuantitiesServerResponse`
|
||
(deser `0x1801758c0`) reads exactly one key `result` (atom `0x288`) → must be
|
||
**`{"result":"SUCCESS"}`** (other enum values gate the store closed:
|
||
`TOO_MANY_TOURNAMENTS`/`LOCKED_PERMANENT`/`LOCKED_RETRY`/`LOCKED_TROPHIES`). Pair with the
|
||
Blaze purchase flags + the `>1024×768` `GetSystemMetrics` resolution check. See Store §.
|
||
2. **Match rewards live in `FutDestroyMatchServerResponse`** (`0x180121b60`), NOT `FutPlayGame`
|
||
(ack-only). Coin fields at struct offsets `0x28–0x48`: `allCoins`/`matchCoins`/`seasonCoins`/
|
||
`tournamentCoins`/`coins`/`boostConis`(sic)/`participationAward`. See Seasons §.
|
||
3. **`GetClubInfo` returns `user` (club-user stat array), not `itemData`.** Rendered cards come
|
||
from **`FutViewCardsServerResponse` (`0x1801293d0`) on `ut/%s/item`** — explains why the
|
||
current `/club` `itemData` is SKIP'd yet cards still render. See Club §.
|
||
4. **SBC requirements = generic `{eligibilityKey,eligibilityOperation,eligibilityValue}` triples**
|
||
in an `elgReq` array — the key to modding challenges. See SBC §.
|
||
5. **Objectives = ManagerQuests**, client-driven with **no ServerResponse struct / no route**;
|
||
gated by `enableObjectives` in settings. See User §.
|
||
|
||
## Catalog corrections to the current backend (`utas_server.py` / `fut_store.py`)
|
||
|
||
- Store `extPrice` inner keys are **`amount`/`currency`**, not `mtx`; pack identity is
|
||
**`assetId`** (0x23), not `id` (harmless SKIPs otherwise).
|
||
- `ut/v2/store` must return `{"result":"SUCCESS"}` (currently unhandled → contributes to store error).
|
||
- ~~`GetUserMassInfo` top-level wrapper key is **`user`**~~ — **WRONG, corrected 2026-08-03.**
|
||
The body is FLAT: `userInfo`(0x370)/`squad`(0x2cd)/`settings`(0x2bf)/`userData`(0x36d) are
|
||
top-level keys. `user`(0x36c) only appears nested inside `clubUser`. Now served populated.
|
||
- No-op deserializers (bare `ret`) where `{}` always suffices: ChangeClubName, ActivateCard,
|
||
SignLoanPlayer, and most ack responses.
|
||
|
||
---
|
||
|
||
# Deep sections
|
||
## Transfer Market / Auction House
|
||
|
||
Clean-room RE of the FIFA 17 CardsDLL (`cardsdll.dll`, image base `0x180000000`)
|
||
Internet-Shopping (IS = auction house / transfer market) response deserializers,
|
||
via the RECIPE.md method (name-string → `.text` `lea r8` xref → atom dispatch →
|
||
`atoms.tsv`). All VAs are static CardsDLL VAs.
|
||
|
||
### Shared parsers (the spine of every IS response)
|
||
|
||
| VA | Role |
|
||
|---|---|
|
||
| `0x18013e410` | **Core auction/trade RECORD deserializer** (one item in `auctionInfo[]`). Reversed deeply below. |
|
||
| `0x18013e7f0` | **Shared IS-list response body** — `{auctionInfo:[record…], credits, total, duplicateItemIdList}`. Search / WatchList / TradePile all tail-delegate to it. |
|
||
| `0x18013fe00` | Shared ITEM/card element deserializer (the `itemData` object; same one used by club/squad/pack). |
|
||
| `0x180135ff0` | value-SKIP (unknown atoms — safe to send extra keys). |
|
||
| Leaf getters | int/number `0x1801c79d0` · bool `0x1801c7620` · string `0x1801c7aa0`. |
|
||
|
||
Scalar-convert helpers seen: `0x1800d7b30` (num→int32), `0x1800d7b50` (num→bool/byte),
|
||
`0x180166380` (str→bidState enum), `0x180166bd0` (str→tradeState enum),
|
||
`0x180008120`/`0x180008020` (bounded string copy).
|
||
|
||
---
|
||
|
||
### ★ Core record: `auctionInfo[]` element — deserializer `0x18013e410` (confidence: HIGH)
|
||
|
||
Key dispatch (atom in `edi`, FNV-1a via `0x180180d00`; ordered by atom value):
|
||
|
||
| atom | key | JSON type | getter | notes |
|
||
|---|---|---|---|---|
|
||
| `0x57` | `bidState` | string (enum) | `0x1801c7aa0`→`0x180166380` | e.g. `none`/`highest`/`outbid`/`buyNow` |
|
||
| `0x65` | `buyNowPrice` | int | `0x1801c79d0`→`0x1800d7b30` | |
|
||
| `0xc1` | `currentBid` | int | `0x1801c79d0`→`0x1800d7b30` | current highest bid |
|
||
| `0x116` | `expires` | int | `0x1801c79d0` (QWORD) | **seconds remaining** (not epoch) |
|
||
| `0x16b` | `itemData` | **nested OBJECT** | `0x18013fe00` | the card. **FREEZE-RISK: must be an object**, feeding a scalar desyncs the SAX reader (spin at `0x1801c7f1a`) |
|
||
| `0x2b6` | `sellerEstablished` | int | `0x1801c79d0`→`0x1800d7b30` | |
|
||
| `0x2b7` | `sellerName` | string | `0x1801c7aa0`→`0x180008120` | bounded copy, max 0x1e=30 chars |
|
||
| `0x2e6` | `startingBid` | int | `0x1801c79d0`→`0x1800d7b30` | |
|
||
| `0x2f4` | `coinsProcessed` | int/bool | `0x1801c79d0`→`0x1800d7b50` | truncated to byte |
|
||
| `0x331` | `tradeId` | int (64-bit) | `0x1801c79d0` (QWORD) | |
|
||
| `0x335` | `tradeState` | string (enum) | `0x1801c7aa0`→`0x180166bd0` | `active`/`closed`/`expired` |
|
||
| `0x380` | `watched` | bool | `0x1801c7620` | true if on watch list |
|
||
|
||
Not present at record level: `bid` (0x55), `seller` id, `offers`. Extra keys are
|
||
skipped safely.
|
||
|
||
Minimal known-good record:
|
||
```json
|
||
{
|
||
"tradeId": 100000001,
|
||
"itemData": { "id": 100000001, "resourceId": 1610612736, "assetId": 20801,
|
||
"itemType": "player", "rating": 94, "preferredPosition": "ST",
|
||
"untradeable": false, "itemState": "free" },
|
||
"tradeState": "active",
|
||
"buyNowPrice": 3000,
|
||
"startingBid": 1500,
|
||
"currentBid": 0,
|
||
"bidState": "none",
|
||
"expires": 3600,
|
||
"sellerName": "OpenFUT",
|
||
"sellerEstablished": 1,
|
||
"watched": false,
|
||
"coinsProcessed": 0
|
||
}
|
||
```
|
||
|
||
### Shared IS-list body — `0x18013e7f0` (confidence: HIGH)
|
||
|
||
| atom | key | type | getter |
|
||
|---|---|---|---|
|
||
| `0x35` | `auctionInfo` | **array of records** (`0x18013e410` in a loop) | FREEZE-RISK: must be array |
|
||
| `0xc0` | `credits` | int | `0x1801c79d0`→`0x1800d7af0`→object setter |
|
||
| `0xec` | `duplicateItemIdList` | nested (array) `0x180138e10` | FREEZE-RISK: must be array/obj |
|
||
| `0x325` | `total` | int | `0x1801c79d0`→`0x1800d7b30` (→ `obj+0x60`) |
|
||
|
||
---
|
||
|
||
### The 12 response structs
|
||
|
||
All 12 are **GAPs** — `utas_server.py` currently has **no** `auctionhouse` / `trade`
|
||
/ `tradePile` / `watchList` / `marketdata` routes. The nested `itemData` card is the
|
||
one piece already served (by the existing `/club` + `item_def` machinery in
|
||
`utas_server.py`), so it can be reused verbatim inside these responses.
|
||
|
||
Path template `%s = "game/fifa17"`. Methods inferred from struct verb + endpoint.
|
||
|
||
| # | Struct | Deser VA | Method + Path (inferred) | Schema | Conf |
|
||
|---|---|---|---|---|---|
|
||
| 1 | **FutISSearchServerResponse** | `0x180163420` → `0x18013e7f0` | `GET ut/%s/auctionhouse?...` (market search) | `{auctionInfo:[record], credits, total, duplicateItemIdList}` | HIGH |
|
||
| 2 | **FutISStartServerResponse** | `0x180165d70` (dispatch `…e98`) | `POST ut/%s/auctionhouse` (list item for sale) | `{id:int}` (new tradeId) — only atom `0x15c`=`id` | HIGH |
|
||
| 3 | **FutISViewTradeServerResponse** | `0x1801644d0` (rec call `…461f`) | `GET ut/%s/trade/{id}` (view one auction) | `{auctionInfo:[record], credits}` — atoms `0x35`,`0xc0` | HIGH |
|
||
| 4 | **FutISWatchListServerResponse** | `0x180166130` → `0x18013e7f0` | `GET ut/%s/watchList` | `{auctionInfo:[record], credits, total}` | HIGH |
|
||
| 5 | **FutISWatchTradeServerResponse** | `0x180164cd0` | `PUT ut/%s/watchList` (add to watch list) | ack; parses per-item status enum ladder (`0x28/0x14/0x11/0x24`, `-1`), no atom-keyed body → `{}` known-good | MED (partial) |
|
||
| 6 | **FutISOfferTradeServerResponse** | `0x180165410` (rec call `…56cf`) | `POST ut/%s/trade/{id}/bid` (place bid) | `{auctionInfo:[record], credits}` (echoes updated auction) | HIGH |
|
||
| 7 | **FutISRemoveTradeServerResponse** | `0x1801648d0` | `DELETE ut/delete/%s/trade/{id}` (clear from trade pile) | ack, no atom-keyed body → `{}` | MED |
|
||
| 8 | **FutISRemoveWatchServerResponse** | `0x1801659f0` | `DELETE ut/delete/%s/watchList/{id}` | ack, no atom-keyed body → `{}` | MED |
|
||
| 9 | **FutGetTradePileServerResponse** | `0x180170810` → `0x18013e7f0` | `GET ut/%s/tradePile` | `{auctionInfo:[record], credits, total}` | HIGH |
|
||
| 10 | **FutRelistAllServerResponse** | `0x180164210` (req-ser `0x180164370`) | `PUT ut/%s/auctionhouse/relist` (relist all expired) | ack; response body minimal → `{}` (request-side serializer builds a tradeId list) | MED (partial) |
|
||
| 11 | **FutGetAuctionCountServerResponse** | `0x180163670` (dispatch `…83e`) | `GET ut/%s/auctionhouse` count (or `marketdata`) | `{count, maxAuctionsAllowed, offered, selling, sold}` — atoms `0xbc,0x1bf,0x1e5,0x2b8,0x2c9`, all int | HIGH |
|
||
| 12 | **FutGetSuggestedPricingServerResponse** | `0x180163bb0` (dispatch `…ffa`) | `GET ut/%s/marketdata?defId=…` (price bands) | `{defId, minPrice, maxPrice}` — atoms `0xcf,0x1c2,0x1ca`, all int | HIGH |
|
||
|
||
### Minimal known-good JSON per struct
|
||
|
||
```jsonc
|
||
// 1 FutISSearch (GET auctionhouse)
|
||
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1, "duplicateItemIdList": [] }
|
||
// 2 FutISStart (POST auctionhouse)
|
||
{ "id": 100000001 }
|
||
// 3 FutISViewTrade (GET trade/{id})
|
||
{ "auctionInfo": [ <record> ], "credits": 100000 }
|
||
// 4 FutISWatchList (GET watchList)
|
||
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
|
||
// 5 FutISWatchTrade (PUT watchList) -> {}
|
||
// 6 FutISOfferTrade (POST trade/{id}/bid)
|
||
{ "auctionInfo": [ <record> ], "credits": 99000 }
|
||
// 7 FutISRemoveTrade (DELETE trade) -> {}
|
||
// 8 FutISRemoveWatch (DELETE watchList) -> {}
|
||
// 9 FutGetTradePile (GET tradePile)
|
||
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
|
||
// 10 FutRelistAll (PUT auctionhouse/relist) -> {}
|
||
// 11 FutGetAuctionCount
|
||
{ "count": 0, "maxAuctionsAllowed": 100, "offered": 0, "selling": 0, "sold": 0 }
|
||
// 12 FutGetSuggestedPricing
|
||
{ "defId": 1610612736, "minPrice": 900, "maxPrice": 10000 }
|
||
```
|
||
|
||
### Freeze-risk summary (type fidelity is mandatory)
|
||
- `auctionInfo` → **array** (never object/scalar).
|
||
- `itemData` inside each record → **object** (the card; reuse `item_def`).
|
||
- `duplicateItemIdList` → **array of objects** (element deser `0x180138e10`: `itemId` 0x16d,
|
||
`duplicateItemId` 0xeb, `itemLoans` 0x16f, `duplicateItemLoans` 0xed). Not an int list.
|
||
`[]` is safe; a list of bare ints is a freeze. Control that this is not a misread:
|
||
`dreamSquads` 0xe9 in FutMoveCard genuinely IS a bare int array, parsed by a
|
||
`while (tok != 0xd)` loop calling the int getter with no inner object loop.
|
||
- `bidState`, `tradeState`, `sellerName` → **strings**.
|
||
- `credits`, `total`, `count`, `*Price`, `*Bid`, `expires`, `tradeId` → **numbers**.
|
||
- `watched` → **bool**.
|
||
Any scalar fed where an object/array is expected desyncs the tokenizer → hard
|
||
busy-loop freeze at `0x1801c7f1a` (same failure mode documented for
|
||
`userMassInfo`/squad in CARD_SYSTEM.md).
|
||
|
||
### Implementation notes for utas_server.py
|
||
- Add routes (all currently missing): `GET /auctionhouse` (search + count),
|
||
`POST /auctionhouse` (start), `PUT /auctionhouse/relist`, `GET/POST /trade`,
|
||
`GET /tradePile`, `GET/PUT /watchList`, `GET /marketdata`,
|
||
`DELETE (ut/delete)/trade`, `DELETE (ut/delete)/watchList`.
|
||
- Every list response shares one builder: `{auctionInfo, credits, total}`.
|
||
- `<record>.itemData` = exactly the object returned by `item_def(rid)`.
|
||
- Ack endpoints (5,7,8,10) can safely return `{}` (bodies are non-keyed/ack).
|
||
## Squad Building Challenges (SBC)
|
||
|
||
Reversed from FIFA17 `cardsdll.dll` (base `0x180000000`) using the deserializer-reversal
|
||
recipe: struct name → `.rdata` VA → `.text` xref (`lea r8`) → deserializer function →
|
||
atom `cmp`/`sub`/`dec` ladder + jump-table → `atoms.tsv` key names → leaf getter type.
|
||
Getter fingerprints: `0x1801c7aa0`=string, `0x1801c79d0`=int/number, `0x1801c7620`=bool,
|
||
`0x180008120`=fixed-buffer string-copy (string), `0x180135ff0`=value-SKIP (safe unknown key),
|
||
peek+loop via `0x1801c7f10`/`0x1801369f0`/`0x18015a750`=nested object/array.
|
||
|
||
**Endpoint family:** `ut/%s/sbs` (`%s` = `game/fifa17`), base string @ fileoff `0x21d908`.
|
||
The baseline `utas_server.py` has **no `sbs*` routes at all** → every endpoint below is a **GAP**.
|
||
|
||
### Path ↔ response bindings (proven via path-template `lea r8` adjacent to the response-struct `lea r8` in each request-builder)
|
||
|
||
| Response struct | Method* | Path (under `ut/game/fifa17/`) | Builder site |
|
||
|---|---|---|---|
|
||
| FutSBCTagSetsServerResponse | POST/PUT | `sbs/sets/tag` | `0x180153fd1`→`0x18015405d` |
|
||
| FutLoadSetTypesServerResponse | GET | `sbs/challenge/%d/squad` | `0x1801545b9`→`0x18015469d` |
|
||
| FutSBCStartChallengeResponse | POST | `sbs/challenge/%d` | `0x1801552a9`→`0x18015531d` |
|
||
| FutSBCSubmitChallengeServerResponse | POST/PUT | `sbs/challenge/%d` (submit) | `0x1801618ff`→`0x18016196d` |
|
||
| FutSBCSetDataServerResponse | GET | `sbs/sets` (set-list / by category) | builder `0x18016fa7d` (no distinct path lea) |
|
||
| FutSBCLoadCategoryDetailsServerResponse | GET | `sbs/sets` | `0x18017a9a5`→`0x18017aa3d` |
|
||
| FutLoadSetChallengesResponse | GET | `sbs/setId/%d/challenges` | `0x18017b979`→`0x18017b9ed` |
|
||
| FutSBCSaveSquadChallengeServerResponse | PUT | `sbs/challenge/%d/squad` | `0x18017ce6f`→`0x18017cedd` |
|
||
|
||
\*Method column is inferred from FUT16/17 REST conventions (builder method-enum not decoded); path bindings themselves are byte-proven.
|
||
|
||
---
|
||
|
||
### 1. FutLoadSetChallengesResponse — CONFIDENCE: HIGH ✅ fully reversed
|
||
Deserializer dispatch @ **`0x18017bbbb`** (key-iter `call 0x180141ee0`); wrapper/ctor `0x18017b9ed`.
|
||
Parses an **array of flat SET+CHALLENGE records** (each record carries both set-level and
|
||
challenge-level fields). Low atoms via `cmp r8d`/`sub` ladder; high atoms (`0x280–0x354`)
|
||
via byte+dword jump table at `0x18017c334`/`0x18017c310` (decoded from binary).
|
||
|
||
Method+path: **GET `ut/game/fifa17/sbs/setId/{setId}/challenges`**
|
||
|
||
| key | atom | type | notes / struct offset |
|
||
|---|---|---|---|
|
||
| challengeId | 0x074 | int | `[rdi+0x34]` |
|
||
| categoryId | 0x073 | int | `[rdi+0x38]` |
|
||
| index | 0x163 | int | `[rdi+0x3c]` |
|
||
| setId | 0x2bc | int | `[rdi+0x3c]`-cluster |
|
||
| type | 0x2c4 | **string→enum** | string-compared: `"OPEN_CHALLENGE"`→0, `"BRICK_CHALLENGE"`→2 → `[rdi+0x40]` |
|
||
| name | 0x1d0 | string | `[rdi+0x44]`, max 0x7f |
|
||
| description | 0x0d1 | string | `[rdi+0xc3]`, max 0xff |
|
||
| challengeImageId | 0x075 | string | `[rdi+0x1c8]`, max 0x64 |
|
||
| formation | 0x12b | **string** | mapped via `0x180166590` → `[rdi+0x22c]`. **FREEZE-RISK: must be a JSON string, not int** |
|
||
| endTime | 0x106 | int (epoch s) | `[rdi+0x240]`, day-scaled (cmp 0x16d=365) |
|
||
| repeatable | 0x280 | bool | `[rdi+0x239]` |
|
||
| trophyId | 0x2ee | int | `[rdi+0x1c4]` |
|
||
| status | 0x30e | string | hash-mapped state |
|
||
| timesCompleted | 0x322 | int | |
|
||
| squadId | 0x2dc | int | |
|
||
| tutorial | 0x33e | (nested) | |
|
||
| **awards** | 0x047 | **nested ARRAY** | reward objects (see shared records). **FREEZE-RISK: must be array** |
|
||
| **elgReq** | 0x0f7 | **nested ARRAY** | the SBC requirement/constraint list (see shared records). **FREEZE-RISK: must be array** |
|
||
|
||
All other high atoms (205 of them) route to the value-SKIP default → extra keys are safe.
|
||
|
||
Minimal known-good:
|
||
```json
|
||
[{"challengeId":1,"setId":1,"categoryId":0,"index":0,"type":"OPEN_CHALLENGE",
|
||
"name":"League Basics","description":"Submit 11 players.","challengeImageId":"sbc_challenge_image_1",
|
||
"formation":"f442","endTime":0,"repeatable":false,"trophyId":0,"status":"OPEN",
|
||
"timesCompleted":0,"awards":[],"elgReq":[]}]
|
||
```
|
||
|
||
### 2. FutSBCSubmitChallengeServerResponse — CONFIDENCE: HIGH ✅ fully reversed
|
||
Dispatch @ **`0x180161bda`**; ctor `0x18016196d`. Method+path: **POST `sbs/challenge/{challengeId}`** (submit).
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| challengeId | 0x074 | int |
|
||
| setId | 0x2bc | int |
|
||
| credits | 0x0c0 | int |
|
||
| preOrderPacks | 0x24b | int |
|
||
| recoveredPacks | 0x27b | int |
|
||
| grantedChallengeAwards | 0x14a | **nested array** (freeze-risk) |
|
||
| grantedSetAwards | 0x14b | **nested array** (freeze-risk) |
|
||
|
||
```json
|
||
{"challengeId":1,"setId":1,"credits":500,"preOrderPacks":0,"recoveredPacks":0,
|
||
"grantedChallengeAwards":[],"grantedSetAwards":[]}
|
||
```
|
||
|
||
### 3. FutSBCStartChallengeResponse / squadChallenge record — CONFIDENCE: HIGH ✅ fully reversed
|
||
Dispatch @ **`0x180155949`**; ctor `0x18015531d`. Method+path: **POST `sbs/challenge/{challengeId}`**.
|
||
Returns the **squadChallenge** record (the working squad for a challenge). This same record
|
||
parser is shared by FutLoadSetTypesServerResponse.
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| challengeId | 0x074 | int/nested |
|
||
| index | 0x163 | int |
|
||
| playerType | 0x23d | string |
|
||
| playerRequirements | 0x237 | **nested array** (per-slot constraint list; freeze-risk) |
|
||
| squad | 0x2cd | **nested array** of slot objects (freeze-risk) |
|
||
|
||
Slot object = `{index:int, playerType:string, playerRequirements:[...]}`.
|
||
|
||
```json
|
||
{"challengeId":1,"squad":[{"index":0,"playerType":"","playerRequirements":[]}]}
|
||
```
|
||
|
||
### 4. FutSBCLoadCategoryDetailsServerResponse — CONFIDENCE: HIGH ✅ fully reversed
|
||
Dispatch @ **`0x18017ac08`**; ctor `0x18017aa3d`. Method+path: **GET `sbs/sets`**.
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| categoryId | 0x073 | int |
|
||
| name | 0x1d0 | string |
|
||
| priority | 0x250 | int |
|
||
| sets | 0x2be | **nested array** of set records (freeze-risk) |
|
||
|
||
```json
|
||
{"categoryId":0,"name":"Challenges","priority":0,"sets":[]}
|
||
```
|
||
|
||
### 5. FutSBCSaveSquadChallengeServerResponse — CONFIDENCE: MEDIUM-HIGH ✅ fully reversed (minimal)
|
||
Dispatch @ **`0x18017d08a`**; ctor `0x18017cedd`. Method+path: **PUT `sbs/challenge/{challengeId}/squad`**.
|
||
Only one scalar field parsed:
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| id | 0x15c | int (saved squad id) |
|
||
|
||
```json
|
||
{"id":1}
|
||
```
|
||
|
||
### 6. FutLoadSetTypesServerResponse — CONFIDENCE: MEDIUM ⚠️ partial
|
||
Dispatch @ **`0x180154d69`**; ctor `0x18015469d`. Method+path: **GET `sbs/challenge/{challengeId}/squad`**.
|
||
Parses an **array of squadChallenge templates** ("set types" = squad-building slot templates),
|
||
same key set as the squadChallenge record: `{challengeId(0x074), index(0x163), playerType(0x23d str),
|
||
playerRequirements(0x237 nested), squad(0x2cd nested)}`. Outer top-level wrapper (array framing at
|
||
`0x180154a2d`) is callback-driven; exact top-level envelope key not resolved.
|
||
|
||
```json
|
||
[{"challengeId":1,"index":0,"playerType":"","playerRequirements":[],"squad":[]}]
|
||
```
|
||
|
||
### 7. FutSBCSetDataServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial
|
||
Dispatch @ **`0x18016ff2a`**; ctor `0x18016fa7d`. Method+path: **GET `sbs/sets`** (set-data / by category).
|
||
Top-level is a **callback array-parser** (`lea r8,[rsi+0x50]` vector-append at `0x18016fb5e`)
|
||
that appends **set records** parsed by the shared flat set-record deserializer; only one
|
||
top-level scalar was resolved:
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| reset | 0x283 | bool |
|
||
| (set records) | — | **nested array**; each element has the SET-level fields (setId, name, description, awards, repeatable, endTime, challenges, starRating, setImageId, timesCompleted, sortPriority) — reuse struct #1's field vocabulary |
|
||
|
||
```json
|
||
{"reset":false,"sets":[{"setId":1,"name":"League Basics","description":"",
|
||
"repeatable":false,"endTime":0,"awards":[],"challenges":[]}]}
|
||
```
|
||
|
||
### 8. FutSBCTagSetsServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial
|
||
Ctor `0x18015405d`; deserializer is a **callback-based array parser** (installs per-element
|
||
handler `0x180154280`, vector-append) rather than an atom `cmp` ladder — no scalar keys resolved
|
||
from the dispatch. Semantically it acknowledges a set-tag operation and returns the updated
|
||
tagged sets. Method+path: **POST/PUT `sbs/sets/tag`**.
|
||
|
||
```json
|
||
{"sets":[]}
|
||
```
|
||
|
||
---
|
||
|
||
### Shared record shapes
|
||
|
||
**elgReq — the SBC requirement/constraint list** (the tricky part; parsed in struct #1 @ `0x18017bd9d`,
|
||
vector at `[rdi+0x358]`). Array of constraint objects, inner dispatch @ `0x18017bde0`:
|
||
|
||
| key | atom | type |
|
||
|---|---|---|
|
||
| eligibilityKey | 0x0f2 | int (constraint selector) |
|
||
| eligibilitySlot | 0x0f4 | int/bool |
|
||
| eligibilityValue | 0x0f5 | int (target value) |
|
||
| eligibilityOperation | 0x0f3 | int (comparator; skip-handled here) |
|
||
| eligibilities | 0x0f1 | (container atom) |
|
||
|
||
Chemistry/rating/nation/league-count constraints (`teamChemistry 0x307`, `starRating 0x2e2`,
|
||
`sameNationCount 0x297`, `sameLeagueCount 0x296`, `sameClubCount 0x295`, `nationCount 0x1d4`,
|
||
`leagueCount 0x18c`, `clubCount 0x8b`, `playerCount 0x22f`, `chemistry 0x81`) are encoded
|
||
generically as `{eligibilityKey, eligibilityOperation, eligibilityValue}` triples, **not** as
|
||
named scalar fields on the record. **FREEZE-RISK: elgReq must be a JSON array of objects.**
|
||
|
||
> **2026-08-19 — `eligibilityKey`/`eligibilityOperation` are LOCALIZATION ORDINALS, not the
|
||
> atom hex ids above.** Reversed from the pinned CardsDLL (`4706a881…`). The client's sole
|
||
> confirmed consumer of these fields is the requirement-display string builder at
|
||
> `~0x1800ef900`: it loads the eligibility int fields (`0x148(rcx)`) and formats them through
|
||
> *indexed localization keys* — `ELIGIBILITY_STRING%d` (`0x1802186b8`), `LOC_SBC_ELG_KEY_%d`
|
||
> (`0x180226710`), `ELIGIBILITY_OPERATION` (`0x1802186e8`) — appending to a string builder via
|
||
> vtable `*0x10`/`*0x20`. There is **no comparison/branch**: the client does not validate on
|
||
> these ints, it renders `LOC_SBC_ELG_KEY_<eligibilityKey>` (and an operation string) as
|
||
> display text. Therefore `eligibilityKey` is a small ordinal that indexes the **packed FIFA17
|
||
> locale**, NOT `0x307`/`0x22f`/etc. (those hex values are the atom ids of the *named* fields
|
||
> the encoding replaces, not the ordinal values). CONSEQUENCE: correct projection needs the
|
||
> ordinal→locale-string map, which lives only in the packed locale (absent from CardsDLL and
|
||
> every `fifa17-recon/data` file; a game-dir locale probe on the live client found none) or a
|
||
> real EA `elgReq` capture (unavailable on a private server). Emitting a *guessed* ordinal
|
||
> renders the WRONG requirement text to the player, so `elgReq` stays `[]` until the ordinal
|
||
> map is recovered. This is a display-only gap: SBC submission is fully validated server-side
|
||
> (Core), and an invalid squad's generic comms modal originates from the server 400, not from
|
||
> the empty `elgReq`.
|
||
|
||
**awards / grantedAwards** — nested array of reward objects (atoms: `rewardType 0x28e`,
|
||
`rewardValue 0x28f`, `rewardQuantity 0x28d`, `rewardMultiplier 0x28c`, `awardCount 0x40`,
|
||
`awardSet 0x45`, `awardSetId 0x46`, `prizeSet 0x253`). **FREEZE-RISK: must be array.**
|
||
|
||
**SET-level field vocabulary** (available atoms for a full set record):
|
||
`setId 0x2bc`, `name 0x1d0`, `description 0x0d1`, `challenges 0x76`, `challengesCount 0x78`,
|
||
`challengesCompletedCount 0x77`, `repeatable 0x280`, `endTime 0x106`, `awardSet 0x45`,
|
||
`setImageId 0x2bd`, `starRating 0x2e2`, `sortPriority 0x2cb`, `timesCompleted 0x322`,
|
||
`categoryId 0x73`, `priority 0x250`.
|
||
|
||
### Freeze-risk summary (type fidelity mandatory)
|
||
- `formation` = **string** (e.g. `"f442"`), never int.
|
||
- `awards`, `elgReq`, `squad`, `playerRequirements`, `grantedChallengeAwards`,
|
||
`grantedSetAwards`, `sets`, `challenges` = **arrays/objects**, never scalar.
|
||
Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer spin
|
||
(freeze at `0x1801c7f1a`).
|
||
- Unknown/extra keys are safe (value-SKIP handler `0x180135ff0`).
|
||
## FUT Draft
|
||
|
||
Clean-room reverse of the FIFA 17 CardsDLL (base `0x180000000`) FUT **Draft** response
|
||
deserializers. Method: located each `RS4:Fut*DraftServerResponse` name string, computed its
|
||
`.rdata` VA, found the `.text` factory (`lea r8,[name]`), then read the adjacent JSON
|
||
deserializer's atom dispatch (FNV key → atom → getter). Atoms translated via `atoms.tsv`.
|
||
**All 7 endpoints are a total GAP** — `tools/utas_server.py` has zero `draft` routes today.
|
||
|
||
Endpoint family: `ut/%s/draft/mode` (+ sub-paths below), `%s = "game/fifa17"`.
|
||
|
||
Shared sub-deserializers used by Draft:
|
||
- `0x18013fe00` — ITEM/card element deser (a full player card, same schema as club/squad `itemData`; see CARD_SYSTEM.md). **Must be a JSON object.**
|
||
- `0x18013d1f0` — SQUAD deser (the whole squad model: `formation`, `players[].itemData`, `manager`, `custom`, `kicktakers`…; identical to `GET /squad/0` LoadActiveSquad). **Must be a JSON object.**
|
||
|
||
Leaf getters: int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`; unknown-key SKIP `0x180135ff0` (extra keys are safe). Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer freeze — so every field flagged **[freeze-risk]** below MUST be emitted as the right container type (or omitted entirely).
|
||
|
||
---
|
||
|
||
### 1. FutGetDraftChoicesServerResponse — CONFIDENCE: HIGH (fully reversed, incl. deep choice record)
|
||
- Struct name `.rdata` `0x180225ea0`; factory `0x18014f250`; **deserializer `0x18014f2d0`**.
|
||
- Method/Path: **GET** `ut/game/fifa17/{champId}/draft` (path fragment `"/%d/draft"` @0x2252c8). Returns the choices offered for the current pick.
|
||
- Top-level keys (ordered):
|
||
| key | atom | type | notes |
|
||
|---|---|---|---|
|
||
| `choices` | 0x83 | **array** [freeze-risk] | array of choice records (below) → vec@+0x30 |
|
||
| `positionid` | 0x244 | int | @+0x28 |
|
||
| `tier` | 0x315 | int | @+0x2c |
|
||
- **Choice record** (each element of `choices`, element size 0x20; sub-dispatch @0x18014f44c):
|
||
| key | atom | type | notes |
|
||
|---|---|---|---|
|
||
| `formation` | 0x12b | **string** | formation-choice rounds (e.g. `"f442"`); parsed str→id |
|
||
| `index` | 0x163 | int (byte) | choice slot index 0..n → byte@+0x28 |
|
||
| `itemData` | 0x16b | **object** [freeze-risk] | the offered player card, parsed by ITEM deser `0x18013fe00` |
|
||
So a pick round is either a **formation** round (choices carry `formation`) or a **player** round (choices carry `itemData` + `index`), with the round's slot given by top-level `positionid` and `tier`.
|
||
- MINIMAL known-good (player round, 5 choices for one position):
|
||
```json
|
||
{"positionid":0,"tier":1,
|
||
"choices":[
|
||
{"index":0,"itemData":{"id":1000001,"resourceId":20801,"assetId":20801,"itemType":"player","rating":94,"preferredPosition":"ST","nation":38,"teamid":243,"leagueId":53,"rareflag":1,"untradeable":true,"attributeList":[{"index":0,"value":90}],"itemState":"free","owners":1,"contract":7,"fitness":99}}
|
||
]}
|
||
```
|
||
Formation round: `{"positionid":0,"tier":1,"choices":[{"index":0,"formation":"f442"},{"index":1,"formation":"f433"}]}`.
|
||
|
||
---
|
||
|
||
### 2. FutGetDraftCurrentStateServerResponse — CONFIDENCE: HIGH (fully reversed, incl. roundsInfo element + squad delegation)
|
||
- Struct name `0x180224200`; factory `0x180146cc0`; **deserializer `0x180147070`** (begin-obj @0x1801470f2, dispatch @0x18014715c).
|
||
- Method/Path: **GET** `ut/game/fifa17/squad/mode/draft/state?mode=ONLINE` | `?mode=SINGLE_PLAYER`.
|
||
**CORRECTED 2026-08-04 from a LIVE CAPTURE.** The path above previously omitted the
|
||
`squad/mode` segment. The real URL is `ut/%s/squad/mode` (template @`0x18021e7f8`)
|
||
with `/draft/state` appended, which is why it is invisible to the request-template
|
||
table and why the generic `/squad` route swallowed it. The suffix is appended to a
|
||
caller-supplied buffer by `FUN_180146ac0`, which has no resolvable callers, so the
|
||
full path is established by the capture in `REBUILD_RESEARCH.md` §15, NOT statically.
|
||
- Top-level keys:
|
||
| key | atom | type | notes |
|
||
|---|---|---|---|
|
||
| `squad` | 0x2cd | **object** [freeze-risk] | drafted squad, SQUAD deser `0x18013d1f0` |
|
||
| `entranceCriteria` | 0x108 | **object** [freeze-risk] | nested object (safe to omit) |
|
||
| `gamesWonCurrentMatch` | 0x13b | int | @+0x60 |
|
||
| `roundsInfo` | 0x293 | **array** [freeze-risk] | array of round records (below), elem deser `0x180146eb0`, elem size 0x20 → vec@+0x80 |
|
||
| `squadState` | 0x2d5 | string | e.g. `"DRAFTSQUAD_ON"` |
|
||
| `stateParam1` | 0x2ee | string→enum | @+0x5c (default 5) |
|
||
| `stateParam2` | 0x2ef | string→int | @+0x64 |
|
||
- **Round record** (`roundsInfo[]`, all scalar; deser `0x180146eb0`):
|
||
`round`(0x290,int@+0xc), `score`(0x29a,int@**+0x10**), `opponentScore`(0x200,int@+0x14), `penaltyScore`(0x217,int@**+0x18**), `opponentPenaltyScore`(0x1fd,int@+0x1c), `opponentId`(0x1fc,int/long@+0x0), `difficulty`(0xd4,string enum@+0x8).
|
||
(`score` and `penaltyScore` offsets were SWAPPED here until 2026-08-04.)
|
||
- **THE ROOT CONTAINER IS A JSON ARRAY.** `0x180147070` initialises, discards two
|
||
tokens, then tests `if (t3 != 0xd)` around a `do { ... } while (t != 0xd)` element
|
||
loop. Handed a top-level OBJECT it never reaches its exit condition and spins in the
|
||
inner `while (t != 10)` loop while the tokenizer returns EOF forever: process alive,
|
||
no crash dump, no dialog. That is the hang observed live on 2026-08-03.
|
||
- **The body previously printed here was a HANG RECIPE** and is replaced below. It was
|
||
object-root, it used the spelling `DRAFTSQUAD_ON` which is NOT an accepted value of
|
||
the `squadState` enum, and it embedded a full `squad` object. Anyone who served it
|
||
would have reproduced the exact freeze this entry was supposed to help avoid.
|
||
- MINIMAL known-good (verified: two agents independently walked this body through the
|
||
deserializer token by token to a clean exit in 16 reads):
|
||
```json
|
||
[{"squadState":"INVALID","stateParam1":"INVALID","stateParam2":"0","gamesWonCurrentMatch":0,"roundsInfo":[]}]
|
||
```
|
||
`squad`(0x2cd) and `entranceCriteria`(0x108) are OMITTED and omission is provably
|
||
inert: `FUN_180135ff0` is called from both defaults, so unknown keys are skipped.
|
||
`entranceCriteria`'s shape is now known anyway (an object of three int keys
|
||
COINS / DRAFT_TOKEN / POINTS) but knowing a shape is not a reason to send it.
|
||
`stateParam2` may be a JSON number or a string; the string getter stringifies token
|
||
types 2/3/4, so `"0"` is correct but not mandatory.
|
||
Served today behind `FUT_DRAFT_STATE` (default on) in `utas_server.py`.
|
||
|
||
---
|
||
|
||
### 3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk)
|
||
- Struct name `0x180226540`; factory `0x18015076d`; **deserializer `0x1801508c0`** (begin-obj @0x18015093b, dispatch @0x1801509bc).
|
||
- Method/Path: **GET** `ut/game/fifa17/draft/mode` (GetDraftStats RPC; historical/aggregate draft stats).
|
||
- Keys (all int, except `draftChampion` bool):
|
||
`gamesWon`(0x13a), `gamesLost`(0x138), `scoredGoals`(0x29b), `concededGoals`(0xa1), `bestBuilderScore`(0x52), `draftChampion`(0xe1, bool), `draftsCompleted`(0xe2), `passAccuracyTotal`(0x212), `possessionPercentage`(0x247), `possessionTotal`(0x249).
|
||
- MINIMAL known-good:
|
||
```json
|
||
{"gamesWon":0,"gamesLost":0,"scoredGoals":0,"concededGoals":0,"bestBuilderScore":0,"draftChampion":false,"draftsCompleted":0,"passAccuracyTotal":0,"possessionPercentage":0,"possessionTotal":0}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. FutGetDraftAwardServerResponse — CONFIDENCE: HIGH (fully reversed)
|
||
- Struct name `0x1802266f8`; factory `0x18015106c`; **deserializer `0x1801510c0`** (begin-obj @0x180151129, dispatch @0x1801511ac).
|
||
- Method/Path: **GET/POST** `ut/game/fifa17/draft/mode` (GetDraftAward — claim/return the draft prize).
|
||
- Keys:
|
||
| key | atom | type | notes |
|
||
|---|---|---|---|
|
||
| `item` | 0x16a | **array** [freeze-risk] | awarded item cards, ITEM deser `0x18013fe00` (loops) |
|
||
| `halId` | 0x150 | int | @ (prize/hal id) |
|
||
| `type` | 0x354 | int | prize type code |
|
||
| `value` | 0x377 | int | prize value (e.g. coins) |
|
||
- **ROOT CONTAINER: ARRAY, not object. CORRECTED 2026-08-04.** `0x1801510c0` has the
|
||
same array-root prologue as `FutGetDraftCurrentState`. The object-root body that used
|
||
to be printed here would hang the client identically, and it was sitting in this file
|
||
labelled "known-good".
|
||
- MINIMAL (root shape corrected; the key set itself is unchanged and was not re-verified
|
||
this pass, so `TODO/CONFIRM` the members before serving):
|
||
```json
|
||
[{"type":1,"value":15000,"halId":0,"item":[]}]
|
||
```
|
||
With an item prize: `"item":[{ …full card object as in itemData… }]`.
|
||
- **Why both of these were wrong at once:** a census claimed only three array-root
|
||
readers existed in the DLL. It missed this one. A second census, run to check the
|
||
first, was wrong in the opposite direction. Roughly 23 of 86 top-level readers remain
|
||
unclassified. **Do not serve any endpoint in this document until its root container
|
||
has been classified by reading the actual prologue, not by regex.**
|
||
|
||
---
|
||
|
||
### 5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred)
|
||
- Struct name `0x180226078`; factory `0x18014fba0`. The factory installs the **generic base ServerResponse vtable `0x18022cb58`** (shared by dozens of structs) — i.e. **no struct-specific deserializer / no parsed fields**. The neighboring `0x18014fcb0` is the *request* serializer, not a response reader.
|
||
- Method/Path: **PUT/POST** `ut/game/fifa17/{champId}/draft/choose` and `ut/game/fifa17/draft/choose/difficulty` (fragments @0x2254c0 / 0x2254a0). Commits one pick (or the difficulty choice).
|
||
- Body: an empty ack — the client re-reads state via GetDraftCurrentState / GetDraftChoices afterward.
|
||
- MINIMAL known-good: `{}`
|
||
|
||
---
|
||
|
||
### 6. FutPickDraftAutoChoiceServerResponse — CONFIDENCE: HIGH (delegates to squad deser)
|
||
- Struct name `0x180226208`; factory `0x18014fd80`; **deserializer `0x18014fdf0`**. The body is parsed by a single call to the SQUAD deser `0x18013d1f0` (@0x18014fe82) into the struct's squad member.
|
||
- Method/Path: **POST** `ut/game/fifa17/{champId}/draft/autocomplete` (fragment @0x225638). Auto-fills the remaining picks and returns the completed squad.
|
||
- Body: a **squad object** [freeze-risk] (same schema as `GET /squad/0`).
|
||
- MINIMAL known-good:
|
||
```json
|
||
{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]}
|
||
```
|
||
(Emit a fully-populated `players[].itemData` for a real auto-drafted XI.)
|
||
|
||
---
|
||
|
||
### 7. FutPurchaseDraftModeServerResponse — CONFIDENCE: MEDIUM-HIGH (field set fully reversed; response-variant ambiguity)
|
||
- Struct name `0x180224fb0`; factory `0x18014c0bd`; **deserializer `0x18014c260`** (begin-obj @0x18014c2d5, dispatch @0x18014c3bb).
|
||
- Method/Path: **POST** `ut/game/fifa17/purchase/mode/{price}/draft` (fragment @0x2257a8). Buys entry into draft mode; returns the fresh draft session summary.
|
||
- Keys (all scalar int):
|
||
`championEventId`(0x7b), `expectedTierLevel`(0x115), `gamesPlayed`(0x139), `gamesRemaining`(0x13c), `rank`(0x26d), `score`(0x29a), `tierLevel`(0x317).
|
||
- Note: a **second, larger struct (size 0x38, deser @0x180150379, factory @0x18015028d)** also references this name string. It carries no visible scalar field ladder and is likely an alternate/summary envelope; the `0x18014c260` field-parser above is treated as authoritative. Confirming which body the live client reads is the remaining gap.
|
||
- MINIMAL known-good:
|
||
```json
|
||
{"championEventId":0,"expectedTierLevel":1,"gamesPlayed":0,"gamesRemaining":4,"rank":0,"score":0,"tierLevel":1}
|
||
```
|
||
|
||
---
|
||
|
||
### Implementation notes for utas_server.py
|
||
- Add routes under `G + r"/draft"`: `GET …/draft/state`, `GET …/{id}/draft`, `POST …/{id}/draft/choose`, `POST …/draft/choose/difficulty`, `POST …/{id}/draft/autocomplete`, `POST …/purchase/mode/{n}/draft`, plus GetDraftStats / GetDraftAward on `…/draft/mode`.
|
||
- Reuse the existing squad/item JSON builders (`fut_seed.player_item`, `_base_squad`) for `squad`, `itemData`, `item[]` — those objects are already known-good through deser `0x18013d1f0` / `0x18013fe00`.
|
||
- Freeze-risk containers to never send as scalars: `choices`, `choices[].itemData`, `squad`, `entranceCriteria`, `roundsInfo`, `item`, and the PickAutoChoice squad body.
|
||
## Match / Seasons / Tournaments
|
||
|
||
Reversed from `cardsdll.dll` (base `0x180000000`) per RECIPE.md. CardsDLL base `0x180000000`.
|
||
Parser shared internals: each JSON key → FNV-1a → atom int; deserializer dispatches on atom via
|
||
`cmp/sub/dec + je` binary tree. **Unknown atoms are routed to the container-aware SKIP handler
|
||
`0x180135ff0` — extra keys are SAFE.** Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`,
|
||
str `0x1801c7aa0`. Nested object/array = `call` to a sub-deserializer or array loop. Feeding a
|
||
scalar getter an object/array desyncs the SAX reader → infinite spin freeze at `0x1801c7f1a`.
|
||
|
||
**SAX token-type constants seen in every loop (NOT keys, ignore):** atom 6 (`=` end-object token),
|
||
atom 10 (end-array/container token). Atom values > 906 in a raw dump are jump-table offsets, not atoms.
|
||
|
||
**Key-spelling caveat:** the wire key is FNV-hashed, so JSON keys must match EA's *exact* spelling
|
||
incl. typos — notably `boostConis` (atom 96, not "boostCoins") and `matchCoinMultipliers`.
|
||
|
||
### Two response families found
|
||
1. **Bespoke deserializer** (custom atom switch) — most Season/Match/Tournament *load/list* responses.
|
||
2. **Ack-only / base-response** — `FutMatchReady`, `FutPlayGame`, `FutUpdateSeason`,
|
||
`FutUpdateFriendlySeason`, `FutGetStoryModeReward`. Their factory allocates a 0x28-byte object and
|
||
calls **only the base ServerResponse constructor `0x18011f850`** (sets vtable `0x18022cb58`,
|
||
timeout `0x7530`, status `-1`). They parse **NO** body fields → the client accepts any body incl.
|
||
`{}`. **The match reward/coins are NOT in FutPlayGame — they are carried by FutDestroyMatch.**
|
||
|
||
### HTTP method/path note
|
||
Endpoint path strings live in an `.rdata` string-pool (pointer table at fileoff `0x21d480`), not
|
||
lea-referenced from code, and the request-descriptor table carries the verb in a parallel array that
|
||
is not statically recoverable here. **Methods below are inferred from REST/UTAS semantics — confidence
|
||
LOW on method, HIGH on struct fields.** Path templates (verified in binary, `%s`="game/fifa17"):
|
||
`ut/%s/match`, `ut/%s/season`, `ut/%s/season/user`, `ut/%s/season/%s/user`, `ut/%s/season/%s/reset`,
|
||
`ut/%s/season/friendly`, `ut/%s/tournament`, `ut/%s/tournament/user`, `ut/delete/%s/tournament/user`,
|
||
`/season/user/history`.
|
||
|
||
### Baseline (utas_server.py) status
|
||
All of these are currently **GAP / stubbed**: `/season` → `(200, {})`, `/match/keepalive` → `(204)`.
|
||
No match/season/tournament body is currently reversed in the baseline.
|
||
|
||
---
|
||
|
||
## HIGHEST VALUE — MATCH RESULT / REWARD RECORD
|
||
|
||
### FutDestroyMatchServerResponse — the post-match coin/credit reward record ★CONFIDENCE: HIGH
|
||
- **Deserializer VA:** `0x180121b60` (factory `0x180121700`, name-lea `0x18012170c`)
|
||
- **Method+path (inferred):** `DELETE ut/game/fifa17/match/{matchId}` — closes the match and returns
|
||
the credited rewards. This is where a completed match reports its coin/XP award. **Score/win-loss are
|
||
NOT here — the client SENDS the result in the request; the server RESPONDS with the coins.**
|
||
- **Ordered reward fields (all int unless noted; offsets in the parsed struct):**
|
||
|
||
| atom | key | type | offset | notes |
|
||
|---|---|---|---|---|
|
||
| 20 | `allCoins` | int | 0x28 | new total coin balance |
|
||
| 436 | `matchCoins` | int | 0x2c | coins awarded for this match |
|
||
| 809 | `tournamentCoins` | int | 0x30 | |
|
||
| 776 | `teamOfTournamentWinner` | bool | 0x34 | |
|
||
| 670 | `seasonCoins` | int | 0x38 | |
|
||
| 149 | `coins` | int | 0x3c | reward amount (this txn) |
|
||
| 529 | `participationAward` | int | 0x44 | |
|
||
| 96 | `boostConis` | int | 0x48 | (EA typo — exact key) |
|
||
| 617 | `qualifiedChampionEventId` | int | 0xb0 | |
|
||
| 310 | `gameModeAward` | nested | — | object, SKIP-safe |
|
||
| 437 | `matchCoinMultipliers` | nested | — | array/object, SKIP-safe |
|
||
| 619/805/852/887 | `quantity`/`total`/`type`/`value` | nested item | — | reward-item sub-object fields (prize list element; `type` is str) |
|
||
| 877 | `userData` | nested | via `0x180142470` | user snapshot object (FREEZE-RISK: must be object) |
|
||
|
||
- **Coin fields cluster tightly at 0x28–0x48**, confirming a coherent credits struct. All top-level
|
||
coin fields are scalar int → safe.
|
||
- **MINIMAL known-good JSON** (scalars only, zero freeze risk):
|
||
```json
|
||
{
|
||
"coins": 400,
|
||
"allCoins": 15400,
|
||
"matchCoins": 400,
|
||
"seasonCoins": 0,
|
||
"tournamentCoins": 0,
|
||
"boostConis": 0,
|
||
"participationAward": 0,
|
||
"qualifiedChampionEventId": 0,
|
||
"teamOfTournamentWinner": false
|
||
}
|
||
```
|
||
- **GAP** (baseline has no match reward body).
|
||
|
||
---
|
||
|
||
## SINGLE-PLAYER PLAYABLE LOOP
|
||
|
||
### FutCreateMatchServerResponse ★CONFIDENCE: HIGH
|
||
- **Deserializer VA:** `0x180120380` (name-lea `0x18011ffbd`)
|
||
- **Method+path (inferred):** `POST ut/game/fifa17/match` — creates the match, returns match/squad info.
|
||
- **Fields:**
|
||
| atom | key | type | offset | notes |
|
||
|---|---|---|---|---|
|
||
| 740 | `startDateTime` | int | 0x28 | epoch |
|
||
| 641 | `reportIdEnabled`| bool | — | |
|
||
| 717 | `squad` | nested (`0x18011a830`) | — | squad/ITEM array — FREEZE-RISK (must be array) |
|
||
- **MINIMAL JSON:** `{"startDateTime": 1580000000, "reportIdEnabled": false}` (omit `squad` — SKIP-safe)
|
||
- **GAP.**
|
||
|
||
### FutMatchReadyServerResponse ★CONFIDENCE: HIGH (ack-only)
|
||
- **Deserializer:** none — factory `0x180120810` → base ctor `0x18011f850` only (name `0x18021d380`).
|
||
- **Method+path (inferred):** `PUT ut/game/fifa17/match/{matchId}` (mark ready).
|
||
- Parses **no** fields. **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
### FutPlayGameServerResponse ★CONFIDENCE: HIGH (ack-only)
|
||
- **Deserializer:** none — factory `0x180162170` → base ctor `0x18011f850` only (name `0x180228050`).
|
||
- **Method+path (inferred):** `POST ut/game/fifa17/match/{matchId}` (submit game result; body carries
|
||
score/stats client→server). Response body is ignored by the client. **Rewards arrive via DestroyMatch.**
|
||
- **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
### FutResetMatchServerResponse ★CONFIDENCE: HIGH
|
||
- **Deserializer VA:** `0x18016fd10` (name-lea `0x18016fccd`)
|
||
- **Method+path (inferred):** `POST ut/game/fifa17/season/{seasonId}/reset`.
|
||
- **Fields:** atom 643 `reset` = **bool** @0x28.
|
||
- **MINIMAL JSON:** `{"reset": true}`. **GAP.**
|
||
|
||
### FutSeasonListServerResponse ★CONFIDENCE: HIGH (root shape CORRECTED 2026-08-04)
|
||
- **Deserializer VA:** `0x1801683f0`. The previously documented `0x180167740` is the
|
||
per-ELEMENT parser, not the response deserializer.
|
||
- **THE ROOT IS AN OBJECT WITH ONE KEY.** `0x1801683f0` runs a key loop and matches
|
||
exactly one atom, `seasons`(0x2ad); the array opens only inside it. This entry
|
||
previously described an array root, because someone read the element parser and
|
||
documented its key set at the document level. A bare array populates nothing, and
|
||
`utas_server.py` served one for months on the strength of this row.
|
||
- **Method+path:** `GET ut/game/fifa17/season`.
|
||
- **Element ordering matters:** `type` MUST precede `divisionId`, because the
|
||
`divisionId` branch reads the already-parsed type field at `elem+0x1b4`.
|
||
- **Element stride is `0x318`.** (`0x1f8`, recorded elsewhere, is the offset of the
|
||
compared short WITHIN an element.) The short the online path matches on is written
|
||
from `divisionId` as `(0xb - divisionId)`, not from `id`.
|
||
- `eligibilityKey`/`eligibilitySlot`/`eligibilityValue` below are inner members of
|
||
`elgReq` and are **inert at element level**, so the old minimal body was wrong twice.
|
||
- **Element fields:**
|
||
| atom | key | type | notes |
|
||
|---|---|---|---|
|
||
| 348 | `id` | int | season/division id |
|
||
| 220 | `divisionId` | int | |
|
||
| 242 | `eligibilityKey` | int | |
|
||
| 244 | `eligibilitySlot` | int | |
|
||
| 245 | `eligibilityValue` | int | |
|
||
| 246 | `elgOperation` | str | |
|
||
| 247 | `elgReq` | nested | SKIP-safe |
|
||
| 595 | `prizeSet` | nested | array — FREEZE-RISK |
|
||
- **MINIMAL JSON** (object root, single `seasons` key):
|
||
```json
|
||
{"seasons":[{"type":"OFFLINE","id":1,"divisionId":10}]}
|
||
```
|
||
`prizeSet`(0x253), `elgReq`(0xf7) and `matches`(0x1b8) are all `while (tok != 0xd)`
|
||
ARRAY loops: a scalar in any of them is the `0x1801c7f1a` spin. Omit all three.
|
||
Semantic hazard: omitting `untilEndSeconds` makes the season end timestamp equal now.
|
||
- **Do not serve this yet.** Across 486 real client requests (User-Agent `ProtoHttp`,
|
||
roughly 30 boots) the game has **never** requested `/season`. Every `/season` line in
|
||
`/tmp/utas_server.log` is our own `curl` or `Python-urllib`. Serving a body here
|
||
changes nothing observable until something upstream makes the client ask.
|
||
- **GAP.** (Note: root JSON shape is an array/object wrapper — verify container before shipping.)
|
||
|
||
### FutSeasonLoadDataServerResponse ★CONFIDENCE: HIGH (fully traced switch)
|
||
- **Deserializer VA:** `0x180131450` (name-lea `0x18013141c`)
|
||
- **Method+path (inferred):** `GET ut/game/fifa17/season/user` — load the user's current season state.
|
||
- **Fields (verified from switch at `0x18013153c`):**
|
||
| atom | key | type | offset | notes |
|
||
|---|---|---|---|---|
|
||
| 674 | `seasonId` | int | 0x5c | |
|
||
| 220 | `divisionId` | int | 0x58 | (as int here, not str) |
|
||
| 656 | `round` | int | 0x60 | current round |
|
||
| 881 | `userPoints` | int | 0x64 | points/progress |
|
||
| 202 | `dataVersion` | str | 0x68 (enum byte) | |
|
||
| 201 | `data` | str | interned via `0x1801c7d40` | large opaque season-state blob string |
|
||
| — | root/`friendlySeasonHistory` | nested | via `0x180136880` | container — FREEZE-RISK |
|
||
- Unknown atoms → SKIP `0x180135ff0` (safe).
|
||
- **MINIMAL JSON:**
|
||
```json
|
||
{"seasonId":1,"divisionId":10,"round":1,"userPoints":0,"dataVersion":"1","data":""}
|
||
```
|
||
- **GAP.**
|
||
|
||
### FutUpdateSeasonServerResponse ★CONFIDENCE: HIGH (ack-only)
|
||
- **Deserializer:** none — factory `0x180168b10` → base ctor `0x18011f850` only (name `0x18022a748`).
|
||
- **Method+path (inferred):** `PUT/POST ut/game/fifa17/season/user` (submit season match result).
|
||
- **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
### FutSeasonQuitServerResponse ★CONFIDENCE: MEDIUM
|
||
- **Deserializer VA:** `0x180131a30` (name-lea `0x18013197d`)
|
||
- **Method+path (inferred):** `DELETE ut/delete/game/fifa17/season/user` (quit current season).
|
||
- **Fields:** atom 490 `offlineDivision` = nested (`0x18011a830`) — division-descriptor object.
|
||
- **MINIMAL JSON:** `{}` (offlineDivision optional/SKIP-safe; if included must be an object). **GAP.**
|
||
|
||
---
|
||
|
||
## SECONDARY (best-effort catalog)
|
||
|
||
### FutUpdateFriendlySeasonServerResponse ★CONFIDENCE: HIGH (ack-only)
|
||
- Factory → base ctor `0x18011f850` only (name `0x18022a840`). Method (inferred): `PUT ut/game/fifa17/season/friendly`.
|
||
Parses no fields. **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
### FutGetFriendlyHistoryDataServerResponse ★CONFIDENCE: MEDIUM
|
||
- **Deserializer VA:** `0x18014d570` (name-lea `0x18014d36d`). Path: `GET ut/game/fifa17/season/friendly` history.
|
||
- Fields: atom 675 `seasonGamesDraw` int @0x24; atom 151 `coinsEarned` int; (siblings `seasonGamesWon/Lost`
|
||
in same offset cluster likely present via SKIP). **MINIMAL JSON:** `{"seasonGamesDraw":0,"coinsEarned":0}`. **GAP.**
|
||
|
||
### FutTournamentListServerResponse ★CONFIDENCE: MEDIUM
|
||
- **Deserializer VA:** `0x180169ef0` (name-lea `0x180169d7d`). Path (inferred): `GET ut/game/fifa17/tournament`.
|
||
- Clean scalar fields: atom 348 `id` int, 212 `difficulty` int, 149 `coins` int, 652 `rewardMultiplier` int,
|
||
19 `aigroup` int, 37 `assetName` str, 243 `eligibilityOperation` str. Many nested (rounds, prizeSet, staff,
|
||
stadiumid, kit atoms 13/16 — treat as FREEZE-RISK objects, SKIP-safe if omitted).
|
||
- **MINIMAL JSON (array root):**
|
||
```json
|
||
[{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}]
|
||
```
|
||
- **GAP.**
|
||
|
||
### FutTournamentLoadDataServerResponse ★CONFIDENCE: MEDIUM
|
||
- **Deserializer VA:** `0x180147cb0` (name-lea `0x180147c7c`). Path (inferred): `GET ut/game/fifa17/tournament/user`.
|
||
- Fields: atom 202 `dataVersion` str, atom 656 `round` int, atom 810 `tournamentData` (str/opaque blob,
|
||
same interned-getter `0x1801c7d40` as season `data`), atom 92 `bonus` nested. Mirrors SeasonLoadData shape.
|
||
- **MINIMAL JSON:** `{"round":1,"dataVersion":"1","tournamentData":""}`. **GAP.**
|
||
|
||
### FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse ★CONFIDENCE: LOW
|
||
- Both resolve to deserializer `0x18016bcf0` (names `0x18022af10` / `0x18022b068`) — near-empty switch;
|
||
only nested/opaque atoms seen (teamId, activeBadge). Likely thin/ack-ish. Paths (inferred):
|
||
Quit = `DELETE ut/delete/game/fifa17/tournament/user`; Teams = `GET ut/game/fifa17/tournament/user`.
|
||
**MINIMAL JSON:** `{}` (or `[]` for Teams if array-rooted). **GAP.**
|
||
|
||
### FutUpdateTournamentServerResponse ★CONFIDENCE: LOW
|
||
- **Deserializer VA:** `0x1801758c0` (name-lea `0x18017580d`). Only enum/opaque atoms
|
||
(LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES/SUCCESS) — a status-enum response.
|
||
Path (inferred): `PUT/POST ut/game/fifa17/tournament/user`. **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
### FutGetActiveTournamentsServerResponse ★CONFIDENCE: LOW
|
||
- **Deserializer VA:** `0x18016b660` (name-lea `0x18016b55d`). Atoms `tournamentId`(811), `awardCount`(64)
|
||
seen but resolve nested/opaque. Path (inferred): `GET ut/game/fifa17/tournament`. Likely array of active
|
||
tournament ids. **MINIMAL JSON:** `[]` or `{}`. **GAP.**
|
||
|
||
### FutGetHistoricalServerResponse ★CONFIDENCE: MEDIUM
|
||
- **Deserializer VA:** `0x180172930` (name-lea `0x18017279c`). Path (inferred): `GET ut/game/fifa17/season/user/history`.
|
||
- Fields: atom 336 `halId` int @0xc; atom 363 `itemData` = **ITEM** (shared card/item deser `0x18013fe00`)
|
||
→ FREEZE-RISK (array of item cards); atom 653 `rewardQuantity` int @0x8; atom 654 `rewardType` int;
|
||
atom 655 `rewardValue` int; atom 656 `round` nested.
|
||
- **MINIMAL JSON:** `{"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0}` (omit itemData or send `[]`). **GAP.**
|
||
|
||
### FutGetStoryModeRewardServerResponse ★CONFIDENCE: HIGH (ack-only)
|
||
- Factory → base ctor `0x18011f850` only (name `0x18022bc30`). Parses no fields. **MINIMAL JSON:** `{}`. **GAP.**
|
||
|
||
---
|
||
|
||
### Endpoint → response cross-reference (summary)
|
||
| Method(inf) | Path | Response struct | Reversal |
|
||
|---|---|---|---|
|
||
| POST | ut/%s/match | FutCreateMatchServerResponse | FULL |
|
||
| PUT | ut/%s/match/{id} | FutMatchReadyServerResponse | FULL (ack) |
|
||
| POST | ut/%s/match/{id} | FutPlayGameServerResponse | FULL (ack) |
|
||
| DELETE | ut/%s/match/{id} | **FutDestroyMatchServerResponse (rewards)** | FULL |
|
||
| POST | ut/%s/season/{id}/reset | FutResetMatchServerResponse | FULL |
|
||
| GET | ut/%s/season | FutSeasonListServerResponse | FULL |
|
||
| GET | ut/%s/season/user | FutSeasonLoadDataServerResponse | FULL |
|
||
| PUT | ut/%s/season/user | FutUpdateSeasonServerResponse | FULL (ack) |
|
||
| DELETE | ut/delete/%s/season/user | FutSeasonQuitServerResponse | PARTIAL |
|
||
| PUT | ut/%s/season/friendly | FutUpdateFriendlySeasonServerResponse | FULL (ack) |
|
||
| GET | ut/%s/season/friendly (hist) | FutGetFriendlyHistoryDataServerResponse | PARTIAL |
|
||
| GET | ut/%s/season/user/history | FutGetHistoricalServerResponse | PARTIAL |
|
||
| GET | ut/%s/tournament | FutTournamentListServerResponse | PARTIAL |
|
||
| GET | ut/%s/tournament/user | FutTournamentLoadDataServerResponse | PARTIAL |
|
||
| PUT | ut/%s/tournament/user | FutUpdateTournamentServerResponse | PARTIAL |
|
||
| GET | ut/%s/tournament | FutGetActiveTournamentsServerResponse | PARTIAL |
|
||
| GET | ut/%s/tournament/user | FutGetTournamentTeamsServerResponse | PARTIAL |
|
||
| DELETE | ut/delete/%s/tournament/user | FutTournamentQuitServerResponse | PARTIAL |
|
||
| — | (story reward grant) | FutGetStoryModeRewardServerResponse | FULL (ack) |
|
||
## Club / Cards / Consumables
|
||
|
||
Clean-room RE of the FIFA17 CardsDLL (base `0x180000000`). Method: each `FutXServerResponse`
|
||
class registers a vtable; **vtable slot `+0x08` is the JSON deserializer**. Located via
|
||
struct-name string → `VA = 0x1801e5000 + (fileoff - 0x1e4400)` → factory `lea r8` xref →
|
||
vtable install → slot `+0x08`. Every deserializer shares the same skeleton:
|
||
|
||
```
|
||
scratch init (0x1801c63e0/0x180008130/0x1801c8270) → NextToken×2 →
|
||
get target model (0x18011a830) → key-loop { KEYRD 0x180141ee0 → atom → dispatch } →
|
||
unknown atom → SKIP 0x180135ff0 (container-aware, extra keys are SAFE)
|
||
```
|
||
|
||
**KEYRD `0x180141ee0`** = read next key, FNV-hash (`0x180180d00`) → atom int, advance to value.
|
||
Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`.
|
||
Shared **ITEM/card element** deserializer = **`0x18013fe00`** (see `CARD_SYSTEM.md`; produces the
|
||
0x100-byte resolved-card record). **TYPE FIDELITY IS MANDATORY**: an array/object key fed to a
|
||
scalar getter desyncs the SAX reader → infinite tokenizer spin (freeze at `0x1801c7f1a`). Arrays
|
||
are flagged **[FREEZE-RISK]** below — they must be emitted as JSON arrays, never scalars.
|
||
|
||
### Key structural finding (contradicts the initial brief)
|
||
`FutGetClubInfoServerResponse` does **NOT** return `itemData`/card items. Its only recognized
|
||
top-level key is **`user`** (atom `0x36c`), an array of **club-user summary records** (element
|
||
parser `0x18012c990`, which never calls the card element `0x18013fe00`). The card list that
|
||
actually renders in the club UI is **`FutViewCardsServerResponse`** (`itemData`, via
|
||
`0x18013fe00`) served on `ut/%s/item`. So "club-wide item search → cards" is ViewCards on `/item`,
|
||
while GetClubInfo/GetClubUsers carry club-user stat records. `data/` mental model of GetClubInfo =
|
||
{itemData,count,actives} is wrong for this binary. Actives (homekit/awaykit/badge/etc.) appear as
|
||
**fields inside each user record**, not as top-level keys.
|
||
|
||
---
|
||
|
||
### Per-struct table
|
||
|
||
Legend: method/path from the `ut/%s/...` templates in the binary (`%s="game/fifa17"`).
|
||
GAP = not (correctly) served by `utas_server.py`. All unknown keys are SKIP-safe, so `{}` never
|
||
freezes any of these — GAPs are "feature missing", not "crash".
|
||
|
||
| # | Struct | Deser VA | Method + Path | Top-level keys (atom → type) | Status | Conf |
|
||
|---|--------|----------|---------------|------------------------------|--------|------|
|
||
| 1 | FutGetClubInfo | `0x18012d280` | GET `ut/%s/club` | `user`(0x36c) → **array[user-record]** [FREEZE-RISK] | GAP (utas serves `itemData`, which is SKIP'd here → empty user list) | deser HIGH / element PARTIAL |
|
||
| 2 | FutGetClubUsers | `0x180145c00` | GET `ut/%s/clubUser` | `user`(0x36c) → **array[user-record]** (elem `0x180145480`) [FREEZE-RISK] | GAP (utas `/clubUser` → `{}`) | deser HIGH / element PARTIAL |
|
||
| 3 | FutChangeClubName | `0x1801642c0` | PUT `ut/%s/club` (changeClubName) | **none** (deser is immediate `ret`) | ack — any parseable JSON works; `{}` fine | HIGH |
|
||
| 4 | FutViewCards | `0x1801293d0` | GET `ut/%s/item` | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | HANDLED (utas `/item` `defs_route` serves `itemData`) | HIGH |
|
||
| 5 | FutActivateCard | `0x1801642c0` | PUT `ut/%s/item` (FUT_CLUB_ACTIVATE_ITEM_DP) | **none** (immediate `ret`) | ack — `{}` fine | HIGH |
|
||
| 6 | FutApplyCard | `0x18012a710` | PUT `ut/%s/item` (apply by itemId) | `itemData`(0x16b) → **array[updated card-item]** via `0x18013fe00` [FREEZE-RISK] | GAP | HIGH |
|
||
| 7 | FutApplyCardByRes | `0x18012ad10` | PUT `ut/%s/item` (apply by resourceId) | `itemData`(0x16b) → **array[updated card-item]** [FREEZE-RISK] | GAP | HIGH |
|
||
| 8 | FutDiscardCard | `0x180127300` | DELETE `ut/delete/%s/item` (CardsDiscardCard) | `items`(0x171) → **array[int ids]** [FREEZE-RISK]; `totalCredits`(0x326) → int; `id`(0x15c) → int | GAP | HIGH |
|
||
| 9 | FutDiscardCardByRes | `0x1801279c0` | DELETE `ut/delete/%s/item` (by res) | `totalCredits`(0x326) → int | GAP | HIGH |
|
||
| 10 | FutMoveCard | `0x180128600` | PUT `ut/%s/item` (move) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool | GAP | HIGH |
|
||
| 11 | FutMoveCardByRes | `0x180128e30` | PUT `ut/%s/item` (move by res) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool (+ 2 str/1 int minor) | GAP | HIGH / extra-fields MED |
|
||
| 12 | FutConsumablesSearch | `0x180130d10` | GET `ut/%s/club/consumables/<cat>` (ConsumablesSearch) **[CORRECTED 2026-08-21]** | `itemData`(0x16b) → **array[consumable-stack]** via `0x18013fe00` [FREEZE-RISK]; `displayGroupUseDefaultImage`(0xdb) → int + count scalars | SERVED (Rust host) | deser HIGH / scalars MED |
|
||
| 13 | FutStaffBonus | `0x18012b730` | GET `ut/%s/club/stats/staff` (StaffStats, thunk `0x18012b080`) **[CORRECTED 2026-08-21]** | `bonus`(0x5c) → **nested** (branch sets bool @rbp+0x51) [FREEZE-RISK]; `assetId`(0x23) → int | SERVED (`{}`, the oracle body) | MED |
|
||
| 14 | FutGetAvailableLoanPlayers | `0x18014e030` → sub `0x18013a1c0` | GET `ut/%s/item` (FUT_AVAILABLE_LOAN_PLAYERS_DP) | `loans`(0x19b) → **array** [FREEZE-RISK]; `itemData`(0x16b) → **array[card-item]** [FREEZE-RISK]; `default`(0xcd) → int | GAP | deser HIGH / fields MED |
|
||
| 15 | FutSignLoanPlayer | `0x1801642c0` | PUT `ut/%s/item` (sign loan) | **none** (immediate `ret`) | ack — `{}` fine | HIGH |
|
||
| 16 | FutStickerBookSearch | `0x18012eff0` | GET `ut/%s/club?<query>` (ClubSearch, `FUN_18012ddf0`) **[CORRECTED 2026-08-21]** | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | SERVED (Rust host) | HIGH |
|
||
|
||
|
||
### The four `ut/%s/club` routes are a TABLE, not an inference (2026-08-21)
|
||
|
||
The URLs for rows 12, 13 and 16 above were previously guessed as `ut/%s/item?…`
|
||
or left as `ut/%s/…`. The binding is exact: the 125-row action table at
|
||
`0x1802caa20` indexes the 48-entry URL-base table at `0x18021df80` through column
|
||
1, and **base index 3 = `ut/%s/club` is carried by exactly four rows** — so the
|
||
client can emit exactly four request families on that base and no others.
|
||
|
||
```
|
||
| ClubSearch | FUN_18012ddf0 | GET ut/%s/club?<query> | FutStickerBookSearchServerResponse |
|
||
| ClubStats | FUN_18012f4f0 | GET ut/%s/club/stats/<f>[/<id>] | FutStickerBookStats2ServerResponse |
|
||
| StaffStats | thunk 0x18012b080 | GET ut/%s/club/stats/staff | FutStaffBonusServerResponse |
|
||
| ConsumablesSearch | FUN_1801308c0 | GET ut/%s/club/consumables/<cat> | FutConsumablesSearchServerResponse |
|
||
```
|
||
|
||
**Club query grammar**, complete and ordered: `?year=2017` (always, hardcoded),
|
||
then `type`, `start` (omitted at 0), `count` (omitted at 100), `filter`, then
|
||
EITHER the filter block (`position, formation, state, level, rare, nation,
|
||
country, league, playStyle, team, sort`) OR a comma-joined `defId=` list, never
|
||
both. Live control from the log:
|
||
`GET /ut/game/fifa17/club?year=2017&type=equippables&count=11&level=any&sort=desc`
|
||
matches the predicted order and every suppression rule.
|
||
|
||
Sub-vocabularies: `filter` = available/base/exact/any; `level` =
|
||
bronze/silver/gold/any; `sort` = asc/desc; `rare` = the literal string `SP`, not
|
||
a boolean; `state` = the itemState names plus `any` — and note the REQUEST spells
|
||
it `onSale` where the RESPONSE value is `forSale`.
|
||
|
||
`?type=` has 30 values. Decoded 2026-08-21 from the jump table itself rather
|
||
than from a case count: `FUN_18012ec50` is `cmp ecx,0x1d` + a 30-entry table at
|
||
`0x18012ed9c`, and each case is `mov ecx,<atom>; jmp 0x180180cd0` (atom → string).
|
||
Resolving those atoms against `fut_atoms.tsv` gives the vocabulary in table order:
|
||
|
||
```
|
||
0 any 1 player 2 manager 3 headcoach
|
||
4 fitnesscoach 5 physio 6 development 7 custom
|
||
8 unlocks 9 gkcoach 10 staff 11 badge
|
||
12 kit 13 stadium 14 ball 15 equippables
|
||
16 leaguelogos 17 offlinetrophy 18 onlinetrophy 19 featuredofflinetrophy
|
||
20 featuredonlinetrophy 21 allofflinetrophy
|
||
22 allonlinetrophy 23 healing 24 contract
|
||
25 training 26 misc 27 playerdefender
|
||
28 playermidfielder 29 playerforward
|
||
```
|
||
|
||
Notes worth having: there is **no `playergoalkeeper`** — the client has only
|
||
DEF/MID/FWD tabs, so goalkeepers belong to `playerdefender`, and a GK appearing
|
||
there is correct rather than a filter bug. `healing`, `contract` and `training`
|
||
exist here as `?type=` arms even though consumables have their own
|
||
`club/consumables/<cat>` route. Six of the thirty are trophy arms.
|
||
|
||
`openfut-utas-host`'s `club_type_filter` implements all 30 with no extras; a unit
|
||
test pins the list so a missing arm (an empty real tab) or an invented one (dead
|
||
code that looks like coverage) fails the build.
|
||
|
||
**`/club/stats` has exactly seven forms**: `club`, `year`, `country/<id>`,
|
||
`league/<id>`, `newcards`, `consumables`, and the separately-dispatched `staff`.
|
||
**There is no `/club/stats/team/<id>`** — verified twice (the switch has six cases
|
||
with no such arm, and an exhaustive PE string scan finds no literal containing
|
||
`stats/team`). Any handling of a `team` stats mode is dead code.
|
||
|
||
**Two holes in the base table**, recorded so nobody re-derives them as findings:
|
||
base index 43 = `ut/v2/%s/store` is carried by no action row and has zero
|
||
references in `.text`, yet `ut/v2/store` is live-proven; base index 9 =
|
||
`ut/%s/activeMessage` is a second hole of the same kind. So at least one route is
|
||
composed OUTSIDE CardsDLL, most likely in the packed exe — every "the table bounds
|
||
it" statement here is bounded to CardsDLL only.
|
||
|
||
Notes:
|
||
- **`0x1801642c0`** is a shared no-op deserializer (function body = `ret`). Three responses
|
||
(ChangeClubName, ActivateCard, SignLoanPlayer) use it → their HTTP body is fully ignored; only a
|
||
200 + valid JSON (`{}`) is required. This is why "change name / activate / sign loan" succeed
|
||
with a bare stub.
|
||
- **user-record** element (`0x18012c990`, GetClubInfo) is a jump-table parser; caught fields
|
||
include `established`(0x110,str), `homekit`(0x159), `awaykit`(0x3f), `cleansheets`(0x84,int),
|
||
`attributeList`(0x31), `passing`(0x214), `awardType`(0x44), `categoryId`(0x73),
|
||
`controls`(0xb9), `changed`(0x7e), `currentChampionEvent`(0xc2). Full field map is PARTIAL
|
||
(jump table at `[0x1412c4]` not fully decoded); all fields optional/SKIP-safe.
|
||
- **card-item** element = `0x18013fe00`, already reversed in `CARD_SYSTEM.md`
|
||
(rating/position/nation/teamid/attributeList/name/resourceId…). Not re-derived here.
|
||
- Atom→key mapping from `atoms.tsv` (FNV-indexed sequential IDs 0x0–0x38a).
|
||
|
||
---
|
||
|
||
### Minimal known-good JSON
|
||
|
||
```jsonc
|
||
// 1 GetClubInfo — GET ut/game/fifa17/club (club-user summary list)
|
||
{ "user": [ { "established": "2026", "cleansheets": 0, "homekit": 0, "awaykit": 0 } ] }
|
||
// or safe empty: { "user": [] }
|
||
|
||
// 2 GetClubUsers — GET ut/game/fifa17/clubUser
|
||
{ "user": [] }
|
||
|
||
// 3 ChangeClubName — PUT ut/game/fifa17/club
|
||
{}
|
||
|
||
// 4 ViewCards — GET ut/game/fifa17/item (THE card list; renders real cards)
|
||
{ "itemData": [ /* card items, deser 0x18013fe00 shape */ ] }
|
||
|
||
// 5 ActivateCard — PUT ut/game/fifa17/item
|
||
{}
|
||
|
||
// 6 ApplyCard / 7 ApplyCardByRes — PUT ut/game/fifa17/item
|
||
{ "itemData": [ /* the single updated card item */ ] }
|
||
|
||
// 8 DiscardCard — DELETE ut/delete/game/fifa17/item
|
||
// CORRECTED 2026-08-05: `items` is an array of OBJECTS and there is no top-level `id`.
|
||
// The previous shape, { "items": [ 123456789 ], ..., "id": 123456789 }, was wrong twice
|
||
// over, and feeding a bare int where the element parser expects an object is a tokenizer
|
||
// desync, i.e. a hard freeze at 0x1801c7f1a, not a soft failure.
|
||
{ "items": [ { "id": 123456789 } ], "totalCredits": 15000 }
|
||
|
||
// 9 DiscardCardByRes — DELETE ut/delete/game/fifa17/item
|
||
{ "totalCredits": 15000 }
|
||
|
||
// 10 MoveCard / 11 MoveCardByRes — PUT ut/game/fifa17/item
|
||
{ "itemData": [ /* moved item */ ], "chemistry": true }
|
||
|
||
// 12 ConsumablesSearch — GET ut/game/fifa17/item?type=<consumable>
|
||
{ "itemData": [ /* consumable items */ ], "displayGroupUseDefaultImage": 0 }
|
||
|
||
// 13 StaffBonus — GET ut/game/fifa17/... (staff bonuses)
|
||
{ "bonus": [], "assetId": 0 }
|
||
|
||
// 14 GetAvailableLoanPlayers — GET ut/game/fifa17/item (loans)
|
||
{ "loans": [], "itemData": [ /* loan card items */ ], "default": 0 }
|
||
|
||
// 15 SignLoanPlayer — PUT ut/game/fifa17/item
|
||
{}
|
||
|
||
// 16 StickerBookSearch — GET ut/game/fifa17/... (sticker book)
|
||
{ "itemData": [] }
|
||
```
|
||
|
||
### utas_server.py integration status
|
||
- `/item` (`defs_route`) already serves `{"itemData":[…]}` → satisfies **ViewCards, ApplyCard,
|
||
ApplyCardByRes, MoveCard, ConsumablesSearch, StickerBook, loan itemData** shape (though action
|
||
semantics — updated item / chemistry / credits — are not modelled).
|
||
- `/club` serves `{"itemData":…}` but GetClubInfo wants **`user`** → the itemData is SKIP'd; club
|
||
card render still works because it goes through `/item` ViewCards, not `/club`. To populate the
|
||
club-user summary, serve `{"user":[…]}` on `/club`.
|
||
- `/clubUser` → `{}` (GAP: should be `{"user":[…]}`; `{}` is non-freezing).
|
||
- No routes for discard-credits, move-chemistry, staff-bonus, loan `loans[]`, stickerbook → all
|
||
fall to catch-all `{}` (safe, feature-inert).
|
||
```
|
||
## Store / Packs / Purchases
|
||
|
||
Reversed from CardsDLL (base `0x180000000`) via the RECIPE deserializer method. All
|
||
struct-name VAs computed as `0x1801e5000 + (fileoff - 0x1e4400)`; deserializers located
|
||
from the `.text` `lea r8,[name]` xref; atoms translated through `atoms.tsv`.
|
||
|
||
Parser primitives: INT `0x1801c79d0` · BOOL `0x1801c7620` · STR `0x1801c7aa0` ·
|
||
value-SKIP (unknown atom, safe) `0x180135ff0` · next-token `0x1801c7f10` ·
|
||
begin-object `0x1801c8270` · key→atom FNV `0x180180d00` · shared ITEM element deser
|
||
`0x18013fe00`. **Type fidelity is mandatory** — feeding a scalar getter an object/array
|
||
desyncs the SAX reader → tokenizer freeze at `0x1801c7f1a`.
|
||
|
||
### Endpoint → struct map (paths use %s = "game/fifa17")
|
||
|
||
| Method | Path | Request→Response struct | Blaze cmd token |
|
||
|---|---|---|---|
|
||
| GET | `ut/%s/store` (`store/purchasegroup/...`) | FutStoreGetPackTypesServerResponse | STOREPACKTYPES |
|
||
| GET | `ut/v2/%s/store` | FutStorePackQuantitiesServerResponse | STOREPACKQUANTITIES / V2STORE |
|
||
| POST/PUT | `store/transaction` | FutCreatePackServerResponse (buy=create) / FutPurchaseItemsServerResponse | CREATEPACK / PURCHASEITEMS |
|
||
| GET | `ut/%s/purchased` | FutGetPurchasedItemsServerResponse | PURCHASEDITEMS |
|
||
| — | credits refresh (embedded) | FutUpdateCreditsServerResponse | UPDATECREDITS |
|
||
|
||
---
|
||
|
||
### 1. FutStoreGetPackTypesServerResponse — confidence: HIGH
|
||
- **name VA** `0x18021de20` · **deserializer** `0x1801234e0` · **pack element deser** `0x18013af30`
|
||
- **Root keys** (deser `0x1801234e0`):
|
||
- `purchase` (atom **0x260**) → **ARRAY** of pack objects (each → `0x18013af30`) *(freeze-risk: must be array)*
|
||
- `timestamp` (atom **0x31b**) → INT scalar → `[rdi+0x5c]`
|
||
- **Pack object fields** (deser `0x18013af30`, all optional; unknown keys skipped):
|
||
|
||
| key | atom | type | notes |
|
||
|---|---|---|---|
|
||
| `assetId` | 0x23 | INT | **real pack identity** → `[rbp-0x3c]` |
|
||
| `actionType` | 0x08 | INT | |
|
||
| `bonus` | 0x5c | INT | |
|
||
| `dealType` | 0xcc | STR | |
|
||
| `description` | 0xd1 | STR | display name |
|
||
| `displayGroup` | 0xd9 | **ARRAY** | nested (freeze-risk) |
|
||
| `displayGroupAssetId` | 0xda | INT | `[rbp-0x80]` |
|
||
| `displayGroupUseDefaultImage` | 0xdb | BOOL | |
|
||
| `currencies` | 0xc5 | **ARRAY** | coin price: `[{name,funds,finalFunds}]` (freeze-risk). **`finalFunds` is the number the tile RENDERS. CONFIRMED LIVE 2026-08-05** by serving `funds=15000, finalFunds=4321` on one pack and reading `4,321` off the store tile. `funds` is not displayed. |
|
||
| `extPrice` | 0x119 | **OBJECT** | → `finalPrice`(0x125,obj `0x180139070`) + `originalPrice`(0x205,obj `0x18013aae0`); inner uses `amount`(0x1b)/`currency`(0xc4) (freeze-risk) |
|
||
| `packContentInfo` | 0x20c | **OBJECT** | → `bronzeQuantity`(0x63), `silverQuantity`(0x2c6), `goldQuantity`(0x149), `rareQuantity`(0x273), `itemQuantity`(0x170), `start`(0x2e3), `unopened`(0x35d,bool) (freeze-risk) |
|
||
| `sortPriority` | 0x2cb | INT | |
|
||
|
||
- **Status: already handled (renders in-game), but with CORRECTIONS**
|
||
- `store_catalog()` currently emits `id, packType, quantity, purchaseLimit, purchaseCount, isPremium, saleType` — **none of these atoms exist in the pack deser** (`id`=0x15c, `quantity`=0x26b, `saleType`=0x298, `packType`=0x20f, `isPremium`=0x176 are all routed to SKIP `0x180135ff0`). They are harmless no-ops but do nothing.
|
||
- The **real identity field is `assetId` (0x23)**, which the current handler does NOT send. Recommend adding `assetId` per pack (packs currently work off `currencies`+`extPrice`+`packContentInfo` presence + the transaction-body `packId`, but `assetId` is the field the client actually deserializes).
|
||
- `extPrice.finalPrice/originalPrice` inner keys are NOT `mtx` (no such atom) — real inner atoms are `amount`/`currency`; current `{"mtx":N}` is skipped, so extPrice objects are effectively empty-but-present (enough to pass validation; FIFA-Points price shown comes from elsewhere).
|
||
- **Minimal known-good** (corrected):
|
||
```json
|
||
{"purchase":[{"assetId":101,"description":"Gold Pack","sortPriority":1,
|
||
"currencies":[{"name":"coins","funds":5000,"finalFunds":5000}],
|
||
"extPrice":{"finalPrice":{"amount":100,"currency":"fifapoints"},"originalPrice":{"amount":100,"currency":"fifapoints"}},
|
||
"packContentInfo":{"bronzeQuantity":0,"silverQuantity":0,"goldQuantity":7,"rareQuantity":1,"itemQuantity":7}}],
|
||
"timestamp":1596326400}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. FutStorePackQuantitiesServerResponse — confidence: HIGH ⟵ GAP (UNBUILT)
|
||
- **name VA** `0x18022d440` · **deserializer** `0x1801758c0` (token loop `0x180175920`)
|
||
- **This is the `ut/v2/%s/store` response.** It is NOT a per-pack quantity list — it is a single
|
||
**eligibility/result gate**. The deser reads exactly ONE key:
|
||
- `result` (atom **0x288**) → STRING; the string VALUE is FNV-hashed (`0x180180d00`) and mapped to an enum stored at `[rdi+0x28]`:
|
||
|
||
| string value | atom hit | enum |
|
||
|---|---|---|
|
||
| `SUCCESS` | 0x2fb | 0 |
|
||
| `TOO_MANY_TOURNAMENTS` | 0x324 | 1 |
|
||
| `LOCKED_PERMANENT` | 0x1a1 | 2 |
|
||
| `LOCKED_RETRY` | 0x1a2 | 3 |
|
||
| `LOCKED_TROPHIES` | 0x1a3 | 4 |
|
||
|
||
All other keys are skipped. Constructor default of `[rdi+0x28]` is 0 (SUCCESS), so an empty
|
||
`{}` also parses as SUCCESS, but send `result` explicitly.
|
||
- **Status: GAP** — no route builds this. Add a handler for `ut/v2/%s/store`.
|
||
- **Minimal known-good**: `{"result":"SUCCESS"}`
|
||
|
||
---
|
||
|
||
### 3. FutCreatePackServerResponse — confidence: HIGH
|
||
- **name VA** `0x180228318` · **deserializer** `0x180162880`
|
||
- Wrapper key `createPackResponse` (atom **0xbe**) → OBJECT with:
|
||
|
||
| key | atom | type | store |
|
||
|---|---|---|---|
|
||
| `itemList` | 0x16e | **ARRAY** of items (element deser `0x18013fe00`) | freeze-risk |
|
||
| `numberItems` | 0x1dd | INT | `[rsi+0x28]` |
|
||
| `purchasedPackId` | 0x264 | INT | `[rsi+0x70]` |
|
||
| `duplicateItemIdList` | 0xec | **ARRAY of OBJECTS** (element deser `0x180138e10`) | freeze-risk |
|
||
|
||
- **Status: already handled — VERIFIED byte-exact** against `store_buy()`.
|
||
- **Minimal known-good**:
|
||
```json
|
||
{"createPackResponse":{"itemList":[<item>],"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 `[<item>...]`)
|
||
|
||
---
|
||
|
||
### 6. FutUpdateCreditsServerResponse — confidence: MEDIUM
|
||
- **name VA** `0x18022cc10` · **deserializer** `0x1801738b2` (delegates whole body to shared object parser `0x180139610`, `rdx=[rdi+0x10]`)
|
||
- Carries the credits/currencies balance object. The verified coin-binding path is the shared
|
||
currencies parser (deser `0x180122c50`, atom `currencies` 0xc5 → `[].funds`), already served by
|
||
`credits_route()` as `{"credits":N,"currencies":[{name,funds,finalFunds}]}`. Exact per-offset
|
||
field map of `0x180139610` not fully traced (it is a large shared parser); no correction needed
|
||
since the coin counter already binds correctly.
|
||
- **Status: effectively handled** via `credits_route`. Keep `{"credits":N,"currencies":[...]}`.
|
||
|
||
---
|
||
|
||
## Definitive store-availability config flag list (for Blaze client-config)
|
||
|
||
The store "not available" screen is gated by (a) a **resolution check** — `GetSystemMetrics`
|
||
must be > 1024×768 — and (b) the following Blaze **client-config** flags. `FUT_STORE_DISABLED`
|
||
is a UI **message** string, not a gate. Confirmed present in `cardsdll.strings`:
|
||
|
||
**Blaze client-config booleans (must be "1"/true):**
|
||
- `IS_STORE_ENABLED`
|
||
- `IS_COIN_PURCHASABLE`
|
||
- `IS_FIFAPOINT_AVAILABLE`
|
||
- `IS_FIFAPOINT_PURCHASABLE` ← additional (found adjacent)
|
||
- `IS_EASTORE_SERVICE_READY` ← additional (found adjacent)
|
||
- `COINS_PURCHASE_ENABLED`
|
||
- `POINTS_PURCHASE_ENABLED`
|
||
- `MONEY_PURCHASE_ENABLED` ← additional (found adjacent)
|
||
|
||
**FUT data/config flags (lowercase, JP-region variants exist):**
|
||
- `cardPackStoreEnabled` / `cardPackStoreEnabled_JP`
|
||
- `coinEnabled` / `coinEnabled_JP`
|
||
|
||
Set all Blaze booleans to enabled and ensure the client renders above 1024×768.
|
||
# Section: User / Hub / Settings / Objectives / Leaderboards / Champions
|
||
|
||
Clean-room RE of the FIFA 17 FUT boot/hub API from `cardsdll.dll` disassembly
|
||
(base `0x180000000`). Method: RECIPE.md — struct-name string → `lea r8` xref →
|
||
deserializer field-loop → FNV atom immediates (cumulative sub/cmp jump-ladders)
|
||
→ `atoms.tsv`. `%s = "game/fifa17"`.
|
||
|
||
**Shared token-type noise** (excluded from field lists below): after every
|
||
`NextToken` (`0x1801c7f10`) the parser does `cmp eax,0xa` / `cmp eax,0x6` /
|
||
`cmp eax,0xd` — these are JSON token-TYPE checks, NOT field atoms.
|
||
|
||
**Leaf getters:** int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`.
|
||
**Value-SKIP** (unknown atom, safe extra keys): `0x180135ff0`. **Card/item**
|
||
sub-deser: `0x18013fe00`. Feeding a scalar getter an object/array desyncs the SAX
|
||
reader → infinite spin at `0x1801c7f1a` (the hub freeze).
|
||
|
||
---
|
||
|
||
## BOOT / HUB path (gates reaching the FUT hub — highest priority)
|
||
|
||
### FutCreateUserServerResponse — CONFIDENCE: HIGH ✅ HANDLED
|
||
- **Deser:** `0x18014cc60` (lea r8 @ `0x18014cc2c`)
|
||
- **HTTP:** `POST ut/%s/user`
|
||
- **Fields (exact, matches util baseline):**
|
||
- `login` (0x1a5) — bool
|
||
- `userData` (0x36d) — nested → userInfo record (deser `0x18013ec10`)
|
||
- `squad` (0x2cd) — nested object (→ squad deser `0x18013d1f0`)
|
||
- `starterPack` (0x2e5) — nested object
|
||
- `bonusPacks` (0x5d) — array
|
||
- **Handled:** `utas_server.USER_POST`. Min JSON:
|
||
```json
|
||
{"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]}
|
||
```
|
||
|
||
### FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) ✅ HANDLED
|
||
- **Wrapper parser:** `0x180146970` — does `Parse` + TWO `NextToken` before
|
||
deserializing, so the body MUST be wrapped in exactly one member (member NAME
|
||
not compared, nesting level required).
|
||
- **userInfo record deser:** `0x18013ec10` (freeze-critical — shared with massinfo).
|
||
- **HTTP:** `GET ut/%s/user` (NOT called at boot; only reachable via massinfo).
|
||
- **userInfo fields — confirmed atoms (cumulative-ladder):**
|
||
- `personaId` (0x21b) — int
|
||
- `sessionCoinsBankBalance` (0x2bb) — int
|
||
- `trophies` (0x340) — int
|
||
- `won` (0x387) — int
|
||
- `purchased` (0x262) — bool
|
||
- `feature` (0x11c) — **nested object** (e.g. `{trade:bool}`) ⚠ freeze-risk
|
||
- `fifaPointsFromLastYear` (0x121) — int
|
||
- `squadList` (0x2d4) — **array** ⚠ freeze-risk
|
||
- `unopenedPacks` (0x35e) — **nested object** ⚠ freeze-risk; children
|
||
`preOrderPacks`(0x24b), `recoveredPacks`(0x27b), `count`(0xbc)
|
||
- nested notification sub-obj: `notification`(0x1da), `outbid`(0x206), `winning`(0x384)
|
||
- **From validated baseline (util) — also parse (unknown→SKIP):** `clubName`,
|
||
`clubAbbr`, `established`, `clubNameChangeAllowed`, `currencies`[] (array of
|
||
`{name,value}` ⚠ array), `won/draw/loss`, `divisionOffline/divisionOnline`,
|
||
`reliability`{reliability,matchUnfinishedTime}, `bidTokens`{count,updateTime},
|
||
`actives`[] (array).
|
||
- **Handled:** `utas_server.USER_GET = {"userInfo": user_info()}`.
|
||
|
||
### FutGetUserMassInfoServerResponse — CONFIDENCE: HIGH (schema) ✅ SERVED POPULATED (live-verify)
|
||
- **Deser:** `0x180174630` (full decompile: `/tmp/ghidra_fut/massinfo.txt`).
|
||
- **HTTP:** `GET ut/%s/userMassInfo`
|
||
- **Shape: FLAT object, NO wrapper.** (Corrected 2026-08-03 — the earlier "wrapper key is
|
||
`user`" note was wrong; `user`(0x36c) occurs only nested inside `clubUser`. The prologue,
|
||
2×NextToken before the key loop, is identical to the proven-flat CreateUser parser
|
||
`0x18014cc60`.)
|
||
- **Top-level keys this deser dispatches:**
|
||
- `userInfo` (0x370) → userInfo deser `0x18013ec10`
|
||
- `squad` (0x2cd) → LoadActiveSquad deser `0x18013d1f0` (loads the ACTIVE squad model)
|
||
- `settings` (0x2bf) → settings deser `0x18013c6d0`
|
||
- `userData` (0x36d) → `0x180142470`
|
||
- `clubUser` (0x91), `errors` (0x10c), `loanPlayerClientData` (0x199),
|
||
`loanPlayers` (0x19a), `pileSizeClientData` (0x227)
|
||
- everything else → SKIP (`0x180135ff0`)
|
||
- **Freeze history:** the documented "any content desyncs" was recorded before the squad
|
||
schema was reversed; the prime suspect is the malformed `squad` member fed to
|
||
`0x18013d1f0`. Every `user_info()` field type-checks against `0x18013ec10`.
|
||
- **Handled:** `utas_server.massinfo()` → `{userInfo, squad, settings, userData}`;
|
||
`FUT_MASSINFO=full|squad|userinfo|settings|empty` bisects it one member per relaunch.
|
||
|
||
### FutGetSettingsServerResponse — CONFIDENCE: HIGH ✅ HANDLED (schema) / the 42 flags are RECOVERED, UNTESTED
|
||
- **Deser:** `0x18013c6d0` (1982 bytes, 12061-char decompile, read end to end)
|
||
- **HTTP:** `GET ut/%s/settings`, and the `settings` (0x2bf) member of `userMassInfo`
|
||
(both callers of the deser: `0x18014e590` and `0x180174630`)
|
||
- **Fields:** single wrapper key `configs` (0xa2) → array of config entries
|
||
`{ type (0x354), value (0x377) }`. The key ladder really does hold nothing else.
|
||
|
||
**The mechanism the key ladder hides.** A flag is not a JSON key. When an element
|
||
closes, the client feeds the STRING VALUE of `type` back through the atom hasher
|
||
(`FUN_180180d00`) and switches on the result, 42 arms wide:
|
||
|
||
```json
|
||
{"configs": [{"type": "friendlySeasonsEnabled", "value": 1}]}
|
||
```
|
||
|
||
So the flag vocabulary is the same atom table everything else uses, and the client
|
||
hashes our string itself — a flag cannot be misnamed silently, it simply falls
|
||
through to the default arm and is ignored.
|
||
|
||
- **`value` is type-forgiving.** Its getter `0x1801c79d0` accepts int (token 2),
|
||
float (3), bool (4) and string (5, via `sscanf "%I64d"`), coercing all four to
|
||
int64. `1`, `"1"` and `true` are equivalent. This is one of the few scalar
|
||
getters in the API with NO desync risk on scalars. An object or array is still
|
||
a freeze.
|
||
- **The applier demands exactly 1.** `FUN_18011dc50` is the only writer of the
|
||
gate bytes and every line is `gate_byte = (field == 1)`. Not truthiness. `2`,
|
||
`-1` and `"yes"` all read as OFF.
|
||
|
||
**Flags that publish a UI gate key.** `FUN_18006cc60` publishes IS_* state keys by
|
||
reading single bytes inside `FutDataManagerImpl` (service id `0xed84b11`, ctor
|
||
`0x18010cdc0`). Those bytes are written ONLY by the applier, and the ctor never
|
||
touches them (whole 16620-char ctor scanned):
|
||
|
||
| flag `type` | field | gate byte | UI key |
|
||
|---|---|---|---|
|
||
| `tradingEnabled` | `[10]` | `0x1fd2e` | `IS_TRADING_ENABLED` |
|
||
| `storeEnabled` / `_JP` | `[0xb]` / `[0xc]` | `0x1fd2f` / `0x1fd30` | `IS_STORE_ENABLED` (accessor `0x18011c600` picks `_JP` when region == 4) |
|
||
| `friendlySeasonsEnabled` | `[0x16]` | `0x1fd3a` | `IS_FRIENDLY_SEASON_ENABLED` |
|
||
| `tournamentQuitEnabled` | `[0x20]` | `0x1fd3b` | `IS_TOURNAMENT_QUIT_ENABLED` |
|
||
| `processingStateEnabled` | `[0x21]` | `0x1fd3c` | `IS_PROCESSING_STATE_ENABLED` |
|
||
| `enableDraftMode` | `[0x17]` | `0x1fd3d` | `IS_DRAFT_MODE_ENABLED` |
|
||
| `enableOfflineDraftMode` = `enableSinglePlayerDraftMode` | `[0x18]` | `0x1fd3e` | (shared arm, one field) |
|
||
| `storyModeRewardEnabled` | `[0x1f]` | `0x1fd3f` | `IS_STORY_MODE_REWARD_ENABLED` |
|
||
| `returningUserRewardsScreenEnabled` | `[0x19]` | `0x1fd40` | `IS_RETURNING_USER_REWARDS_SCREEN_ENABLED` |
|
||
|
||
**Why this is the standing suspect for Seasons and Draft.** Both refuse while
|
||
making zero requests to any of the four servers, which no response shape can
|
||
explain. A UI key evaluated from a byte that nothing ever wrote does explain it.
|
||
The store is the control: `IS_STORE_ENABLED` reads the same kind of byte and its
|
||
screen works, because `storeEnabled` and friends are already shipped through the
|
||
**Blaze** client-config store (`FUT_RS4_CONFIG` in `blaze_responder_v3b.py`) —
|
||
and that list contains no seasons, draft or tournament flag. Same mechanism, one
|
||
population, one blank.
|
||
|
||
This is a hypothesis with a mechanism, not a confirmed cause. It predicts that
|
||
sending the flags opens the screens; if they still refuse, the gate is upstream
|
||
of the UI key and the whole settings line is dead.
|
||
|
||
**Two arms that are not simple assignments:**
|
||
- `enableObjectives` (0xfd) and `enableObjectivesAsManagerTasks` (0xfe) share an
|
||
arm that can only ever CLEAR `[0x1c]`: `if (value == 0) field = 0`. Sending 1
|
||
is a no-op. Objectives cannot be turned ON here, only off.
|
||
- `clientKeepAliveResetTimeoutSec` (0x86, vtable +0x68) and `getOperationTimeoutSec`
|
||
(0x13d, +0x58) do not store a field; they call a timer object with `value * 1000`.
|
||
Sending a small number shortens client timeouts. Leave them alone.
|
||
|
||
**`maximumTradePileSize` (0x1c0) is the positive control.** It lands in `[0]` and
|
||
is passed to `FUN_18011f380`, and transfer-list capacity is visible in game. It
|
||
distinguishes "the flag did not help" from "the configs array never reached the
|
||
consumer at all", which no boolean flag can do on its own.
|
||
|
||
**Not in the switch:** `enableSquadBuildingSetsFeature` (0x100) is a real atom but
|
||
has NO arm here, so SBC is gated somewhere else. Scanned the full decompile;
|
||
this absence is asserted over the whole function, not a slice.
|
||
|
||
- **Handled:** `utas_server.SETTINGS`, `FUT_SETTINGS` (default `gates`).
|
||
`off` restores the historical `{"configs": []}`.
|
||
|
||
### FutGetHubDataServerResponse — CONFIDENCE: HIGH (schema fully enumerated) — ✅ HANDLED (tiles populated)
|
||
- **Deser:** `FUN_180139610` (root object parser). Wrapper `0x1801736ad`.
|
||
- **HTTP:** `GET ut/%s/hub`
|
||
- **CORRECTION (2026-08-06):** the earlier note here — "uses C++ reflection /
|
||
vtable dispatch, NOT an inline atom ladder, no static field ladder to read,
|
||
GAP" — was **WRONG**. `FUN_180139610` has an ordinary inline atom ladder: a
|
||
running-sum `sub ecx,d / … / cmp ecx,d` dispatch plus a few direct `cmp esi,imm`.
|
||
It reads **18 atoms**, all enumerated below straight from the on-disk CardsDLL
|
||
via objdump (`fifa17-recon` scratchpad `hub_ladder.py`). The vtable calls are the
|
||
per-sub-object dispatch one indirection deeper, not the field read itself.
|
||
- **The 18 root atoms** (name ← `fut_atoms.tsv`):
|
||
`allObjectivesForCurrentGameSpaceId`(0x15), `auctionCount`(0x33),
|
||
`championEvent`(0x7a), `clubPlayers`(0x90), `draftSummary`(0xe4),
|
||
`friendlySeason`(0x131), `leaderboard`(0x186), `liveMessagesAvailable`(0x190),
|
||
`objectivesForCurrentUser`(0x1e3), `offlineSeason`(0x1ec), `ONLINE`(0x1f1),
|
||
`onlineSeason`(0x1f6), `SINGLE_PLAYER`(0x29d), `squad`(0x2cd),
|
||
`tournament`(0x328), `tournamentProgress`(0x32c), `tradePile`(0x333),
|
||
`watchlist`(0x381).
|
||
- **TILE MAP (which atom drives which hub tile):**
|
||
- `clubPlayers`(0x90) int → MY CLUB tile "N players" (TILE_ID 0x210)
|
||
- `auctionCount`(0x33) int → TRANSFER MARKET tile "N LIVE TRANSFERS" (TILE_ID 0x1b0)
|
||
- `tradePile`(0x333) **nested object**, sub-deser `0x18013ead0` → TRANSFER LIST
|
||
tile "N ITEMS / Selling / Sold". Sub-atoms: `count`(0xbc), `notification`(0x1da),
|
||
`selling`(0x2b8), `sold`(0x2c9) — all scalar int via `0x1801c79d0` (5 int reads,
|
||
one SKIP, object field loop; no array/nested object → no type-desync surface).
|
||
Same atom scheme as `FutGetAuctionCount`. **All active listings are `selling`;
|
||
`count == selling == len(listings)`, `sold == 0`.**
|
||
- `watchlist`(0x381) nested object, sub-deser `0x18013f3b0` → WATCH LIST tile (not
|
||
yet populated; empty watch list defaults to 0, which is correct today).
|
||
- **LIVE SYMPTOM this fixed (2026-08-06):** a card was actively listed
|
||
(`auctionCount` 1, Listed Items screen showed it) yet the TRANSFER LIST tile read
|
||
"0 items / Selling 0". The tile reads `hub.tradePile`, which we were omitting; it
|
||
does **not** re-poll `/tradePile/counts` (the standalone GetAuctionCount endpoint)
|
||
once at the hub. Serving `hub.tradePile:{count,selling,sold}` corrected the tile.
|
||
- **Handled:** `utas_server.hub_data()` serves `clubPlayers`, `auctionCount`, and
|
||
`tradePile:{count,selling,sold}` (`FUT_HUBDATA=1`, default on). Remaining atoms
|
||
(seasons/draft/tournament/objectives/leaderboard summaries) default to 0/absent,
|
||
which is correct while those modes are unpopulated.
|
||
|
||
### FutUserDataServerResponse — CONFIDENCE: MEDIUM
|
||
- **Deser:** `0x18016dd50` (lea r8 @ `0x18016d98d`)
|
||
- **Fields:** `actives` (0xb, array), `key` (0x177), `value` (0x377) — key/value
|
||
user-data entries. Part of create/user path. Served inside `userData`.
|
||
|
||
---
|
||
|
||
## THEN — user lifecycle structs
|
||
|
||
| Struct | Deser VA | HTTP | Fields (atoms) | Status |
|
||
|---|---|---|---|---|
|
||
| FutGamerSetInfoServerResponse | 0x18016d85d | POST `ut/%s/user` (set gamer info) | none (empty ack) | GAP-trivial |
|
||
| FutKeepAliveServerResponse | 0x18016232d | `ut/%s/match/keepalive` | none | ✅ HANDLED (204) |
|
||
| FutLogoutServerResponse | 0x18017006d | logout | none (empty ack) | GAP-trivial |
|
||
| FutResetUserServerResponse | 0x18017548d | `DELETE ut/delete/%s/user` | none | GAP-trivial |
|
||
| FutGetUserActionServerResponse | 0x1801781ad | GET user actions | `actions`(0x7,array), `actionType`(0x8) | partial |
|
||
| FutUpdateUserActionServerResponse | 0x18012378d | PUT user action | none (empty ack) | GAP-trivial |
|
||
| FutSetFavFeatureServerResponse | 0x18016f2cd | set fav feature | none | GAP-trivial |
|
||
| FutLiveMessageUpdateServerResponse | 0x180153a2d | `ut/%s/livemessage` | none (empty ack) | GAP-trivial |
|
||
| FutGetTrustedConsoleListServerResponse | 0x18012a01d | `ut/%s/phishing/trusteddevice` | `changed`(0x7e,bool), `exists`(0x117,bool), `locked`(0x19e,bool), `trusted`(0x351,bool) | ✅ HANDLED (`trusted:true` skips security Q) |
|
||
|
||
Min JSON for the empty-ack structs: `{}` (200) or `204` — all validated safe.
|
||
`activeMessage` (`ut/%s/activeMessage`), `clientdata` (`ut/%s/clientdata`),
|
||
`livemessage` — no dedicated field-ladder deser; served generically ({} / 200).
|
||
|
||
---
|
||
|
||
## OBJECTIVES / CHALLENGES
|
||
|
||
FUT 17 objectives = **ManagerQuests** (viewmodel `futmanagerquestsviewmodel`;
|
||
data providers `FUT_MQ_QUESTS_DATA_DP`, `FUT_SQUAD_QUESTS_DP`,
|
||
`FUT_PLAYER_IDENTITY_QUESTS_DP`). Enable flags live in **settings**:
|
||
`enableObjectives` (0xfd), `enableObjectivesAsManagerTasks` (0xfe).
|
||
Relevant atoms: `objectives`(0x1e2), `objectivesForCurrentUser`(0x1e3),
|
||
`allObjectivesForCurrentGameSpaceId`(0x15), `challenges`(0x76),
|
||
`challengesCount`(0x78), `challengesCompletedCount`(0x77), `challengeId`(0x74),
|
||
`challengeImageId`(0x75), `grantedChallengeAwards`(0x14a), `squadChallenge`(0x2d1).
|
||
|
||
- **FutManagerQuestGetRewardServerCall** — deser `0x1801516cd` — reward-claim call;
|
||
0 inline atoms (response reuses shared item/award desers). No dedicated
|
||
`...ServerResponse` struct exists — the quests UI is client-driven via the
|
||
viewmodel + generic item/award payloads. **GAP** (no route yet).
|
||
- **FutGetTowChallengeServerResponse** — deser `0x18016dbcd` — challenge progress as
|
||
`key`(0x177)/`value`(0x377) pairs, `actives`(0xb). GET. **GAP**. Min: `{}`.
|
||
- **FutSetTowChallengeServerResponse** — deser `0x18016ef6d` — none (empty ack). PUT. GAP-trivial.
|
||
- **FutLoadSetChallengesResponse** — deser `0x18017b9ed` — SBC set-challenge defs (adjacent):
|
||
`awards`(0x47), `categoryId`(0x73), `elgReq`(0xf7), `endTime`(0x106),
|
||
`formation`(0x12b), `eligibilityKey`(0xf2), `eligibilitySlot`(0xf4). partial/GAP.
|
||
|
||
---
|
||
|
||
## LEADERBOARDS
|
||
|
||
### FutGetLBEntriesServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x180144c8d`
|
||
- **HTTP:** `GET ut/%s/leaderboards`
|
||
- **Entry fields (array):** `clubName`(0x8e), `badge`(0x49), `est`(0x10f,
|
||
established), `score`(0x29a), `seasonOnlineDraws`(0x2a6), `TalkRating`(0x303),
|
||
`insetUrl`(0x166), `inset`(0x165).
|
||
- **Min JSON:** `{"entries":[]}` (empty list safe).
|
||
|
||
### FutGetLBOptionsServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x18014351c`
|
||
- **HTTP:** `GET ut/%s/leaderboards/options`
|
||
- **Fields:** `category`(0x70), `id`(0x15c), `period`(0x218), `view`(0x37a), `url`(0x366).
|
||
|
||
---
|
||
|
||
## CHAMPIONS
|
||
|
||
### FutChampionsRegistrationServerResponse — CONFIDENCE: MEDIUM — GAP-trivial
|
||
- **Deser:** `0x18014980d` — no inline atoms (empty/status ack). `POST ut/%s/champion`. Min `{}`.
|
||
|
||
### FutGetChampionsFriendsServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x18014b7ad`. `GET ut/%s/champion` (friends).
|
||
- **Fields:** `stats`(0x2ec, nested), `gamesPlayed`(0x139), `persona`(0x21a). Min `{}`.
|
||
|
||
### FutGetChampionsTopXServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x18014a09d`. `GET ut/%s/champion` (topX).
|
||
- **Fields:** `entries`(0x109, array) of `{ clubName(0x8e), badge(0x49), est(0x10f) }`. Min `{"entries":[]}`.
|
||
|
||
---
|
||
|
||
## CAPTCHA / PHISHING / TFA
|
||
|
||
### FutGetCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x18014e78d`. `GET ut/%s/captcha`.
|
||
- **Fields:** `encodedImg`(0x101, string base64), `sequence`(0x2ba, int),
|
||
`sizeBeforeEncode`(0x2c7, int).
|
||
|
||
### FutValidateCaptchaServerResponse — `0x18014ed5d` — none (status ack). `POST ut/%s/captcha`. GAP-trivial. Min `{}`.
|
||
|
||
### FutExchangeCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
|
||
- **Deser:** `0x180177d5d`. Fields: `token`(0x321, string). Min `{"token":"..."}`.
|
||
|
||
### FutGetPhishingQuestionServerResponse — CONFIDENCE: HIGH ✅ HANDLED
|
||
- **Deser:** `0x18012980d`. **HTTP:** `GET ut/%s/phishing/question?deviceId=%s`.
|
||
- **Fields:** `question`(0x26c, int id), `answer`, `attempts`(0x28, int),
|
||
`recoverAttempts`(0x27a, int).
|
||
- **Handled:** util `{"question":0,"answer":"","attempts":5}`.
|
||
|
||
### FutSetPhishingAnswerServerResponse — `0x180129b9d` — none. `POST /question?deviceId=%s&question=%d&answer=%s`. ✅ HANDLED.
|
||
|
||
### FutValidatePhishingAnswerServerResponse — CONFIDENCE: HIGH ✅ HANDLED
|
||
- **Deser:** `0x180129ddd`. Returns a trust `token`. **HTTP:** `ut/%s/phishing/validate`.
|
||
- **Handled:** util `{"token":"OPENFUT-TRUST-..."}`.
|
||
|
||
### TFA — `ut/%s/tfa` — endpoint present; no dedicated field-ladder deser (served generically). GAP-untriggered.
|
||
|
||
---
|
||
|
||
## Summary of atom / path cross-checks
|
||
- Path templates confirmed in binary strings: `ut/%s/{user,user/list,hub,clientdata,
|
||
activeMessage,livemessage,leaderboards,leaderboards/options,champion,phishing,
|
||
captcha,tfa}`, plus `/question?deviceId=%s(&question=%d&answer=%s)`.
|
||
- Freeze-risk nested fields (must be object/array, never scalar): userInfo.`feature`,
|
||
userInfo.`unopenedPacks`, userInfo.`squadList`, userInfo.`currencies`,
|
||
userInfo.`actives`; and the whole userMassInfo body (keep `{}`).
|
||
|
||
|
||
---
|
||
|
||
## Regenerating the analysis inputs
|
||
|
||
```
|
||
cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll
|
||
objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm
|
||
strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings
|
||
python3 tools/atomdump.py > /tmp/fut/atoms.tsv # 907 atom->key rows
|
||
```
|
||
Recipe: docs/OPENCODE_ENDPOINT_PROMPT.md · atom table VA 0x1802d2760 · deser locator in header above.
|
||
|
||
---
|
||
|
||
## Implementation status & leads (updated 2026-08-02, autonomous session)
|
||
|
||
- **Store** — FIXED (v2/store SUCCESS gate + 14 Blaze flags + catalog assetId/extPrice). Live-verified server-side.
|
||
- **Transfer market** — IMPLEMENTED read path: `auctionhouse` search serves 18 real-player
|
||
listings (auction record 0x18013e410, itemData via proven 0x18013fe00); tradePile/watchList
|
||
empty; validated freeze-safe offline by tools/test_fut_contract.py (311 checks). Toggle FUT_MARKET=empty.
|
||
NEXT: buy/bid flow (stateful — deduct coins, grant card, echo updated auction) — needs live test.
|
||
- **SBC** — LEADS (not yet enabled; need live test): feature gate flag `enableSquadBuildingSetsFeature`;
|
||
`FUT/SBC_USE_STUBS` (BRICK) may enable client-side stub SBCs with NO server content (safest path — try first);
|
||
set-list deser 0x180154990 (vtable 0x180226fc0 slot+0x08) is a string-scanning/callback parser, not a
|
||
clean atom ladder (envelope key not cleanly resolvable statically); requirements are `SBC_ELG_KEY_*`
|
||
eligibility triples. `SBC_TIMER_EXPIRED` present. Recommended: set enableSquadBuildingSetsFeature +
|
||
FUT/SBC_USE_STUBS in Blaze config and observe whether the SBC menu populates from client stubs.
|
||
- **Draft** — deferred (stateful pick-progression state machine; broken state soft-locks — needs live test).
|
||
- **Match rewards** — reversed (FutDestroyMatch 0x180121b60 coin fields) but stateful — needs live test.
|