From e18304d365509a66d4fb317938b13adc6a0c81ff Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 23 Aug 2026 04:30:19 +0000 Subject: [PATCH] fix(fifa17): serve the full kit identity triple so the selector can match Operator report: selecting a kit in the pre-match selector gives "This kit is currently locked. To unlock and use it, please go to the football club catalogue." Root cause, from static RE of the UNPACKED CardsDLL. CardsDLL registers a kit provider into the FIFA engine (singleton FUN_1800338f0, vtable 0x1801f1d68): slot +0x08 FUN_180033770 enumerate kits for a team slot +0x10 sub_180033430 describe one kit <- the lock gate The describe function decodes a packed kit id into (teamid, year, slot) and compares it against the club's ACTIVE HOME and ACTIVE AWAY triples. On a match it writes NAME/TYPE (and, for historical kits, LOCKED). On no match it writes NOTHING AT ALL and the descriptor falls through to the engine's own default, which is where the locked-catalogue message comes from. The active triple is 100% server-driven. FUN_1801c26d0 -> FUN_1800d73d0 scans club items for cardtype 7 + cardsubtypeid 9 + itemState 101/102, then reads: teamid <- item+0x94 (atom 0x306) we were sending this year <- item+0xba (atom 0x389) WE WERE NOT SENDING THIS slot <- item+0xb8 (category) WE WERE NOT SENDING THIS category 2 -> slot 0 home, 3 -> slot 1 away, 5 -> slot 3 third So we shipped kits carrying only teamid, and the triple could never match. fifa17-recon/data/club_items.json already has category and year per kit resourceId (1482 kits; category {2:740, 3:654, 5:88}; 85 historical years), so this is carried through the catalog rather than invented: Fifa17CardIdentity gains category/year, Fifa17KitIdentity carries them, and shape_club_item emits them for KIT_SUBTYPE only. Badges and stadiums deliberately do not gain the fields - the slot mapping is kit-specific, and sending a family a field its resolver does not read is how this project previously froze the client. Also corrects the Vault note: item+0xba is `year`, not `kittype`. The side comes from itemState 101/102 and the slot from category. Two things this does NOT fix, both client-owned and recorded rather than guessed: - the runtime teamkits clone into FUT club 130000 (the kit ART) is gated on item+0x60 == 4, and +0x60 has no wire atom at all: the deserialiser unconditionally zeroes it, the only CMP against 4 in the whole DLL is 0x1801c34f1, and a live probe measured it as 1 for players / 0 for staff, never 4. The existing lib.rs claim that a server can never produce 4 is CONFIRMED, though its stated reason was a live observation rather than the real one (no wire atom exists). - whether a year==0 kit needs an explicit LOCKED write is UNKNOWN: CardsDLL only writes LOCKED for year != 0, and the engine's default for an untouched descriptor is in Denuvo-packed FIFA17.exe. --- openfut-adapter-fifa17/src/fut/catalog.rs | 25 +++++++ .../src/fut/club_response.rs | 4 + openfut-adapter-fifa17/src/fut/item.rs | 73 +++++++++++++++++++ openfut-utas-host/src/lib.rs | 2 + 4 files changed, 104 insertions(+) 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, }) }