host: answer club?type=equippables by default — kits now render in My Club

OPERATOR-CONFIRMED on the retail client: the My Club kit tab shows both kits, the
first time kits have ever rendered in this project.

The tab asks for `?type=equippables`, and that arm was withheld, so the screen got
an empty body and showed nothing. That is exactly what "0 kits in my club" was —
not a shaping problem, a refused question. Log line that identified it:

    route=club outcome=withheld filter=[type=equippables,
                    reason=multi_family_crash_2026_08_05] total=0 emitted=0

TWO fixes were both required, so neither alone is the cause:
  * this arm answers (KITS ONLY), and
  * GET ut/%s/item (FutViewCards) returns the OWNED INSTANCE rather than a
    definition placeholder, so the kit arrives as cardsubtypeid 9 /
    itemState active{Home,Away}Kit instead of "a free player" (d4a39ba).

The 2026-08-05 crash that motivated the withholding was 30 items across FIVE
families. This arm serves one family, and the live body is two items. Serving the
other four families here is untested and stays unserved.

Default flips from opt-in to opt-out: OPENFUT_FIFA17_EQUIPPABLES=0 restores the
withheld body with a restart and no rebuild. The switch is kept rather than
deleted because the only live evidence is a club holding exactly TWO kits; a club
holding many is untested and the original crash was about size and family mixing.

Verified with no env flag set: ?type=equippables -> 2 items, family {9}.
cargo test -p openfut-utas-host: 190 passed, 0 failed. clippy -D warnings clean.
Production untouched — staging only.
This commit is contained in:
funman300
2026-08-23 21:34:05 +00:00
parent d4a39ba75d
commit d146f9c3fc
+35 -32
View File
@@ -2339,34 +2339,38 @@ fn club_type_filter(token: Option<&str>) -> Option<ClubTypeFilter> {
Some("stadium") => ClubTypeFilter::kind("stadium", ContentKind::Stadium), Some("stadium") => ClubTypeFilter::kind("stadium", ContentKind::Stadium),
Some("ball") => ClubTypeFilter::kind("ball", ContentKind::Ball), Some("ball") => ClubTypeFilter::kind("ball", ContentKind::Ball),
Some("misc") => ClubTypeFilter::kind("misc", ContentKind::Misc), Some("misc") => ClubTypeFilter::kind("misc", ContentKind::Misc),
// WITHHELD, each for a recorded reason.
Some("equippables") => ClubTypeFilter { Some("equippables") => ClubTypeFilter {
label: "equippables", label: "equippables",
// The combined customisation view, and the one response that has ever // The combined club-customisation view, and the screen the My Club
// crashed this client: 30 items across five families at once // KITS tab actually asks for. It is ALSO the one response that has
// (2026-08-05). // ever crashed this client — but the crash was 30 items across five
// families at once (2026-08-05), not this arm.
// //
// 2026-08-24: this is also the prime suspect for the LOCKED kit. A // RESOLVED 2026-08-23, operator-confirmed on the retail client: with
// live `kit_trace` run showed the kit clone driver only ever sees // this arm answering KITS ONLY, the My Club kit tab renders both kits
// PLAYERS (~19 records, cardtype 1 / itemState 1 / `+0x60` 1) and // for the first time. Before it, the tab asked `?type=equippables`,
// never a kit, with no `KIT_DBCLONE` at all — so the failure is // got the withheld empty body, and showed nothing — which is exactly
// upstream of the `item+0x60 == 4` gate. In the same session the // what "0 kits in my club" was.
// client asked for `?type=kit` (6x, which we answer and which feeds
// the items BROWSER) and `?type=equippables` (2x, which we answer
// empty). If the equippable view is what populates the collection
// `FUN_1800d73d0` scans for the active-kit triple, an empty answer
// is exactly why the triple stays zero and the engine falls back to
// its own catalogue kit.
// //
// So the arm is now selectable behind `OPENFUT_FIFA17_EQUIPPABLES=1` // Two things had to be true together, so neither alone is the fix:
// and answers with KITS ONLY — two items, not the thirty across five // * this arm answers, and
// families that crashed the client. Default OFF: the crash is real // * `GET ut/%s/item` (FutViewCards) returns the OWNED INSTANCE
// and reproducible, and this narrower body is a hypothesis under // rather than a definition placeholder, so the kit arrives with
// test, not an established safe response. // `cardsubtypeid` 9 and `itemState` active{Home,Away}Kit instead
selector: if equippables_enabled() { // of "a free player" (see [`UtasHost::handle_view_cards`]).
ClubSelector::Kind(ContentKind::Kit) //
} else { // Still KITS ONLY, deliberately. Serving the other four families here
// is the shape that crashed, and nothing has retested it.
//
// `OPENFUT_FIFA17_EQUIPPABLES=0` restores the withheld body with a
// restart and no rebuild, because the ONLY live evidence is a club
// holding exactly TWO kits. A club holding many is untested, and the
// crash that motivated the original withholding was about response
// size and family mixing.
selector: if equippables_disabled() {
ClubSelector::Withheld("multi_family_crash_2026_08_05") ClubSelector::Withheld("multi_family_crash_2026_08_05")
} else {
ClubSelector::Kind(ContentKind::Kit)
}, },
}, },
Some("leaguelogos") => ClubTypeFilter { Some("leaguelogos") => ClubTypeFilter {
@@ -5717,16 +5721,15 @@ fn discard_table_enabled() -> bool {
*ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_DISCARD_TABLE").as_deref() == Ok("1")) *ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_DISCARD_TABLE").as_deref() == Ok("1"))
} }
/// Answer `club?type=equippables` with a KITS-ONLY body. /// Withhold `club?type=equippables` again (`OPENFUT_FIFA17_EQUIPPABLES=0`).
/// ///
/// Default OFF. The multi-family form of this response crashed the client on /// Default is now ON: the My Club kit tab requests this family, and answering it
/// 2026-08-05, so serving it at all is an experiment: the narrow two-kit body is /// with a KITS-ONLY body is what made both kits render on the retail client
/// a hypothesis about why the pre-match kit selector reports every kit locked, /// (operator-confirmed 2026-08-23). This is the OFF switch, kept because the only
/// not a response we have established as safe. Flip the env var off to revert /// live evidence is a club holding exactly two kits.
/// with a restart and no rebuild. fn equippables_disabled() -> bool {
fn equippables_enabled() -> bool { static DISABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new(); *DISABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_EQUIPPABLES").as_deref() == Ok("0"))
*ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_EQUIPPABLES").as_deref() == Ok("1"))
} }
/// A JSON response with an explicit status. /// A JSON response with an explicit status.