diff --git a/openfut-adapter-fifa17/src/fut/squad_projection.rs b/openfut-adapter-fifa17/src/fut/squad_projection.rs index 3ef3273..e3cb248 100644 --- a/openfut-adapter-fifa17/src/fut/squad_projection.rs +++ b/openfut-adapter-fifa17/src/fut/squad_projection.rs @@ -188,23 +188,37 @@ pub fn project_squad( } } - // 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( .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( /// 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" ); } diff --git a/openfut-adapter-fifa17/tests/squad_projection.rs b/openfut-adapter-fifa17/tests/squad_projection.rs index b8d1e3f..9256228 100644 --- a/openfut-adapter-fifa17/tests/squad_projection.rs +++ b/openfut-adapter-fifa17/tests/squad_projection.rs @@ -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!(