diff --git a/openfut-adapter-fifa17/src/fut/catalog.rs b/openfut-adapter-fifa17/src/fut/catalog.rs index 678c20d..41767b0 100644 --- a/openfut-adapter-fifa17/src/fut/catalog.rs +++ b/openfut-adapter-fifa17/src/fut/catalog.rs @@ -44,6 +44,23 @@ pub struct Fifa17CardIdentity { /// Source team id for a club kit, or a manager's real club. Zero for content /// kinds that do not use it. pub team_id: i64, + /// Kit slot family (`club_items.json → kits[].category`): `2` home, `3` + /// away, `5` third. Zero for definitions that do not use it. + /// + /// Load-bearing for the pre-match kit selector, not cosmetic. The client's + /// active-kit resolver (`FUN_1800d73d0`) reads it at record `+0xb8` and maps + /// it to the engine's kit SLOT — 2→0, 3→1, 5→3 — which then forms part of + /// the `(teamid, year, slot)` triple the kit descriptor + /// (`sub_180033430`) must match. Omit it and the triple cannot match, so the + /// engine falls through to its own catalogue kit and reports the kit as + /// locked. + pub category: i64, + /// Kit season (`club_items.json → kits[].year`), `0` for a current-season + /// kit and e.g. `2002` for a historical one. + /// + /// Record `+0xba`, atom `0x389`. The third member of the identity triple + /// above, and the key the runtime `teamkits` clone queries on. + pub year: i64, /// Manager chemistry nation (`managercards.nation`), zero when unused. /// /// The client NEVER supplies this: the managercards merge (`FUN_1801356c0`) @@ -207,6 +224,12 @@ struct RawCard { /// Contract-card grant (atom 0xb8); absent → key omitted. #[serde(default)] contract: Option, + /// Kit slot family (2 home / 3 away / 5 third); absent → `0`. + #[serde(default)] + category: Option, + /// Kit season; absent → `0` (current season). + #[serde(default)] + year: Option, } fn default_rareflag() -> i64 { @@ -265,6 +288,8 @@ impl Fifa17CardCatalog { subtype: rc.subtype, card_asset_id: rc.card_asset_id.unwrap_or(rc.asset_id), team_id: rc.team_id.unwrap_or(0), + category: rc.category.unwrap_or(0), + year: rc.year.unwrap_or(0), nation: rc.nation.unwrap_or(0), league_id: rc.league_id.unwrap_or(0), rating: rc.rating, diff --git a/openfut-adapter-fifa17/src/fut/club_response.rs b/openfut-adapter-fifa17/src/fut/club_response.rs index 344c57d..bc8d859 100644 --- a/openfut-adapter-fifa17/src/fut/club_response.rs +++ b/openfut-adapter-fifa17/src/fut/club_response.rs @@ -527,6 +527,8 @@ mod tests { card_asset_id: 35, subtype: 9, team_id, + category: 2, + year: 0, }; let ident = KindMapIdentity { ids: HashMap::new(), @@ -579,6 +581,8 @@ mod tests { card_asset_id: art, subtype, team_id: 21, + category: 2, + year: 0, }; let ident = KindMapIdentity { ids: HashMap::new(), diff --git a/openfut-adapter-fifa17/src/fut/item.rs b/openfut-adapter-fifa17/src/fut/item.rs index c6f5259..c234be5 100644 --- a/openfut-adapter-fifa17/src/fut/item.rs +++ b/openfut-adapter-fifa17/src/fut/item.rs @@ -94,6 +94,11 @@ pub struct Fifa17KitIdentity { pub card_asset_id: u32, pub subtype: i64, pub team_id: i64, + /// Kit slot family: `2` home, `3` away, `5` third. Read at record `+0xb8` + /// and mapped to the engine kit SLOT (2→0, 3→1, 5→3). + pub category: i64, + /// Kit season; `0` = current season. Record `+0xba`. + pub year: i64, } /// FIFA-side identity fields needed to render an owned staff card (manager or @@ -399,6 +404,19 @@ pub fn shape_club_item(id: Fifa17KitIdentity, item_state: &str) -> Value { if matches!(id.subtype, KIT_SUBTYPE | BADGE_SUBTYPE) { item["teamid"] = json!(id.team_id); } + // Kits only. `category` and `year` complete the `(teamid, year, slot)` + // identity the client's active-kit resolver builds at record `+0xb8`/`+0xba` + // (`FUN_1800d73d0`), and which the engine's kit descriptor + // (`sub_180033430`) compares against before it will name — rather than + // lock — a kit. Without them the triple can never match and the pre-match + // selector reports "This kit is currently locked". + // + // Deliberately NOT emitted for badges or stadiums: the slot mapping is + // kit-specific, and those families resolve their caption by other fields. + if id.subtype == KIT_SUBTYPE { + item["category"] = json!(id.category); + item["year"] = json!(id.year); + } item } @@ -877,6 +895,8 @@ mod tests { card_asset_id: 35, subtype: 9, team_id: 21, + category: 2, + year: 0, }; for state in [ item_state::FREE, @@ -914,6 +934,8 @@ mod tests { card_asset_id: 39, subtype, team_id: 21, + category: 2, + year: 0, }; for subtype in [KIT_SUBTYPE, BADGE_SUBTYPE] { let it = shape_club_item(ident(subtype), item_state::FREE); @@ -956,4 +978,55 @@ mod tests { assert!(stadium.get(key).is_none(), "club item must not carry {key}"); } } + + /// The pre-match kit selector needs the WHOLE identity triple, not just + /// `teamid`. + /// + /// The client's active-kit resolver `FUN_1800d73d0` reads `category` at + /// record `+0xb8` (mapping 2→slot 0, 3→slot 1, 5→slot 3) and `year` at + /// `+0xba`, and the engine's kit descriptor `sub_180033430` will only NAME a + /// kit whose decoded `(teamid, year, slot)` equals the club's active home or + /// away triple. A descriptor it does not match is left completely unwritten + /// and the engine reports "This kit is currently locked" instead. + /// + /// Regression: we shipped kits with `teamid` alone, which cannot match. + #[test] + fn a_kit_carries_the_full_identity_triple_the_selector_matches_on() { + let kit = Fifa17KitIdentity { + item_id: 100_004_874, + asset_id: 6_300_006, + resource_id: 6_300_006, + card_asset_id: 35, + subtype: KIT_SUBTYPE, + team_id: 21, + category: 2, + year: 0, + }; + let home = shape_club_item(kit, item_state::ACTIVE_HOME_KIT); + assert_eq!(home["teamid"], 21); + assert_eq!(home["category"], 2, "category is the SLOT source"); + assert_eq!(home["year"], 0, "year completes the triple"); + assert_eq!(home["itemState"], item_state::ACTIVE_HOME_KIT); + + // A historical kit must round-trip its real season, not be flattened. + let historical = shape_club_item( + Fifa17KitIdentity { + year: 2002, + category: 5, + ..kit + }, + item_state::ACTIVE_HOME_KIT, + ); + assert_eq!(historical["year"], 2002); + assert_eq!(historical["category"], 5); + + // Badges and stadiums must NOT gain the kit-only fields: the slot map is + // kit-specific and this project has frozen the client before by sending + // a family a field its resolver does not read. + for subtype in [BADGE_SUBTYPE, STADIUM_SUBTYPE] { + let other = shape_club_item(Fifa17KitIdentity { subtype, ..kit }, item_state::FREE); + assert!(other.get("category").is_none(), "subtype {subtype}"); + assert!(other.get("year").is_none(), "subtype {subtype}"); + } + } } diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index cce637b..532591f 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -2097,6 +2097,8 @@ impl ItemIdentityResolver for Fifa17IdentityResolver { card_asset_id: ident.card_asset_id, subtype: ident.subtype, team_id: ident.team_id, + category: ident.category, + year: ident.year, }) }