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
+31 -7
View File
@@ -141,6 +141,13 @@ pub struct ProposedSquad {
/// never used to derive slot layout.
pub formation: Option<String>,
pub slots: Vec<ProposedSlot>,
/// The owned instance assigned as the squad's **manager**, reverse-resolved
/// from the wire `manager` ref to a Core `owned_card_id` (so the assignment
/// is ownership-backed, never a dangling wire id). `None` when the save
/// carries no manager. A manager wire id the resolver cannot map is reported
/// in `unresolved_wire_ids` — a save is refused rather than assigning a
/// manager the club does not own.
pub manager_owned_card_id: Option<String>,
/// Occupied wire item ids the resolver could not map. A caller MUST refuse the
/// replacement if this is non-empty — a save must never silently drop an
/// owned player it failed to identify.
@@ -180,12 +187,14 @@ pub fn parse_squad_put(body: &[u8]) -> Result<Fifa17SquadPut, SquadError> {
/// Resolve a parsed save into a **canonical** [`ProposedSquad`]: drop empty
/// (`id == 0`) slots, reverse-map each occupied slot's wire id to a Core
/// `owned_card_id`, flag the captain, and derive the bench split from the fixed
/// 23-slot array. FIFA-only state (`custom`, manager, kicktakers, kit numbers,
/// squadType) and client-reported evaluation are NOT canonical — they are built
/// separately into [`crate::fut::squad_ext::Fifa17SquadExtensionV1`]. The
/// formation token is carried verbatim (never mapped). Unresolvable occupied ids
/// are reported, never guessed or dropped.
/// `owned_card_id`, flag the captain, derive the bench split from the fixed
/// 23-slot array, and reverse-resolve the manager ref to an owned instance
/// (the manager assignment is ownership-backed canonical state, migration 0023).
/// The remaining FIFA-only state (`custom`, kicktakers, kit numbers, squadType)
/// and client-reported evaluation are NOT canonical — they are built separately
/// into [`crate::fut::squad_ext::Fifa17SquadExtensionV1`]. The formation token is
/// carried verbatim (never mapped). Unresolvable occupied ids are reported,
/// never guessed or dropped.
pub fn to_proposed(put: &Fifa17SquadPut, resolver: &dyn SquadWireResolver) -> ProposedSquad {
let captain = put.captain.unwrap_or(0);
let mut slots = Vec::new();
@@ -205,11 +214,22 @@ pub fn to_proposed(put: &Fifa17SquadPut, resolver: &dyn SquadWireResolver) -> Pr
None => unresolved.push(p.item_data.id),
}
}
// Manager: the first non-zero manager ref, reverse-resolved to an owned
// instance. An unresolvable manager is an unresolved wire id (refused), not a
// silently dropped assignment — you cannot manage with a card you don't own.
let mut manager_owned_card_id = None;
if let Some(wire) = put.manager.iter().map(|m| m.id).find(|&id| id != 0) {
match resolver.owned_id_for_wire(wire) {
Some(owned) => manager_owned_card_id = Some(owned),
None => unresolved.push(wire),
}
}
ProposedSquad {
squad_id: put.id,
name: put.squad_name.clone(),
formation: put.formation.clone(),
slots,
manager_owned_card_id,
unresolved_wire_ids: unresolved,
}
}
@@ -237,9 +257,11 @@ mod tests {
}
fn full_resolver() -> MapResolver {
// indices 0..=10 (11 starters); the rest of the 23 slots are id==0 (empty).
// 100000427 is the fixture's manager ref — the host resolves it like any
// other owned instance, so the manager assignment is ownership-backed.
let ids = [
100000003, 100000010, 100000005, 100000008, 100000007, 100000006, 100000004, 100000009,
100000001, 100000002, 100000025,
100000001, 100000002, 100000025, 100000427,
];
MapResolver(ids.iter().map(|&w| (w, format!("oc-{w}"))).collect())
}
@@ -283,6 +305,8 @@ mod tests {
assert_eq!(caps[0].owned_card_id, "oc-100000001");
assert_eq!(caps[0].index, 8);
assert_eq!(caps[0].kit_number, 8);
// The manager ref is reverse-resolved to an owned instance (canonical).
assert_eq!(sq.manager_owned_card_id.as_deref(), Some("oc-100000427"));
}
#[test]