diff --git a/openfut-utas-host/src/config.rs b/openfut-utas-host/src/config.rs index 184caf8..4847a49 100644 --- a/openfut-utas-host/src/config.rs +++ b/openfut-utas-host/src/config.rs @@ -59,6 +59,22 @@ pub struct HostConfig { /// Disabled by default. Non-off values require three explicit staging guards; /// see [`parse_sbc_post_commit_fault`]. pub sbc_post_commit_fault: SbcPostCommitFault, + /// Emit the club's active club items in `squad.actives`. **Disabled by + /// default.** + /// + /// `actives` is the proven carrier that makes a kit resident (the squad + /// parser writes each element straight into a club-item slot), and with it + /// populated the pre-match kit selector works. But a non-empty array was + /// observed to cost the rest of the squad: the resident item map dropped from + /// 24 entries to just the 2 kits, the 23-slot player vector came back fully + /// null, and the starting-11 screen was empty. `actives` sorts first in the + /// object, so everything after it — `players`, `manager`, `captain`, + /// `formation` — is lost. + /// + /// Off until that interaction is understood: an empty squad is far worse than + /// a missing kit. Env `OPENFUT_FIFA17_SQUAD_ACTIVES=1` to experiment on + /// staging. + pub squad_actives: bool, } #[derive(Debug)] @@ -177,6 +193,15 @@ impl HostConfig { clientdata_path, account_path, sbc_post_commit_fault, + // Default OFF: see the field docs. Only an explicit 1/true/on enables it. + squad_actives: matches!( + env::var("OPENFUT_FIFA17_SQUAD_ACTIVES") + .unwrap_or_default() + .trim() + .to_ascii_lowercase() + .as_str(), + "1" | "true" | "on" | "yes" + ), }) } } diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index ce526d8..abcacba 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -2652,6 +2652,8 @@ pub struct SquadDeps<'a> { pub core: &'a dyn CoreAccess, pub resolver: &'a Fifa17IdentityResolver, pub entities: &'a Fifa17Entities, + /// Emit `squad.actives`. Default OFF — see [`crate::config::HostConfig`]. + pub squad_actives: bool, } /// Secret-free structured log line for a handled squad request. @@ -2750,23 +2752,33 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection { manager, }; // The active club designations Core already owns (`/club/active-items`), shaped - // into the `actives` array the client needs to make a club item resident. A - // transport error here is non-fatal and yields no actives: the squad still - // renders, exactly as it did before this array was populated. It is reported, - // because silently empty actives is precisely the failure that left the - // pre-match kit selector blank. - let actives = match deps.core.get_active_kits() { - Ok(assignments) => squad_actives( - &owned_by_id, - deps.resolver, - ActiveKitAssignments { - home: assignments.home_owned_card_id.as_deref(), - away: assignments.away_owned_card_id.as_deref(), - }, - ), - Err(error) => { - eprintln!("utas-host WARN active club items unavailable, squad.actives empty: {error}"); - json!([]) + // into the `actives` array that makes a club item resident. + // + // DEFAULT OFF (`HostConfig::squad_actives`). Populating this array does make + // the kits resident and the pre-match selector work, but it was also observed + // to cost the rest of the squad: the resident item map fell from 24 entries to + // just the 2 kits, the 23-slot player vector came back fully null, and the + // starting-11 screen was empty. `actives` sorts first in the squad object, so + // everything after it is lost. Until that is understood an empty squad is far + // worse than a missing kit. + let actives = if !deps.squad_actives { + json!([]) + } else { + match deps.core.get_active_kits() { + Ok(assignments) => squad_actives( + &owned_by_id, + deps.resolver, + ActiveKitAssignments { + home: assignments.home_owned_card_id.as_deref(), + away: assignments.away_owned_card_id.as_deref(), + }, + ), + Err(error) => { + eprintln!( + "utas-host WARN active club items unavailable, squad.actives empty: {error}" + ); + json!([]) + } } }; match project_squad(&input, deps.resolver, deps.entities) { @@ -3557,6 +3569,8 @@ pub struct Server { economy_gate: Arc>, /// Staging-only simulation of losing the successful SBC submit receipt. sbc_post_commit_fault: SbcPostCommitFault, + /// Emit `squad.actives`. Default OFF; see `HostConfig::squad_actives`. + squad_actives: bool, } impl Server { @@ -3581,6 +3595,7 @@ impl Server { clientdata: Arc::new(ClientDataStore::open(ephemeral_clientdata_path())), economy_gate: Arc::new(Mutex::new(())), sbc_post_commit_fault: SbcPostCommitFault::Off, + squad_actives: false, current_match: Arc::new(PlMutex::new(None)), } } @@ -3649,6 +3664,10 @@ impl Server { "utas-host sbc_post_commit_fault={:?}", cfg.sbc_post_commit_fault ); + eprintln!( + "utas-host squad_actives={} (OFF keeps squad.actives empty; ON makes kits resident but has been seen to empty the squad)", + cfg.squad_actives + ); Ok(Server::new( core, entities, @@ -3659,7 +3678,8 @@ impl Server { .with_economy(economy) .with_clientdata(clientdata) .with_account(account) - .with_sbc_post_commit_fault(cfg.sbc_post_commit_fault)) + .with_sbc_post_commit_fault(cfg.sbc_post_commit_fault) + .with_squad_actives(cfg.squad_actives)) } /// Assemble the shared squad dependencies (Core access + the one production @@ -3669,6 +3689,7 @@ impl Server { core: self.core.as_ref(), resolver: self.resolver.as_ref(), entities: self.entities.as_ref(), + squad_actives: self.squad_actives, } } @@ -3694,6 +3715,14 @@ impl Server { self } + /// Emit `squad.actives`. Default OFF: a populated array makes the kits + /// resident but was observed to cost the rest of the squad (see + /// `HostConfig::squad_actives`). + fn with_squad_actives(mut self, on: bool) -> Self { + self.squad_actives = on; + self + } + fn with_sbc_post_commit_fault(mut self, fault: SbcPostCommitFault) -> Self { self.sbc_post_commit_fault = fault; self diff --git a/openfut-utas-host/tests/economy_integration.rs b/openfut-utas-host/tests/economy_integration.rs index 8bc9035..5ee95b6 100644 --- a/openfut-utas-host/tests/economy_integration.rs +++ b/openfut-utas-host/tests/economy_integration.rs @@ -902,6 +902,7 @@ fn from_config(base: &str, dir: &std::path::Path) -> openfut_utas_host::config:: .to_string_lossy() .into_owned(), sbc_post_commit_fault: openfut_utas_host::config::SbcPostCommitFault::Off, + squad_actives: false, } } diff --git a/openfut-utas-host/tests/host_test.rs b/openfut-utas-host/tests/host_test.rs index e18844f..59f74a4 100644 --- a/openfut-utas-host/tests/host_test.rs +++ b/openfut-utas-host/tests/host_test.rs @@ -1150,6 +1150,7 @@ fn put_full_replacement_commits_canonical_and_extension_and_acks_id0() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1192,6 +1193,7 @@ fn put_assigns_the_owned_manager_and_a_later_save_clears_it() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let with_manager = put_body( @@ -1234,6 +1236,7 @@ fn put_saves_the_squad_when_the_manager_ref_does_not_resolve() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( @@ -1279,6 +1282,7 @@ fn put_rejects_wire_id_owned_by_another_profile_core_unchanged() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1305,6 +1309,7 @@ fn put_rejects_unknown_wire_id() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; // 999_999_999 was never allocated → unresolvable. let body = put_body( @@ -1330,6 +1335,7 @@ fn put_rejects_duplicate_owned_item() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1355,6 +1361,7 @@ fn put_core_failure_returns_error_never_python() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1)], "[]", None); let (resp, log) = handle_put_squad(&body, &deps); @@ -1372,6 +1379,7 @@ fn repeated_identical_put_is_idempotent() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1408,6 +1416,7 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1513,6 +1522,7 @@ fn squad_active_serves_core_backed_object_with_configured_persona() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; // Commit a squad so Core has a fresh canonical squad + extension. let body = put_body( @@ -1560,6 +1570,7 @@ fn read_path_is_bounded_no_per_slot_lookup() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442", @@ -1614,6 +1625,7 @@ fn stale_extension_is_not_applied_on_reads() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let (resp, log) = handle_squad_list(&deps); assert_eq!(log.outcome, "stale_integrity"); @@ -1635,6 +1647,7 @@ fn missing_extension_is_explicit_on_reads() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let (resp, log) = handle_squad_list(&deps); assert_eq!(log.outcome, "missing_integrity"); @@ -1656,6 +1669,7 @@ fn usermassinfo_never_serves_python_squad_on_integrity_failure() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let py_body = json!({ "userInfo": {"personaId": 7}, @@ -1730,6 +1744,7 @@ fn duplicate_definition_instances_stay_distinct_through_host() { core: &core, resolver: &resolver, entities: &ent, + squad_actives: false, }; let body = put_body( "f442",