Files
OpenFUT/fifa17-recon/docs/ENDPOINT_MAP.md
T
funman300 b05ccd7ce5 fifa17-recon: record market-implemented status + SBC config-flag leads
enableSquadBuildingSetsFeature / FUT/SBC_USE_STUBS (client-side stub SBCs) /
SBC_ELG_KEY_ found in CardsDLL -- the next lead for SBCs, to validate live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
2026-08-02 20:03:46 -07:00

78 KiB
Raw Blame History

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
Squad Building Challenges (SBC) 8 5 SBC
FUT Draft 7 5 Draft
Match / Seasons / Tournaments 19 9 Seasons
Club / Cards / Consumables 16 15 Club
Store / Packs / Purchases 6 5 Store
User / Hub / Settings / Objectives / LB 32 7 deep + 11 partial User

"Fully reversed" = top-level field schema + types decoded HIGH-confidence. Nested card/squad elements reuse the shared item (0x18013fe00) / squad (0x18013d1f0) parsers documented in CARD_SYSTEM.md. Every struct in the sections has its deserializer VA, key list, and a minimal known-good JSON — including ack-only ({}) responses.

Shared record parsers (reused across groups)

VA Record Used by
0x18013fe00 ITEM / card element (itemData) club, squad, packs, purchased, market, draft, SBC
0x18013d1f0 full SQUAD object squad, draft, SBC squad-challenge
0x18013e410 auction/trade record (auctionInfo[] element) market search/watch/tradepile
0x18013e7f0 IS-list body {auctionInfo,credits,total,duplicateItemIdList} market
0x180135ff0 value-SKIP (unknown keys) all

★ Highest-value findings (actionable now)

  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 0x280x48: allCoins/matchCoins/seasonCoins/ tournamentCoins/coins/boostConis(sic)/participationAward. See Seasons §.
  3. GetClubInfo returns user (club-user stat array), not itemData. Rendered cards come from FutViewCardsServerResponse (0x1801293d0) on ut/%s/item — explains why the current /club itemData is SKIP'd yet cards still render. See Club §.
  4. SBC requirements = generic {eligibilityKey,eligibilityOperation,eligibilityValue} triples in an elgReq array — the key to modding challenges. See SBC §.
  5. Objectives = ManagerQuests, client-driven with no ServerResponse struct / no route; gated by enableObjectives in settings. See User §.

Catalog corrections to the current backend (utas_server.py / fut_store.py)

  • Store extPrice inner keys are amount/currency, not mtx; pack identity is assetId (0x23), not id (harmless SKIPs otherwise).
  • ut/v2/store must return {"result":"SUCCESS"} (currently unhandled → contributes to store error).
  • GetUserMassInfo top-level wrapper key is user (calls userInfo/squad/settings sub-parsers); keep {} until every nested shape is exact (freeze-sensitive).
  • No-op deserializers (bare ret) where {} always suffices: ChangeClubName, ActivateCard, SignLoanPlayer, and most ack responses.

Deep sections

Transfer Market / Auction House

Clean-room RE of the FIFA 17 CardsDLL (cardsdll.dll, image base 0x180000000) Internet-Shopping (IS = auction house / transfer market) response deserializers, via the RECIPE.md method (name-string → .text lea r8 xref → atom dispatch → atoms.tsv). All VAs are static CardsDLL VAs.

Shared parsers (the spine of every IS response)

VA Role
0x18013e410 Core auction/trade RECORD deserializer (one item in auctionInfo[]). Reversed deeply below.
0x18013e7f0 Shared IS-list response body{auctionInfo:[record…], credits, total, duplicateItemIdList}. Search / WatchList / TradePile all tail-delegate to it.
0x18013fe00 Shared ITEM/card element deserializer (the itemData object; same one used by club/squad/pack).
0x180135ff0 value-SKIP (unknown atoms — safe to send extra keys).
Leaf getters int/number 0x1801c79d0 · bool 0x1801c7620 · string 0x1801c7aa0.

Scalar-convert helpers seen: 0x1800d7b30 (num→int32), 0x1800d7b50 (num→bool/byte), 0x180166380 (str→bidState enum), 0x180166bd0 (str→tradeState enum), 0x180008120/0x180008020 (bounded string copy).


★ Core record: auctionInfo[] element — deserializer 0x18013e410 (confidence: HIGH)

Key dispatch (atom in edi, FNV-1a via 0x180180d00; ordered by atom value):

atom key JSON type getter notes
0x57 bidState string (enum) 0x1801c7aa00x180166380 e.g. none/highest/outbid/buyNow
0x65 buyNowPrice int 0x1801c79d00x1800d7b30
0xc1 currentBid int 0x1801c79d00x1800d7b30 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 0x1801c79d00x1800d7b30
0x2b7 sellerName string 0x1801c7aa00x180008120 bounded copy, max 0x1e=30 chars
0x2e6 startingBid int 0x1801c79d00x1800d7b30
0x2f4 coinsProcessed int/bool 0x1801c79d00x1800d7b50 truncated to byte
0x331 tradeId int (64-bit) 0x1801c79d0 (QWORD)
0x335 tradeState string (enum) 0x1801c7aa00x180166bd0 active/closed/expired
0x380 watched bool 0x1801c7620 true if on watch list

Not present at record level: bid (0x55), seller id, offers. Extra keys are skipped safely.

Minimal known-good record:

{
  "tradeId": 100000001,
  "itemData": { "id": 100000001, "resourceId": 1610612736, "assetId": 20801,
                "itemType": "player", "rating": 94, "preferredPosition": "ST",
                "untradeable": false, "itemState": "free" },
  "tradeState": "active",
  "buyNowPrice": 3000,
  "startingBid": 1500,
  "currentBid": 0,
  "bidState": "none",
  "expires": 3600,
  "sellerName": "OpenFUT",
  "sellerEstablished": 1,
  "watched": false,
  "coinsProcessed": 0
}

Shared IS-list body — 0x18013e7f0 (confidence: HIGH)

atom key type getter
0x35 auctionInfo array of records (0x18013e410 in a loop) FREEZE-RISK: must be array
0xc0 credits int 0x1801c79d00x1800d7af0→object setter
0xec duplicateItemIdList nested (array) 0x180138e10 FREEZE-RISK: must be array/obj
0x325 total int 0x1801c79d00x1800d7b30 (→ obj+0x60)

The 12 response structs

All 12 are GAPsutas_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 0x1801634200x18013e7f0 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 0x1801661300x18013e7f0 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 0x1801708100x18013e7f0 GET ut/%s/tradePile {auctionInfo:[record], credits, total} HIGH
10 FutRelistAllServerResponse 0x180164210 (req-ser 0x180164370) PUT ut/%s/auctionhouse/relist (relist all expired) ack; response body minimal → {} (request-side serializer builds a tradeId list) MED (partial)
11 FutGetAuctionCountServerResponse 0x180163670 (dispatch …83e) GET ut/%s/auctionhouse count (or marketdata) {count, maxAuctionsAllowed, offered, selling, sold} — atoms 0xbc,0x1bf,0x1e5,0x2b8,0x2c9, all int HIGH
12 FutGetSuggestedPricingServerResponse 0x180163bb0 (dispatch …ffa) GET ut/%s/marketdata?defId=… (price bands) {defId, minPrice, maxPrice} — atoms 0xcf,0x1c2,0x1ca, all int HIGH

