fix(fifa17): disable squad.actives by default after it emptied the squad
Populating `squad.actives` is the proven way to make a club item resident — the squad parser writes each element straight into a club-item slot, both kits came back resident with the client writing `category 4` itself, and the pre-match kit selector worked. But a populated array was then observed to cost the rest of the squad. On a full client relaunch 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; the manager and staff were gone too. Every host response was 200/outcome=ok with no warning, so the loss is entirely client-side parse behaviour. `actives` sorts first in the squad object, so `captain`, `formation`, `manager`, `players` and everything else after it are lost — consistent with the element loop leaving the tokenizer misaligned. The same payload produced a correct 24-node map on an earlier relaunch, so the interaction is not yet understood and is not safely shippable. An empty squad is far worse than a missing kit, so the array is now gated behind `HostConfig::squad_actives` (env `OPENFUT_FIFA17_SQUAD_ACTIVES`) and off by default. The projection, identity plumbing and tests are kept intact: they are correct and are what the investigation will re-enable. Staging redeployed with the flag off and verified back to 23/23 populated player slots and `actives: []`.
This commit is contained in:
@@ -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"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Mutex<()>>,
|
||||
/// 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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user