From 6ca735749e56597904ff6829584335aebe1b8b69 Mon Sep 17 00:00:00 2001 From: funman300 Date: Sat, 22 Aug 2026 00:49:04 +0000 Subject: [PATCH] fix(fifa17): serve the development and formation consumable categories The live client asked for `club/consumables/development` and got an empty screen: `consumable_families_for_category` had no arm for it. Tracing that segment recovered the client's OWN category vocabulary, and it is nine segments, not the seven this file assumed. CardsDLL, live 2026-08-22: the literal table at 0x1801f5a38 (under MyClubAdapterClass / CONSUMABLE_TYPE) and the switch at 0x180048820, which indexes by `enum + 1` through the byte table at 0x180048a90 into the case table at 0x180048a6c: enum -1 (unset) -> development enum 1, 2 -> contracts enum 3 -> healing enum 4 -> fitness enum 16 -> formation enum 17 -> position enum 23 -> playStyle enum 24 -> managerLeagueModifier enum 0, 5..15, 18..22 -> training (switch default) Two consequences: 1. `formation` HAS a segment (enum 16). This file claimed the two formation modifier families "have NO group code, so no segment can reach them -- that is the client's own gap, not an omission here", and a test asserted it. Both were wrong, and wrong in the direction that hides a server bug: it was our gap. `formation` now maps to manager_formation_mod + formation_mod, so all THIRTEEN families are reachable instead of eleven. 2. `development` is the type-UNSET bucket -- index 0 of a table indexed by `enum + 1` -- i.e. no type filter. It is therefore the unfiltered view and maps to every family via ALL_CONSUMABLE_FAMILIES. That is consistent rather than overlapping by accident: the eight TYPED segments already reach all thirteen families exactly once, so there is no family for `development` to own privately. The partition test now asserts the eight typed segments cover all thirteen families with no duplicates, and that `development` is exactly their union, so a family added to the taxonomy cannot silently vanish from the unfiltered screen. Ownership and classification are untouched; this is projection only. 248 adapter tests, clippy and fmt clean. --- .../src/fut/content_taxonomy.rs | 96 ++++++++++++++----- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/openfut-adapter-fifa17/src/fut/content_taxonomy.rs b/openfut-adapter-fifa17/src/fut/content_taxonomy.rs index 367bb51..13d6506 100644 --- a/openfut-adapter-fifa17/src/fut/content_taxonomy.rs +++ b/openfut-adapter-fifa17/src/fut/content_taxonomy.rs @@ -218,18 +218,38 @@ pub fn consumable_needs(family: &str) -> ConsumableNeeds { /// `club/stats/consumables` reports a non-zero count — the counter is the gate /// and this route is the door). /// -/// The segment names are the consumable UI group table at `0x180203260` (seven -/// codes: `training`, `contracts`, `fitness`, `healing`, `playStyle`, -/// `managerLeagueModifier`, `position`); `training` and `contracts` are CONFIRMED -/// on the wire and the singular `contract` is accepted because the client has -/// used both spellings. Segments are matched lower-cased. +/// The segment names are the client's own CONSUMABLE_TYPE→segment switch, +/// recovered live 2026-08-22 from CardsDLL: the literal table at `0x1801f5a38` +/// (under `MyClubAdapterClass`/`CONSUMABLE_TYPE`) and the jump table at +/// `0x180048820`, which indexes by `enum + 1` through the byte table at +/// `0x180048a90`. Nine segments, not seven: /// -/// The family sets are the `FUN_18013f4d0` categories those codes name, and the -/// correspondence is checkable against the panel: training→42, contracts→13, -/// healing→21, fitness→6, position→20, chemistry style→24 items in the oracle's -/// own shelf. NOTE the two formation-modifier families (categories 6 and 7) have -/// NO group code, so no segment can reach them — that is the client's own gap, -/// not an omission here. +/// | enum | segment | +/// |------|---------| +/// | -1 (unset) | `development` | +/// | 1, 2 | `contracts` | +/// | 3 | `healing` | +/// | 4 | `fitness` | +/// | 16 | `formation` | +/// | 17 | `position` | +/// | 23 | `playStyle` | +/// | 24 | `managerLeagueModifier` | +/// | 0, 5..15, 18..22 | `training` (the switch default) | +/// +/// This CORRECTS the previous note here, which read the seven-code UI group +/// table at `0x180203260` and concluded the two formation-modifier families +/// "have NO group code, so no segment can reach them — the client's own gap". +/// The client does have a `formation` segment (enum 16), and it asked for +/// `development` live, so both were server-side gaps, not client ones. +/// +/// `development` is the **type-unset** bucket: index 0 of a table indexed by +/// `enum + 1`, i.e. no type filter was set. It is therefore the unfiltered view +/// and maps to every family — which is consistent, since the eight TYPED +/// segments already reach all thirteen families exactly once. +/// +/// `training` and `contracts` are CONFIRMED on the wire, `development` was +/// observed live, and the singular `contract` is accepted because the client has +/// used both spellings. Segments are matched lower-cased. pub fn consumable_families_for_category(segment: &str) -> Option<&'static [&'static str]> { Some(match segment { "training" => &["gk_training", "player_training"], @@ -239,10 +259,31 @@ pub fn consumable_families_for_category(segment: &str) -> Option<&'static [&'sta "position" => &["position_mod"], "playstyle" => &["player_playstyle", "gk_playstyle"], "managerleaguemodifier" => &["manager_league"], + "formation" => &["manager_formation_mod", "formation_mod"], + "development" => ALL_CONSUMABLE_FAMILIES, _ => return None, }) } +/// Every consumable family, i.e. the `development` (type-unset) view. Kept as one +/// list so a new family cannot be added to the taxonomy and silently omitted from +/// the unfiltered screen. +pub const ALL_CONSUMABLE_FAMILIES: &[&str] = &[ + "gk_training", + "player_training", + "player_contract", + "manager_contract", + "player_fitness", + "squad_fitness", + "healing", + "position_mod", + "player_playstyle", + "gk_playstyle", + "manager_league", + "manager_formation_mod", + "formation_mod", +]; + /// The club-customisation `cardsubtypeid`s, SETTLED (supersedes /// `CARD_SYSTEM.md`'s "STILL UNKNOWN, AND NOT GUESSED" section, which is stale). /// @@ -430,7 +471,8 @@ mod tests { #[test] fn consumable_route_categories_partition_the_reachable_families() { - // The seven group codes, plus the singular `contract` spelling. + // The eight TYPED segments of the client's own switch (enum 1,2,3,4,16, + // 17,23,24 plus the default), and the singular `contract` spelling. let segments = [ "training", "contracts", @@ -439,6 +481,7 @@ mod tests { "position", "playstyle", "managerleaguemodifier", + "formation", ]; let mut seen: Vec<&str> = Vec::new(); for seg in segments { @@ -452,22 +495,27 @@ mod tests { consumable_families_for_category("contracts"), "both spellings the client has used mean the same set" ); - // Eleven of the thirteen families are reachable; the two formation - // modifiers have no group code in the client's own table. - assert_eq!(seen.len(), 11, "no duplicates: {seen:?}"); - for subtype in [51, 61, 91, 201, 202, 211, 219, 220, 250, 269, 300] { + // All THIRTEEN families are reachable: the client does have a `formation` + // segment (enum 16), so the two formation modifiers were a server-side + // gap, not the client gap this test used to assert. + assert_eq!(seen.len(), 13, "no duplicates: {seen:?}"); + for subtype in [51, 61, 71, 91, 121, 201, 202, 211, 219, 220, 250, 269, 300] { let (family, _) = consumable_family(subtype).unwrap(); assert!(seen.contains(&family), "no category serves {family}"); } - for unreachable in [71, 121] { - let (family, _) = consumable_family(unreachable).unwrap(); - assert!( - !seen.contains(&family), - "{family} has no group code; claiming it would invent a segment" - ); - } + // `development` is the type-UNSET bucket (index 0 of an `enum + 1` table), + // i.e. the unfiltered view. It deliberately overlaps the typed segments, + // and must stay exactly the union of them so a new family cannot be added + // to the taxonomy and silently vanish from the unfiltered screen. + let mut dev = consumable_families_for_category("development") + .unwrap() + .to_vec(); + dev.sort_unstable(); + let mut all = seen.clone(); + all.sort_unstable(); + assert_eq!(dev, all, "development must be exactly the unfiltered set"); // Not a consumables segment (and NOT a `?type=` token either). - for s in ["", "player", "kit", "Training", "development"] { + for s in ["", "player", "kit", "Training"] { assert!( consumable_families_for_category(s).is_none(), "{s:?} is not a consumable category"