Minimal known-good JSON per struct

// 1 FutISSearch  (GET auctionhouse)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1, "duplicateItemIdList": [] }
// 2 FutISStart   (POST auctionhouse)
{ "id": 100000001 }
// 3 FutISViewTrade (GET trade/{id})
{ "auctionInfo": [ <record> ], "credits": 100000 }
// 4 FutISWatchList (GET watchList)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 5 FutISWatchTrade (PUT watchList)   -> {}
// 6 FutISOfferTrade (POST trade/{id}/bid)
{ "auctionInfo": [ <record> ], "credits": 99000 }
// 7 FutISRemoveTrade (DELETE trade)   -> {}
// 8 FutISRemoveWatch (DELETE watchList) -> {}
// 9 FutGetTradePile (GET tradePile)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 10 FutRelistAll (PUT auctionhouse/relist) -> {}
// 11 FutGetAuctionCount
{ "count": 0, "maxAuctionsAllowed": 100, "offered": 0, "selling": 0, "sold": 0 }
// 12 FutGetSuggestedPricing
{ "defId": 1610612736, "minPrice": 900, "maxPrice": 10000 }

Freeze-risk summary (type fidelity is mandatory)

  • auctionInfoarray (never object/scalar).
  • itemData inside each record → object (the card; reuse item_def).
  • duplicateItemIdListarray.
  • bidState, tradeState, sellerNamestrings.
  • credits, total, count, *Price, *Bid, expires, tradeIdnumbers.
  • watchedbool. 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 0x180153fd10x18015405d
FutLoadSetTypesServerResponse GET sbs/challenge/%d/squad 0x1801545b90x18015469d
FutSBCStartChallengeResponse POST sbs/challenge/%d 0x1801552a90x18015531d
FutSBCSubmitChallengeServerResponse POST/PUT sbs/challenge/%d (submit) 0x1801618ff0x18016196d
FutSBCSetDataServerResponse GET sbs/sets (set-list / by category) builder 0x18016fa7d (no distinct path lea)
FutSBCLoadCategoryDetailsServerResponse GET sbs/sets 0x18017a9a50x18017aa3d
FutLoadSetChallengesResponse GET sbs/setId/%d/challenges 0x18017b9790x18017b9ed
FutSBCSaveSquadChallengeServerResponse PUT sbs/challenge/%d/squad 0x18017ce6f0x18017cedd

*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 (0x2800x354) via byte+dword jump table at 0x18017c334/0x18017c310 (decoded from binary).

Method+path: GET ut/game/fifa17/sbs/setId/{setId}/challenges

key atom type notes / struct offset
challengeId 0x074 int [rdi+0x34]
categoryId 0x073 int [rdi+0x38]
index 0x163 int [rdi+0x3c]
setId 0x2bc int [rdi+0x3c]-cluster
type 0x2c4 string→enum string-compared: "OPEN_CHALLENGE"→0, "BRICK_CHALLENGE"→2 → [rdi+0x40]
name 0x1d0 string [rdi+0x44], max 0x7f
description 0x0d1 string [rdi+0xc3], max 0xff
challengeImageId 0x075 string [rdi+0x1c8], max 0x64
formation 0x12b string mapped via 0x180166590[rdi+0x22c]. FREEZE-RISK: must be a JSON string, not int
endTime 0x106 int (epoch s) [rdi+0x240], day-scaled (cmp 0x16d=365)
repeatable 0x280 bool [rdi+0x239]
trophyId 0x2ee int [rdi+0x1c4]
status 0x30e string hash-mapped state
timesCompleted 0x322 int
squadId 0x2dc int
tutorial 0x33e (nested)
awards 0x047 nested ARRAY reward objects (see shared records). FREEZE-RISK: must be array
elgReq 0x0f7 nested ARRAY the SBC requirement/constraint list (see shared records). FREEZE-RISK: must be array

All other high atoms (205 of them) route to the value-SKIP default → extra keys are safe.

Minimal known-good:

[{"challengeId":1,"setId":1,"categoryId":0,"index":0,"type":"OPEN_CHALLENGE",
  "name":"League Basics","description":"Submit 11 players.","challengeImageId":"sbc_challenge_image_1",
  "formation":"f442","endTime":0,"repeatable":false,"trophyId":0,"status":"OPEN",
  "timesCompleted":0,"awards":[],"elgReq":[]}]

2. FutSBCSubmitChallengeServerResponse — CONFIDENCE: HIGH fully reversed

Dispatch @ 0x180161bda; ctor 0x18016196d. Method+path: POST sbs/challenge/{challengeId} (submit).

key atom type
challengeId 0x074 int
setId 0x2bc int
credits 0x0c0 int
preOrderPacks 0x24b int
recoveredPacks 0x27b int
grantedChallengeAwards 0x14a nested array (freeze-risk)
grantedSetAwards 0x14b nested array (freeze-risk)
{"challengeId":1,"setId":1,"credits":500,"preOrderPacks":0,"recoveredPacks":0,
 "grantedChallengeAwards":[],"grantedSetAwards":[]}

3. FutSBCStartChallengeResponse / squadChallenge record — CONFIDENCE: HIGH fully reversed

Dispatch @ 0x180155949; ctor 0x18015531d. Method+path: POST sbs/challenge/{challengeId}. Returns the squadChallenge record (the working squad for a challenge). This same record parser is shared by FutLoadSetTypesServerResponse.

key atom type
challengeId 0x074 int/nested
index 0x163 int
playerType 0x23d string
playerRequirements 0x237 nested array (per-slot constraint list; freeze-risk)
squad 0x2cd nested array of slot objects (freeze-risk)

Slot object = {index:int, playerType:string, playerRequirements:[...]}.

{"challengeId":1,"squad":[{"index":0,"playerType":"","playerRequirements":[]}]}

4. FutSBCLoadCategoryDetailsServerResponse — CONFIDENCE: HIGH fully reversed

Dispatch @ 0x18017ac08; ctor 0x18017aa3d. Method+path: GET sbs/sets.

key atom type
categoryId 0x073 int
name 0x1d0 string
priority 0x250 int
sets 0x2be nested array of set records (freeze-risk)
{"categoryId":0,"name":"Challenges","priority":0,"sets":[]}

5. FutSBCSaveSquadChallengeServerResponse — CONFIDENCE: MEDIUM-HIGH fully reversed (minimal)

Dispatch @ 0x18017d08a; ctor 0x18017cedd. Method+path: PUT sbs/challenge/{challengeId}/squad. Only one scalar field parsed:

key atom type
id 0x15c int (saved squad id)
{"id":1}

6. FutLoadSetTypesServerResponse — CONFIDENCE: MEDIUM ⚠️ partial

Dispatch @ 0x180154d69; ctor 0x18015469d. Method+path: GET sbs/challenge/{challengeId}/squad. Parses an array of squadChallenge templates ("set types" = squad-building slot templates), same key set as the squadChallenge record: {challengeId(0x074), index(0x163), playerType(0x23d str), playerRequirements(0x237 nested), squad(0x2cd nested)}. Outer top-level wrapper (array framing at 0x180154a2d) is callback-driven; exact top-level envelope key not resolved.

[{"challengeId":1,"index":0,"playerType":"","playerRequirements":[],"squad":[]}]

7. FutSBCSetDataServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial

