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:
@@ -12,11 +12,12 @@ use serde_json::{json, Value};
|
||||
use crate::fut::content_taxonomy::ContentKind;
|
||||
use crate::fut::entities::ReverseEntityResolver;
|
||||
use crate::fut::item::{shape_item, shape_kit_item, shape_staff_item, STAFF_CONTRACT};
|
||||
use crate::fut::item_state;
|
||||
// Re-exported so existing `club_response::{…}` callers keep working; the types
|
||||
// are now defined once in `fut::item`.
|
||||
pub use crate::fut::item::{
|
||||
CoreOwnedItem, Fifa17Identity, Fifa17KitIdentity, Fifa17StaffIdentity, ItemIdentityResolver,
|
||||
ShapeStats,
|
||||
CoreOwnedItem, Fifa17ConsumableIdentity, Fifa17Identity, Fifa17KitIdentity,
|
||||
Fifa17StaffIdentity, ItemIdentityResolver, ShapeStats,
|
||||
};
|
||||
|
||||
/// Active club-level kit roles, keyed by Core owned-instance id.
|
||||
@@ -36,7 +37,11 @@ pub fn shape_club_response<I: ItemIdentityResolver + ?Sized>(
|
||||
}
|
||||
|
||||
/// Shape `/club` items, including ownership-backed active kit designations.
|
||||
/// Consumables/staff remain excluded because they use separate wire envelopes.
|
||||
///
|
||||
/// This envelope carries the two families whose record shape it can carry:
|
||||
/// players and kits, plus the staff family (manager + the four coach families).
|
||||
/// Consumables have their own route and their own STACK envelope, and the
|
||||
/// club-customisation families are counted and withheld — see each arm.
|
||||
pub fn shape_club_response_with_kits<I: ItemIdentityResolver + ?Sized>(
|
||||
items: &[CoreOwnedItem],
|
||||
ent: &impl ReverseEntityResolver,
|
||||
@@ -56,28 +61,50 @@ pub fn shape_club_response_with_kits<I: ItemIdentityResolver + ?Sized>(
|
||||
},
|
||||
ContentKind::Kit => match ident.resolve_kit(item) {
|
||||
Some(id) => {
|
||||
let item_state = if active_kits.home == Some(item.owned_card_id.as_str()) {
|
||||
"activeHomeKit"
|
||||
let state = if active_kits.home == Some(item.owned_card_id.as_str()) {
|
||||
item_state::ACTIVE_HOME_KIT
|
||||
} else if active_kits.away == Some(item.owned_card_id.as_str()) {
|
||||
"activeAwayKit"
|
||||
item_state::ACTIVE_AWAY_KIT
|
||||
} else {
|
||||
"free"
|
||||
item_state::FREE
|
||||
};
|
||||
out.push(shape_kit_item(id, item_state));
|
||||
out.push(shape_kit_item(id, state));
|
||||
stats.emitted += 1;
|
||||
}
|
||||
None => stats.dropped_no_asset += 1,
|
||||
},
|
||||
ContentKind::Staff => match ident.resolve_staff(item) {
|
||||
// A manager is a staff card: both Core kinds resolve through the one
|
||||
// staff record shape, discriminated on the wire by `cardsubtypeid`
|
||||
// (the same set as `ContentKind::is_staff_family`, spelled out here
|
||||
// because a guard arm would not prove exhaustiveness).
|
||||
ContentKind::Manager | ContentKind::Staff => match ident.resolve_staff(item) {
|
||||
Some(id) => {
|
||||
out.push(shape_staff_item(id, STAFF_CONTRACT));
|
||||
stats.emitted += 1;
|
||||
}
|
||||
None => stats.dropped_no_asset += 1,
|
||||
},
|
||||
// Consumables have their OWN route and their own envelope:
|
||||
// `GET club/consumables/<category>`, whose element is a stack
|
||||
// wrapper, not an item (see [`crate::fut::consumables`]). A bare
|
||||
// consumable item in THIS envelope is accepted by the client and
|
||||
// silently discarded, so emitting one here would be a 200 that does
|
||||
// nothing — the worst failure shape in this project. Counted.
|
||||
ContentKind::Consumable => {
|
||||
stats.excluded_non_player += 1;
|
||||
}
|
||||
// Club customisation. The SUBTYPES are settled (kit 9, stadium 10,
|
||||
// badge 11, ball 30, league logo 31), but the RECORD is not: a
|
||||
// badge/stadium still needs the narrow `teamid`/`assetId` test that
|
||||
// the 2026-08-05 crash denied us, and a ball has no display name at
|
||||
// all except `localizedName`, which this project's own rule scores as
|
||||
// "the parser reads it" and NOT "sending it is safe". Counted and
|
||||
// withheld rather than guessed — ownership stays authoritative in
|
||||
// Core either way, and club/stats still counts these families so the
|
||||
// screen's own numbers are right.
|
||||
ContentKind::Badge | ContentKind::Ball | ContentKind::Stadium | ContentKind::Misc => {
|
||||
stats.excluded_non_player += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
(json!({ "itemData": out }), stats)
|
||||
|
||||
Reference in New Issue
Block a user