Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).
Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
reads identity/rating/face from a resolved record at item+0x10, filled
by a lookup (0x18011cca0) in the FUT item-definition std::map at
CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
(JSON fields routed to the skip handler). Owned items don't auto-trigger
a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
ready; the fetch trigger lives in the packed FIFA17.exe.
New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
6.5 KiB
FIFA 17 FUT — Card System RE + Next-Steps Plan
Status as of 2026-08-01. All clean-room (our own binaries + running client only).
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)
userMassInfoMUST 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_STEPselects the squad (s2v0 = 1 real item). GET /useris 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) andcheckServerDbVersion(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 BlazeitemDbVersion=999999999both 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
idListfetches — empirical proof. - In-place map overwrite is dead — the map at
+0x160c0stays 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=<ids>, 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
- 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).
- Build the dbdata extractor (needed by everything).
- Attempt A (find the FIFA17.exe fetch trigger) — the clean, durable win.
- Fall back to B1 (drive the game's insert) if A's trigger proves unreachable.
Reusable tooling
/proc/PID/memread/write pattern:autopatch.py, the poke/force tools (ptrace_scope=0 armed byroot_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.