feat(fifa17): import consumable + staff content as first-class Core content
Close 20 of the 33-record content gap (17 consumables + 3 staff; 13 Legends are unrecoverable from PC data). Verdict A (no Core change): consumables/staff become ordinary Core CardDefinitions (neutral player fields + honest family/role names) and owned instances via the SAME generic import path; a catalog kind lets the adapter exclude them from the player-only /club projection. - adapter fut::content_taxonomy: evidence-based cardsubtypeid->family/label (Ghidra-derived ranges) + staff role map; unknown subtype => defer, never fabricate. - adapter catalog: Fifa17CardIdentity/RawCard gain optional kind+subtype (backward-compat: legacy catalogs load as player); kind_of/subtype_of lookups. - adapter item/club_response: shape_club_response excludes non-player kinds (ShapeStats.excluded_non_player); ItemIdentityResolver::kind_of default=Player. - host Fifa17IdentityResolver overrides kind_of to delegate to the catalog so /club excludes consumables/staff in production. - import: Item gains cardsubtypeid/cardassetid/amount/contract; plan_non_player_definitions (resourceId-grouped, subtype-consistency gated); emit_content writes non-player defs + catalog kind + manifest; apply mints owned instances via owned_item_id. Real profile 33068179: 1962 players + 20 non-player = 1982 owned; 18 non-player defs (16 consumable + 2 staff, dup resourceIds shared); 0 deferred non-player; 0 blockers.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::fut::content_taxonomy::ContentKind;
|
||||
use crate::fut::entities::ReverseEntityResolver;
|
||||
use crate::fut::item::shape_item;
|
||||
// Re-exported so existing `club_response::{…}` callers keep working; the types
|
||||
@@ -25,6 +26,12 @@ pub fn shape_club_response<I: ItemIdentityResolver + ?Sized>(
|
||||
let mut out = Vec::with_capacity(items.len());
|
||||
let mut stats = ShapeStats::default();
|
||||
for item in items {
|
||||
// Exclude non-player content (consumables/staff): a `/club` player list
|
||||
// must never render them as 0-rated players. Counted, never emitted.
|
||||
if ident.kind_of(item) != ContentKind::Player {
|
||||
stats.excluded_non_player += 1;
|
||||
continue;
|
||||
}
|
||||
match ident.resolve(item) {
|
||||
Some(id) => {
|
||||
out.push(shape_item(item, id, ent));
|
||||
@@ -193,4 +200,66 @@ mod tests {
|
||||
"only itemData at top level"
|
||||
);
|
||||
}
|
||||
|
||||
/// A resolver that resolves an asset id for EVERY item (so exclusion is not
|
||||
/// an artifact of a missing asset) but classifies some card_ids as non-player
|
||||
/// via an explicit kind table.
|
||||
struct KindMapIdentity {
|
||||
ids: HashMap<String, Fifa17Identity>,
|
||||
kinds: HashMap<String, ContentKind>,
|
||||
}
|
||||
impl ItemIdentityResolver for KindMapIdentity {
|
||||
fn resolve(&self, it: &CoreOwnedItem) -> Option<Fifa17Identity> {
|
||||
self.ids.get(&it.card_id).copied()
|
||||
}
|
||||
fn kind_of(&self, it: &CoreOwnedItem) -> ContentKind {
|
||||
self.kinds
|
||||
.get(&it.card_id)
|
||||
.copied()
|
||||
.unwrap_or(ContentKind::Player)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumable_and_staff_are_excluded_from_club_players() {
|
||||
let ent = entities();
|
||||
let id = |item_id: u32, asset: u32| Fifa17Identity {
|
||||
item_id,
|
||||
asset_id: asset,
|
||||
resource_id: asset,
|
||||
rareflag: 1,
|
||||
};
|
||||
let ident = KindMapIdentity {
|
||||
ids: HashMap::from([
|
||||
("card_player".to_string(), id(100000001, 20801)),
|
||||
("card_consumable".to_string(), id(100000002, 5003012)),
|
||||
("card_staff".to_string(), id(100000003, 3000083)),
|
||||
]),
|
||||
kinds: HashMap::from([
|
||||
("card_consumable".to_string(), ContentKind::Consumable),
|
||||
("card_staff".to_string(), ContentKind::Staff),
|
||||
]),
|
||||
};
|
||||
let items = vec![
|
||||
item(
|
||||
"oc1",
|
||||
"card_player",
|
||||
86,
|
||||
"ST",
|
||||
"Argentina",
|
||||
"Premier League",
|
||||
"Chelsea",
|
||||
),
|
||||
item("oc2", "card_consumable", 0, "", "", "", ""),
|
||||
item("oc3", "card_staff", 0, "", "", "", ""),
|
||||
];
|
||||
let (body, stats) = shape_club_response(&items, &ent, &ident);
|
||||
assert_eq!(stats.emitted, 1, "only the player is emitted");
|
||||
assert_eq!(stats.excluded_non_player, 2, "consumable + staff excluded");
|
||||
assert_eq!(stats.dropped_no_asset, 0);
|
||||
let arr = body["itemData"].as_array().unwrap();
|
||||
assert_eq!(arr.len(), 1);
|
||||
assert_eq!(arr[0]["id"], 100000001, "the player survives");
|
||||
assert_eq!(arr[0]["itemType"], "player");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user