docs(fifa17): apply the plan's corrections to CARD_SYSTEM.md and fut_store.py

Section 7 of plan-2026-08-06-card-subsystem.md listed these and they were never
applied, so the stale text kept misleading readers — it already cost this project
a ten-row itemState table.

CARD_SYSTEM.md:
  - "STILL UNKNOWN, AND NOT GUESSED" is ANSWERED. Its candidate set was wrong:
    it asked which of 0x1e/0x1f/0x91..0x96 meant kit/badge/stadium, but three of
    the five families are cardtype 7 (kit 9, stadium 10, badge 11) and are not in
    that set at all, and 0x91..0x96 are trophies. Replaced with the settled map
    and how each family's caption resolves.
  - itemState table starts at 0x180229cc0, not 0x180229d20 — the recorded address
    points MID-table, which is why six rows were missing. Added that
    WAITING_FOR_GAME/inGame are aliases, that omitting the key yields invalid and
    not free, and that the match is case-sensitive (measured).
  - the consumables route claim "the /consumables/%s template ... the client has
    still never used" is false; it IS that template, with base index 3 = ut/%s/club.
  - added the dated field-map correction block, extended with the +0x60 and
    definitionId findings measured on 2026-08-21.

tools/fut_store.py: the discard_value premise "that lookup returns no row for our
cards" / "WHY its lookup misses is still UNKNOWN" is false — it does not miss, the
tile reads a different property. That story sent one round of work chasing a table
defect that never existed.
This commit is contained in:
funman300
2026-08-21 21:31:59 +00:00
parent 404e859cb6
commit 22cfae830f
2 changed files with 116 additions and 25 deletions
+106 -21
View File
@@ -347,10 +347,17 @@ The client's own dialog names the class: "Search Type: Consumables Search".
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, and not the "/consumables/%s" template in .rdata,
which the client has still never used. Worse, that path is a /club PREFIX, so it
fell through to the generic route and the consumables screen was answered with the
194-card player list.
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
@@ -402,21 +409,40 @@ the same mapping: balls 37, kits 35, stadium 36, badges 39, league logos 40.
# Club items: what the research established, 2026-08-05
Researched after a guessed field crashed the client. Facts first, and the one thing
still unknown is named as unknown.
> **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. That leaves 0x1e, 0x1f and 0x91..0x96 for badges, kits, stadia, balls
and league logos.
2. ITEMSTATE CARRIES THE EQUIPPED STATE. The enum table at 0x180229d20 (stride 0x10)
is: WAITING_FOR_GAME, inGame, forSale, offered, activeBadge, activeHomeKit,
activeAwayKit, activeBall, activeStadium, active. So an EQUIPPED club item is not a
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.
"free" is correct for owned-but-not-equipped, which is what we send.
**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
@@ -427,16 +453,30 @@ still unknown is named as unknown.
type=ball, type=equippables (the combined customisation view). Not the plural stat
names, and not a club/<family> path.
## STILL UNKNOWN, AND NOT GUESSED
## ANSWERED 2026-08-06 (was "STILL UNKNOWN, AND NOT GUESSED")
Which of 0x1e, 0x1f, 0x91..0x96 means ball versus stadium versus badge versus kit.
It is in none of the 149 dumped tables, there is no group table, and cardtype 9 has NO
arm in the merge, so a wrong subtype cannot announce itself the way a coach's "DB
Error" does. Two ways to settle it, in order of preference:
a. more RE: find the consumer that switches on subtype for a club item, most likely
in the equip path that writes itemState = activeBadge and friends;
b. FUT_CLUBITEMS=probe:<family>, which serves ONE family as eight items, one per
candidate subtype, so the screen names the right one.
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
@@ -447,3 +487,48 @@ taking its time and then dies. None of the three was needed to draw a card. Comp
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, and the stronger statement is now measured: the pre-match kit selector
gates on `+0x60 == 4` at `0x1801c34f2`, and **nothing anywhere stores 4 into that
field** — not in CardsDLL (29 immediate stores, constants `{-2,0,1,908,0x3f800000}`),
not in FIFA17.exe (zero across 79 MB), and no resident record has ever held it
(live: `{1: players, 0: staff}`). Every OTHER input to that gate is already
served. So the empty kit-selection screen is a client dead end, not a missing
wire field. Tool: `fifa17-recon/tools/kit_gate_probe.py`.
**`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.
+10 -4
View File
@@ -299,10 +299,16 @@ def player_item(item_id, player, special=False):
# The cause is the guard the table work reversed. FUN_18013fe00 stores our
# discardValue at item +0x38; at 0x180141025 a `cmp dword [rbp+0x198],0` / `ja` skips
# the client's own local computation when that value is NON-ZERO. We seed 0, so the
# client runs its own fcc_discardcoins lookup, that lookup returns no row for our
# cards, the price register stays 0, and it renders 0. WHY its lookup misses is still
# UNKNOWN and worth knowing, but it does not have to be answered to fix the display:
# sending a non-zero value bypasses the lookup entirely and the client uses ours.
# client runs its own fcc_discardcoins lookup and the price register stays 0.
#
# CORRECTED 2026-08-06: the two claims that used to sit here -- "that lookup
# returns no row for our cards" and "WHY its lookup misses is still UNKNOWN" --
# are both FALSE. The lookup does not miss; real rows exist for both rare values
# on (cardtype 6, level, rare). The tile reads a DIFFERENT property, which is why
# the wallet and the screen disagreed. Sending a non-zero value still fixes the
# display, for the reason below -- it bypasses the local computation entirely --
# but do not carry the "missing row" story forward: it sent one round of work
# looking for a table defect that was never there.
#
# Freeze risk: low and in the safe direction. discardValue is a plain INT read by the
# scalar getter 0x1801c79d0. The freezes on this project have all come from feeding an