# opencode task — map the remaining FIFA 17 FUT endpoints for the offline rebuild

## Context
OpenFUT runs FIFA 17 Ultimate Team fully offline (clean-room; EA servers are dead).
A working Python backend already exists at `~/Documents/OpenFUT/fifa17-recon/tools/` and
FIFA talks to it (`/etc/hosts` → `easw.easports.com` → `127.0.0.1:8099`). Many endpoints
are already reversed and served. Your job is **RESEARCH ONLY**: produce a complete map of
the FUT endpoints that are LEFT to build, using the tools/logs/disassembly that already
exist. Do NOT rewrite the backend — output `~/Documents/OpenFUT/fifa17-recon/docs/ENDPOINT_MAP.md`.

## CLEAN-ROOM RULE (HARD)
Use ONLY files we own + our own client's traffic. NEVER use leaked EA source of any kind.

## Use the ALREADY-BUILT tools — do not rebuild these
1. **`tools/utas_server.py`** — the live backend. Its `ROUTES` table is the AUTHORITATIVE
   list of endpoints already handled: `auth`, `delete/auth`, `phishing/{trusteddevice,
   validate,question}`, `user/credits`, `user/list`, `user/accountinfo`, `user`, `squad`
   (+`/list`), `hub`, `userMassInfo`, `season`, `club`, `item(/resource)/defid`,
   `store/purchasegroup`, `store/transaction`, `purchased`. Read it to see what's DONE and
   the exact response shapes used. Its `_handle()` logs `!! UNMAPPED PATH -> catch-all 200 {}`
   for any endpoint FIFA hits that ISN'T handled yet — those are your gaps.
2. **`/tmp/utas_server.log`** — GROUND TRUTH of every request FIFA makes (method, path,
   headers, body) and our response. Start here:
   ```
   grep 'UNMAPPED PATH' -B1 /tmp/utas_server.log            # endpoints we stub {}
   grep -oE '(GET|POST|PUT) /ut/(v2/)?game/fifa17/[^ ?]*' /tmp/utas_server.log | sort -u
   ```
   This is the real, ordered list of what the client requests per FUT screen.
   (Caveat: the log is temp — cleared on reboot — and only reflects FUT screens actually
   visited. For richer traffic, run the harness and navigate more FUT menus first.)
3. **`tools/memtool.py`** — live FIFA17 `/proc/mem` reader/patcher (base `0x140000000`),
   for inspecting parsed structs live if a format is ambiguous.
4. **`tools/fut_store.py` / `tools/fut_seed.py`** — the data models (club items, squad,
   packs) the backend already uses; extend these conceptually, don't reinvent.
5. **`tools/openfut-fut.sh`** — starts the whole harness (lsx/blaze/roster/utas) if you
   need it running to capture more traffic. `tools/root_arm.sh` arms host state (needs sudo).
6. **`docs/CARD_SYSTEM.md`** — the reversed card/parser system + method (READ FIRST). It
   already documents the SAX-parser internals so you don't re-derive them.

