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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user