Files
OpenFUT/fifa17-recon/docs/CARD_SYSTEM.md
T
funman300 59934b4ef0 fifa17-recon: FUT squad blocker solved + userInfo delivered
The client now issues PUT /squad and the hub renders coins, record and the
squad roster. Three separate root causes, all verified live.

Squad blocker (the long-standing "client never sends PUT /squad"):
  AddPlayerToSquad, GetSquads and SelectSquadById issue ZERO network requests
  (pure local model reads/mutations, FutSquadServiceImpl vtable 0x180233ff0);
  only SaveCurrentSquad writes, and it is unguarded. The client simply needed a
  populated ACTIVE squad model, which arrives via the massinfo `squad` member.
  No response of ours was ever being rejected.

userMassInfo is NOT required to be {}:
  0x180174630 is a FLAT {userInfo, squad, settings, userData} body -- the old
  "wrapper key is user" note was wrong, and the historical freeze was the
  malformed squad member, not the envelope.

clubNameChangeAllowed must be false:
  sending true advertises a club-rename flow whose UI model is never populated;
  the client shows a naming prompt and dies confirming it (ACCESS_VIOLATION
  reading 0x0 at FIFA17.exe+0x71b8651, 4/4 runs, no CardsDLL frame and no request
  in flight). Isolated by a single-variable run; guarded by a contract check.

Endpoint/schema corrections found in live traffic, invisible to static analysis:
  * GET ut/%s/squad/list is a real endpoint and must return {"squad":[...]},
    not the active-squad object (the /list suffix is appended by the caller, so
    it never appeared in the request table)
  * PUT lands on ut/%s/squad/<id>, not a bare ut/%s/squad
  * userInfo currencies are read as name/funds/finalFunds/active -- there is no
    "value" key, so coins always rendered 0
  * squad-list elements take STRING formation/squadType, not ints
  * the CardsDLL script-API thunk<->name table was off by one (AddPlayerToSquad
    is 0x18004aa70; 0x18004aff0 is GetPotentialChemistry_Club)

FUT_MASSINFO / FUT_USERINFO ladders keep every step of the bisect reproducible.
Contract suite 311 -> 358 checks. Tooling added: PyGhidra harness (Ghidra's
Java/OSGi script path is broken on this box), minidump reader, live code grabber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-03 20:47:16 -07:00

8.0 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).

★ 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 {}superseded 2026-08-03. 0x180174630 is fully decompiled: a FLAT {userInfo, squad, settings, userData} body. The old desync is attributed to the squad member, which was built before 0x18013d1f0's schema was known. utas_server.py now defaults to FUT_MASSINFO=full; fall back through squad/userinfo/settings/empty to bisect if the hub freezes at 0x1801c7f1a.
  • 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 is now populated (above), so the hub's coins/record and the squad roster (userInfo.squadList) are delivered at boot. Pending live verify.

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=<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.

  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.