40e53ed02c
The 2026-08-21 entry concluded the pre-match kit selector "is a client dead end,
not a missing wire field" because nothing stores 4 into item +0x60. Withdrawn.
It rested on two mistakes:
1. +0x60 == 4 DOES occur live - a record with +0x4c == 2 and +0x60 == 4 reached
the art-clone driver FUN_1801c3480. The immediate-store scan cannot see it,
so "nothing can satisfy the gate" was never licensed by that evidence.
2. It annotated `cmp [rdi+0x4c], 7` with "<- we produce this" without measuring
it. Its own live half showed only {1: players, 0: staff}: zero cardtype-7
records. That is the finding, and it was read as the opposite.
Measured now against the live client parked on the kit selector, read-only via
/proc/PID/mem over 3047 MiB, searching the exact u32 values the server sent:
players and staff are resident with sane fields; kit, badge and stadium are all
absent by resourceId AND by instance id. The host served ?type=kit total=2
emitted=2 at 17:50:09 this session and neither kit produced a record.
So the blocker sits upstream of the +0x60 gate: no cardtype-7 record is ever
created, so the club scan FUN_1800d73d0 has nothing to match, KIT_DESC never
fires and KITS_AVAILABLE reads 0. Cause is not yet settled - either our wire
shape (the cardtype-7 arm wants name/localizedName/description, which we do not
send) or cardtype-7 items being transient. Neither is recorded as fact.
Also: kit_gate_probe.py's live half is unreliable. On pid 8793 it reported
"CardsDb is empty" while a byte scan found 1966 resident players, so its
structural chain is stale and its record counts understate reality. Adds
club_record_residency_probe.py, which is read-only and cannot disturb the game.
582 lines
32 KiB
Markdown
582 lines
32 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.
|
|
|
|
---
|
|
|
|
# SOLVED 2026-08-04 (evening). Card identity, end to end.
|
|
|
|
Everything above this line is superseded where it disagrees. Two of its premises were
|
|
wrong: the CardsDb map is NOT empty offline, and `dbdata.dll` is NOT the player
|
|
database.
|
|
|
|
## The mechanism
|
|
|
|
Every item object in every response is inserted into the CardsDb map by the item
|
|
parser tail (`0x18014115b` -> registrar vtable `+0xa08` = `0x18011cca0`, a
|
|
find-or-insert). There is no fetch to trigger; `item?idList` was a red herring.
|
|
|
|
IMMEDIATELY BEFORE registering, `FUN_180141660` merges in the client's OWN local
|
|
database. It switches on `record+0x4c`, which `FUN_1800d8330` derives from the JSON
|
|
atom `0x6c cardsubtypeid` alone:
|
|
|
|
0..3 -> 1 players 4 -> 2 managercards
|
|
5 -> 3 headcoachcards 6 -> 10 gkcoachcards
|
|
7 -> 5 physiocards 8 -> 4 fitnesscoachcards
|
|
9..b -> 7 UNIDENTIFIED absent -> 0x156 -> 0, NO merge at all
|
|
|
|
For players `FUN_180135890` queries `players` by `playerid = record+0x18 & 0xffffff`,
|
|
where `record+0x18` is atom `0x287 resourceId`. Atom `0x23 assetId` lands at `+0x20`
|
|
and is NEVER read by the merge: sending assetId alone does nothing.
|
|
|
|
On a HIT it fills the name (`+0xb8` first, `+0xc8` last, `+0xdd` knownAs, all inline
|
|
char arrays), fills `nation +0x148` and `teamid +0x94` ONLY IF THEY ARRIVED AS ZERO,
|
|
and always recomputes `leagueid +0x154`. It never touches `rating +0xb4`,
|
|
`position +0x146` or `attributes +0x98..+0xac`.
|
|
|
|
So: send zero for everything the client knows better than us, and send our own value
|
|
only where the client has nothing. That asymmetry is the whole design of `fut_cards`.
|
|
|
|
## The three-state oracle (all three confirmed live)
|
|
|
|
NAMED sentinel rating survives, real name -> the id is REAL
|
|
placeholder sentinel survives, name is "Jamal Blackman", team 0 -> the row
|
|
exists but is an EMPTY SLOT. This is the trap: 169193 does this and
|
|
it was in VERIFIED_ASSET_IDS.
|
|
MISS rating 0x32, teamid 0x78d, nation 0xe, position 2, attributes 1,
|
|
name " " -> the id does NOT exist. This fingerprint matched a
|
|
user's blank pack cards field for field.
|
|
|
|
`FUT_ID_SWEEP` (utas_server) serves a window of candidate ids as a synthetic club;
|
|
`tools/card_identity_probe.py` reads back what the client resolved. 5000 items per
|
|
response ingests cleanly; 20000 was served and silently NOT ingested. THE MAP IS
|
|
WIPED ON EVERY CLUB FETCH, so an auto sweep must be collected continuously
|
|
(`sweep_collect.py --watch`) and not once at the end.
|
|
|
|
## Where the roster came from
|
|
|
|
`tools/dbdata_extract.py` reads FIFA's own rating-sorted index out of a running
|
|
process (0x40 stride, self-validating {begin,end,end+1} name-pointer triple, anchored
|
|
on 20801 = Ronaldo 94) -> `data/roster.json`, 17,547 players. Cross-validated against
|
|
the sweep oracle, a completely independent method: 573 of 573 overlapping names
|
|
agreed. The one id in the sweep and not the index is 26501, the target of the
|
|
documented 22800..22879 Legends remap, which is also what produced "Alex Hunter x80"
|
|
in a sweep and had looked like a bug.
|
|
|
|
`dbdata.dll` is an anti-tamper decoy. Its single export `getTableData` returns a fixed
|
|
759-byte self-integrity blob and the file contains no tables. Do not re-attempt it.
|
|
|
|
## Still missing for players
|
|
|
|
`position`, `nationality`, `teamId` and the six attributes are NOT in the rating
|
|
index. Two live sources were found and BOTH are per-materialised-card caches, not
|
|
tables: the 0x180-stride resolved card records, and a 32-byte keyed container
|
|
(entries `{playerId, position | hash<<32, rating, ?}`). 59 positions came from the
|
|
second. A full-roster position source has NOT been found.
|
|
|
|
## The other card families, as of 2026-08-04
|
|
|
|
Mechanism known, id spaces NOT. `FUN_1801356c0` queries `managercards` by column
|
|
`carddbid` taken RAW from `record+0x18` (no mask), selecting firstname, lastname,
|
|
assetid, value, talkrating, negotiation, rare; it writes firstname to `+0xb8`,
|
|
assetid to `+0x20`, a byte to `+0xb4`, bytes to `+0xe2/+0xe3`, and `+0x58 = (rare==1)`.
|
|
|
|
Sweeps of carddbid 1..5000 and 6000..8000 both produced 2000+ manager records with
|
|
cardtype 2 and NOTHING written. THE MANAGER BRANCH WRITES NO MISS-FILL, so a wrong id
|
|
is SILENT and a negative result does not distinguish a wrong id from a wrong
|
|
mechanism. Manager name strings are in memory near 0x42800000 but are inline with NO
|
|
inbound pointers, so the players trick (find the index that points into the name pool)
|
|
does not transfer.
|
|
|
|
Tables named in the DLL and not yet probed: `headcoachcards`, `fitnesscoachcards`,
|
|
`gkcoachcards`, `physiocards`, `fancards`, `newcards`. Consumables are a separate
|
|
family (consumablesContract/Fitness/Healing/Position/Training/Formation plus
|
|
`FUT_CONSUMABLE_NAME_*` loc keys) and may be enum-driven rather than DB-driven.
|
|
|
|
`GET /club?type=` has only ever been observed with two values: `player` (paged,
|
|
start=N&count=11) and `manager` (count=200, from the STAFF tab).
|
|
|
|
---
|
|
|
|
# CONFIRMED LIVE 2026-08-05: managers and the four coach families
|
|
|
|
All five non-player card tables render correctly in game. Nothing here is inferred.
|
|
|
|
## Coaches: 34 staff cards, ZERO DB Error
|
|
|
|
Served subtypes 5/6/7/8 (headcoach, gkcoach, physio, fitnesscoach) from ids read out
|
|
of the game's own dumped tables. Every id hit its table on the first attempt:
|
|
A Shaikh 80, J Kim 77, M Kuhn 80, P Trenouth 80, C Duke 74 and the rest, ratings
|
|
55..80, with real face photos and their bonus percentages (+15% SHO and so on) drawn
|
|
by the client. The whistle / gloves / heart icons distinguish the families.
|
|
|
|
That is the payoff from enumeration over probing. The miss-fill exists and is loud
|
|
("DB Error", rating 0x32), and it never fired once, because the ids came from reading
|
|
the database rather than sweeping for it.
|
|
|
|
## Managers: 10 of 10 resolved, and the template paints OUR fields
|
|
|
|
Luis Enrique 88, Conte 87, Wenger 86, Klopp 84, Koeman 82, Pardew 80, Di Francesco
|
|
78, Maes 75, Wdowczyk 70, Canning 65. Every nation and league is historically right:
|
|
Conte/Wenger/Klopp/Koeman/Pardew all Premier League, Di Francesco Serie A, Maes
|
|
Jupiler, Canning Scottish.
|
|
|
|
RESOLVED, previously TODO/CONFIRM: the manager card template DOES paint record+0xde
|
|
(nation) and record+0xe0 (league). The Luis Enrique card draws the Spain flag and the
|
|
words "LaLiga Santander"; the Premier League managers draw their flag and "ENG 1".
|
|
Neither field is written by the merge (FUN_1801356c0 does not touch them), so nothing
|
|
but our own JSON could have supplied them. That is the cleanest possible proof that
|
|
those two offsets are ours.
|
|
|
|
NOT confirmed: negotiation at record+0xe3 is NOT on the card front. The card shows
|
|
"CONTRACT 7" there instead. FUN_1800e5940 does read +0xe3 and publish it as
|
|
ATTRIB_CONTRACT_NEGOTIATION, so it surfaces somewhere else or not at all.
|
|
|
|
## Why managers needed the coaches beside them
|
|
|
|
A wrong manager carddbid is completely SILENT: FUN_1801356c0 has no else-branch and
|
|
writes no miss-fill, so a bad id and a bad wire shape look identical. The coaches were
|
|
put on the same screen precisely because their miss IS loud. Nothing failed, but the
|
|
test was designed so that a failure would have been diagnosable rather than mute.
|
|
|
|
---
|
|
|
|
# CONFIRMED LIVE 2026-08-05: consumables
|
|
|
|
Rendering with real artwork, stack quantity badges and correct amounts (+5 / +10 /
|
|
+15, so atom 0x1b reaches record+0xbf and FUN_1801a8040's sign-extension never fires).
|
|
The client's own dialog names the class: "Search Type: Consumables Search".
|
|
|
|
## Three things had to be right, and each failed SILENTLY with a 200
|
|
|
|
1. THE COUNT IS THE GATE. The client will not ask for consumable items until
|
|
GET club/stats/consumables reports a non-zero count. We answered that route 41
|
|
times a session with the PLAYER stat set, so the panel read seven zeros and never
|
|
proceeded. Two rounds of item-shape work sat unrequested for want of a counter.
|
|
2. THE ROUTE IS GET club/consumables/<category>. Not club?type=, which a previous
|
|
round shipped four arms for. That path is a /club PREFIX, so a naive router
|
|
falls it through to the generic route and answers the consumables screen with
|
|
the 194-card player list.
|
|
|
|
**CORRECTED 2026-08-21.** This item used to add "and not the
|
|
`/consumables/%s` template in .rdata, which the client has still never used".
|
|
That is false, and the same sentence is in commit `ccb736f`. It IS exactly
|
|
that template: action row 9 `ConsumablesSearch` carries base index 3 =
|
|
`ut/%s/club`, and `FUN_1801308c0` appends `/consumables/%s`. The base was
|
|
`ut/%s/club` all along, which is why the observed URL and the template look
|
|
like different things and are not.
|
|
3. THE ELEMENT IS A STACK WRAPPER, NOT AN ITEM. FutConsumablesSearchServerResponse
|
|
(RS4 literal 0x1802222f8, factory 0x180130a10, vtable 0x180222200, deser +0x08 =
|
|
0x180130d10, 6873 chars) reads itemData(0x16b) at the root like the club list, but
|
|
its element is five atoms:
|
|
0xbc count 0xd7 discardValue
|
|
0x16a item -> FUN_18013fe00, the item parser itself
|
|
0x287 resourceId 0x362 untradeableCount
|
|
Everything else goes to the value-skip handler, so a bare item was ACCEPTED and did
|
|
nothing: the live card map held only the 11 squad players afterwards. That is also
|
|
why FUT draws consumables as one stack with a quantity rather than N cards.
|
|
|
|
## The lesson, since it has now cost three rounds
|
|
|
|
A 200 with a well-formed body that the consumer silently discards is the worst failure
|
|
shape in this project. Nothing errors, nothing logs as unmapped, the screen is just
|
|
empty. The diagnostic that worked every time was reading the CardsDb map: if the
|
|
client ingested nothing, the shape is wrong; if it ingested records that do not draw,
|
|
the failure is downstream. Guessing between those two costs a human a menu trip each
|
|
time; the probe costs 0.03 seconds.
|
|
|
|
## The green box, and why two guesses missed it
|
|
|
|
CONFIRMED FIXED 2026-08-05. Consumable cards draw real artwork once cardassetid
|
|
carries the ART id from the fcc_ tables (training 3, contract 7, healing 10, misc 45)
|
|
instead of a copy of the resourceId. The placeholder was
|
|
external/ion_fut/artAssets/.../notfound.swf, the client's stand-in for art it cannot
|
|
resolve.
|
|
|
|
The two wrong guesses are recorded because the reasoning behind each was plausible:
|
|
|
|
1. "It is the untradeable badge." The deserializer really does set a UI flag from
|
|
(untradeableCount < count) and UNTRADEABLE_COUNT really is a state key. Changing
|
|
it flipped the flag in the live record (+0x49 went to 1) and the tag did not
|
|
move. Refuted by measurement, not by argument.
|
|
2. "It is an untranslated loc key." Also wrong: the string NOT FOUND does not exist
|
|
anywhere in cardsdll.dll, in any casing.
|
|
|
|
What settled it was reading the badge TEXT off an unobstructed screenshot. "NOT FOUND"
|
|
is not a status, it is a missing-asset placeholder, and that turned a UI question into
|
|
an id question. The lesson is the cheap one: get the exact on-screen string before
|
|
theorising about what produces it.
|
|
|
|
THE TRAP GENERALISES. An fcc_ row carries BOTH carddbid and cardassetid and they are
|
|
not interchangeable. fut_store._item copies resourceId into cardassetid, which is
|
|
correct for players and wrong for every other family. The club-item family will need
|
|
the same mapping: balls 37, kits 35, stadium 36, badges 39, league logos 40.
|
|
|
|
---
|
|
|
|
# Club items: what the research established, 2026-08-05
|
|
|
|
> **SUPERSEDED 2026-08-21 in part.** `docs/plan-2026-08-06-card-subsystem.md` is
|
|
> the authority for club items and for the `itemState` vocabulary; where this
|
|
> file and that one disagree, that one wins. The corrections are applied inline
|
|
> below and marked. The subtype question this section calls UNKNOWN is ANSWERED.
|
|
|
|
Researched after a guessed field crashed the client. Facts first.
|
|
|
|
## VERIFIED IN BINARY
|
|
|
|
1. THE CARDTYPE MAP IS EXACT. FUN_1800d8330 (714 chars, read in full) returns cardtype
|
|
9 for cardsubtypeid 0x1e, 0x1f, 0x91..0x96, 0xe7..0xe9 and 0xec, and nothing else.
|
|
fcc_misccards carries cardsubtype 231 = 0xe7, which anchors the 0xe7..0xe9 block to
|
|
misc cards.
|
|
|
|
**CORRECTED 2026-08-21.** The first half is right; the inference that followed
|
|
it was wrong. It read "that leaves 0x1e, 0x1f and 0x91..0x96 for badges, kits,
|
|
stadia, balls and league logos". In fact `0x91..0x96` are TROPHIES, and three
|
|
of the five club families are **cardtype 7, not 9** — `FUN_1800d8330` contains
|
|
`case 9: case 10: case 0xb: return 7;`. Only ball (0x1e) and league logo
|
|
(0x1f) are cardtype 9.
|
|
2. ITEMSTATE CARRIES THE EQUIPPED STATE. So an EQUIPPED club item is not a
|
|
different subtype, it is the same item with itemState set to one of those five.
|
|
|
|
**CORRECTED 2026-08-21.** The table starts at **`0x180229cc0`**, not
|
|
`0x180229d20` — the recorded address points into the MIDDLE of it, which is why
|
|
only ten rows were seen. The full vocabulary is TWELVE rows; the six missing
|
|
from the reading below are `invalid`, `free`, `WAITING_FOR_GAME`, `inGame`,
|
|
`forSale` and `offered`. Two further consequences the ten-row reading hid:
|
|
`WAITING_FOR_GAME` and `inGame` are genuine ALIASES (both decode to 2), and
|
|
OMITTING the key yields `0` = `invalid`, which is NOT the same as `free` — an
|
|
item left at 0 fails the squad builder's `state == 1 || state == 2` test. The
|
|
match is also CASE-SENSITIVE (measured 2026-08-21: the comparator is
|
|
`msvcr120.dll+0x3c330`, a plain `strncmp` with no case folding), so the casing
|
|
in the table is a contract. See `openfut-adapter-fifa17/src/fut/item_state.rs`.
|
|
3. CLUB ITEMS HAVE NO CATEGORY GROUP TABLE. Consumables have one at 0x180203260 (seven
|
|
codes: training, contracts, fitness, healing, playStyle, managerLeagueModifier,
|
|
position) and staff have one at 0x180203310 (five codes). There is no equivalent
|
|
for club items; the only club-item strings are the COUNT labels
|
|
FUT_MYCLUB_KITS_AVAILABLE / BADGES_AVAILABLE / STADIA_OWNED / BALLS_EARNED, which
|
|
we already serve correctly.
|
|
4. THE ROUTE IS club?type= WITH SINGULAR NAMES. Observed live: type=stadium,
|
|
type=ball, type=equippables (the combined customisation view). Not the plural stat
|
|
names, and not a club/<family> path.
|
|
|
|
## ANSWERED 2026-08-06 (was "STILL UNKNOWN, AND NOT GUESSED")
|
|
|
|
The question was "which of 0x1e, 0x1f, 0x91..0x96 means ball versus stadium versus
|
|
badge versus kit". It was the wrong candidate set — three of the families are not
|
|
in it at all. The settled map:
|
|
|
|
| family | cardsubtypeid | cardtype | how the caption resolves |
|
|
|---|---|---|---|
|
|
| kit | **9** | 7 | `TeamName_Abbr15_<teamid>` |
|
|
| stadium | **10** | 7 | `StadiumName_<assetId>` |
|
|
| badge | **11** | 7 | `TeamName_Abbr15_<teamid>` |
|
|
| ball | **30** (0x1e) | 9 | no DB resolver; `FUT_UC_BALL` caption only |
|
|
| league logo | **31** (0x1f) | 9 | by elimination |
|
|
|
|
`0x91..0x96` are TROPHIES, not club items. Route (a) of the two proposals above is
|
|
what paid off — the consumer is the manager vtable slot `+0x498` =
|
|
`FUN_180119bd0`, dispatched when `item+0x4c == 7`. Route (b),
|
|
`FUT_CLUBITEMS=probe:<family>`, would have FAILED for three of the five families,
|
|
because its candidate set never contained 9, 10 or 11.
|
|
|
|
Kit, badge and stadium are served by OpenFUT today. Ball and league logo are
|
|
withheld: cardtype 9 has no database name resolver, so their name could only come
|
|
from `localizedName` on the wire, and that is not established as safe to send.
|
|
One residual probe remains, specified in `plan-2026-08-06-card-subsystem.md` §3.
|
|
|
|
## WHY THE CRASH HAPPENED, recorded so it is not repeated
|
|
|
|
teamid, leagueid and value were copied out of the fcc row as "extras". `value` appears
|
|
elsewhere as an OBJECT member (displayGroup {"value": ...}); a scalar where an object
|
|
is expected is the type-desync busy loop at 0x1801c7f1a, which presents as the game
|
|
taking its time and then dies. None of the three was needed to draw a card. Compounding
|
|
it, the response that crashed was type=equippables carrying 30 items across FIVE
|
|
unverified subtypes at once, so even the crash taught us nothing about which subtype
|
|
was wrong. Both are fixed: no extras, equippables withheld, one family per test.
|
|
|
|
---
|
|
|
|
# Field-map corrections (dated)
|
|
|
|
This file's earlier field notes predate the deserializer frame arithmetic. Where
|
|
they disagree with the table in `plan-2026-08-06-card-subsystem.md` §2, that
|
|
table wins — it is derived structurally (`FUN_18013fe00` builds the record as a
|
|
stack struct and hands `&local_188` to the merge, so `record_offset = 0x188 - X`)
|
|
rather than inferred backwards from an accessor.
|
|
|
|
```
|
|
CORRECTED 2026-08-06 (live diff + deserializer frame arithmetic, record_off = 0x188 - X):
|
|
+0x34 lastSalePrice (atom 0x185), published to Flash as BOUGHT_FOR
|
|
+0x48 owners (atom 0x207, u8; constructor default 0)
|
|
+0x49 TRADEABLE (atom 0x361 untradeable, u8, stored INVERTED; default 1)
|
|
+0x54 discard LEVEL (3/2/1 by rating >= 0x4b / >= 0x41), NOT an itemType enum
|
|
+0x5c itemState (atom 0x172 via FUN_180166660, u32)
|
|
+0x88 playStyle (atom 0x23f via FUN_180136480; only 0xfb..0x111 map to 1..0x17)
|
|
+0x90 loans (atom 0x19b) -- do not send; loans>0 with contract 0 greys MODIFY
|
|
+0xbe amount (atom 0x1b, u8) for cardsubtypeid 250..273 (chemistry styles)
|
|
+0xbf amount (atom 0x1b, u8) for the other consumable classes
|
|
+0xd9 localizedName (atom 0x19c, 0x38 bytes) for cardtype 9; +0xbc (0x1f) for cardtype 7
|
|
+0x111 description (atom 0xd1, 0x1f bytes) for cardtype 9; +0x10f for cardtype 7
|
|
+0x30 is a CLIENT timestamp from FUN_1800d84e0(), not a wire field
|
|
+0x60 pile is assigned by the owning list, not parsed; there is no 0x226 arm
|
|
itemType (atom 0x173) is parsed into a heap string and never stored
|
|
definitionId is NOT AN ATOM
|
|
```
|
|
|
|
**`+0x60`, extended 2026-08-21.** "Assigned by the owning list, not parsed" is
|
|
right. The pre-match kit selector gates on `+0x60 == 4` at `0x1801c34f2`, and no
|
|
instruction in CardsDLL stores that constant immediately (29 stores, constants
|
|
`{-2,0,1,908,0x3f800000}`), nor does FIFA17.exe across 79 MB.
|
|
Tool: `fifa17-recon/tools/kit_gate_probe.py`.
|
|
|
|
**CORRECTED 2026-08-23 (live, pid 8793, read-only `/proc/PID/mem`).** The
|
|
2026-08-21 entry went on to call the kit selector "a client dead end, not a
|
|
missing wire field", on the grounds that "every OTHER input to that gate is
|
|
already served". That conclusion is WITHDRAWN. It rested on two mistakes.
|
|
|
|
1. **`+0x60 == 4` does occur.** A live record reached the art-clone driver
|
|
`FUN_1801c3480` holding `+0x4c == 2`, `+0x60 == 4`. So the value arrives by
|
|
some path the immediate-store scan cannot see (register copy or computed),
|
|
and "nothing can ever satisfy the gate" is false. What the static scan
|
|
actually licenses is the narrower claim above.
|
|
2. **cardtype 7 was never verified to be produced at all.** The probe annotates
|
|
`cmp [rdi+0x4c], 7` with "<- we produce this". Nothing measured that. Its own
|
|
live half showed `{1: players, 0: staff}` -- i.e. zero cardtype-7 records --
|
|
and that was read as "the only thing missing is +0x60".
|
|
|
|
**What is actually measured now.** With the client parked on the kit selector,
|
|
scanning all 3047 MiB of readable process memory for the exact u32 values the
|
|
server sent:
|
|
|
|
```
|
|
resident (record-shaped, sane fields):
|
|
player resourceId 83906881 -> cardtype 1, itemState 1, teamid 243, +0x60 1
|
|
staff resourceId 9000081 -> cardtype 2
|
|
staff resourceId 3000083 -> cardtype 4, subtype 8
|
|
staff resourceId 1000509 -> cardtype 2, subtype 4, teamid 241
|
|
NOT resident, by resourceId AND by instance id, zero hits each:
|
|
kit 6300006 / 100004874 (cardsubtypeid 9)
|
|
kit 6400003 / 100004873 (cardsubtypeid 9)
|
|
badge 6000005 / 100004875 (cardsubtypeid 11)
|
|
stadium 6200000 / 100004876 (cardsubtypeid 10)
|
|
```
|
|
|
|
The client fetched `?type=kit` at 17:50:09 this session and the host logged
|
|
`total=2 emitted=2`. Both kits were delivered and NEITHER produced a record.
|
|
Every cardtype-7 family is absent while cardtype 1/2/4 are resident.
|
|
|
|
So the blocker is upstream of the `+0x60` gate: no cardtype-7 record is ever
|
|
created, therefore the club scan `FUN_1800d73d0` (`+0x4c==7 && +0x50==9 &&
|
|
`+0x5c in {101,102}`) has nothing to match, `KIT_DESC` never fires, and
|
|
`KITS_AVAILABLE` reads 0. Whether that is a bad wire shape (the cardtype-7 parse
|
|
arm wants `name`/`localizedName`/`description`, which OpenFUT does not send) or
|
|
cardtype-7 items being transient by design is NOT yet settled -- do not record
|
|
either as fact.
|
|
|
|
**Method note.** `kit_gate_probe.py`'s live half is unreliable as written: on
|
|
pid 8793 it printed "CardsDb is empty (no FUT session loaded)" while a byte scan
|
|
found 1966 resident players. Its structural chain is stale, so its record counts
|
|
(including the original "27 resident records") understate reality. Prefer the
|
|
value scan until the chain is re-derived.
|
|
|
|
**`definitionId is NOT AN ATOM`, confirmed a fourth way 2026-08-21.** Every real
|
|
atom name appears exactly once in CardsDLL's `.rdata` — `resourceId`,
|
|
`cardsubtypeid`, `itemState`, `assetId`, `cardassetid`, `rareflag`, `owners`,
|
|
`contract`, `discardValue`, `localizedName` — while `definitionId` is absent
|
|
entirely. It is still sent on the live-proven player path; it is inert, not
|
|
harmful, and has not been removed.
|