feat(fifa17): project every owned content kind, from one recovered vocabulary
Extends the FIFA17 adapter past players so the wire can carry the rest of a real club's inventory. itemState: the recovered 12-row table at 0x180229cc0 becomes the single source (`fut::item_state`), replacing scattered literals. Every shaper draws from it and the tests assert no shaper can emit a state the client does not know. CARD_SYSTEM.md's 0x180229d20 is the middle of that table, not its start. ContentKind covers all nine tokens. Managers stay inside the staff family for counting, because the client's own club-stats model puts a manager INSIDE the staff total with staffManager as a sub-bucket — a parallel Manager kind would silently under-count. Consumables get their own route (`club/consumables/<category>`) and a stack-wrapper envelope, classified BEFORE the other club/ arms; they are not a `?type=` family. This path previously fell through to Python, so owned inventory was being served by the oracle. The shaper refuses to emit a card it cannot render: no known art id, or a missing `amount`/`contract` for the families that read them, or the subtype-219 rareflag trap that silently turns Player Fitness into Squad Fitness. A dropped card is counted and logged, never faked.
This commit is contained in:
@@ -65,6 +65,8 @@ const S_KITS: i64 = 0x28;
|
||||
const S_KITS_HOME: i64 = 0x29;
|
||||
const S_KITS_AWAY: i64 = 0x2A;
|
||||
const S_BADGES: i64 = 0x2D;
|
||||
const S_STADIA: i64 = 0x14;
|
||||
const S_BALLS: i64 = 0x1E;
|
||||
|
||||
/// First `carddbid` of the AWAY kit family. `fcc_kitcards` is split into a
|
||||
/// `63xxxxx` home family and a `64xxxxx` away family, and the table's own
|
||||
@@ -206,10 +208,10 @@ pub fn club_stats_body(items: &[ClubStatInput], ctx: ContextField) -> Value {
|
||||
g.insert(sid, 0);
|
||||
}
|
||||
let mut staff_total = 0i64;
|
||||
for it in items
|
||||
.iter()
|
||||
.filter(|i| matches!(i.kind, ContentKind::Staff))
|
||||
{
|
||||
// A manager counts INSIDE the staff total (`staffManager` is a bucket within
|
||||
// it), so this selects the whole staff FAMILY, not `ContentKind::Staff`
|
||||
// alone — a `manager`-classified row would otherwise vanish from the panel.
|
||||
for it in items.iter().filter(|i| i.kind.is_staff_family()) {
|
||||
if let Some(sid) = staff_stat(it.subtype) {
|
||||
*g.get_mut(&sid).unwrap() += 1;
|
||||
staff_total += 1;
|
||||
@@ -237,13 +239,23 @@ pub fn club_stats_body(items: &[ClubStatInput], ctx: ContextField) -> Value {
|
||||
}
|
||||
g.insert(S_CONSUMABLES, cons_total);
|
||||
|
||||
// Club items: kits are Core-owned and counted (total plus the home/away
|
||||
// family split); unimplemented families stay honest zeros.
|
||||
for sid in [
|
||||
0x14, 0x1E, 0x2D, 0x2E, 0x2F, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
] {
|
||||
// Club items. THESE COUNTS ARE THE GATE: the client does not ask for a
|
||||
// family's items until club/stats reports a non-zero count for it (proven by
|
||||
// the consumables round, where two rounds of item work sat unrequested
|
||||
// because this panel answered zero). They are plain ints read by the same
|
||||
// getter/publisher shape as the live-proven PLAYERS_EMPLOYED rows, so every
|
||||
// family Core can own is counted here — including the ones whose ITEM record
|
||||
// shape is still withheld, because a count cannot desync a parser and a zero
|
||||
// guarantees the family is never even asked about. Unowned families stay
|
||||
// honest zeros.
|
||||
for sid in [0x2E, 0x2F, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38] {
|
||||
g.entry(sid).or_insert(0);
|
||||
}
|
||||
let count_kind =
|
||||
|want: ContentKind| items.iter().filter(|item| item.kind == want).count() as i64;
|
||||
g.insert(S_STADIA, count_kind(ContentKind::Stadium));
|
||||
g.insert(S_BALLS, count_kind(ContentKind::Ball));
|
||||
g.insert(S_BADGES, count_kind(ContentKind::Badge));
|
||||
let kits: Vec<&ClubStatInput> = items
|
||||
.iter()
|
||||
.filter(|item| matches!(item.kind, ContentKind::Kit))
|
||||
@@ -414,6 +426,59 @@ mod tests {
|
||||
assert_eq!(g["staffManager"], 0);
|
||||
}
|
||||
|
||||
/// A row Core classifies as `manager` must still land in the STAFF bucket and
|
||||
/// in `staffManager`: the client's own model counts a manager inside its staff
|
||||
/// total, and the two encodings (`manager`, or `staff` + subtype 4) are the
|
||||
/// same card.
|
||||
#[test]
|
||||
fn a_manager_counts_inside_staff_under_either_kind_token() {
|
||||
for kind in [ContentKind::Manager, ContentKind::Staff] {
|
||||
let mut manager = staff(4);
|
||||
manager.kind = kind;
|
||||
let g = global(&club_stats_body(&[manager, staff(8)], ContextField::Nation));
|
||||
assert_eq!(g["staffManager"], 1, "kind={}", kind.as_str());
|
||||
assert_eq!(
|
||||
g["staff"],
|
||||
2,
|
||||
"the manager is INSIDE the staff total (kind={})",
|
||||
kind.as_str()
|
||||
);
|
||||
assert_eq!(g["staffFitnessCoach"], 1);
|
||||
assert_eq!(g["players"], 0, "a manager is not a player");
|
||||
}
|
||||
}
|
||||
|
||||
/// The count is the GATE: the client will not ask for a family's items until
|
||||
/// this panel reports a non-zero count for it, so an owned badge/ball/stadium
|
||||
/// must be counted even while its item record is withheld.
|
||||
#[test]
|
||||
fn owned_club_items_are_counted_per_family() {
|
||||
let club_item = |kind: ContentKind, subtype: i64| ClubStatInput {
|
||||
kind,
|
||||
subtype,
|
||||
rating: 0,
|
||||
rare: false,
|
||||
asset_id: 0,
|
||||
nation_id: None,
|
||||
league_id: None,
|
||||
team_id: None,
|
||||
};
|
||||
let items = vec![
|
||||
club_item(ContentKind::Badge, 11),
|
||||
club_item(ContentKind::Badge, 11),
|
||||
club_item(ContentKind::Ball, 30),
|
||||
club_item(ContentKind::Stadium, 10),
|
||||
kit(),
|
||||
];
|
||||
let g = global(&club_stats_body(&items, ContextField::Nation));
|
||||
assert_eq!(g["badges"], 2);
|
||||
assert_eq!(g["balls"], 1);
|
||||
assert_eq!(g["stadia"], 1);
|
||||
assert_eq!(g["kits"], 1);
|
||||
assert_eq!(g["players"], 0, "no club item is ever a player");
|
||||
assert_eq!(g["leagueLogos"], 0, "not an ownable Core kind: honest zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumables_by_family() {
|
||||
// 54 gk_training, 201 player_contract, 217 healing, 258 player_playstyle
|
||||
|
||||
Reference in New Issue
Block a user