4e89cce37d
Store "not available" root cause reversed from CardsDLL:
- ut/v2/game/fifa17/store is an ELIGIBILITY gate (FutStorePackQuantities
deser 0x1801758c0), not a quantity list. It reads one key "result"
(atom 0x288); the store screen refuses to open unless SUCCESS. Was
unhandled -> catch-all {} -> "not available". Now returns {"result":"SUCCESS"}.
- Store-screen entitlement checks (0x18001749d/0x1800175a2) read IS_*/
*_PURCHASE_ENABLED Blaze flags, separate from storeEnabled. Added the full
confirmed set (14 flags) to FUT_RS4_CONFIG.
- Catalog: assetId (0x23) is the real pack identity; extPrice inner keys are
amount/currency (not mtx). (Also gated client-side by GetSystemMetrics>1024x768.)
Full FUT API reversed (clean-room, CardsDLL only) into docs/ENDPOINT_MAP.md:
~100 FutXServerResponse types across 7 feature groups (market, SBC, draft,
seasons/match, club, store, user/hub), each with deserializer VA, atom-mapped
field schema + types, freeze-risk flags, and minimal known-good JSON.
Tooling kept: tools/atomdump.py (dumps the 907-atom key table at 0x1802d2760)
-> docs/fut_atoms.tsv. Research prompt: docs/OPENCODE_ENDPOINT_PROMPT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
171 lines
11 KiB
Markdown
171 lines
11 KiB
Markdown
# 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.
|