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:
funman300
2026-08-24 18:53:28 +00:00
parent 98f30931a0
commit d929efdffe
4 changed files with 88 additions and 18 deletions
@@ -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,
}
}
+15
View File
@@ -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",