## Disassembly (regenerate once; same method `docs/CARD_SYSTEM.md` uses)
```
mkdir -p /tmp/fut && cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll
objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm
strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings
```
Shared-parser CHEATSHEET (reuse, don't re-derive): endpoint paths are strings `"ut/%s/..."`
(`%s` = `"game/fifa17"`); response structs are `"RS4:Fut<X>ServerResponse"`; each has a SAX
deserializer that hashes each key via FNV-1a fn `0x180180d00` (seed `0x811c9dc5`) to an ATOM
int, looks the name up in the table at `0x1802d2760` (`table[atom]=char*`), and dispatches
via a jump-table; unknown keys hit the value-SKIP handler `0x180135ff0` (container-aware,
safe). Field TYPE must match its handler (scalar getters `0x1801c79d0`/`0x1801c7620`/
`0x1801c7aa0` vs nested object/array handlers) — a scalar handler fed an object/array
**DESYNCS the parser and hard-freezes the game**, so type fidelity is mandatory.
Endpoint resolve table: `RS4::ServerSettings::resolve 0x180124270`.

## Method (per endpoint)
For each gap endpoint (from the log's UNMAPPED list + the `"ut/%s/..."` strings not yet in
`ROUTES`): grep the strings for its path → find its `Fut<X>ServerResponse` struct → find its
deserializer → list the atoms it dispatches, map atom→key via `0x1802d2760`, note each
field's JSON type + required-vs-skip → write the minimal known-good response JSON.

## WHERE TO START
1. Read `docs/CARD_SYSTEM.md` and skim `tools/utas_server.py` `ROUTES`.
2. Run the two log greps above → the definitive list of endpoints FIFA calls but we only
   stub `{}`. Rank them by FUT feature.
3. List ALL `"ut/%s/..."` path strings and subtract the ones already in `ROUTES` → the
   endpoints not yet even discovered in traffic.
4. Reverse the response format for each gap, prioritizing the core loop first:
   **transfermarket** (search/bid/buy/list/watchlist), **tradepile**, **SBC**
   (challenges/submit), **objectives**, **draft**, **seasons/single-player**, **match**
   (squad-battles/kickoff result), then club stats / concept squads / loans.

## Deliverable
`docs/ENDPOINT_MAP.md`, one section per feature: for each endpoint — method, path, whether
already handled or a gap, request body shape, response shape (exact keys+types, required vs
optional), deserializer VA, and a minimal known-good example JSON. This is the spec for
finishing the FUT backend (target: port into the Rust `openfut-core` behind a FIFA-17 bridge).

---

## APPENDIX — known atoms + verified response templates (reuse these; don't re-derive)

### Parser internals (already reversed)
- key → atom: FNV-1a fn `0x180180d00` (seed `0x811c9dc5`) → `table[atom]=char*` at `0x1802d2760`
- dispatch: range jump-tables; unknown key → value-SKIP `0x180135ff0` (container-aware, safe)
- scalar getters (leaf, no descend): int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`
- endpoint resolve: `RS4::ServerSettings::resolve 0x180124270`; path `"ut/%s/.."`, `%s="game/fifa17"`
- shared ITEM/card deserializer: `0x18013fe00` (used by club, squad slots, pack itemList, purchased)

### Common shared keys (atom in hex | JSON type)
```
itemData 0x16b (obj/array-elem) | id 0x15c (int) | resourceId 0x287 (int) | assetId 0x23 (int)
index 0x163 (int) | kitNumber 0x17a (int) | rating 0x274 (int) | preferredPosition 0x24a (str)
cardsubtypeid 0x6c (int) | attributeList 0x31 (array) | currencies 0xc5 (array)
  currencies element: name 0x1d0 (str) | funds 0x134 (int) | finalFunds 0x124 (int)
  currency-name literals are CASE-SENSITIVE strings: "coins", "points", "DRAFT_TOKEN"
configs 0xa2 (array)  [settings response, key = "configs"]
```

### FutUserCreditsServerResponse — GET user/credits (deser 0x180122c50) [VERIFIED WORKS]
```json
{"currencies":[{"name":"coins","funds":15000,"finalFunds":15000},
               {"name":"points","funds":0,"finalFunds":0}],
 "unopenedPacks":{"preOrderPacks":0,"recoveredPacks":0}}
```
Coins read from `currencies[name=="coins"].funds`. A bare `{"credits":n}` is SKIPPED → 0.

### FutStoreGetPackTypesServerResponse — GET store/purchasegroup/all (deser 0x1801234e0)
Root key MUST be `"purchase"` `0x260` (array); optional `"timestamp"` `0x31b`. Per-pack
(parser `0x18013af30`): `id 0x15c`(int16, the pack identity) | `packType 0x20f`(str) |
`description 0xd1`(str) | `currencies 0xc5`(array {name,funds,finalFunds} = coins price) |
`extPrice 0x119`(obj {`finalPrice 0x125`, `originalPrice 0x205`}, each a currency→amount map
incl `"mtx"`=FIFA-Points) | `packContentInfo 0x20c`(obj: `bronzeQuantity 0x63`,
`silverQuantity 0x2c6`, `goldQuantity 0x149`, `rareQuantity 0x273`, `itemQuantity 0x170`) |
`quantity 0x26b`(int, 0=unlimited) | `isPremium 0x176` | `saleType 0x298` | `state 0x2eb` |
`visible 0x37d`(sets flag, DON'T send the value—desyncs).

**⚠ The parser is NOT the store gate (CONFIRMED by disassembly).** Per-pack parser epilogue
`0x18013badc` pushes every parsed pack unconditionally — no valid/drop predicate exists in
the parse path. The `"not available"` error (`FUT_CatalogNotAvailable`, msg-id `0x7550`)
comes from TWO downstream gates, neither JSON-schema-related:
1. **Resolution gate `0x18001756e`**: calls `GetSystemMetrics` — if the display is
   **≤ 1024×768**, the store is declared unavailable regardless of any JSON. Run FIFA at
   **> 1024×768** (1280×720 passes). *Cheapest cause to eliminate — check this first.*
2. **Store-data-model load status** (`0x180013cf0`): `model+0x30` must become `1` and the
   screen's cached status `[rsi+0x250]` must not stay `-1`; else post `0x7550` at
   `0x180013d6c`. Fed by two entitlement checks — `0x18001749d` (`vtable+0x138`) and
   `0x1800175a2` (`vtable+0x280`) — that read the **Blaze client-config purchase flags**:
   `IS_STORE_ENABLED`, `IS_COIN_PURCHASABLE`, `IS_FIFAPOINT_AVAILABLE`,
   `COINS_PURCHASE_ENABLED`, `POINTS_PURCHASE_ENABLED`, `MONEY_PURCHASE_ENABLED`. These are
   SEPARATE from `storeEnabled`/`cardPackStoreEnabled` and must ALSO be set in the Blaze config.
Recognized per-pack enum tokens (send these exact strings to avoid enum-reject):
`saleType` → `"promo"`/`"deal"`; a limit-type field → `NONE`/`QUANTITY`/`TIME`/`TIME_QUANTITY`;
pack `state` → `"active"`. Currency token is lowercase `"coins"`/`"mtx"` (NOT uppercase).

### FutCreatePackServerResponse — PUT store/transaction (pack reveal, deser 0x180162880)
Wrapper key `"createPackResponse"` `0xbe`:
```json
{"createPackResponse":{"itemList":[/*cards*/],"numberItems":7,
  "purchasedPackId":102,"duplicateItemIdList":[]}}
```
(atoms: itemList `0x16e`, numberItems `0x1dd`, purchasedPackId `0x264`, duplicateItemIdList `0xec`.)
BUY signal = transaction body has `"packId"` `0x20b` AND `state 0x2eb != "TRANSACTIONCANCEL"`.
state enum strings: `TRANSACTIONCREATED`(carries packId=the buy), `PURCHASECOMPLETE`,
`TRANSACTIONCOMPLETE`, `TRANSACTIONCANCEL`.
`FutGetPurchasedItemsServerResponse` — GET purchased: `{"itemData":[/*cards*/]}` (itemData `0x16b`).

### GetUserMassInfo — GET userMassInfo (deser 0x180174630) [FREEZE-SENSITIVE]
Top-level: `userInfo 0x370`, `squad 0x2cd`, `settings`, `userData`. MUST serve `{}` unless
every field type-matches, else the parser hard-freezes. `userInfo` deser `0x18013ec10`;
SAFE minimal that carries coins:
```json
{"userInfo":{"currencies":[{"name":"coins","funds":15000}],"sessionCoinsBankBalance":15000}}
```
(`sessionCoinsBankBalance 0x2bb`.) DANGER: `squadList` must be an OBJECT `{"squad":[...]}`
NOT an array (array → desync → freeze). Container fields `feature`/`reliability`/
`unopenedPacks`/`bidTokens`/`actives` must match exact shape or be omitted.

### LoadActiveSquad — GET squad/0 (deser 0x18013d1f0)
Keys: `id 0x15c` | `personaId 0x21b` | `squadName 0x2d3` | `formation 0x12b`(str) |
`squadType 0x2d6` | `chemistry 0x81` | `starRating 0x2e2` | `captain 0x69` | `manager 0x1a8`(array)
| `actives 0xb`(array) | `custom 0xc6`(STRING of 33 ints) | `players 0x238`(array of
{index,itemData,kitNumber}) | `kicktakers 0x178`(array). Squad PUT stores slots as
`itemData={id:<clubItemId>}` references (re-embed full items on GET, see `reconstruct_squad`).

> Atoms are index-into-`0x1802d2760`; a few above came from mixed-confidence passes — when a
> value doesn't take, verify the atom by locating the key string in `cardsdll.strings` and
> re-hashing. Field TYPE fidelity is mandatory (scalar-vs-container mismatch = freeze).
> High-confidence/verified: `user/credits`, `createPackResponse`, the store gate analysis,
> and the `userMassInfo` safe-shape.
