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;
|
/// Disabled by default. Non-off values require three explicit staging guards;
|
||||||
/// see [`parse_sbc_post_commit_fault`].
|
/// see [`parse_sbc_post_commit_fault`].
|
||||||
pub sbc_post_commit_fault: SbcPostCommitFault,
|
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)]
|
#[derive(Debug)]
|
||||||
@@ -177,6 +193,15 @@ impl HostConfig {
|
|||||||
clientdata_path,
|
clientdata_path,
|
||||||
account_path,
|
account_path,
|
||||||
sbc_post_commit_fault,
|
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 core: &'a dyn CoreAccess,
|
||||||
pub resolver: &'a Fifa17IdentityResolver,
|
pub resolver: &'a Fifa17IdentityResolver,
|
||||||
pub entities: &'a Fifa17Entities,
|
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.
|
/// Secret-free structured log line for a handled squad request.
|
||||||
@@ -2750,23 +2752,33 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
|||||||
manager,
|
manager,
|
||||||
};
|
};
|
||||||
// The active club designations Core already owns (`/club/active-items`), shaped
|
// 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
|
// into the `actives` array that makes a club item resident.
|
||||||
// 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,
|
// DEFAULT OFF (`HostConfig::squad_actives`). Populating this array does make
|
||||||
// because silently empty actives is precisely the failure that left the
|
// the kits resident and the pre-match selector work, but it was also observed
|
||||||
// pre-match kit selector blank.
|
// to cost the rest of the squad: the resident item map fell from 24 entries to
|
||||||
let actives = match deps.core.get_active_kits() {
|
// just the 2 kits, the 23-slot player vector came back fully null, and the
|
||||||
Ok(assignments) => squad_actives(
|
// starting-11 screen was empty. `actives` sorts first in the squad object, so
|
||||||
&owned_by_id,
|
// everything after it is lost. Until that is understood an empty squad is far
|
||||||
deps.resolver,
|
// worse than a missing kit.
|
||||||
ActiveKitAssignments {
|
let actives = if !deps.squad_actives {
|
||||||
home: assignments.home_owned_card_id.as_deref(),
|
json!([])
|
||||||
away: assignments.away_owned_card_id.as_deref(),
|
} else {
|
||||||
},
|
match deps.core.get_active_kits() {
|
||||||
),
|
Ok(assignments) => squad_actives(
|
||||||
Err(error) => {
|
&owned_by_id,
|
||||||
eprintln!("utas-host WARN active club items unavailable, squad.actives empty: {error}");
|
deps.resolver,
|
||||||
json!([])
|
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) {
|
match project_squad(&input, deps.resolver, deps.entities) {
|
||||||
@@ -3557,6 +3569,8 @@ pub struct Server {
|
|||||||
economy_gate: Arc<Mutex<()>>,
|
economy_gate: Arc<Mutex<()>>,
|
||||||
/// Staging-only simulation of losing the successful SBC submit receipt.
|
/// Staging-only simulation of losing the successful SBC submit receipt.
|
||||||
sbc_post_commit_fault: SbcPostCommitFault,
|
sbc_post_commit_fault: SbcPostCommitFault,
|
||||||
|
/// Emit `squad.actives`. Default OFF; see `HostConfig::squad_actives`.
|
||||||
|
squad_actives: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@@ -3581,6 +3595,7 @@ impl Server {
|
|||||||
clientdata: Arc::new(ClientDataStore::open(ephemeral_clientdata_path())),
|
clientdata: Arc::new(ClientDataStore::open(ephemeral_clientdata_path())),
|
||||||
economy_gate: Arc::new(Mutex::new(())),
|
economy_gate: Arc::new(Mutex::new(())),
|
||||||
sbc_post_commit_fault: SbcPostCommitFault::Off,
|
sbc_post_commit_fault: SbcPostCommitFault::Off,
|
||||||
|
squad_actives: false,
|
||||||
current_match: Arc::new(PlMutex::new(None)),
|
current_match: Arc::new(PlMutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3649,6 +3664,10 @@ impl Server {
|
|||||||
"utas-host sbc_post_commit_fault={:?}",
|
"utas-host sbc_post_commit_fault={:?}",
|
||||||
cfg.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(
|
Ok(Server::new(
|
||||||
core,
|
core,
|
||||||
entities,
|
entities,
|
||||||
@@ -3659,7 +3678,8 @@ impl Server {
|
|||||||
.with_economy(economy)
|
.with_economy(economy)
|
||||||
.with_clientdata(clientdata)
|
.with_clientdata(clientdata)
|
||||||
.with_account(account)
|
.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
|
/// Assemble the shared squad dependencies (Core access + the one production
|
||||||
@@ -3669,6 +3689,7 @@ impl Server {
|
|||||||
core: self.core.as_ref(),
|
core: self.core.as_ref(),
|
||||||
resolver: self.resolver.as_ref(),
|
resolver: self.resolver.as_ref(),
|
||||||
entities: self.entities.as_ref(),
|
entities: self.entities.as_ref(),
|
||||||
|
squad_actives: self.squad_actives,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3694,6 +3715,14 @@ impl Server {
|
|||||||
self
|
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 {
|
fn with_sbc_post_commit_fault(mut self, fault: SbcPostCommitFault) -> Self {
|
||||||
self.sbc_post_commit_fault = fault;
|
self.sbc_post_commit_fault = fault;
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -902,6 +902,7 @@ fn from_config(base: &str, dir: &std::path::Path) -> openfut_utas_host::config::
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
sbc_post_commit_fault: openfut_utas_host::config::SbcPostCommitFault::Off,
|
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,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1192,6 +1193,7 @@ fn put_assigns_the_owned_manager_and_a_later_save_clears_it() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let with_manager = put_body(
|
let with_manager = put_body(
|
||||||
@@ -1234,6 +1236,7 @@ fn put_saves_the_squad_when_the_manager_ref_does_not_resolve() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
@@ -1279,6 +1282,7 @@ fn put_rejects_wire_id_owned_by_another_profile_core_unchanged() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1305,6 +1309,7 @@ fn put_rejects_unknown_wire_id() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
// 999_999_999 was never allocated → unresolvable.
|
// 999_999_999 was never allocated → unresolvable.
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
@@ -1330,6 +1335,7 @@ fn put_rejects_duplicate_owned_item() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1355,6 +1361,7 @@ fn put_core_failure_returns_error_never_python() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1)], "[]", None);
|
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1)], "[]", None);
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
@@ -1372,6 +1379,7 @@ fn repeated_identical_put_is_idempotent() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1408,6 +1416,7 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1513,6 +1522,7 @@ fn squad_active_serves_core_backed_object_with_configured_persona() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
// Commit a squad so Core has a fresh canonical squad + extension.
|
// Commit a squad so Core has a fresh canonical squad + extension.
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
@@ -1560,6 +1570,7 @@ fn read_path_is_bounded_no_per_slot_lookup() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
@@ -1614,6 +1625,7 @@ fn stale_extension_is_not_applied_on_reads() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let (resp, log) = handle_squad_list(&deps);
|
let (resp, log) = handle_squad_list(&deps);
|
||||||
assert_eq!(log.outcome, "stale_integrity");
|
assert_eq!(log.outcome, "stale_integrity");
|
||||||
@@ -1635,6 +1647,7 @@ fn missing_extension_is_explicit_on_reads() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let (resp, log) = handle_squad_list(&deps);
|
let (resp, log) = handle_squad_list(&deps);
|
||||||
assert_eq!(log.outcome, "missing_integrity");
|
assert_eq!(log.outcome, "missing_integrity");
|
||||||
@@ -1656,6 +1669,7 @@ fn usermassinfo_never_serves_python_squad_on_integrity_failure() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let py_body = json!({
|
let py_body = json!({
|
||||||
"userInfo": {"personaId": 7},
|
"userInfo": {"personaId": 7},
|
||||||
@@ -1730,6 +1744,7 @@ fn duplicate_definition_instances_stay_distinct_through_host() {
|
|||||||
core: &core,
|
core: &core,
|
||||||
resolver: &resolver,
|
resolver: &resolver,
|
||||||
entities: &ent,
|
entities: &ent,
|
||||||
|
squad_actives: false,
|
||||||
};
|
};
|
||||||
let body = put_body(
|
let body = put_body(
|
||||||
"f442",
|
"f442",
|
||||||
|
|||||||
Reference in New Issue
Block a user