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
+27 -24
View File
@@ -12,7 +12,7 @@
//! | `custom` | opaque 33-int string; meaning UNKNOWN, round-tripped verbatim |
//! | `squad_type` | an observed FIFA wire token; no matching generic Core concept |
//! | `kit_numbers` | keyed by **`owned_card_id`** — evidence: kit follows the player |
//! | `manager` | a FIFA manager item ref; not a squad player, semantics opaque |
//! | ~~manager~~ | MOVED to ownership-backed canonical Core state (migration 0023 `squad_managers`); resolved to an `owned_card_id`, no longer opaque here |
//! | `kicktakers` | role→item refs; relationship to captain UNKNOWN, kept opaque |
//! | `client_reported` | chemistry/rating/starRating — client shadow, NOT authority |
//!
@@ -30,7 +30,7 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::fut::squad::{ClientReportedSquadEval, Fifa17SquadPut, ProposedSquad, SquadEntityRef};
use crate::fut::squad::{ClientReportedSquadEval, Fifa17SquadPut, ProposedSquad};
/// Opaque scope key Core files this extension under (`game_entity_ext.namespace`).
pub const EXT_NAMESPACE: &str = "fifa17.squad";
@@ -49,15 +49,6 @@ pub struct WireItemRef {
pub dream: bool,
}
impl From<&SquadEntityRef> for WireItemRef {
fn from(r: &SquadEntityRef) -> Self {
WireItemRef {
id: r.id,
dream: r.dream,
}
}
}
/// A kicktaker slot preserved verbatim. `index` is the role slot (0..=4 observed);
/// `item` is the referenced FIFA wire item. The role→player meaning and any
/// relationship to the captain are UNKNOWN, so this is stored opaquely and never
@@ -83,9 +74,9 @@ pub struct Fifa17SquadExtensionV1 {
/// proves the kit number follows the player across swaps and formation change.
#[serde(default)]
pub kit_numbers: BTreeMap<String, i64>,
/// Manager item ref(s), opaque. Not a squad player; not shaped as an item.
#[serde(default)]
pub manager: Vec<WireItemRef>,
// NOTE: the squad manager is NO LONGER carried here. It is ownership-backed
// canonical Core state (migration 0023 `squad_managers`), resolved to an
// `owned_card_id` on the ProposedSquad — never a dangling opaque wire ref.
/// Kicktaker role refs, opaque (see [`KicktakerRef`]).
#[serde(default)]
pub kicktakers: Vec<KicktakerRef>,
@@ -132,7 +123,6 @@ impl Fifa17SquadExtensionV1 {
custom: put.custom.clone(),
squad_type: put.squad_type.clone(),
kit_numbers,
manager: put.manager.iter().map(WireItemRef::from).collect(),
kicktakers: put
.kicktakers
.iter()
@@ -256,6 +246,9 @@ mod tests {
let ids = [
100000003, 100000010, 100000005, 100000008, 100000007, 100000006, 100000004, 100000009,
100000001, 100000002, 100000025,
// The f442 fixture's manager ref — the host resolves it like any other
// owned instance, so the ownership-backed manager assignment is present.
100000427,
];
MapResolver(ids.iter().map(|&w| (w, format!("oc-{w}"))).collect())
}
@@ -308,22 +301,32 @@ mod tests {
}
#[test]
fn manager_and_kicktakers_preserved_opaquely() {
let ext = built().extension;
fn manager_is_canonical_and_kicktakers_stay_opaque() {
let build = built();
// Manager is now ownership-backed canonical state: the wire ref resolved
// to a Core owned_card_id on the ProposedSquad, not an opaque ext blob.
assert_eq!(
ext.manager,
vec![WireItemRef {
id: 100000427,
dream: false
}]
build.canonical.manager_owned_card_id.as_deref(),
Some("oc-100000427")
);
// Kicktakers remain opaque in the extension.
let ext = build.extension;
assert_eq!(ext.kicktakers.len(), 5);
// All five reference the same wire id in this capture; carried verbatim,
// NEVER normalized to the captain even though they coincide here.
assert!(ext.kicktakers.iter().all(|k| k.item.id == 100000001));
assert_eq!(ext.kicktakers[0].index, 0);
}
#[test]
fn build_refuses_an_unowned_manager() {
// Manager wire id present but NOT resolvable → refused, never assigned a
// manager the club does not own.
let put = parse_squad_put(PUT_F442.as_bytes()).unwrap();
let mut ids = full_resolver().0;
ids.remove(&100000427);
let err = build_squad_write(&put, &MapResolver(ids)).unwrap_err();
assert_eq!(err, SquadBuildError::UnresolvedWireIds(vec![100000427]));
}
#[test]
fn unknown_schema_version_is_rejected_not_coerced() {
let payload = built().extension.to_payload();