feat(fifa17): ownership-backed squad manager, not opaque round-trip

Move the squad manager from an opaque, unvalidated wire ref in the squad
extension to a resolved, ownership-backed assignment (Core migration 0023
squad_managers).

- squad::to_proposed reverse-resolves the manager wire ref to a Core
  owned_card_id on ProposedSquad; an unresolvable manager is reported as
  an unresolved wire id (a live save is refused rather than assigning a
  manager the club does not own).
- squad_ext: drop the opaque manager field from Fifa17SquadExtensionV1
  (clean cutover) and the now-unused SquadEntityRef->WireItemRef From.
- squad_projection: project the manager as the STATIC_REVERSED [{id,dream}]
  wire ref resolved from the owned assignment; absent -> [] (never faked).
  Richer manager itemData (contract/league/nation) is INFERRED-only and
  left out pending wire reversal.
- import(apply): drop a historical dangling manager ref rather than
  failing the whole import (live PUTs still refuse an unresolved manager).
This commit is contained in:
funman300
2026-08-20 16:43:39 +00:00
parent e5d356e8be
commit d37a9d5b5e
4 changed files with 147 additions and 34 deletions
@@ -77,6 +77,11 @@ pub struct SquadProjectionInput<'a> {
/// Every owned item a slot references, keyed by `owned_card_id`. Assembled by
/// the host in one batch — the projector only reads from it.
pub owned: &'a HashMap<String, CoreOwnedItem>,
/// The owned instance assigned as this squad's **manager** (Core's
/// ownership-backed `squad_managers` assignment, migration 0023), or `None`.
/// Projected as the FIFA `manager` wire ref resolved from ownership — never a
/// dangling wire id, and never fabricated when absent.
pub manager: Option<CoreOwnedItem>,
}
/// Result of a projection, with the extension-freshness verdict surfaced.
@@ -171,6 +176,17 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
}
}
// Manager: the ownership-backed assignment, resolved to its FIFA wire ref.
// Emitted as the STATIC_REVERSED `[{id, dream}]` shape (the same shape the
// client sends on save). An owned manager with no resolvable FIFA identity
// is omitted (non-fatal, like /club dropping an unrenderable card) rather
// than emitted with a fabricated id. NOTE: a richer manager itemData
// (contract/leagueId/nation) is INFERRED-only from the offline-season
// diagnosis and deliberately NOT invented here pending wire reversal.
let manager = match input.manager.as_ref().and_then(|m| ident.resolve(m)) {
Some(id) => json!([{ "id": id.item_id, "dream": false }]),
None => json!([]),
};
let squad = json!({
"id": input.fifa_squad_id,
"squadName": input.name,
@@ -180,7 +196,7 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
"starRating": ext.client_reported.star_rating,
"rating": ext.client_reported.rating,
"captain": captain_wire,
"manager": ext.manager,
"manager": manager,
"custom": ext.custom,
"players": players,
"kicktakers": ext.kicktakers,
@@ -262,6 +278,7 @@ mod tests {
}],
ext,
owned,
manager: None,
}
}
@@ -272,7 +289,6 @@ mod tests {
custom: Some("[1,2,3]".into()),
squad_type: Some("REGULAR_SQUAD".into()),
kit_numbers: kit,
manager: vec![],
kicktakers: vec![],
client_reported: Default::default(),
}
@@ -403,6 +419,7 @@ mod tests {
],
ext: SquadExtInput::Fresh(ext),
owned: owned_ref,
manager: None,
};
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
panic!();
@@ -423,4 +440,65 @@ mod tests {
"kit stays with the instance"
);
}
#[test]
fn manager_projected_from_ownership_as_wire_ref() {
let mut owned = HashMap::new();
owned.insert("oc1".to_string(), owned_item("oc1", "card_x"));
let ident = TableIdentity(HashMap::from([
(
"oc1".to_string(),
Fifa17Identity {
item_id: 100000042,
asset_id: 20801,
resource_id: 20801,
rareflag: 1,
},
),
(
"oc-mgr".to_string(),
Fifa17Identity {
item_id: 100000427,
asset_id: 5001,
resource_id: 5001,
rareflag: 1,
},
),
]));
let mut input = one_slot_input(&owned, SquadExtInput::Fresh(fresh_ext()));
input.manager = Some(owned_item("oc-mgr", "fifa17_mgr"));
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
panic!("expected Projected");
};
assert_eq!(
v["manager"],
json!([{ "id": 100000427, "dream": false }]),
"manager is the ownership-backed wire ref, resolved from the owned item"
);
}
#[test]
fn absent_manager_projects_empty_array_never_fabricated() {
let mut owned = HashMap::new();
owned.insert("oc1".to_string(), owned_item("oc1", "card_x"));
let ident = TableIdentity(HashMap::from([(
"oc1".to_string(),
Fifa17Identity {
item_id: 100000042,
asset_id: 20801,
resource_id: 20801,
rareflag: 1,
},
)]));
// one_slot_input leaves manager: None.
let input = one_slot_input(&owned, SquadExtInput::Fresh(fresh_ext()));
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
panic!("expected Projected");
};
assert_eq!(
v["manager"],
json!([]),
"no manager assignment => empty array, nothing fabricated"
);
}
}