fix(fifa17): project active club items through squad.actives
FIFA 17 makes a club item resident ONLY through squad.actives. The squad
parser's arm for atom 11 computes the address of the i-th element of the
client's five-element club-item array and hands it to the item
deserializer as the out-handle:
cmp edi,0x5 ; at most five entries are read
mov rax,QWORD PTR [r13+0x108] ; the club-item array
lea rcx,[rax+rcx*8] ; &array[edi]
call 0x18013fe00 ; item deserializer, writing that slot
That deserializer inserts the record into the client's resident item map
- keyed by wire instance id, gated only on the id being non-zero - and
binds the slot handle to it. So each element must be a full item object
like squad.manager[].itemData; an id reference alone installs nothing,
because the manager installer looks its id up in that same map and does
nothing on a miss.
We emitted actives: [] as an 'observed constant', which was circular: it
came from our own captures and the Python oracle seeded it. The client
then read the array-end token immediately, parsed nothing, and left all
five slots null, so every lookup resolved to the static not-found
sentinel whose item pointer is NULL. That is why the pre-match kit
selector had no kits, and it is also why /club?type=kit could never fix
it: no /club response feeds that array.
Core already owns the designations via /club/active-items, so the host
reuses get_active_kits() and the adapter shapes each entry with the same
shape_club_item primitive /club?type=kit uses, keeping one wire dialect.
A Core transport error yields no actives and is reported rather than
silently empty. userInfo.actives already mirrors the squad's.
Verified against the client's own fcc_kitcards table: 6300006 is team
21's home card (category 2) and 6400003 the away card (category 3).
This commit is contained in:
@@ -24,16 +24,18 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use openfut_adapter_fifa17::fut::club_response::ActiveKitAssignments;
|
||||
use openfut_adapter_fifa17::fut::item::{
|
||||
CoreOwnedItem, Fifa17Identity, Fifa17StaffIdentity, ItemIdentityResolver, STAFF_CONTRACT,
|
||||
CoreOwnedItem, Fifa17Identity, Fifa17KitIdentity, Fifa17StaffIdentity, ItemIdentityResolver,
|
||||
STAFF_CONTRACT,
|
||||
};
|
||||
use openfut_adapter_fifa17::fut::squad::{parse_squad_put, Fifa17SquadPut, SquadWireResolver};
|
||||
use openfut_adapter_fifa17::fut::squad_ext::{build_squad_write, SquadWriteBuild};
|
||||
use openfut_adapter_fifa17::fut::squad_projection::{
|
||||
project_squad, squad_list, user_mass_info_squad, ProjectionSlot, SquadExtInput,
|
||||
project_squad, squad_actives, squad_list, user_mass_info_squad, ProjectionSlot, SquadExtInput,
|
||||
SquadProjection, SquadProjectionInput,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const PUT_BASELINE: &str = include_str!("../fixtures/utas/squad_put_f442.json");
|
||||
const PUT_SWAP: &str = include_str!("../fixtures/utas/squad_put_swap_f442.json");
|
||||
@@ -503,11 +505,17 @@ fn one_projector_serves_every_endpoint_no_divergence() {
|
||||
&ident,
|
||||
);
|
||||
|
||||
// userMassInfo.squad = the projected object + session envelope.
|
||||
let ummi = user_mass_info_squad(projected.clone(), 33068179);
|
||||
// userMassInfo.squad = the projected object + session envelope. `actives` is
|
||||
// supplied by the caller now, so the envelope must carry it through verbatim
|
||||
// rather than hardcoding an empty array.
|
||||
let actives = json!([{ "id": 100004874, "itemState": "activeHomeKit" }]);
|
||||
let ummi = user_mass_info_squad(projected.clone(), 33068179, actives.clone());
|
||||
assert_eq!(ummi["personaId"], 33068179);
|
||||
assert_eq!(ummi["changed"], 0);
|
||||
assert!(ummi["actives"].is_array());
|
||||
assert_eq!(
|
||||
ummi["actives"], actives,
|
||||
"the envelope must pass actives through, not replace it"
|
||||
);
|
||||
assert_eq!(ummi["players"], projected["players"], "same projected body");
|
||||
assert_eq!(ummi["formation"], projected["formation"]);
|
||||
|
||||
@@ -530,3 +538,153 @@ fn one_projector_serves_every_endpoint_no_divergence() {
|
||||
// The summary carries only those six keys — no divergent squad shape.
|
||||
assert_eq!(entry.as_object().unwrap().len(), 6);
|
||||
}
|
||||
|
||||
// ---- squad.actives: the only carrier that makes a club item resident --------
|
||||
|
||||
/// A resolver that can answer `resolve_kit`, which the default trait method
|
||||
/// cannot (it returns `None` for player-only resolvers).
|
||||
struct KitIdentity(HashMap<String, Fifa17KitIdentity>);
|
||||
impl ItemIdentityResolver for KitIdentity {
|
||||
fn resolve(&self, _it: &CoreOwnedItem) -> Option<Fifa17Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_kit(&self, it: &CoreOwnedItem) -> Option<Fifa17KitIdentity> {
|
||||
self.0.get(&it.owned_card_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
fn kit_owned(owned_card_id: &str) -> CoreOwnedItem {
|
||||
CoreOwnedItem {
|
||||
owned_card_id: owned_card_id.to_string(),
|
||||
card_id: format!("def-{owned_card_id}"),
|
||||
rating: 0,
|
||||
position: String::new(),
|
||||
nation: String::new(),
|
||||
league: String::new(),
|
||||
club: String::new(),
|
||||
attributes: [0; 6],
|
||||
contract_matches: None,
|
||||
source_rating: None,
|
||||
core_content_kind: Some("kit".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn home_away_fixture() -> (HashMap<String, CoreOwnedItem>, KitIdentity) {
|
||||
let owned = HashMap::from([
|
||||
("oc-home".to_string(), kit_owned("oc-home")),
|
||||
("oc-away".to_string(), kit_owned("oc-away")),
|
||||
]);
|
||||
// Real `fcc_kitcards` rows for team 21: 6300006 is the home card (category 2,
|
||||
// assetid 14) and 6400003 the away card (category 3, assetid 15).
|
||||
let ident = KitIdentity(HashMap::from([
|
||||
(
|
||||
"oc-home".to_string(),
|
||||
Fifa17KitIdentity {
|
||||
item_id: 100004874,
|
||||
asset_id: 14,
|
||||
resource_id: 6300006,
|
||||
card_asset_id: 35,
|
||||
subtype: 9,
|
||||
team_id: 21,
|
||||
category: 2,
|
||||
year: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
"oc-away".to_string(),
|
||||
Fifa17KitIdentity {
|
||||
item_id: 100004873,
|
||||
asset_id: 15,
|
||||
resource_id: 6400003,
|
||||
card_asset_id: 35,
|
||||
subtype: 9,
|
||||
team_id: 21,
|
||||
category: 3,
|
||||
year: 0,
|
||||
},
|
||||
),
|
||||
]));
|
||||
(owned, ident)
|
||||
}
|
||||
|
||||
/// The client's squad parser reads at most five `actives` entries and parses each
|
||||
/// one straight into a slot of its five-element club-item array, so each element
|
||||
/// must be a full item object carrying a non-zero `id` — an id reference alone
|
||||
/// installs nothing.
|
||||
#[test]
|
||||
fn squad_actives_emits_full_items_for_the_designated_kits() {
|
||||
let (owned, ident) = home_away_fixture();
|
||||
let actives = squad_actives(
|
||||
&owned,
|
||||
&ident,
|
||||
ActiveKitAssignments {
|
||||
home: Some("oc-home"),
|
||||
away: Some("oc-away"),
|
||||
},
|
||||
);
|
||||
let arr = actives.as_array().expect("actives is an array");
|
||||
assert_eq!(arr.len(), 2, "one entry per designated kit");
|
||||
|
||||
assert_eq!(arr[0]["id"], 100004874);
|
||||
assert_eq!(arr[0]["itemState"], "activeHomeKit");
|
||||
assert_eq!(arr[0]["resourceId"], 6300006);
|
||||
assert_eq!(arr[1]["id"], 100004873);
|
||||
assert_eq!(arr[1]["itemState"], "activeAwayKit");
|
||||
assert_eq!(arr[1]["resourceId"], 6400003);
|
||||
|
||||
for entry in arr {
|
||||
assert_eq!(entry["itemType"], "kit");
|
||||
assert_eq!(
|
||||
entry["cardsubtypeid"], 9,
|
||||
"cardsubtypeid 9 derives cardtype 7"
|
||||
);
|
||||
assert_eq!(entry["teamid"], 21, "the clone path keys kit art on teamid");
|
||||
assert_ne!(entry["id"], 0, "a zero id is never made resident");
|
||||
}
|
||||
}
|
||||
|
||||
/// Undesignated slots contribute nothing, and an unresolvable designation is
|
||||
/// omitted rather than emitted with a fabricated id — the same policy `/club`
|
||||
/// applies when a card has no FIFA identity.
|
||||
#[test]
|
||||
fn squad_actives_omits_absent_and_unresolvable_designations() {
|
||||
let (owned, ident) = home_away_fixture();
|
||||
|
||||
let home_only = squad_actives(
|
||||
&owned,
|
||||
&ident,
|
||||
ActiveKitAssignments {
|
||||
home: Some("oc-home"),
|
||||
away: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(home_only.as_array().unwrap().len(), 1);
|
||||
assert_eq!(home_only[0]["itemState"], "activeHomeKit");
|
||||
|
||||
// Designated but not present in the owned collection.
|
||||
let dangling = squad_actives(
|
||||
&owned,
|
||||
&ident,
|
||||
ActiveKitAssignments {
|
||||
home: Some("oc-missing"),
|
||||
away: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(dangling.as_array().unwrap().len(), 0);
|
||||
|
||||
// Present and designated, but with no resolvable FIFA kit identity.
|
||||
let unresolvable = squad_actives(
|
||||
&HashMap::from([("oc-x".to_string(), kit_owned("oc-x"))]),
|
||||
&ident,
|
||||
ActiveKitAssignments {
|
||||
home: Some("oc-x"),
|
||||
away: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(unresolvable.as_array().unwrap().len(), 0);
|
||||
|
||||
// Nothing designated at all is an empty array, which is what left every
|
||||
// club-item slot null before this projector existed.
|
||||
let none = squad_actives(&owned, &ident, ActiveKitAssignments::default());
|
||||
assert_eq!(none.as_array().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user