fifa17-recon: CARD_SYSTEM -- record the solved identity mechanism and the oracle

Supersedes the parts of this document that were wrong: the CardsDb map is not empty
offline, and dbdata.dll is not the player database.

Records the merge dispatch table, the field-fill asymmetry that the pool design
depends on (send zero for what the client knows, send our own only where it knows
nothing), the three-state oracle with all three fingerprints, the 5000-item ingest
ceiling, the fact that the map is WIPED on every club fetch, and where the 17,547
player roster came from plus its independent cross-validation.

Also records the state of the other card families so the next session starts from
the manager branch writing no miss-fill, rather than rediscovering it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 22:00:18 -07:00
parent b996c0673d
commit c76cf706ef
+94
View File
@@ -194,3 +194,97 @@ getting the real id list out of `dbdata.dll`, and not a code-injection problem.
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).