a3fd9e870f
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
197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# 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.
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## 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.
|