club items: send itemType, which every ingested family already carries

Measured live 2026-08-23 against the client parked on the pre-match kit selector
(pid 8793, read-only /proc/PID/mem). Whether a served club item becomes a
resident item record correlates perfectly with whether we send itemType:

    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 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
"This kit is currently locked. To unlock and use it, please go to the Football
Club Catalogue." dialog the operator sees. That string lives in the Flash UI, and
Blaze receives no catalogue or unlock request at any point, so the lock is
decided client-side from data the client already holds.

CARD_SYSTEM.md records that itemType "is parsed into a heap string and never
stored". That stays true of the RECORD; it does not follow that the string is
unused, and the correlation is evidence it is consulted before the record is
kept.

Tokens come from the ?type= vocabulary decoded from the FUN_18012ec50 jump table
(kit 12, stadium 13, badge 11) - the same vocabulary the two working families
use. INFERRED, not proven: no real EA club item exists anywhere in the capture
corpus, so the exact token is taken from the atom vocabulary, not observed wire.

Near miss worth recording: STADIUM_SUBTYPE was not imported at module scope, so
match treated it as a fresh binding instead of a constant, made that arm
irrefutable and typed badges as "stadium". It compiled with only an
unused-variable warning. every_cardtype7_family_carries_its_own_item_type
asserts three distinct tokens, which is what catches that.

cargo test --workspace: 1240 passed, 0 failed.
This commit is contained in:
funman300
2026-08-23 19:15:38 +00:00
parent 40e53ed02c
commit 6d89d62e41
2 changed files with 84 additions and 2 deletions
@@ -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.
+83 -1
View File
@@ -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");
}
}