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
|
// Manager: the ownership-backed assignment, resolved to its FIFA wire ref and
|
||||||
// AND carrying its item, as `[{id, itemData, dream}]`.
|
// 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
|
// This is a wire-shape contract, recovered from the client rather than
|
||||||
// debugging round: the operator picked a manager in the hub, the save
|
// guessed, after two earlier shapes both failed:
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// The squad response is self-contained for players: `players[].itemData`
|
// `[{id, dream}]` — no merge key, so nothing resolves.
|
||||||
// carries the whole card rather than an id the client resolves out of band.
|
// `[{id, itemData, dream}]` — `itemData` is never read on this path.
|
||||||
// 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
|
// The squad parser FUN_18013d1f0 treats the two slots differently, and that
|
||||||
// squad) emits `id` BESIDE `itemData` exactly like this. Note the element
|
// is the whole point:
|
||||||
// shape differs from a player slot: `{index, itemData, kitNumber}` there,
|
//
|
||||||
// `{id, itemData, dream}` here.
|
// 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
|
// An owned manager with no resolvable FIFA staff identity is omitted
|
||||||
// (non-fatal, like /club dropping an unrenderable card) rather than emitted
|
// (non-fatal, like /club dropping an unrenderable card) rather than emitted
|
||||||
@@ -214,11 +228,13 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|m| ident.resolve_staff(m).map(|id| (m, id)))
|
.and_then(|m| ident.resolve_staff(m).map(|id| (m, id)))
|
||||||
{
|
{
|
||||||
Some((mgr, id)) => json!([{
|
Some((mgr, id)) => {
|
||||||
"id": id.item_id,
|
let mut item = shape_staff_item(id, mgr.contract_matches.unwrap_or(STAFF_CONTRACT));
|
||||||
"itemData": shape_staff_item(id, mgr.contract_matches.unwrap_or(STAFF_CONTRACT)),
|
if let Some(obj) = item.as_object_mut() {
|
||||||
"dream": false,
|
obj.insert("dream".to_string(), json!(false));
|
||||||
}]),
|
}
|
||||||
|
json!([item])
|
||||||
|
}
|
||||||
None => json!([]),
|
None => json!([]),
|
||||||
};
|
};
|
||||||
let squad = 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
|
/// 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
|
/// (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
|
/// 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
|
/// a `squad.manager[]` element — which reaches this same deserializer the same
|
||||||
/// the manager installer looks its id up in that same map and does nothing when
|
/// way, called directly on the array element with no `itemData` step. An id
|
||||||
/// it misses.
|
/// 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
|
/// 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
|
/// parse nothing, which leaves all five slots null. Every later consumer then
|
||||||
@@ -588,14 +605,13 @@ mod tests {
|
|||||||
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
||||||
panic!("expected Projected");
|
panic!("expected Projected");
|
||||||
};
|
};
|
||||||
// The item must ride ALONG with the ref: a bare `{id, dream}` left the
|
// The element IS the item: the squad parser's manager branch calls the
|
||||||
// pre-match squad with no manager even though the assignment had been
|
// item parser on the array element itself, with no `itemData` step, so
|
||||||
// saved, because nothing in the response described the card.
|
// 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!(
|
assert_eq!(
|
||||||
v["manager"],
|
v["manager"],
|
||||||
json!([{
|
json!([{
|
||||||
"id": 100000427,
|
|
||||||
"itemData": {
|
|
||||||
"id": 100000427,
|
"id": 100000427,
|
||||||
"resourceId": 1_000_509,
|
"resourceId": 1_000_509,
|
||||||
"cardsubtypeid": 4,
|
"cardsubtypeid": 4,
|
||||||
@@ -607,10 +623,20 @@ mod tests {
|
|||||||
"itemState": "free",
|
"itemState": "free",
|
||||||
"owners": 1,
|
"owners": 1,
|
||||||
"untradeable": false,
|
"untradeable": false,
|
||||||
},
|
|
||||||
"dream": 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.
|
// EXTENSION + SHADOW: sourced from the read, so they round-trip identically.
|
||||||
assert_eq!(projected["custom"], oracle["custom"]);
|
assert_eq!(projected["custom"], oracle["custom"]);
|
||||||
// The manager REF round-trips; the item now rides with it. The capture this
|
// The manager REF round-trips; the item now rides AT ELEMENT LEVEL. The
|
||||||
// oracle came from carried a bare `{id, dream}`, but its manager was the
|
// capture this oracle came from carried a bare `{id, dream}`, but its
|
||||||
// dangling one every retail capture has, so it never showed that a populated
|
// manager was the dangling one every retail capture has, so it never showed
|
||||||
// ref renders on its own — and in practice it did not.
|
// 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!(
|
assert_eq!(
|
||||||
projected["manager"][0]["id"], oracle["manager"][0]["id"],
|
projected["manager"][0]["id"], oracle["manager"][0]["id"],
|
||||||
"the manager wire ref itself must still round-trip"
|
"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"],
|
projected["manager"][0]["dream"],
|
||||||
oracle["manager"][0]["dream"]
|
oracle["manager"][0]["dream"]
|
||||||
);
|
);
|
||||||
let mgr_item = &projected["manager"][0]["itemData"];
|
let mgr_item = &projected["manager"][0];
|
||||||
assert_eq!(mgr_item["id"], oracle["manager"][0]["id"]);
|
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["cardsubtypeid"], 4);
|
||||||
assert_eq!(mgr_item["resourceId"], 1_000_509);
|
assert_eq!(mgr_item["resourceId"], 1_000_509);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user