Files
OpenFUT/fifa17-recon/docs/ENDPOINT_MAP.md
T
funman300 3bb6b4fc9d fix(fifa17): subtypes 57/67 are the rare all-six training card
They were failing closed as "squad fitness". Four facts say otherwise: each
family holds exactly 21 rows = 7 types x 3 levels matching the published
"6 single attributes + 1 ALL" list; those six rows are the ONLY ones with
weightrare=2 while all 36 single-attribute rows are 0, and the published list
marks ALL rare; their amounts are exactly 3/6/10 against the documented ALL
card's +3/+6/+10; and bc=6 is one past the six real slots, an "all" sentinel,
with c0=0 reading as "not single-ATTRIBUTE" rather than "not single-target".

The misreading came from consumables.json's FUT_FITNESS_UC/MC label, which is
tool-authored -- build_consumables.py names the 7th element of its attribute
array -- and has no documented provenance. The bc/c0 values beside it ARE
reversed; only the name was not. The genuine squad-fitness card is subtype
220 in fcc_healingcards (10/20/30) and player fitness is 219; that family
stays unsupported.

The two families carry different authored ceilings, 15 single-attribute and
10 all-six, so ceiling_for() reports the right one per effect -- sending the
single-attribute ceiling for an all-six card would let a +15 all-six boost
through, granting 90 attribute points from a card worth 60.

Also corrects ENDPOINT_MAP row 7: ApplyCardByRes carries urlIndex 0x0e, which
resolves to ut/%s/item/resource, and the live verb is POST. The row claimed
PUT ut/%s/item for both apply RPCs, and that conflation is what kept the
"apply must ride PUT ut/%s/item" hypothesis alive until the POST capture
settled it -- every observed PUT ut/%s/item is a pile move.
2026-08-22 23:33:43 +00:00

96 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 userWRONG, 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) 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 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, 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.

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 keysELIGIBILITY_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 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/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):
[{"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:
{"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):
[{"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:
{"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 (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):
{"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:
{"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 POST ut/%s/item/resource/<rid> (apply by resourceId) itemData(0x16b) → array[updated card-item] [FREEZE-RISK] SERVED (Rust host, contracts + attribute training) HIGH

Rows 6 and 7 are NOT the same route. ApplyCardByRes carries urlIndex 0x0e, which resolves to ut/%s/item/resource — not ut/%s/item (plan-2026-08-05-pack-opening.md:505-506, shared with DiscardCardByRes and MoveCardByRes). The verb is POST, live-proven by a real-client capture: POST /ut/game/fifa17/item/resource/5001004 {"apply":[{"id":100000003}]}. This row previously read PUT ut/%s/item for both, and that conflation is what kept the "apply must ride PUT ut/%s/item" hypothesis alive (CLIENT_ROUTE_SURFACE.md:104-106) until the POST capture settled it — every observed PUT ut/%s/item is a pile MOVE, never an apply.

| 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 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
// 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:

{"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 (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:

{"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.