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:
@@ -35,11 +35,14 @@ use std::collections::HashMap;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::fut::club_response::ActiveKitAssignments;
|
||||
use crate::fut::contract_cards::PACK_FRESH_CONTRACT_MATCHES;
|
||||
use crate::fut::entities::ReverseEntityResolver;
|
||||
use crate::fut::item::{
|
||||
shape_item, shape_staff_item, CoreOwnedItem, ItemIdentityResolver, STAFF_CONTRACT,
|
||||
shape_club_item, shape_item, shape_staff_item, CoreOwnedItem, ItemIdentityResolver,
|
||||
STAFF_CONTRACT,
|
||||
};
|
||||
use crate::fut::item_state;
|
||||
use crate::fut::squad::FIFA17_SQUAD_SLOTS;
|
||||
use crate::fut::squad_ext::Fifa17SquadExtensionV1;
|
||||
|
||||
@@ -235,15 +238,75 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
||||
Ok(SquadProjection::Projected(squad))
|
||||
}
|
||||
|
||||
/// The active club items for `squad.actives`, in slot order.
|
||||
///
|
||||
/// ## Why this exists, and why it is NOT `[]`
|
||||
///
|
||||
/// `actives` is the ONLY carrier that makes a club item resident in FIFA 17.
|
||||
/// The squad parser's arm for atom 11 (`actives`) 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:
|
||||
///
|
||||
/// ```text
|
||||
/// cmp edi,0x5 ; at most five entries are read
|
||||
/// jge <skip>
|
||||
/// mov rax,QWORD PTR [r13+0x108] ; the club-item array
|
||||
/// lea rcx,[rax+rcx*8] ; &array[edi] (edi * 24)
|
||||
/// call 0x18013fe00 ; the item deserializer, writing that slot
|
||||
/// ```
|
||||
///
|
||||
/// That deserializer inserts the record into the client's resident item map
|
||||
/// (keyed by wire instance id, and its only gate is a non-zero id) and binds the
|
||||
/// slot handle to it. So each element must be a FULL item object, exactly 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 when
|
||||
/// it misses.
|
||||
///
|
||||
/// An empty array makes the client read the array-end token immediately and
|
||||
/// parse nothing, which leaves all five slots null. Every later consumer then
|
||||
/// resolves to the client's static not-found sentinel, whose item pointer is
|
||||
/// NULL — which is exactly why the pre-match kit selector had no kits.
|
||||
///
|
||||
/// Elements are shaped by the shared [`shape_club_item`], the same primitive
|
||||
/// `/club?type=kit` uses, so the two routes cannot drift. Ordering is positional
|
||||
/// on the wire but not semantic: both client consumers (the activate path and
|
||||
/// the store lookup) search the five slots by content — itemState, or
|
||||
/// cardtype/cardsubtypeid — never by index.
|
||||
///
|
||||
/// A designated kit whose owned row or FIFA kit identity cannot be resolved is
|
||||
/// omitted rather than emitted with a fabricated id, matching `/club`.
|
||||
pub fn squad_actives<I: ItemIdentityResolver + ?Sized>(
|
||||
owned: &HashMap<String, CoreOwnedItem>,
|
||||
ident: &I,
|
||||
active_kits: ActiveKitAssignments<'_>,
|
||||
) -> Value {
|
||||
let mut out = Vec::new();
|
||||
for (owned_card_id, state) in [
|
||||
(active_kits.home, item_state::ACTIVE_HOME_KIT),
|
||||
(active_kits.away, item_state::ACTIVE_AWAY_KIT),
|
||||
] {
|
||||
let Some(owned_card_id) = owned_card_id else {
|
||||
continue;
|
||||
};
|
||||
let Some(item) = owned.get(owned_card_id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(id) = ident.resolve_kit(item) {
|
||||
out.push(shape_club_item(id, state));
|
||||
}
|
||||
}
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
/// Wrap a projected squad object into the `userMassInfo.squad` shape, injecting
|
||||
/// the session-envelope fields the projector does not own (`personaId`, plus the
|
||||
/// observed constants `changed: 0`, `actives: []`).
|
||||
pub fn user_mass_info_squad(projected: Value, persona_id: i64) -> Value {
|
||||
/// the session-envelope fields the projector does not own: `personaId`, the
|
||||
/// observed constant `changed: 0`, and `actives` from [`squad_actives`].
|
||||
pub fn user_mass_info_squad(projected: Value, persona_id: i64, actives: Value) -> Value {
|
||||
let mut obj = projected;
|
||||
if let Value::Object(map) = &mut obj {
|
||||
map.insert("personaId".into(), json!(persona_id));
|
||||
map.insert("changed".into(), json!(0));
|
||||
map.insert("actives".into(), json!([]));
|
||||
map.insert("actives".into(), actives);
|
||||
}
|
||||
obj
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user