fix(fifa17): carry the manager's item in the squad, not just its id
The operator picked a manager in the FUT hub, then found no manager on the
pre-match squad. The save was NOT the problem: the host logged three
`route=squad-replace status=200 outcome=ok detail=[]` with no unresolved ref,
Core wrote the `squad_managers` row, and every read projected the assignment
back. The client simply had nothing to draw.
`squad.manager[]` was emitted as a bare `[{id, dream}]`. That looked
retail-faithful, and the previous commit defended it on the grounds that no
capture had ever shown otherwise. Re-reading the captures with a populated
manager in hand shows why that was the wrong conclusion: every retail capture
carrying the bare form has `id: 0` — an EMPTY manager. None of them ever
demonstrated that a POPULATED ref renders without its item, because none of them
had one. `plan-2026-08-05-families.md` says as much outright: "FUN_18013d1f0 was
never read for a staff member".
The squad object is self-contained everywhere else: `players[].itemData` carries
the whole card rather than an id resolved 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`.
The element shapes differ and both are now pinned by tests: a player slot is
`{index, itemData, kitNumber}`, the manager is `{id, itemData, dream}`.
So the manager is projected through `resolve_staff` and its item embedded with
`shape_staff_item`, the same 11-key record `/club` serves. An assignment with no
resolvable staff identity still yields `[]` rather than a fabricated ref.
`STAFF_CONTRACT` moves next to `shape_staff_item` in `fut::item` (re-exported
from `club_response`) so `/club` and the squad cannot disagree about the
contract the client checks before kickoff.
Verified on the restored club: userMassInfo, /squad/0 and /squad/active all
carry the manager with resourceId 1000509, contract 7 and the nation/league/team
the client cannot supply itself. Adapter 217 lib + 25 integration, host 114 lib +
36 host_test and every economy suite green.
This commit is contained in:
@@ -24,7 +24,9 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use openfut_adapter_fifa17::fut::item::{CoreOwnedItem, Fifa17Identity, ItemIdentityResolver};
|
||||
use openfut_adapter_fifa17::fut::item::{
|
||||
CoreOwnedItem, Fifa17Identity, 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::{
|
||||
@@ -50,11 +52,19 @@ impl SquadWireResolver for OcResolver {
|
||||
}
|
||||
|
||||
/// owned_card_id → FIFA identity, so two copies of one definition stay distinct.
|
||||
struct TableIdentity(HashMap<String, Fifa17Identity>);
|
||||
/// `staff` is a second table because a manager resolves through the STAFF
|
||||
/// identity, which carries the chemistry fields a player identity cannot hold.
|
||||
struct TableIdentity(
|
||||
HashMap<String, Fifa17Identity>,
|
||||
HashMap<String, Fifa17StaffIdentity>,
|
||||
);
|
||||
impl ItemIdentityResolver for TableIdentity {
|
||||
fn resolve(&self, it: &CoreOwnedItem) -> Option<Fifa17Identity> {
|
||||
self.0.get(&it.owned_card_id).copied()
|
||||
}
|
||||
fn resolve_staff(&self, it: &CoreOwnedItem) -> Option<Fifa17StaffIdentity> {
|
||||
self.1.get(&it.owned_card_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Neutral entity resolver — badge/flag ids are covered by `fut::item` tests; the
|
||||
@@ -116,7 +126,7 @@ fn oracle_tables() -> (HashMap<String, CoreOwnedItem>, TableIdentity) {
|
||||
},
|
||||
);
|
||||
}
|
||||
(owned, TableIdentity(ident))
|
||||
(owned, TableIdentity(ident, HashMap::new()))
|
||||
}
|
||||
|
||||
/// The full pipeline: parse a captured PUT, build the canonical + extension, then
|
||||
@@ -298,17 +308,22 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
||||
is_on_bench: index >= 11,
|
||||
});
|
||||
}
|
||||
// The manager is now ownership-backed: register its owned instance + identity
|
||||
// and pass it as the assignment, not as an opaque extension field.
|
||||
// The manager is ownership-backed: register its owned instance + STAFF
|
||||
// identity and pass it as the assignment, not as an opaque extension field.
|
||||
// A real managercards row is used (1000509 Luis Enrique, nation 45, LaLiga
|
||||
// 53, Barcelona 241) so the projected item is a shape the client could
|
||||
// actually merge.
|
||||
let mgr_wire = oracle["manager"][0]["id"].as_i64().unwrap();
|
||||
let mgr_oc = format!("oc-{mgr_wire}");
|
||||
ident.0.insert(
|
||||
ident.1.insert(
|
||||
mgr_oc.clone(),
|
||||
Fifa17Identity {
|
||||
Fifa17StaffIdentity {
|
||||
item_id: mgr_wire as u32,
|
||||
asset_id: 5001,
|
||||
resource_id: 5001,
|
||||
rareflag: 1,
|
||||
resource_id: 1_000_509,
|
||||
subtype: 4,
|
||||
nation: 45,
|
||||
league_id: 53,
|
||||
team_id: 241,
|
||||
},
|
||||
);
|
||||
let manager_item = CoreOwnedItem {
|
||||
@@ -378,7 +393,23 @@ 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"]);
|
||||
assert_eq!(projected["manager"], oracle["manager"]);
|
||||
// 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.
|
||||
assert_eq!(
|
||||
projected["manager"][0]["id"], oracle["manager"][0]["id"],
|
||||
"the manager wire ref itself must still round-trip"
|
||||
);
|
||||
assert_eq!(
|
||||
projected["manager"][0]["dream"],
|
||||
oracle["manager"][0]["dream"]
|
||||
);
|
||||
let mgr_item = &projected["manager"][0]["itemData"];
|
||||
assert_eq!(mgr_item["id"], oracle["manager"][0]["id"]);
|
||||
assert_eq!(mgr_item["cardsubtypeid"], 4);
|
||||
assert_eq!(mgr_item["resourceId"], 1_000_509);
|
||||
assert_eq!(mgr_item["contract"], STAFF_CONTRACT);
|
||||
assert_eq!(projected["kicktakers"], oracle["kicktakers"]);
|
||||
assert_eq!(projected["squadType"], oracle["squadType"]);
|
||||
assert_eq!(projected["chemistry"], oracle["chemistry"]);
|
||||
|
||||
Reference in New Issue
Block a user