diff --git a/fifa17-recon/docs/CARD_SYSTEM.md b/fifa17-recon/docs/CARD_SYSTEM.md index 4b7f307..998d1d7 100644 --- a/fifa17-recon/docs/CARD_SYSTEM.md +++ b/fifa17-recon/docs/CARD_SYSTEM.md @@ -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/. 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/ 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:, 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_` | +| stadium | **10** | 7 | `StadiumName_` | +| badge | **11** | 7 | `TeamName_Abbr15_` | +| 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:`, 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. diff --git a/fifa17-recon/tools/fut_store.py b/fifa17-recon/tools/fut_store.py index 1a1c8d2..ac3875b 100644 --- a/fifa17-recon/tools/fut_store.py +++ b/fifa17-recon/tools/fut_store.py @@ -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