diff --git a/openfut-adapter-fifa17/src/fut/club_response.rs b/openfut-adapter-fifa17/src/fut/club_response.rs index bc8d859..6940df2 100644 --- a/openfut-adapter-fifa17/src/fut/club_response.rs +++ b/openfut-adapter-fifa17/src/fut/club_response.rs @@ -563,7 +563,7 @@ mod tests { assert_eq!(body["itemData"][0]["itemState"], "activeHomeKit"); assert_eq!(body["itemData"][1]["itemState"], "activeAwayKit"); assert!(body["itemData"][0].get("attributeList").is_none()); - assert!(body["itemData"][0].get("itemType").is_none()); + assert_eq!(body["itemData"][0]["itemType"], "kit"); } /// Kit, badge and stadium are one cardtype-7 record and MUST all project. diff --git a/openfut-adapter-fifa17/src/fut/item.rs b/openfut-adapter-fifa17/src/fut/item.rs index c234be5..37f2a1d 100644 --- a/openfut-adapter-fifa17/src/fut/item.rs +++ b/openfut-adapter-fifa17/src/fut/item.rs @@ -26,7 +26,7 @@ use serde_json::{json, Value}; use crate::fut::content_taxonomy::{ consumable_family, consumable_needs, ConsumableNeeds, ContentKind, BADGE_SUBTYPE, KIT_SUBTYPE, - MANAGER_SUBTYPE, + MANAGER_SUBTYPE, STADIUM_SUBTYPE, }; use crate::fut::entities::ReverseEntityResolver; use crate::fut::item_state; @@ -369,6 +369,49 @@ pub fn shape_item( }) } +/// Wire `itemType` (atom 0x173) for a cardtype-7 club item. +/// +/// MEASURED 2026-08-23, live client pid 8793 parked on the pre-match kit +/// selector, read-only `/proc/PID/mem`. Whether a served club item becomes a +/// resident item record correlates perfectly with whether we send `itemType`: +/// +/// ```text +/// family itemType sent resident record? +/// player "player" yes +/// staff "staff" yes +/// kit (absent) NO +/// badge (absent) NO +/// stadium (absent) NO +/// ``` +/// +/// Two of two families that carry it are ingested; none of the three that omit +/// it is. With no cardtype-7 record resident, the club scan `FUN_1800d73d0` +/// (`+0x4c == 7 && +0x50 == 9 && +0x5c in {101,102}`) matches nothing, the FUT +/// match-kit DataProvider is built empty (traced: `KITS_AVAILABLE = 0`), and the +/// selector falls back to catalogue-gated engine kits — which is the observed +/// "This kit is currently locked" dialog. +/// +/// `CARD_SYSTEM.md` records that `itemType` "is parsed into a heap string and +/// never stored". That remains true of the RECORD; it does not follow that the +/// string is unused, and the correlation above is evidence that it is consulted +/// before the record is retained. +/// +/// The tokens are the `?type=` vocabulary decoded from the `FUN_18012ec50` jump +/// table (`kit` 12, `stadium` 13, `badge` 11), which is the same vocabulary the +/// two working families already use (`player` 1, `staff` 10). +/// +/// INFERRED, not proven: no capture of a real EA club item exists anywhere in +/// the corpus, so the exact token for these three families is taken from the +/// atom vocabulary rather than observed on the wire. +fn club_item_type(subtype: i64) -> &'static str { + match subtype { + KIT_SUBTYPE => "kit", + STADIUM_SUBTYPE => "stadium", + BADGE_SUBTYPE => "badge", + _ => "misc", + } +} + /// Build one FIFA 17 **cardtype-7 club item**: a kit (subtype 9), a badge (11) /// or a stadium (10). `item_state` is the proven wire enum token — `free`, or /// one of the `active*` designations the client deserializes to 100..104. @@ -397,6 +440,7 @@ pub fn shape_club_item(id: Fifa17KitIdentity, item_state: &str) -> Value { "assetId": id.asset_id, "cardassetid": id.card_asset_id, "cardsubtypeid": id.subtype, + "itemType": club_item_type(id.subtype), "itemState": item_state, "owners": 1, "untradeable": false, @@ -1029,4 +1073,42 @@ mod tests { assert!(other.get("year").is_none(), "subtype {subtype}"); } } + + /// Every cardtype-7 family MUST carry a DISTINCT `itemType`. + /// + /// Measured live 2026-08-23: the two families that carry `itemType` + /// (player, staff) become resident item records and the three that omitted + /// it (kit, badge, stadium) did not, leaving the pre-match kit selector with + /// an empty DataProvider and a "kit is currently locked" dialog. + /// + /// The distinctness half is not pedantry. `STADIUM_SUBTYPE` was initially + /// unimported here, so `match` read it as a fresh binding rather than a + /// constant, silently made the stadium arm irrefutable, and typed badges as + /// `"stadium"`. It compiled with only an unused-variable warning. Asserting + /// three different tokens is what catches that class of mistake. + #[test] + fn every_cardtype7_family_carries_its_own_item_type() { + let base = Fifa17KitIdentity { + item_id: 100004874, + asset_id: 6300006, + resource_id: 6300006, + card_asset_id: 35, + subtype: KIT_SUBTYPE, + team_id: 21, + category: 2, + year: 0, + }; + let type_of = |subtype| { + shape_club_item( + Fifa17KitIdentity { subtype, ..base }, + item_state::FREE, + )["itemType"] + .as_str() + .expect("itemType is always emitted") + .to_string() + }; + assert_eq!(type_of(KIT_SUBTYPE), "kit"); + assert_eq!(type_of(STADIUM_SUBTYPE), "stadium"); + assert_eq!(type_of(BADGE_SUBTYPE), "badge"); + } }