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:
@@ -54,6 +54,22 @@ pub struct Fifa17CardIdentity {
|
||||
/// `manager.teamid` → `leagueteamlinks.leagueid`, because `managercards` has
|
||||
/// no league column. Lands in the equally untouched slot `rec+0xe0`.
|
||||
pub league_id: i64,
|
||||
/// EA's authored `rating` for a NON-PLAYER definition (`fcc_*.rating`), which
|
||||
/// Core does not model: an imported consumable's Core `overall` is 0, while
|
||||
/// the client's own copies carry 55..95 and the value drives the card level
|
||||
/// (`rec+0x54`) and therefore its quick-sell price. `None` → the caller falls
|
||||
/// back to Core's rating, which stays authoritative for players.
|
||||
pub rating: Option<u8>,
|
||||
/// `amount` (atom 0x1b) for a consumable definition — the bonus magnitude EA
|
||||
/// authored in the `fcc_*` row (+5 / +10 / +15 …). MANDATORY for the
|
||||
/// training, healing, fitness, play-style and manager-league families:
|
||||
/// omitting the key draws "-1" on the card, not "0".
|
||||
pub amount: Option<i64>,
|
||||
/// `contract` (atom 0xb8) for a contract-card definition (`cardsubtypeid`
|
||||
/// 201/202) — the number of matches the card grants. `fcc_contractcards` has
|
||||
/// no amount column, so this value comes from observed data; it is never
|
||||
/// defaulted here.
|
||||
pub contract: Option<i64>,
|
||||
}
|
||||
|
||||
/// The FIFA 17 numeric namespace policy for owned-item wire ids.
|
||||
@@ -182,6 +198,15 @@ struct RawCard {
|
||||
/// Manager chemistry league; absent → `0`.
|
||||
#[serde(default)]
|
||||
league_id: Option<i64>,
|
||||
/// EA-authored rating for a non-player definition; absent → Core's rating.
|
||||
#[serde(default)]
|
||||
rating: Option<u8>,
|
||||
/// Consumable bonus magnitude (atom 0x1b); absent → key omitted.
|
||||
#[serde(default)]
|
||||
amount: Option<i64>,
|
||||
/// Contract-card grant (atom 0xb8); absent → key omitted.
|
||||
#[serde(default)]
|
||||
contract: Option<i64>,
|
||||
}
|
||||
|
||||
fn default_rareflag() -> i64 {
|
||||
@@ -242,6 +267,9 @@ impl Fifa17CardCatalog {
|
||||
team_id: rc.team_id.unwrap_or(0),
|
||||
nation: rc.nation.unwrap_or(0),
|
||||
league_id: rc.league_id.unwrap_or(0),
|
||||
rating: rc.rating,
|
||||
amount: rc.amount,
|
||||
contract: rc.contract,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -327,6 +355,52 @@ mod tests {
|
||||
assert_eq!(cat.lookup("card_missing"), None);
|
||||
}
|
||||
|
||||
/// The non-player definition fields a consumable needs, and the ABSENCE that
|
||||
/// must stay an absence: a defaulted `amount` would draw "-1" on the card and
|
||||
/// a defaulted `contract` would invent the number of matches a card grants.
|
||||
#[test]
|
||||
fn consumable_definition_fields_are_carried_and_never_defaulted() {
|
||||
let cat = Fifa17CardCatalog::from_json_str(
|
||||
r#"{"schema_version":1,"game":"fifa17","cards":{
|
||||
"fifa17_5003012":{"asset_id":5003012,"kind":"consumable","subtype":54,
|
||||
"card_asset_id":3,"rareflag":0,"rating":85,"amount":15},
|
||||
"fifa17_5001004":{"asset_id":5001004,"kind":"consumable","subtype":201,
|
||||
"card_asset_id":7,"rareflag":0,"rating":60,"contract":7},
|
||||
"fifa17_5003059":{"asset_id":5003059,"kind":"consumable","subtype":91,
|
||||
"card_asset_id":34,"rareflag":0,"rating":95},
|
||||
"fifa17_20801":{"asset_id":20801}
|
||||
}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
// A training card: art id 3 (NOT the carddbid), EA's rating, amount 15.
|
||||
let training = cat.lookup("fifa17_5003012").unwrap();
|
||||
assert_eq!(training.kind, ContentKind::Consumable);
|
||||
assert_eq!(training.subtype, 54);
|
||||
assert_eq!(training.card_asset_id, 3);
|
||||
assert_eq!(training.rating, Some(85));
|
||||
assert_eq!(training.amount, Some(15));
|
||||
assert_eq!(training.contract, None);
|
||||
// A contract card takes its number from `contract`, not `amount`.
|
||||
let contract = cat.lookup("fifa17_5001004").unwrap();
|
||||
assert_eq!(contract.contract, Some(7));
|
||||
assert_eq!(contract.amount, None);
|
||||
// A position modifier needs neither.
|
||||
let position = cat.lookup("fifa17_5003059").unwrap();
|
||||
assert_eq!(position.amount, None);
|
||||
assert_eq!(position.contract, None);
|
||||
assert_eq!(position.card_asset_id, 34);
|
||||
// A player carries none of them and keeps Core's authoritative rating.
|
||||
let player = cat.lookup("fifa17_20801").unwrap();
|
||||
assert_eq!(player.kind, ContentKind::Player);
|
||||
assert_eq!(player.rating, None);
|
||||
assert_eq!(player.amount, None);
|
||||
assert_eq!(player.contract, None);
|
||||
assert_eq!(
|
||||
player.card_asset_id, player.asset_id,
|
||||
"a player's card art IS its asset id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_cards_same_resource_is_a_conflict() {
|
||||
let err = Fifa17CardCatalog::from_json_str(
|
||||
|
||||
Reference in New Issue
Block a user