# FIFA 17 FUT — Card System RE + Next-Steps Plan Status as of 2026-08-01. All clean-room (our own binaries + running client only). ## ★ SOLVED: real player cards render offline A full 88-rated real squad (Ronaldo/Messi/Suárez/Ramos/Kroos/…) renders 100% offline. The definition FETCH turned out to be unnecessary — FIFA has all player identity data locally in `dbdata.dll`. **The CLUB-SEARCH / ADD-PLAYER flow is the trigger:** when FIFA displays club-search results (`GET /club?year=..&type=player& count=..`, served by our `/club` route), it resolves each item's assetId against its own local DB (real name/photo/club/nation) and merges the rating/attributes our `/club` item carries, then caches a real record in the CardsDb store (`obj+0x160c0`). No `idList` fetch, no leaked data. **Working recipe:** - `utas_server` on `FUT_SQUAD_STEP=s3v0` → `/club` serves the full XI (real asset IDs + our attributes). - In FUT: Squads editor → Add Player / player-name **search** → results render as real cards → **add them to slots** → full real squad. - Distinction: the squad-EMBED path (`GET /squad/0` itemData) does NOT resolve (generic); only the CLUB-SEARCH/ADD path resolves. So: serve-club + search/add. Cosmetic-only nit: card short-name label shows a default "BAS" (real full names are in the record + Player Details panel) — likely the FUT commonName/knownAs field; low priority. Everything below is the reverse-engineering that led here. ## Where we are Offline FIFA 17 Ultimate Team runs end-to-end on our backend: - Every EA online gate cracked (Origin/LSX, Blaze, UTAS/RS4) → **FUT hub**. - **Squads editor renders**: correct 4-4-2 formation, 11 labelled slots, 5-star squad rating, manager slot, benches — **no freezes**. - The one missing piece: player cards render as the generic "FUT 17" back (rating 0) — no real player *identity/face* resolves. ## The squad-shell (what makes it work — don't regress these) - `userMassInfo` MUST return `{}`. Any content (userInfo AND/OR squad) desyncs the massinfo parser (0x180174630) → infinite tokenizer spin (busy-loop freeze at 0x1801c7f1a). `utas_server.py`: `FUT_MASSINFO=empty` (default). - Deliver the squad via **`GET /squad/0`** (LoadActiveSquad, deser 0x18013d1f0), which FIFA fetches on **Squads-tab entry** (re-fetches on tab-switch, not on editor re-open). `FUT_SQUAD_STEP` selects the squad (s2v0 = 1 real item). - `GET /user` is NEVER called at boot — userInfo can only reach FIFA via userMassInfo (which we can't populate without the desync). So the hub's coins/record can't be shown until the userInfo-in-massinfo desync is solved. ## Why cards render generic (definitively reversed — 3 workflows) The card view-model (0x1800d7920) reads EVERY rendered field (rating@+0xb4, position@+0x146, nation@+0x148, teamid@+0x94, 6 attrs@+0x98..0xac, name@+0xdd) from a **resolved player-definition record at `item+0x10`** — NEVER from our item JSON. That's why item-format/version/field changes had zero effect. `item+0x10` is filled by the resolve at 0x180141160-76: `getter 0x18011a830 → CardsDb singleton [0x1802e6398] → call [vtable+0xa08]` (= lookup **0x18011cca0**), output buffer `[rbp+0x160]`. The lookup searches a std::map at `CardsDb_obj+0x160c0` keyed by resourceId. **Offline that map is EMPTY**, so every lookup misses and a **default blank record** is emitted → generic card. Dead ends (proven, do not retry): - **Version advertising is inert.** `itemDbVersion` (atom 0x16c) and `checkServerDbVersion` (atom 0x80) are JSON field names routed to the value-SKIP handler 0x180135ff0 in every parser — parsed and discarded, never compared. Roster-version bump and Blaze `itemDbVersion=999999999` both did nothing. - **Serving owned items does NOT auto-trigger a definition fetch.** The itemData-array parser (0x1801293d0→0x18013fe00) does no membership check and no enqueue; the render-miss is terminal. Our squad already has an owned item rendering generic with 0 `idList` fetches — empirical proof. - **In-place map overwrite is dead** — the map at `+0x160c0` stays empty even with all 11 cards rendering (probed live). The resolve emits a transient default to the caller stack each frame; nothing persistent to edit. The definition FETCH (`ut/17/item?idList=`, `/item/resource`, `/defid`; URL builder 0x180129200) is issued by FUT-controller vtable method 0x180119010 / requestDefinitions 0x180036e20 — **both have zero call-sites inside CardsDLL**. The decision to fetch lives in the **packed FIFA17.exe** (decrypted in live memory only). Our definition-serving endpoints (`item_def`/`defs_route` in `utas_server.py`, routes `item/resource`/`defid`/`item?idList=`) are **built and ready** for if/when the fetch is ever driven. ## Next-steps plan (real player faces) — ordered by tractability ### Option A — Drive the fetch from FIFA17.exe (most "correct", hardest to find) FIFA17.exe is packed on disk but decrypted in live memory (Wine flat-maps at 0x140000000). Find the call-site of the idList issuer (CardsDLL vtable method 0x180119010, .rdata slot 0x18021cb78) inside the live FIFA17.exe image: set a hardware breakpoint / rwatch on that slot's invocation, or scan decrypted .text for the call. Identify what condition it gates on and satisfy it. If FIFA then requests `item?idList=`, our endpoints already answer → cards resolve. This is the clean win: no ongoing memory writes, works via normal data flow. ### Option B — Populate the CardsDb store so lookups HIT (live-memory injection) Pre-insert real records into the std::map at `CardsDb_obj+0x160c0` (node: Left+0x0/Right+0x8/Parent+0x10/color+0x18/key(resourceId)+0x20/record+0x28). Two sub-approaches: - **B1 (call the game's own insert):** drive the map's find-or-insert (0x180115c30, reached from lookup 0x18011cca0) with a resourceId + a record we fill. Requires a small code-injection harness (set up registers + call) since the insert isn't reachable from our side otherwise. - **B2 (hand-build a node):** allocate a node in FIFA's heap, write left/right/parent/color/key/record, splice into the tree + rebalance. Fiddliest; RB-tree invariants must hold or later lookups corrupt. Record fields to fill (from base `dbdata.dll`): rating@+0xb4, position@+0x146, nation@+0x148, teamid@+0x94, 6 attrs@+0x98..0xac, name@+0xdd. ### Option C — Patch the resolve miss-path (proof-of-concept, then key it) Patch lookup 0x18011cca0's miss branch to write real fields into its output record `[rbp+0x160]`. Quickest to see *a* real card, but naively makes ALL cards show one player; must be keyed by resourceId to be useful. Good first experiment to confirm the field offsets end-to-end before investing in A or B. ### Prerequisite for all: a dbdata.dll extractor Build a tool to read the base player DB (`/mnt/games/FIFA 17/dbdata.dll`, single export `getTableData`; tables players/playernames/teams/nations/ teamplayerlinks) → a `resourceId → {name,rating,pos,nation,team,attrs}` table (resourceId = playerId | version<<24; Ronaldo playerId 20801). This feeds B and C and validates A. No such tool exists yet. ## Recommended order 1. **C** as a 30-minute proof: patch the miss-path to emit a fixed real record → confirm a real card face appears (validates the whole record-offset model live). 2. Build the **dbdata extractor** (needed by everything). 3. Attempt **A** (find the FIFA17.exe fetch trigger) — the clean, durable win. 4. Fall back to **B1** (drive the game's insert) if A's trigger proves unreachable. ## Reusable tooling - `/proc/PID/mem` read/write pattern: `autopatch.py`, the poke/force tools (ptrace_scope=0 armed by `root_arm.sh`). - Live vtable/struct probing: the python snippets used this session (singleton `[0x1802e6398]`, static↔live base map). - `fifadrive.sh` (screen capture) + `vgamepad.py` (virtual pad) for headless drive/observe — note keyboard XTEST does NOT reach FIFA; the gamepad is unverified in-game.