CARD_SYSTEM.md has claimed since it was written that offline the CardsDb map is EMPTY,
every lookup misses, and the view-model reads every rendered field from the resolved
record and NEVER from our item JSON. A live pack open falsifies both halves.
One bronze pack, five cards. Two rendered as real players with names, club badges and
national flags. Three rendered blank: rating 50, position RWB, every attribute 1. Our
pool contains no rating 50, no RWB and no all-ones attributes, so the blank is the
client default.
The two that resolved match our item JSON field for field:
(232517, 62, RB, nation 36, league 19, team 175, [72,44,58,60,62,61])
-> SILVA, 62 RB, Wolfsburg badge, Norway flag, 72 PAC 44 SHO 58 PAS 60 DRI 62 DEF
(235066, 60, GK, nation 34, league 31, team 48, [62,63,33,61,17,62])
-> NOWAK, 60 GK, 62/61 63/17 33/62
Those attribute numbers were invented by hand this afternoon. They cannot have come
from a database.
So: stats come from our JSON, name/badge/flag come from the client keyed by assetId,
and an assetId the client does not know collapses the WHOLE card to the blank, which
is why a bad id looks like a rendering failure rather than a lookup failure.
THE CONSEQUENCE IS THAT THE PLANNED WORK IS UNNECESSARY. This document recommended
populating the map by driving the insert, hand-building a red-black tree node, or
patching the resolve miss path, all of which write to a live process. None of it is
needed. The rule is: use asset ids that exist in the client database. fut_cards.py has
18 verified ids and 61 structural placeholders, and the placeholders are the blanks.
The remaining work is a DATA problem, not a code-injection problem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
11 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_serveronFUT_SQUAD_STEP=s3v0→/clubserves 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/0itemData) 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)
— superseded 2026-08-03.userMassInfoMUST return{}0x180174630is fully decompiled: a FLAT{userInfo, squad, settings, userData}body. The old desync is attributed to thesquadmember, which was built before0x18013d1f0's schema was known.utas_server.pynow defaults toFUT_MASSINFO=full; fall back throughsquad/userinfo/settings/emptyto 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_STEPselects the squad (s2v0 = 1 real item). GET /useris 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) 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.
REFUTED 2026-08-04: the CardsDb map is NOT empty offline
This document has claimed since it was written that "Offline that map is EMPTY, so every lookup misses and a default blank record is emitted -> generic card", and that the view-model reads every rendered field from the resolved record and "NEVER from our item JSON". A live pack open falsifies both halves.
The observation
One bronze pack, five cards. Two rendered as real players with names, club badges and national flags. Three rendered as blanks: rating 50, position RWB, every attribute 1, no name. Nothing in our pool has rating 50, position RWB or all-ones attributes, so the blank is the client's default record, exactly as this document describes.
The two that resolved match OUR ITEM JSON field for field:
| we sent | screen showed |
|---|---|
(232517, 62, RB, nation 36, league 19, team 175, [72,44,58,60,62,61]) |
SILVA, 62 RB, Wolfsburg badge, Norway flag, 72 PAC / 44 SHO / 58 PAS / 60 DRI / 62 DEF / 61 PHY |
(235066, 60, GK, nation 34, league 31, team 48, [62,63,33,61,17,62]) |
NOWAK, 60 GK, 62/61, 63/17, 33/62 |
Those attribute numbers were invented by hand. They cannot have come from a database.
What this means
- The map HAS entries offline. Some asset ids resolve.
- Rating, position, nation, league, team and the six attributes come from our item JSON for a card that resolves.
- Name, club badge and national flag come from the client's own data, keyed by assetId.
- When the assetId is NOT in the client's data, the whole card collapses to the blank default, which is why a bad id looks like a rendering failure rather than a lookup failure.
The consequence, which is much cheaper than what this document proposed
The recommended plan here was to populate the client's map by driving its insert, hand building a red-black tree node, or patching the resolve miss-path, all of which write to a live process. None of that is needed to get real cards. The rule is simply:
Use asset ids that exist in the client's database. Valid id gives a real card. Invalid id gives the blank.
fut_cards.py currently carries 18 verified ids and 61 structural placeholders, and
the placeholders are what produce the blanks. Two of them (232517, 235066) happen to be
real, which is why the pack was a mix. The remaining work is therefore a DATA problem,
getting the real id list out of dbdata.dll, and not a code-injection problem.
TODO/CONFIRM: whether a resolved card's rating truly comes from our JSON or whether
the client's record happens to agree. The attributes settle it (they were invented) but
rating specifically has not been isolated. Send a deliberately wrong rating for a known
good id and look.