Dispatch @ 0x18016ff2a; ctor 0x18016fa7d. Method+path: GET sbs/sets (set-data / by category). Top-level is a callback array-parser (lea r8,[rsi+0x50] vector-append at 0x18016fb5e) that appends set records parsed by the shared flat set-record deserializer; only one top-level scalar was resolved:

key atom type
reset 0x283 bool
(set records) nested array; each element has the SET-level fields (setId, name, description, awards, repeatable, endTime, challenges, starRating, setImageId, timesCompleted, sortPriority) — reuse struct #1's field vocabulary
{"reset":false,"sets":[{"setId":1,"name":"League Basics","description":"",
  "repeatable":false,"endTime":0,"awards":[],"challenges":[]}]}

8. FutSBCTagSetsServerResponse — CONFIDENCE: LOW-MEDIUM ⚠️ partial

Ctor 0x18015405d; deserializer is a callback-based array parser (installs per-element handler 0x180154280, vector-append) rather than an atom cmp ladder — no scalar keys resolved from the dispatch. Semantically it acknowledges a set-tag operation and returns the updated tagged sets. Method+path: POST/PUT sbs/sets/tag.

{"sets":[]}

Shared record shapes

elgReq — the SBC requirement/constraint list (the tricky part; parsed in struct #1 @ 0x18017bd9d, vector at [rdi+0x358]). Array of constraint objects, inner dispatch @ 0x18017bde0:

key atom type
eligibilityKey 0x0f2 int (constraint selector)
eligibilitySlot 0x0f4 int/bool
eligibilityValue 0x0f5 int (target value)
eligibilityOperation 0x0f3 int (comparator; skip-handled here)
eligibilities 0x0f1 (container atom)

Chemistry/rating/nation/league-count constraints (teamChemistry 0x307, starRating 0x2e2, sameNationCount 0x297, sameLeagueCount 0x296, sameClubCount 0x295, nationCount 0x1d4, leagueCount 0x18c, clubCount 0x8b, playerCount 0x22f, chemistry 0x81) are encoded generically as {eligibilityKey, eligibilityOperation, eligibilityValue} triples, not as named scalar fields on the record. FREEZE-RISK: elgReq must be a JSON array of objects.

awards / grantedAwards — nested array of reward objects (atoms: rewardType 0x28e, rewardValue 0x28f, rewardQuantity 0x28d, rewardMultiplier 0x28c, awardCount 0x40, awardSet 0x45, awardSetId 0x46, prizeSet 0x253). FREEZE-RISK: must be array.

SET-level field vocabulary (available atoms for a full set record): setId 0x2bc, name 0x1d0, description 0x0d1, challenges 0x76, challengesCount 0x78, challengesCompletedCount 0x77, repeatable 0x280, endTime 0x106, awardSet 0x45, setImageId 0x2bd, starRating 0x2e2, sortPriority 0x2cb, timesCompleted 0x322, categoryId 0x73, priority 0x250.

Freeze-risk summary (type fidelity mandatory)

  • formation = string (e.g. "f442"), never int.
  • awards, elgReq, squad, playerRequirements, grantedChallengeAwards, grantedSetAwards, sets, challenges = arrays/objects, never scalar. Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer spin (freeze 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 GAPtools/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):
{"positionid":0,"tier":1,
 "choices":[
   {"index":0,"itemData":{"id":1000001,"resourceId":20801,"assetId":20801,"itemType":"player","rating":94,"preferredPosition":"ST","nation":38,"teamid":243,"leagueId":53,"rareflag":1,"untradeable":true,"attributeList":[{"index":0,"value":90}],"itemState":"free","owners":1,"contract":7,"fitness":99}}
 ]}

Formation round: {"positionid":0,"tier":1,"choices":[{"index":0,"formation":"f442"},{"index":1,"formation":"f433"}]}.


2. FutGetDraftCurrentStateServerResponse — CONFIDENCE: HIGH (fully reversed, incl. roundsInfo element + squad delegation)

  • Struct name 0x180224200; factory 0x180146cc0; deserializer 0x180147070 (begin-obj @0x1801470f2, dispatch @0x18014715c).
  • Method/Path: GET ut/game/fifa17/draft/state?mode=ONLINE | ?mode=SINGLE_PLAYER (fragments @0x223630/0x223650).
  • Top-level keys:
    key atom type notes
    squad 0x2cd object [freeze-risk] drafted squad, SQUAD deser 0x18013d1f0
    entranceCriteria 0x108 object [freeze-risk] nested object (safe to omit)
    gamesWonCurrentMatch 0x13b int @+0x60
    roundsInfo 0x293 array [freeze-risk] array of round records (below), elem deser 0x180146eb0, elem size 0x20 → vec@+0x80
    squadState 0x2d5 string e.g. "DRAFTSQUAD_ON"
    stateParam1 0x2ee string→enum @+0x5c (default 5)
    stateParam2 0x2ef string→int @+0x64
  • Round record (roundsInfo[], all scalar; deser 0x180146eb0): round(0x290,int@+0xc), score(0x29a,int@+0x18), opponentScore(0x200,int@+0x14), penaltyScore(0x217,int@+0x10), opponentPenaltyScore(0x1fd,int@+0x1c), opponentId(0x1fc,int/long@+0x0), difficulty(0xd4,string enum@+0x8).
  • MINIMAL known-good (fresh single-player draft, round 0, empty squad shell):
{"squadState":"DRAFTSQUAD_ON","stateParam1":"","stateParam2":"0","gamesWonCurrentMatch":0,
 "roundsInfo":[],
 "squad":{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]}}

(Populate squad.players[].itemData with cards from the pick choices as the draft is built; roundsInfo grows one record per completed match. entranceCriteria omitted = safe.)


3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk)

  • Struct name 0x180226540; factory 0x18015076d; deserializer 0x1801508c0 (begin-obj @0x18015093b, dispatch @0x1801509bc).
  • Method/Path: GET ut/game/fifa17/draft/mode (GetDraftStats RPC; historical/aggregate draft stats).
  • Keys (all int, except draftChampion bool): gamesWon(0x13a), gamesLost(0x138), scoredGoals(0x29b), concededGoals(0xa1), bestBuilderScore(0x52), draftChampion(0xe1, bool), draftsCompleted(0xe2), passAccuracyTotal(0x212), possessionPercentage(0x247), possessionTotal(0x249).
  • MINIMAL known-good:
{"gamesWon":0,"gamesLost":0,"scoredGoals":0,"concededGoals":0,"bestBuilderScore":0,"draftChampion":false,"draftsCompleted":0,"passAccuracyTotal":0,"possessionPercentage":0,"possessionTotal":0}

4. FutGetDraftAwardServerResponse — CONFIDENCE: HIGH (fully reversed)

  • Struct name 0x1802266f8; factory 0x18015106c; deserializer 0x1801510c0 (begin-obj @0x180151129, dispatch @0x1801511ac).
  • Method/Path: GET/POST ut/game/fifa17/draft/mode (GetDraftAward — claim/return the draft prize).
  • Keys:
    key atom type notes
    item 0x16a array [freeze-risk] awarded item cards, ITEM deser 0x18013fe00 (loops)
    halId 0x150 int @ (prize/hal id)
    type 0x354 int prize type code
    value 0x377 int prize value (e.g. coins)
  • MINIMAL known-good (coins-only prize, no items):
{"type":1,"value":15000,"halId":0,"item":[]}

