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:
@@ -141,6 +141,13 @@ pub struct ProposedSquad {
|
|||||||
/// never used to derive slot layout.
|
/// never used to derive slot layout.
|
||||||
pub formation: Option<String>,
|
pub formation: Option<String>,
|
||||||
pub slots: Vec<ProposedSlot>,
|
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
|
/// 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
|
/// replacement if this is non-empty — a save must never silently drop an
|
||||||
/// owned player it failed to identify.
|
/// 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
|
/// Resolve a parsed save into a **canonical** [`ProposedSquad`]: drop empty
|
||||||
/// (`id == 0`) slots, reverse-map each occupied slot's wire id to a Core
|
/// (`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
|
/// `owned_card_id`, flag the captain, derive the bench split from the fixed
|
||||||
/// 23-slot array. FIFA-only state (`custom`, manager, kicktakers, kit numbers,
|
/// 23-slot array, and reverse-resolve the manager ref to an owned instance
|
||||||
/// squadType) and client-reported evaluation are NOT canonical — they are built
|
/// (the manager assignment is ownership-backed canonical state, migration 0023).
|
||||||
/// separately into [`crate::fut::squad_ext::Fifa17SquadExtensionV1`]. The
|
/// The remaining FIFA-only state (`custom`, kicktakers, kit numbers, squadType)
|
||||||
/// formation token is carried verbatim (never mapped). Unresolvable occupied ids
|
/// and client-reported evaluation are NOT canonical — they are built separately
|
||||||
/// are reported, never guessed or dropped.
|
/// 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 {
|
pub fn to_proposed(put: &Fifa17SquadPut, resolver: &dyn SquadWireResolver) -> ProposedSquad {
|
||||||
let captain = put.captain.unwrap_or(0);
|
let captain = put.captain.unwrap_or(0);
|
||||||
let mut slots = Vec::new();
|
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),
|
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 {
|
ProposedSquad {
|
||||||
squad_id: put.id,
|
squad_id: put.id,
|
||||||
name: put.squad_name.clone(),
|
name: put.squad_name.clone(),
|
||||||
formation: put.formation.clone(),
|
formation: put.formation.clone(),
|
||||||
slots,
|
slots,
|
||||||
|
manager_owned_card_id,
|
||||||
unresolved_wire_ids: unresolved,
|
unresolved_wire_ids: unresolved,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,9 +257,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
fn full_resolver() -> MapResolver {
|
fn full_resolver() -> MapResolver {
|
||||||
// indices 0..=10 (11 starters); the rest of the 23 slots are id==0 (empty).
|
// 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 = [
|
let ids = [
|
||||||
100000003, 100000010, 100000005, 100000008, 100000007, 100000006, 100000004, 100000009,
|
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())
|
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].owned_card_id, "oc-100000001");
|
||||||
assert_eq!(caps[0].index, 8);
|
assert_eq!(caps[0].index, 8);
|
||||||
assert_eq!(caps[0].kit_number, 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]
|
#[test]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//! | `custom` | opaque 33-int string; meaning UNKNOWN, round-tripped verbatim |
|
//! | `custom` | opaque 33-int string; meaning UNKNOWN, round-tripped verbatim |
|
||||||
//! | `squad_type` | an observed FIFA wire token; no matching generic Core concept |
|
//! | `squad_type` | an observed FIFA wire token; no matching generic Core concept |
|
||||||
//! | `kit_numbers` | keyed by **`owned_card_id`** — evidence: kit follows the player |
|
//! | `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 |
|
//! | `kicktakers` | role→item refs; relationship to captain UNKNOWN, kept opaque |
|
||||||
//! | `client_reported` | chemistry/rating/starRating — client shadow, NOT authority |
|
//! | `client_reported` | chemistry/rating/starRating — client shadow, NOT authority |
|
||||||
//!
|
//!
|
||||||
@@ -30,7 +30,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
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`).
|
/// Opaque scope key Core files this extension under (`game_entity_ext.namespace`).
|
||||||
pub const EXT_NAMESPACE: &str = "fifa17.squad";
|
pub const EXT_NAMESPACE: &str = "fifa17.squad";
|
||||||
@@ -49,15 +49,6 @@ pub struct WireItemRef {
|
|||||||
pub dream: bool,
|
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);
|
/// 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
|
/// `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
|
/// 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.
|
/// proves the kit number follows the player across swaps and formation change.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub kit_numbers: BTreeMap<String, i64>,
|
pub kit_numbers: BTreeMap<String, i64>,
|
||||||
/// Manager item ref(s), opaque. Not a squad player; not shaped as an item.
|
// NOTE: the squad manager is NO LONGER carried here. It is ownership-backed
|
||||||
#[serde(default)]
|
// canonical Core state (migration 0023 `squad_managers`), resolved to an
|
||||||
pub manager: Vec<WireItemRef>,
|
// `owned_card_id` on the ProposedSquad — never a dangling opaque wire ref.
|
||||||
/// Kicktaker role refs, opaque (see [`KicktakerRef`]).
|
/// Kicktaker role refs, opaque (see [`KicktakerRef`]).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub kicktakers: Vec<KicktakerRef>,
|
pub kicktakers: Vec<KicktakerRef>,
|
||||||
@@ -132,7 +123,6 @@ impl Fifa17SquadExtensionV1 {
|
|||||||
custom: put.custom.clone(),
|
custom: put.custom.clone(),
|
||||||
squad_type: put.squad_type.clone(),
|
squad_type: put.squad_type.clone(),
|
||||||
kit_numbers,
|
kit_numbers,
|
||||||
manager: put.manager.iter().map(WireItemRef::from).collect(),
|
|
||||||
kicktakers: put
|
kicktakers: put
|
||||||
.kicktakers
|
.kicktakers
|
||||||
.iter()
|
.iter()
|
||||||
@@ -256,6 +246,9 @@ mod tests {
|
|||||||
let ids = [
|
let ids = [
|
||||||
100000003, 100000010, 100000005, 100000008, 100000007, 100000006, 100000004, 100000009,
|
100000003, 100000010, 100000005, 100000008, 100000007, 100000006, 100000004, 100000009,
|
||||||
100000001, 100000002, 100000025,
|
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())
|
MapResolver(ids.iter().map(|&w| (w, format!("oc-{w}"))).collect())
|
||||||
}
|
}
|
||||||
@@ -308,22 +301,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manager_and_kicktakers_preserved_opaquely() {
|
fn manager_is_canonical_and_kicktakers_stay_opaque() {
|
||||||
let ext = built().extension;
|
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!(
|
assert_eq!(
|
||||||
ext.manager,
|
build.canonical.manager_owned_card_id.as_deref(),
|
||||||
vec![WireItemRef {
|
Some("oc-100000427")
|
||||||
id: 100000427,
|
|
||||||
dream: false
|
|
||||||
}]
|
|
||||||
);
|
);
|
||||||
|
// Kicktakers remain opaque in the extension.
|
||||||
|
let ext = build.extension;
|
||||||
assert_eq!(ext.kicktakers.len(), 5);
|
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!(ext.kicktakers.iter().all(|k| k.item.id == 100000001));
|
||||||
assert_eq!(ext.kicktakers[0].index, 0);
|
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]
|
#[test]
|
||||||
fn unknown_schema_version_is_rejected_not_coerced() {
|
fn unknown_schema_version_is_rejected_not_coerced() {
|
||||||
let payload = built().extension.to_payload();
|
let payload = built().extension.to_payload();
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ pub struct SquadProjectionInput<'a> {
|
|||||||
/// Every owned item a slot references, keyed by `owned_card_id`. Assembled by
|
/// Every owned item a slot references, keyed by `owned_card_id`. Assembled by
|
||||||
/// the host in one batch — the projector only reads from it.
|
/// the host in one batch — the projector only reads from it.
|
||||||
pub owned: &'a HashMap<String, CoreOwnedItem>,
|
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.
|
/// 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!({
|
let squad = json!({
|
||||||
"id": input.fifa_squad_id,
|
"id": input.fifa_squad_id,
|
||||||
"squadName": input.name,
|
"squadName": input.name,
|
||||||
@@ -180,7 +196,7 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
|||||||
"starRating": ext.client_reported.star_rating,
|
"starRating": ext.client_reported.star_rating,
|
||||||
"rating": ext.client_reported.rating,
|
"rating": ext.client_reported.rating,
|
||||||
"captain": captain_wire,
|
"captain": captain_wire,
|
||||||
"manager": ext.manager,
|
"manager": manager,
|
||||||
"custom": ext.custom,
|
"custom": ext.custom,
|
||||||
"players": players,
|
"players": players,
|
||||||
"kicktakers": ext.kicktakers,
|
"kicktakers": ext.kicktakers,
|
||||||
@@ -262,6 +278,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
ext,
|
ext,
|
||||||
owned,
|
owned,
|
||||||
|
manager: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +289,6 @@ mod tests {
|
|||||||
custom: Some("[1,2,3]".into()),
|
custom: Some("[1,2,3]".into()),
|
||||||
squad_type: Some("REGULAR_SQUAD".into()),
|
squad_type: Some("REGULAR_SQUAD".into()),
|
||||||
kit_numbers: kit,
|
kit_numbers: kit,
|
||||||
manager: vec![],
|
|
||||||
kicktakers: vec![],
|
kicktakers: vec![],
|
||||||
client_reported: Default::default(),
|
client_reported: Default::default(),
|
||||||
}
|
}
|
||||||
@@ -403,6 +419,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
ext: SquadExtInput::Fresh(ext),
|
ext: SquadExtInput::Fresh(ext),
|
||||||
owned: owned_ref,
|
owned: owned_ref,
|
||||||
|
manager: None,
|
||||||
};
|
};
|
||||||
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
||||||
panic!();
|
panic!();
|
||||||
@@ -423,4 +440,65 @@ mod tests {
|
|||||||
"kit stays with the instance"
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,15 @@ pub fn plan_apply(
|
|||||||
.and_then(|s| s.get(0))
|
.and_then(|s| s.get(0))
|
||||||
.context("report says a squad is present but profile has no squads[0]")?;
|
.context("report says a squad is present but profile has no squads[0]")?;
|
||||||
let body = serde_json::to_vec(raw_squad).context("re-serialize source squad")?;
|
let body = serde_json::to_vec(raw_squad).context("re-serialize source squad")?;
|
||||||
let put = parse_squad_put(&body).map_err(|e| anyhow::anyhow!("parse source squad: {e}"))?;
|
let mut put =
|
||||||
|
parse_squad_put(&body).map_err(|e| anyhow::anyhow!("parse source squad: {e}"))?;
|
||||||
|
// A HISTORICAL profile may reference a manager whose owned instance is
|
||||||
|
// not imported (unsupported/deferred, or a dangling id with no owned
|
||||||
|
// item at all). Drop such a manager ref here rather than failing the
|
||||||
|
// whole import — the manager assignment is only imported when its owned
|
||||||
|
// instance is. (A LIVE squad PUT still refuses an unresolved manager,
|
||||||
|
// because the client is actively assigning one it must own.)
|
||||||
|
put.manager.retain(|m| wire_to_owned.contains_key(&m.id));
|
||||||
let build = build_squad_write(&put, &MapResolver(&wire_to_owned))
|
let build = build_squad_write(&put, &MapResolver(&wire_to_owned))
|
||||||
.map_err(|e| anyhow::anyhow!("build squad write: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("build squad write: {e}"))?;
|
||||||
let formation = build
|
let formation = build
|
||||||
|
|||||||
Reference in New Issue
Block a user