fix(fifa17): squad.manager elements are bare item objects, not itemData wrappers
An owned manager assigned in Core was present everywhere on the server -- in
/club/manager, in club?type=staff, and in userMassInfo -- but the squad UI
showed no manager after a cold client load.
The squad parser FUN_18013d1f0 reaches the item parser FUN_18013fe00 by two
different routes:
players: atom 568 -> per-element atoms 355 `index`, 363 `itemData`,
378 `kitNumber`; the 363 arm at 0x18013d8d9 calls the item parser
on the NESTED itemData object.
manager: atom 424 -> array loop at 0x18013da29 calls that same item parser
DIRECTLY on the array ELEMENT, into squad+0xC0. No `itemData` step.
So a manager element IS an item. We were nesting the fields one level deeper,
so the parser read only the two keys that happen to be item atoms -- `id` and
`dream` -- and left everything else at its default. Measured on a cold client,
the manager record existed at squad+0xC0 with the correct id and resourceId 0,
while sibling players in the same response carried theirs. resourceId is the
merge key compared RAW against carddbid, so 0 resolves no manager: no name, no
rating, no art, empty slot.
The client's own save corroborates the shape: it PUTs
`"manager":[{"id":...,"dream":false}]` -- flat, and both keys are item atoms.
Flatten the element to the item plus `dream`. Cold-load proven on staging: the
manager record now carries resourceId 1000509 in the same layout as its player
siblings (83906881, 84053575) in the same array region, and the operator
confirms a manager is assigned in the squad management screen.
Two earlier shapes are now both explained and covered by tests: `{id, dream}`
carries no merge key, and `{id, itemData, dream}` hides it from this path.
This commit is contained in:
@@ -188,23 +188,37 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
// Manager: the ownership-backed assignment, resolved to its FIFA wire ref
|
||||
// AND carrying its item, as `[{id, itemData, dream}]`.
|
||||
// Manager: the ownership-backed assignment, resolved to its FIFA wire ref and
|
||||
// emitted as a BARE ITEM OBJECT with `dream` beside the item's own fields —
|
||||
// NOT wrapped in `itemData`.
|
||||
//
|
||||
// The bare `[{id, dream}]` form is NOT sufficient, which cost a real
|
||||
// debugging round: the operator picked a manager in the hub, the save
|
||||
// persisted (Core `squad_managers` row written, `outcome=ok`, no unresolved
|
||||
// ref), and the pre-match squad still showed no manager. Every retail
|
||||
// capture that shows the bare form has `id: 0` — an EMPTY manager — so none
|
||||
// of them ever demonstrated that a POPULATED ref resolves without its item.
|
||||
// This is a wire-shape contract, recovered from the client rather than
|
||||
// guessed, after two earlier shapes both failed:
|
||||
//
|
||||
// The squad response is self-contained for players: `players[].itemData`
|
||||
// carries the whole card rather than an id the client resolves out of band.
|
||||
// The manager is the same kind of slot in the same object, and the one
|
||||
// implementation that ever drove a working manager (the Python oracle's
|
||||
// squad) emits `id` BESIDE `itemData` exactly like this. Note the element
|
||||
// shape differs from a player slot: `{index, itemData, kitNumber}` there,
|
||||
// `{id, itemData, dream}` here.
|
||||
// `[{id, dream}]` — no merge key, so nothing resolves.
|
||||
// `[{id, itemData, dream}]` — `itemData` is never read on this path.
|
||||
//
|
||||
// The squad parser FUN_18013d1f0 treats the two slots differently, and that
|
||||
// is the whole point:
|
||||
//
|
||||
// players: atom 568 -> per-element atoms 355 `index`, 363 `itemData`,
|
||||
// 378 `kitNumber`; the 363 arm (0x18013d8d9) calls the ITEM
|
||||
// parser FUN_18013fe00 on the NESTED itemData object.
|
||||
// manager: atom 424 -> array loop at 0x18013da29 calls that same item
|
||||
// parser DIRECTLY on the array ELEMENT, into squad+0xC0. There is
|
||||
// no `itemData` step at all.
|
||||
//
|
||||
// So a manager element IS an item. Nesting the fields one level deeper left
|
||||
// the parser reading only the two keys that happen to be item atoms — `id`
|
||||
// (0x14c) and `dream` (0xe7) — and leaving `resourceId` at 0. Measured on a
|
||||
// cold client: the manager record existed at squad+0xC0 with the correct id
|
||||
// and `resourceId == 0`, while sibling players in the same response carried
|
||||
// theirs (83906881, 84053575). `resourceId` is the merge key compared RAW
|
||||
// against `carddbid`, so zero can never hit the managercards table: no name,
|
||||
// no rating, no art, and an empty manager slot in the UI.
|
||||
//
|
||||
// The client's own save corroborates the shape: it PUTs
|
||||
// `"manager":[{"id":…,"dream":false}]` — flat, and both keys are item atoms.
|
||||
//
|
||||
// An owned manager with no resolvable FIFA staff identity is omitted
|
||||
// (non-fatal, like /club dropping an unrenderable card) rather than emitted
|
||||
@@ -214,11 +228,13 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
||||
.as_ref()
|
||||
.and_then(|m| ident.resolve_staff(m).map(|id| (m, id)))
|
||||
{
|
||||
Some((mgr, id)) => json!([{
|
||||
"id": id.item_id,
|
||||
"itemData": shape_staff_item(id, mgr.contract_matches.unwrap_or(STAFF_CONTRACT)),
|
||||
"dream": false,
|
||||
}]),
|
||||
Some((mgr, id)) => {
|
||||
let mut item = shape_staff_item(id, mgr.contract_matches.unwrap_or(STAFF_CONTRACT));
|
||||
if let Some(obj) = item.as_object_mut() {
|
||||
obj.insert("dream".to_string(), json!(false));
|
||||
}
|
||||
json!([item])
|
||||
}
|
||||
None => json!([]),
|
||||
};
|
||||
let squad = json!({
|
||||
@@ -258,9 +274,10 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
||||
/// 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.
|
||||
/// a `squad.manager[]` element — which reaches this same deserializer the same
|
||||
/// way, called directly on the array element with no `itemData` step. An id
|
||||
/// reference alone installs nothing, because the 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
|
||||
@@ -588,29 +605,38 @@ mod tests {
|
||||
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
||||
panic!("expected Projected");
|
||||
};
|
||||
// The item must ride ALONG with the ref: a bare `{id, dream}` left the
|
||||
// pre-match squad with no manager even though the assignment had been
|
||||
// saved, because nothing in the response described the card.
|
||||
// The element IS the item: the squad parser's manager branch calls the
|
||||
// item parser on the array element itself, with no `itemData` step, so
|
||||
// the fields must be flat. Nesting them left `resourceId` — the merge
|
||||
// key — at 0 on a cold client and the slot rendered empty.
|
||||
assert_eq!(
|
||||
v["manager"],
|
||||
json!([{
|
||||
"id": 100000427,
|
||||
"itemData": {
|
||||
"id": 100000427,
|
||||
"resourceId": 1_000_509,
|
||||
"cardsubtypeid": 4,
|
||||
"itemType": "staff",
|
||||
"nation": 45,
|
||||
"leagueId": 53,
|
||||
"teamid": 241,
|
||||
"contract": 12,
|
||||
"itemState": "free",
|
||||
"owners": 1,
|
||||
"untradeable": false,
|
||||
},
|
||||
"resourceId": 1_000_509,
|
||||
"cardsubtypeid": 4,
|
||||
"itemType": "staff",
|
||||
"nation": 45,
|
||||
"leagueId": 53,
|
||||
"teamid": 241,
|
||||
"contract": 12,
|
||||
"itemState": "free",
|
||||
"owners": 1,
|
||||
"untradeable": false,
|
||||
"dream": false,
|
||||
}]),
|
||||
"manager is the ownership-backed wire ref WITH its item"
|
||||
"manager element is a bare item object carrying `dream`"
|
||||
);
|
||||
let element = &v["manager"][0];
|
||||
assert!(
|
||||
element.get("itemData").is_none(),
|
||||
"an `itemData` wrapper is never descended into on the manager path, \
|
||||
so its presence means the merge key is invisible to the client"
|
||||
);
|
||||
assert_eq!(
|
||||
element["resourceId"], 1_000_509,
|
||||
"resourceId must be readable at element level: it is the merge key \
|
||||
compared RAW against carddbid, and 0 resolves no manager"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -410,10 +410,13 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
||||
}
|
||||
// EXTENSION + SHADOW: sourced from the read, so they round-trip identically.
|
||||
assert_eq!(projected["custom"], oracle["custom"]);
|
||||
// The manager REF round-trips; the item now rides with it. The capture this
|
||||
// oracle came from carried a bare `{id, dream}`, but its manager was the
|
||||
// dangling one every retail capture has, so it never showed that a populated
|
||||
// ref renders on its own — and in practice it did not.
|
||||
// The manager REF round-trips; the item now rides AT ELEMENT LEVEL. The
|
||||
// capture this oracle came from carried a bare `{id, dream}`, but its
|
||||
// manager was the dangling one every retail capture has, so it never showed
|
||||
// that a populated ref renders on its own — and in practice it did not.
|
||||
// Wrapping the fields in `itemData` did not work either: the squad parser's
|
||||
// manager branch calls the item parser on the element itself, so a nested
|
||||
// item is never read and the merge key stays 0.
|
||||
assert_eq!(
|
||||
projected["manager"][0]["id"], oracle["manager"][0]["id"],
|
||||
"the manager wire ref itself must still round-trip"
|
||||
@@ -422,8 +425,11 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
||||
projected["manager"][0]["dream"],
|
||||
oracle["manager"][0]["dream"]
|
||||
);
|
||||
let mgr_item = &projected["manager"][0]["itemData"];
|
||||
assert_eq!(mgr_item["id"], oracle["manager"][0]["id"]);
|
||||
let mgr_item = &projected["manager"][0];
|
||||
assert!(
|
||||
mgr_item.get("itemData").is_none(),
|
||||
"the manager element IS the item; a wrapper hides the merge key"
|
||||
);
|
||||
assert_eq!(mgr_item["cardsubtypeid"], 4);
|
||||
assert_eq!(mgr_item["resourceId"], 1_000_509);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user