With an item prize: "item":[{ …full card object as in itemData… }].


5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred)

  • Struct name 0x180226078; factory 0x18014fba0. The factory installs the generic base ServerResponse vtable 0x18022cb58 (shared by dozens of structs) — i.e. no struct-specific deserializer / no parsed fields. The neighboring 0x18014fcb0 is the request serializer, not a response reader.
  • Method/Path: PUT/POST ut/game/fifa17/{champId}/draft/choose and ut/game/fifa17/draft/choose/difficulty (fragments @0x2254c0 / 0x2254a0). Commits one pick (or the difficulty choice).
  • Body: an empty ack — the client re-reads state via GetDraftCurrentState / GetDraftChoices afterward.
  • MINIMAL known-good: {}

6. FutPickDraftAutoChoiceServerResponse — CONFIDENCE: HIGH (delegates to squad deser)

  • Struct name 0x180226208; factory 0x18014fd80; deserializer 0x18014fdf0. The body is parsed by a single call to the SQUAD deser 0x18013d1f0 (@0x18014fe82) into the struct's squad member.
  • Method/Path: POST ut/game/fifa17/{champId}/draft/autocomplete (fragment @0x225638). Auto-fills the remaining picks and returns the completed squad.
  • Body: a squad object [freeze-risk] (same schema as GET /squad/0).
  • MINIMAL known-good:
{"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:
{"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-responseFutMatchReady, 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 0x280x48, confirming a coherent credits struct. All top-level coin fields are scalar int → safe.

  • MINIMAL known-good JSON (scalars only, zero freeze risk):

{
  "coins": 400,
  "allCoins": 15400,
  "matchCoins": 400,
  "seasonCoins": 0,
  "tournamentCoins": 0,
  "boostConis": 0,
  "participationAward": 0,
  "qualifiedChampionEventId": 0,
  "teamOfTournamentWinner": false
}
  • GAP (baseline has no match reward body).

SINGLE-PLAYER PLAYABLE LOOP

FutCreateMatchServerResponse ★CONFIDENCE: HIGH

  • Deserializer VA: 0x180120380 (name-lea 0x18011ffbd)
  • Method+path (inferred): POST ut/game/fifa17/match — creates the match, returns match/squad info.
  • Fields:
    atom key type offset notes
    740 startDateTime int 0x28 epoch
    641 reportIdEnabled bool
    717 squad nested (0x18011a830) squad/ITEM array — FREEZE-RISK (must be array)
  • MINIMAL JSON: {"startDateTime": 1580000000, "reportIdEnabled": false} (omit squad — SKIP-safe)
  • GAP.

FutMatchReadyServerResponse ★CONFIDENCE: HIGH (ack-only)

  • Deserializer: none — factory 0x180120810 → base ctor 0x18011f850 only (name 0x18021d380).
  • Method+path (inferred): PUT ut/game/fifa17/match/{matchId} (mark ready).
  • Parses no fields. MINIMAL JSON: {}. GAP.

FutPlayGameServerResponse ★CONFIDENCE: HIGH (ack-only)

  • Deserializer: none — factory 0x180162170 → base ctor 0x18011f850 only (name 0x180228050).
  • Method+path (inferred): POST ut/game/fifa17/match/{matchId} (submit game result; body carries score/stats client→server). Response body is ignored by the client. Rewards arrive via DestroyMatch.
  • MINIMAL JSON: {}. GAP.

FutResetMatchServerResponse ★CONFIDENCE: HIGH

  • Deserializer VA: 0x18016fd10 (name-lea 0x18016fccd)
  • Method+path (inferred): POST ut/game/fifa17/season/{seasonId}/reset.
  • Fields: atom 643 reset = bool @0x28.
  • MINIMAL JSON: {"reset": true}. GAP.

FutSeasonListServerResponse ★CONFIDENCE: HIGH

  • Deserializer VA: 0x180167740 (name-lea 0x18016754d)
  • Method+path (inferred): GET ut/game/fifa17/season — list of available seasons/divisions (array of season-descriptor objects; root container via 0x1800d84e0).
  • Element fields:
    atom key type notes
    348 id int season/division id
    220 divisionId int
    242 eligibilityKey int
    244 eligibilitySlot int
    245 eligibilityValue int
    246 elgOperation str
    247 elgReq nested SKIP-safe
    595 prizeSet nested array — FREEZE-RISK
  • MINIMAL JSON (list root; per element scalars):
[{"id":1,"divisionId":10,"eligibilityKey":0,"eligibilitySlot":0,"eligibilityValue":0,"elgOperation":""}]
  • GAP. (Note: root JSON shape is an array/object wrapper — verify container before shipping.)

FutSeasonLoadDataServerResponse ★CONFIDENCE: HIGH (fully traced switch)

  • Deserializer VA: 0x180131450 (name-lea 0x18013141c)
  • Method+path (inferred): GET ut/game/fifa17/season/user — load the user's current season state.
  • Fields (verified from switch at 0x18013153c):
    atom key type offset notes
    674 seasonId int 0x5c
    220 divisionId int 0x58 (as int here, not str)
    656 round int 0x60 current round
    881 userPoints int 0x64 points/progress
    202 dataVersion str 0x68 (enum byte)
    201 data str interned via 0x1801c7d40 large opaque season-state blob string
    root/friendlySeasonHistory nested via 0x180136880 container — FREEZE-RISK
  • Unknown atoms → SKIP 0x180135ff0 (safe).
  • MINIMAL JSON:
{"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):
[{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}]
  • GAP.

FutTournamentLoadDataServerResponse ★CONFIDENCE: MEDIUM

  • Deserializer VA: 0x180147cb0 (name-lea 0x180147c7c). Path (inferred): GET ut/game/fifa17/tournament/user.
  • Fields: atom 202 dataVersion str, atom 656 round int, atom 810 tournamentData (str/opaque blob, same interned-getter 0x1801c7d40 as season data), atom 92 bonus nested. Mirrors SeasonLoadData shape.
  • MINIMAL JSON: {"round":1,"dataVersion":"1","tournamentData":""}. GAP.

FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse ★CONFIDENCE: LOW

  • Both resolve to deserializer 0x18016bcf0 (names 0x18022af10 / 0x18022b068) — near-empty switch; only nested/opaque atoms seen (teamId, activeBadge). Likely thin/ack-ish. Paths (inferred): Quit = DELETE ut/delete/game/fifa17/tournament/user; Teams = GET ut/game/fifa17/tournament/user. MINIMAL JSON: {} (or [] for Teams if array-rooted). GAP.

FutUpdateTournamentServerResponse ★CONFIDENCE: LOW

  • Deserializer VA: 0x1801758c0 (name-lea 0x18017580d). Only enum/opaque atoms (LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES/SUCCESS) — a status-enum response. Path (inferred): PUT/POST ut/game/fifa17/tournament/user. MINIMAL JSON: {}. GAP.

FutGetActiveTournamentsServerResponse ★CONFIDENCE: LOW

  • Deserializer VA: 0x18016b660 (name-lea 0x18016b55d). Atoms tournamentId(811), awardCount(64) seen but resolve nested/opaque. Path (inferred): GET ut/game/fifa17/tournament. Likely array of active tournament ids. MINIMAL JSON: [] or {}. GAP.

FutGetHistoricalServerResponse ★CONFIDENCE: MEDIUM

  • Deserializer VA: 0x180172930 (name-lea 0x18017279c). Path (inferred): GET ut/game/fifa17/season/user/history.
  • Fields: atom 336 halId int @0xc; atom 363 itemData = ITEM (shared card/item deser 0x18013fe00) → FREEZE-RISK (array of item cards); atom 653 rewardQuantity int @0x8; atom 654 rewardType int; atom 655 rewardValue int; atom 656 round nested.
  • MINIMAL JSON: {"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0} (omit itemData or send []). GAP.

FutGetStoryModeRewardServerResponse ★CONFIDENCE: HIGH (ack-only)

  • Factory → base ctor 0x18011f850 only (name 0x18022bc30). Parses no fields. MINIMAL JSON: {}. GAP.

Endpoint → response cross-reference (summary)

Method(inf) Path Response struct Reversal
POST ut/%s/match FutCreateMatchServerResponse FULL
PUT ut/%s/match/{id} FutMatchReadyServerResponse FULL (ack)
POST ut/%s/match/{id} FutPlayGameServerResponse FULL (ack)
DELETE ut/%s/match/{id} FutDestroyMatchServerResponse (rewards) FULL
POST ut/%s/season/{id}/reset FutResetMatchServerResponse FULL
GET ut/%s/season FutSeasonListServerResponse FULL
GET ut/%s/season/user FutSeasonLoadDataServerResponse FULL
PUT ut/%s/season/user FutUpdateSeasonServerResponse FULL (ack)
DELETE ut/delete/%s/season/user FutSeasonQuitServerResponse PARTIAL
PUT ut/%s/season/friendly FutUpdateFriendlySeasonServerResponse FULL (ack)
GET ut/%s/season/friendly (hist) FutGetFriendlyHistoryDataServerResponse PARTIAL
GET ut/%s/season/user/history FutGetHistoricalServerResponse PARTIAL
GET ut/%s/tournament FutTournamentListServerResponse PARTIAL
GET ut/%s/tournament/user FutTournamentLoadDataServerResponse PARTIAL
PUT ut/%s/tournament/user FutUpdateTournamentServerResponse PARTIAL
GET ut/%s/tournament FutGetActiveTournamentsServerResponse PARTIAL
GET ut/%s/tournament/user FutGetTournamentTeamsServerResponse PARTIAL
DELETE ut/delete/%s/tournament/user FutTournamentQuitServerResponse PARTIAL
(story reward grant) FutGetStoryModeRewardServerResponse FULL (ack)

Club / Cards / Consumables

Clean-room RE of the FIFA17 CardsDLL (base 0x180000000). Method: each FutXServerResponse class registers a vtable; vtable slot +0x08 is the JSON deserializer. Located via struct-name string → VA = 0x1801e5000 + (fileoff - 0x1e4400) → factory lea r8 xref → vtable install → slot +0x08. Every deserializer shares the same skeleton:

scratch init (0x1801c63e0/0x180008130/0x1801c8270) → NextToken×2 →
get target model (0x18011a830) → key-loop { KEYRD 0x180141ee0 → atom → dispatch } →
unknown atom → SKIP 0x180135ff0 (container-aware, extra keys are SAFE)

KEYRD 0x180141ee0 = read next key, FNV-hash (0x180180d00) → atom int, advance to value. Leaf getters: int 0x1801c79d0, bool 0x1801c7620, string 0x1801c7aa0. Shared ITEM/card element deserializer = 0x18013fe00 (see CARD_SYSTEM.md; produces the 0x100-byte resolved-card record). TYPE FIDELITY IS MANDATORY: an array/object key fed to a scalar getter desyncs the SAX reader → infinite tokenizer spin (freeze at 0x1801c7f1a). Arrays are flagged [FREEZE-RISK] below — they must be emitted as JSON arrays, never scalars.

Key structural finding (contradicts the initial brief)

FutGetClubInfoServerResponse does NOT return itemData/card items. Its only recognized top-level key is user (atom 0x36c), an array of club-user summary records (element parser 0x18012c990, which never calls the card element 0x18013fe00). The card list that actually renders in the club UI is FutViewCardsServerResponse (itemData, via 0x18013fe00) served on ut/%s/item. So "club-wide item search → cards" is ViewCards on /item, while GetClubInfo/GetClubUsers carry club-user stat records. data/ mental model of GetClubInfo = {itemData,count,actives} is wrong for this binary. Actives (homekit/awaykit/badge/etc.) appear as fields inside each user record, not as top-level keys.


Per-struct table

Legend: method/path from the ut/%s/... templates in the binary (%s="game/fifa17"). GAP = not (correctly) served by utas_server.py. All unknown keys are SKIP-safe, so {} never freezes any of these — GAPs are "feature missing", not "crash".

# Struct Deser VA Method + Path Top-level keys (atom → type) Status Conf
1 FutGetClubInfo 0x18012d280 GET ut/%s/club user(0x36c) → array[user-record] [FREEZE-RISK] GAP (utas serves itemData, which is SKIP'd here → empty user list) deser HIGH / element PARTIAL
2 FutGetClubUsers 0x180145c00 GET ut/%s/clubUser user(0x36c) → array[user-record] (elem 0x180145480) [FREEZE-RISK] GAP (utas /clubUser{}) deser HIGH / element PARTIAL
3 FutChangeClubName 0x1801642c0 PUT ut/%s/club (changeClubName) none (deser is immediate ret) ack — any parseable JSON works; {} fine HIGH
4 FutViewCards 0x1801293d0 GET ut/%s/item itemData(0x16b) → array[card-item] via 0x18013fe00 [FREEZE-RISK] HANDLED (utas /item defs_route serves itemData) HIGH
5 FutActivateCard 0x1801642c0 PUT ut/%s/item (FUT_CLUB_ACTIVATE_ITEM_DP) none (immediate ret) ack — {} fine HIGH
6 FutApplyCard 0x18012a710 PUT ut/%s/item (apply by itemId) itemData(0x16b) → array[updated card-item] via 0x18013fe00 [FREEZE-RISK] GAP HIGH
7 FutApplyCardByRes 0x18012ad10 PUT ut/%s/item (apply by resourceId) itemData(0x16b) → array[updated card-item] [FREEZE-RISK] GAP HIGH
8 FutDiscardCard 0x180127300 DELETE ut/delete/%s/item (CardsDiscardCard) items(0x171) → array[int ids] [FREEZE-RISK]; totalCredits(0x326) → int; id(0x15c) → int GAP HIGH
9 FutDiscardCardByRes 0x1801279c0 DELETE ut/delete/%s/item (by res) totalCredits(0x326) → int GAP HIGH
10 FutMoveCard 0x180128600 PUT ut/%s/item (move) itemData(0x16b) → array [FREEZE-RISK]; chemistry(0x81) → bool GAP HIGH
11 FutMoveCardByRes 0x180128e30 PUT ut/%s/item (move by res) itemData(0x16b) → array [FREEZE-RISK]; chemistry(0x81) → bool (+ 2 str/1 int minor) GAP HIGH / extra-fields MED
12 FutConsumablesSearch 0x180130d10 GET ut/%s/item?type=… (GetFilteredConsumableSearchResults) itemData(0x16b) → array[consumable-item] via 0x18013fe00 [FREEZE-RISK]; displayGroupUseDefaultImage(0xdb) → int + count scalars GAP deser HIGH / scalars MED
13 FutStaffBonus 0x18012b730 GET ut/%s/… (CardsGetStaffBonuses) bonus(0x5c) → nested (branch sets bool @rbp+0x51) [FREEZE-RISK]; assetId(0x23) → int GAP MED
14 FutGetAvailableLoanPlayers 0x18014e030 → sub 0x18013a1c0 GET ut/%s/item (FUT_AVAILABLE_LOAN_PLAYERS_DP) loans(0x19b) → array [FREEZE-RISK]; itemData(0x16b) → array[card-item] [FREEZE-RISK]; default(0xcd) → int GAP deser HIGH / fields MED
15 FutSignLoanPlayer 0x1801642c0 PUT ut/%s/item (sign loan) none (immediate ret) ack — {} fine HIGH
16 FutStickerBookSearch 0x18012eff0 GET ut/%s/… (stickerbook search) itemData(0x16b) → array[card-item] via 0x18013fe00 [FREEZE-RISK] GAP HIGH

Notes:

  • 0x1801642c0 is a shared no-op deserializer (function body = ret). Three responses (ChangeClubName, ActivateCard, SignLoanPlayer) use it → their HTTP body is fully ignored; only a 200 + valid JSON ({}) is required. This is why "change name / activate / sign loan" succeed with a bare stub.
  • user-record element (0x18012c990, GetClubInfo) is a jump-table parser; caught fields include established(0x110,str), homekit(0x159), awaykit(0x3f), cleansheets(0x84,int), attributeList(0x31), passing(0x214), awardType(0x44), categoryId(0x73), controls(0xb9), changed(0x7e), currentChampionEvent(0xc2). Full field map is PARTIAL (jump table at [0x1412c4] not fully decoded); all fields optional/SKIP-safe.
  • card-item element = 0x18013fe00, already reversed in CARD_SYSTEM.md (rating/position/nation/teamid/attributeList/name/resourceId…). Not re-derived here.
  • Atom→key mapping from atoms.tsv (FNV-indexed sequential IDs 0x00x38a).

Minimal known-good JSON

// 1 GetClubInfo — GET ut/game/fifa17/club   (club-user summary list)
{ "user": [ { "established": "2026", "cleansheets": 0, "homekit": 0, "awaykit": 0 } ] }
// or safe empty:  { "user": [] }

// 2 GetClubUsers — GET ut/game/fifa17/clubUser
{ "user": [] }

// 3 ChangeClubName — PUT ut/game/fifa17/club
{}

// 4 ViewCards — GET ut/game/fifa17/item   (THE card list; renders real cards)
{ "itemData": [ /* card items, deser 0x18013fe00 shape */ ] }

// 5 ActivateCard — PUT ut/game/fifa17/item
{}

// 6 ApplyCard / 7 ApplyCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* the single updated card item */ ] }

// 8 DiscardCard — DELETE ut/delete/game/fifa17/item
{ "items": [ 123456789 ], "totalCredits": 15000, "id": 123456789 }

// 9 DiscardCardByRes — DELETE ut/delete/game/fifa17/item
{ "totalCredits": 15000 }

// 10 MoveCard / 11 MoveCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* moved item */ ], "chemistry": true }

// 12 ConsumablesSearch — GET ut/game/fifa17/item?type=<consumable>
{ "itemData": [ /* consumable items */ ], "displayGroupUseDefaultImage": 0 }

// 13 StaffBonus — GET ut/game/fifa17/... (staff bonuses)
{ "bonus": [], "assetId": 0 }

// 14 GetAvailableLoanPlayers — GET ut/game/fifa17/item (loans)
{ "loans": [], "itemData": [ /* loan card items */ ], "default": 0 }

// 15 SignLoanPlayer — PUT ut/game/fifa17/item
{}

// 16 StickerBookSearch — GET ut/game/fifa17/... (sticker book)
{ "itemData": [] }

utas_server.py integration status

  • /item (defs_route) already serves {"itemData":[…]} → satisfies ViewCards, ApplyCard, ApplyCardByRes, MoveCard, ConsumablesSearch, StickerBook, loan itemData shape (though action semantics — updated item / chemistry / credits — are not modelled).
  • /club serves {"itemData":…} but GetClubInfo wants user → the itemData is SKIP'd; club card render still works because it goes through /item ViewCards, not /club. To populate the club-user summary, serve {"user":[…]} on /club.
  • /clubUser{} (GAP: should be {"user":[…]}; {} is non-freezing).
  • No routes for discard-credits, move-chemistry, staff-bonus, loan loans[], stickerbook → all fall to catch-all {} (safe, feature-inert).
## Store / Packs / Purchases

Reversed from CardsDLL (base `0x180000000`) via the RECIPE deserializer method. All
struct-name VAs computed as `0x1801e5000 + (fileoff - 0x1e4400)`; deserializers located
from the `.text` `lea r8,[name]` xref; atoms translated through `atoms.tsv`.

Parser primitives: INT `0x1801c79d0` · BOOL `0x1801c7620` · STR `0x1801c7aa0` ·
value-SKIP (unknown atom, safe) `0x180135ff0` · next-token `0x1801c7f10` ·
begin-object `0x1801c8270` · key→atom FNV `0x180180d00` · shared ITEM element deser
`0x18013fe00`. **Type fidelity is mandatory** — feeding a scalar getter an object/array
desyncs the SAX reader → tokenizer freeze at `0x1801c7f1a`.

### Endpoint → struct map (paths use %s = "game/fifa17")

| Method | Path | Request→Response struct | Blaze cmd token |
|---|---|---|---|
| GET | `ut/%s/store` (`store/purchasegroup/...`) | FutStoreGetPackTypesServerResponse | STOREPACKTYPES |
| GET | `ut/v2/%s/store` | FutStorePackQuantitiesServerResponse | STOREPACKQUANTITIES / V2STORE |
| POST/PUT | `store/transaction` | FutCreatePackServerResponse (buy=create) / FutPurchaseItemsServerResponse | CREATEPACK / PURCHASEITEMS |
| GET | `ut/%s/purchased` | FutGetPurchasedItemsServerResponse | PURCHASEDITEMS |
| — | credits refresh (embedded) | FutUpdateCreditsServerResponse | UPDATECREDITS |

---

### 1. FutStoreGetPackTypesServerResponse  — confidence: HIGH
- **name VA** `0x18021de20`  · **deserializer** `0x1801234e0`  · **pack element deser** `0x18013af30`
- **Root keys** (deser `0x1801234e0`):
  - `purchase` (atom **0x260**) → **ARRAY** of pack objects (each → `0x18013af30`)  *(freeze-risk: must be array)*
  - `timestamp` (atom **0x31b**) → INT scalar → `[rdi+0x5c]`
- **Pack object fields** (deser `0x18013af30`, all optional; unknown keys skipped):

  | key | atom | type | notes |
  |---|---|---|---|
  | `assetId` | 0x23 | INT | **real pack identity** → `[rbp-0x3c]` |
  | `actionType` | 0x08 | INT | |
  | `bonus` | 0x5c | INT | |
  | `dealType` | 0xcc | STR | |
  | `description` | 0xd1 | STR | display name |
  | `displayGroup` | 0xd9 | **ARRAY** | nested (freeze-risk) |
  | `displayGroupAssetId` | 0xda | INT | `[rbp-0x80]` |
  | `displayGroupUseDefaultImage` | 0xdb | BOOL | |
  | `currencies` | 0xc5 | **ARRAY** | coin price: `[{name,funds,finalFunds}]` (freeze-risk) |
  | `extPrice` | 0x119 | **OBJECT** | → `finalPrice`(0x125,obj `0x180139070`) + `originalPrice`(0x205,obj `0x18013aae0`); inner uses `amount`(0x1b)/`currency`(0xc4) (freeze-risk) |
  | `packContentInfo` | 0x20c | **OBJECT** | → `bronzeQuantity`(0x63), `silverQuantity`(0x2c6), `goldQuantity`(0x149), `rareQuantity`(0x273), `itemQuantity`(0x170), `start`(0x2e3), `unopened`(0x35d,bool) (freeze-risk) |
  | `sortPriority` | 0x2cb | INT | |

- **Status: already handled (renders in-game), but with CORRECTIONS**
  - `store_catalog()` currently emits `id, packType, quantity, purchaseLimit, purchaseCount, isPremium, saleType` — **none of these atoms exist in the pack deser** (`id`=0x15c, `quantity`=0x26b, `saleType`=0x298, `packType`=0x20f, `isPremium`=0x176 are all routed to SKIP `0x180135ff0`). They are harmless no-ops but do nothing.
  - The **real identity field is `assetId` (0x23)**, which the current handler does NOT send. Recommend adding `assetId` per pack (packs currently work off `currencies`+`extPrice`+`packContentInfo` presence + the transaction-body `packId`, but `assetId` is the field the client actually deserializes).
  - `extPrice.finalPrice/originalPrice` inner keys are NOT `mtx` (no such atom) — real inner atoms are `amount`/`currency`; current `{"mtx":N}` is skipped, so extPrice objects are effectively empty-but-present (enough to pass validation; FIFA-Points price shown comes from elsewhere).
- **Minimal known-good** (corrected):
```json
{"purchase":[{"assetId":101,"description":"Gold Pack","sortPriority":1,
  "currencies":[{"name":"coins","funds":5000,"finalFunds":5000}],
  "extPrice":{"finalPrice":{"amount":100,"currency":"fifapoints"},"originalPrice":{"amount":100,"currency":"fifapoints"}},
  "packContentInfo":{"bronzeQuantity":0,"silverQuantity":0,"goldQuantity":7,"rareQuantity":1,"itemQuantity":7}}],
 "timestamp":1596326400}

2. FutStorePackQuantitiesServerResponse — confidence: HIGH ⟵ GAP (UNBUILT)

  • name VA 0x18022d440 · deserializer 0x1801758c0 (token loop 0x180175920)

  • This is the ut/v2/%s/store response. It is NOT a per-pack quantity list — it is a single eligibility/result gate. The deser reads exactly ONE key:

    • result (atom 0x288) → STRING; the string VALUE is FNV-hashed (0x180180d00) and mapped to an enum stored at [rdi+0x28]:
    string value atom hit enum
    SUCCESS 0x2fb 0
    TOO_MANY_TOURNAMENTS 0x324 1
    LOCKED_PERMANENT 0x1a1 2
    LOCKED_RETRY 0x1a2 3
    LOCKED_TROPHIES 0x1a3 4

    All other keys are skipped. Constructor default of [rdi+0x28] is 0 (SUCCESS), so an empty {} also parses as SUCCESS, but send result explicitly.

  • Status: GAP — no route builds this. Add a handler for ut/v2/%s/store.

  • Minimal known-good: {"result":"SUCCESS"}


3. FutCreatePackServerResponse — confidence: HIGH

  • name VA 0x180228318 · deserializer 0x180162880

  • Wrapper key createPackResponse (atom 0xbe) → OBJECT with:

    key atom type store
    itemList 0x16e ARRAY of items (element deser 0x18013fe00) freeze-risk
    numberItems 0x1dd INT [rsi+0x28]
    purchasedPackId 0x264 INT [rsi+0x70]
    duplicateItemIdList 0xec ARRAY (int list) freeze-risk
  • Status: already handled — VERIFIED byte-exact against store_buy().

  • Minimal known-good:

{"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 checkGetSystemMetrics 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:
    {"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]}
    

FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) HANDLED

  • Wrapper parser: 0x180146970 — does Parse + TWO NextToken before deserializing, so the body MUST be wrapped in exactly one member (member NAME not compared, nesting level required).
  • userInfo record deser: 0x18013ec10 (freeze-critical — shared with massinfo).
  • HTTP: GET ut/%s/user (NOT called at boot; only reachable via massinfo).
  • userInfo fields — confirmed atoms (cumulative-ladder):
    • personaId (0x21b) — int
    • sessionCoinsBankBalance (0x2bb) — int
    • trophies (0x340) — int
    • won (0x387) — int
    • purchased (0x262) — bool
    • feature (0x11c) — nested object (e.g. {trade:bool}) ⚠ freeze-risk
    • fifaPointsFromLastYear (0x121) — int
    • squadList (0x2d4) — array ⚠ freeze-risk
    • unopenedPacks (0x35e) — nested object ⚠ freeze-risk; children preOrderPacks(0x24b), recoveredPacks(0x27b), count(0xbc)
    • nested notification sub-obj: notification(0x1da), outbid(0x206), winning(0x384)
  • From validated baseline (util) — also parse (unknown→SKIP): clubName, clubAbbr, established, clubNameChangeAllowed, currencies[] (array of {name,value} ⚠ array), won/draw/loss, divisionOffline/divisionOnline, reliability{reliability,matchUnfinishedTime}, bidTokens{count,updateTime}, actives[] (array).
  • Handled: utas_server.USER_GET = {"userInfo": user_info()}.

FutGetUserMassInfoServerResponse — CONFIDENCE: HIGH (freeze behavior) ⚠ MUST BE {} — GAP for populated

  • Deser: 0x180174630 (freeze-sensitive per CARD_SYSTEM.md).
  • HTTP: GET ut/%s/userMassInfo
  • Top-level keys (this deser dispatches):
    • user (0x36c) — nested → calls userInfo deser 0x18013ec10 (NOTE: the wrapping key is user, not userInfo — relevant if ever populated)
    • clubUser (0x91) — nested
    • settings → calls settings deser 0x18013c6d0
    • squad → calls LoadActiveSquad deser 0x18013d1f0
    • transaction (0x339), errors (0x10c, array), loanPlayers (0x19a, array), pileSizeClientData (0x227), key(0x177)/value(0x377) pairs
  • CRITICAL: any content (userInfo AND/OR squad) desyncs the massinfo parser → infinite tokenizer spin (0x1801c7f1a). Return {} — proven hub-reaching. The exact squad/squadList shape + the userInfo nested-object typing (feature / unopenedPacks / currencies) is the open desync GAP; deliver club/squad via their own endpoints instead.
  • Handled: utas_server FUT_MASSINFO=empty{}.

FutGetSettingsServerResponse — CONFIDENCE: HIGH HANDLED

  • Deser: 0x18013c6d0
  • HTTP: GET ut/%s/settings
  • Fields: single wrapper key configs (0xa2) → array of config entries { type (0x354), value (0x377) }.
  • Handled: utas_server.SETTINGS = {"configs": []}. Min JSON: {"configs":[]}.

FutGetHubDataServerResponse — CONFIDENCE: LOW (full schema) / HIGH (served {} works) — GAP

  • Wrapper: 0x1801736ad → inner 0x180173a50 / 0x180173b10 / 0x180173c00.
  • HTTP: GET ut/%s/hub
  • Note: uses C++ reflection / vtable dispatch (call [rax+0x10], call [rdx+0x1f8]), NOT an inline atom ladder — no static field ladder to read. It aggregates sub-objects (userInfo, settings, messages, etc.), each with its own deser. Empty {} is tolerated (fields default).
  • Handled: utas_server serves {} (validated hub-reaching). Deep populate = GAP.

FutUserDataServerResponse — CONFIDENCE: MEDIUM

  • Deser: 0x18016dd50 (lea r8 @ 0x18016d98d)
  • Fields: actives (0xb, array), key (0x177), value (0x377) — key/value user-data entries. Part of create/user path. Served inside userData.

THEN — user lifecycle structs

Struct Deser VA HTTP Fields (atoms) Status
FutGamerSetInfoServerResponse 0x18016d85d POST ut/%s/user (set gamer info) none (empty ack) GAP-trivial
FutKeepAliveServerResponse 0x18016232d ut/%s/match/keepalive none HANDLED (204)
FutLogoutServerResponse 0x18017006d logout none (empty ack) GAP-trivial
FutResetUserServerResponse 0x18017548d DELETE ut/delete/%s/user none GAP-trivial
FutGetUserActionServerResponse 0x1801781ad GET user actions actions(0x7,array), actionType(0x8) partial
FutUpdateUserActionServerResponse 0x18012378d PUT user action none (empty ack) GAP-trivial
FutSetFavFeatureServerResponse 0x18016f2cd set fav feature none GAP-trivial
FutLiveMessageUpdateServerResponse 0x180153a2d ut/%s/livemessage none (empty ack) GAP-trivial
FutGetTrustedConsoleListServerResponse 0x18012a01d ut/%s/phishing/trusteddevice changed(0x7e,bool), exists(0x117,bool), locked(0x19e,bool), trusted(0x351,bool) HANDLED (trusted:true skips security Q)

Min JSON for the empty-ack structs: {} (200) or 204 — all validated safe. activeMessage (ut/%s/activeMessage), clientdata (ut/%s/clientdata), livemessage — no dedicated field-ladder deser; served generically ({} / 200).


OBJECTIVES / CHALLENGES

FUT 17 objectives = ManagerQuests (viewmodel futmanagerquestsviewmodel; data providers FUT_MQ_QUESTS_DATA_DP, FUT_SQUAD_QUESTS_DP, FUT_PLAYER_IDENTITY_QUESTS_DP). Enable flags live in settings: enableObjectives (0xfd), enableObjectivesAsManagerTasks (0xfe). Relevant atoms: objectives(0x1e2), objectivesForCurrentUser(0x1e3), allObjectivesForCurrentGameSpaceId(0x15), challenges(0x76), challengesCount(0x78), challengesCompletedCount(0x77), challengeId(0x74), challengeImageId(0x75), grantedChallengeAwards(0x14a), squadChallenge(0x2d1).

  • FutManagerQuestGetRewardServerCall — deser 0x1801516cd — reward-claim call; 0 inline atoms (response reuses shared item/award desers). No dedicated ...ServerResponse struct exists — the quests UI is client-driven via the viewmodel + generic item/award payloads. GAP (no route yet).
  • FutGetTowChallengeServerResponse — deser 0x18016dbcd — challenge progress as key(0x177)/value(0x377) pairs, actives(0xb). GET. GAP. Min: {}.
  • FutSetTowChallengeServerResponse — deser 0x18016ef6d — none (empty ack). PUT. GAP-trivial.
  • FutLoadSetChallengesResponse — deser 0x18017b9ed — SBC set-challenge defs (adjacent): awards(0x47), categoryId(0x73), elgReq(0xf7), endTime(0x106), formation(0x12b), eligibilityKey(0xf2), eligibilitySlot(0xf4). partial/GAP.

LEADERBOARDS

FutGetLBEntriesServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x180144c8d
  • HTTP: GET ut/%s/leaderboards
  • Entry fields (array): clubName(0x8e), badge(0x49), est(0x10f, established), score(0x29a), seasonOnlineDraws(0x2a6), TalkRating(0x303), insetUrl(0x166), inset(0x165).
  • Min JSON: {"entries":[]} (empty list safe).

FutGetLBOptionsServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x18014351c
  • HTTP: GET ut/%s/leaderboards/options
  • Fields: category(0x70), id(0x15c), period(0x218), view(0x37a), url(0x366).

CHAMPIONS

FutChampionsRegistrationServerResponse — CONFIDENCE: MEDIUM — GAP-trivial

  • Deser: 0x18014980d — no inline atoms (empty/status ack). POST ut/%s/champion. Min {}.

FutGetChampionsFriendsServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x18014b7ad. GET ut/%s/champion (friends).
  • Fields: stats(0x2ec, nested), gamesPlayed(0x139), persona(0x21a). Min {}.

FutGetChampionsTopXServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x18014a09d. GET ut/%s/champion (topX).
  • Fields: entries(0x109, array) of { clubName(0x8e), badge(0x49), est(0x10f) }. Min {"entries":[]}.

CAPTCHA / PHISHING / TFA

FutGetCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x18014e78d. GET ut/%s/captcha.
  • Fields: encodedImg(0x101, string base64), sequence(0x2ba, int), sizeBeforeEncode(0x2c7, int).

FutValidateCaptchaServerResponse — 0x18014ed5d — none (status ack). POST ut/%s/captcha. GAP-trivial. Min {}.

FutExchangeCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP

  • Deser: 0x180177d5d. Fields: token(0x321, string). Min {"token":"..."}.

FutGetPhishingQuestionServerResponse — CONFIDENCE: HIGH HANDLED

  • Deser: 0x18012980d. HTTP: GET ut/%s/phishing/question?deviceId=%s.
  • Fields: question(0x26c, int id), answer, attempts(0x28, int), recoverAttempts(0x27a, int).
  • Handled: util {"question":0,"answer":"","attempts":5}.

FutSetPhishingAnswerServerResponse — 0x180129b9d — none. POST /question?deviceId=%s&question=%d&answer=%s. HANDLED.

FutValidatePhishingAnswerServerResponse — CONFIDENCE: HIGH HANDLED

  • Deser: 0x180129ddd. Returns a trust token. HTTP: ut/%s/phishing/validate.
  • Handled: util {"token":"OPENFUT-TRUST-..."}.

TFA — ut/%s/tfa — endpoint present; no dedicated field-ladder deser (served generically). GAP-untriggered.


Summary of atom / path cross-checks

  • Path templates confirmed in binary strings: ut/%s/{user,user/list,hub,clientdata, activeMessage,livemessage,leaderboards,leaderboards/options,champion,phishing, captcha,tfa}, plus /question?deviceId=%s(&question=%d&answer=%s).
  • Freeze-risk nested fields (must be object/array, never scalar): userInfo.feature, userInfo.unopenedPacks, userInfo.squadList, userInfo.currencies, userInfo.actives; and the whole userMassInfo body (keep {}).

Regenerating the analysis inputs

cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll
objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm
strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings
python3 tools/atomdump.py > /tmp/fut/atoms.tsv   # 907 atom->key rows

Recipe: docs/OPENCODE_ENDPOINT_PROMPT.md · atom table VA 0x1802d2760 · deser locator in header above.


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.