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.
This commit is contained in:
@@ -218,18 +218,38 @@ pub fn consumable_needs(family: &str) -> ConsumableNeeds {
|
|||||||
/// `club/stats/consumables` reports a non-zero count — the counter is the gate
|
/// `club/stats/consumables` reports a non-zero count — the counter is the gate
|
||||||
/// and this route is the door).
|
/// and this route is the door).
|
||||||
///
|
///
|
||||||
/// The segment names are the consumable UI group table at `0x180203260` (seven
|
/// The segment names are the client's own CONSUMABLE_TYPE→segment switch,
|
||||||
/// codes: `training`, `contracts`, `fitness`, `healing`, `playStyle`,
|
/// recovered live 2026-08-22 from CardsDLL: the literal table at `0x1801f5a38`
|
||||||
/// `managerLeagueModifier`, `position`); `training` and `contracts` are CONFIRMED
|
/// (under `MyClubAdapterClass`/`CONSUMABLE_TYPE`) and the jump table at
|
||||||
/// on the wire and the singular `contract` is accepted because the client has
|
/// `0x180048820`, which indexes by `enum + 1` through the byte table at
|
||||||
/// used both spellings. Segments are matched lower-cased.
|
/// `0x180048a90`. Nine segments, not seven:
|
||||||
///
|
///
|
||||||
/// The family sets are the `FUN_18013f4d0` categories those codes name, and the
|
/// | enum | segment |
|
||||||
/// correspondence is checkable against the panel: training→42, contracts→13,
|
/// |------|---------|
|
||||||
/// healing→21, fitness→6, position→20, chemistry style→24 items in the oracle's
|
/// | -1 (unset) | `development` |
|
||||||
/// own shelf. NOTE the two formation-modifier families (categories 6 and 7) have
|
/// | 1, 2 | `contracts` |
|
||||||
/// NO group code, so no segment can reach them — that is the client's own gap,
|
/// | 3 | `healing` |
|
||||||
/// not an omission here.
|
/// | 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]> {
|
pub fn consumable_families_for_category(segment: &str) -> Option<&'static [&'static str]> {
|
||||||
Some(match segment {
|
Some(match segment {
|
||||||
"training" => &["gk_training", "player_training"],
|
"training" => &["gk_training", "player_training"],
|
||||||
@@ -239,10 +259,31 @@ pub fn consumable_families_for_category(segment: &str) -> Option<&'static [&'sta
|
|||||||
"position" => &["position_mod"],
|
"position" => &["position_mod"],
|
||||||
"playstyle" => &["player_playstyle", "gk_playstyle"],
|
"playstyle" => &["player_playstyle", "gk_playstyle"],
|
||||||
"managerleaguemodifier" => &["manager_league"],
|
"managerleaguemodifier" => &["manager_league"],
|
||||||
|
"formation" => &["manager_formation_mod", "formation_mod"],
|
||||||
|
"development" => ALL_CONSUMABLE_FAMILIES,
|
||||||
_ => return None,
|
_ => 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
|
/// The club-customisation `cardsubtypeid`s, SETTLED (supersedes
|
||||||
/// `CARD_SYSTEM.md`'s "STILL UNKNOWN, AND NOT GUESSED" section, which is stale).
|
/// `CARD_SYSTEM.md`'s "STILL UNKNOWN, AND NOT GUESSED" section, which is stale).
|
||||||
///
|
///
|
||||||
@@ -430,7 +471,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn consumable_route_categories_partition_the_reachable_families() {
|
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 = [
|
let segments = [
|
||||||
"training",
|
"training",
|
||||||
"contracts",
|
"contracts",
|
||||||
@@ -439,6 +481,7 @@ mod tests {
|
|||||||
"position",
|
"position",
|
||||||
"playstyle",
|
"playstyle",
|
||||||
"managerleaguemodifier",
|
"managerleaguemodifier",
|
||||||
|
"formation",
|
||||||
];
|
];
|
||||||
let mut seen: Vec<&str> = Vec::new();
|
let mut seen: Vec<&str> = Vec::new();
|
||||||
for seg in segments {
|
for seg in segments {
|
||||||
@@ -452,22 +495,27 @@ mod tests {
|
|||||||
consumable_families_for_category("contracts"),
|
consumable_families_for_category("contracts"),
|
||||||
"both spellings the client has used mean the same set"
|
"both spellings the client has used mean the same set"
|
||||||
);
|
);
|
||||||
// Eleven of the thirteen families are reachable; the two formation
|
// All THIRTEEN families are reachable: the client does have a `formation`
|
||||||
// modifiers have no group code in the client's own table.
|
// segment (enum 16), so the two formation modifiers were a server-side
|
||||||
assert_eq!(seen.len(), 11, "no duplicates: {seen:?}");
|
// gap, not the client gap this test used to assert.
|
||||||
for subtype in [51, 61, 91, 201, 202, 211, 219, 220, 250, 269, 300] {
|
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();
|
let (family, _) = consumable_family(subtype).unwrap();
|
||||||
assert!(seen.contains(&family), "no category serves {family}");
|
assert!(seen.contains(&family), "no category serves {family}");
|
||||||
}
|
}
|
||||||
for unreachable in [71, 121] {
|
// `development` is the type-UNSET bucket (index 0 of an `enum + 1` table),
|
||||||
let (family, _) = consumable_family(unreachable).unwrap();
|
// i.e. the unfiltered view. It deliberately overlaps the typed segments,
|
||||||
assert!(
|
// and must stay exactly the union of them so a new family cannot be added
|
||||||
!seen.contains(&family),
|
// to the taxonomy and silently vanish from the unfiltered screen.
|
||||||
"{family} has no group code; claiming it would invent a segment"
|
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).
|
// Not a consumables segment (and NOT a `?type=` token either).
|
||||||
for s in ["", "player", "kit", "Training", "development"] {
|
for s in ["", "player", "kit", "Training"] {
|
||||||
assert!(
|
assert!(
|
||||||
consumable_families_for_category(s).is_none(),
|
consumable_families_for_category(s).is_none(),
|
||||||
"{s:?} is not a consumable category"
|
"{s:?} is not a consumable category"
|
||||||
|
|||||||
Reference in New Issue
Block a user