diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index c72278b..6e594e1 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -2382,6 +2382,73 @@ fn club_type_filter(token: Option<&str>) -> Option { Some(filter) } +/// DIAGNOSTIC, `OPENFUT_FIFA17_KIT_PROBE=1`, staging only, default OFF. +/// +/// Appends two synthetic kits to `?type=kit` so one client restart can settle +/// which of the two remaining kit-ingest hypotheses is right. It answers a +/// question no static reading has: the client is served two kits, returns +/// `total=2 emitted=2`, and yet NO cardtype-7 record is ever resident (measured +/// read-only over 3.6 GB of process memory, while player and staff records from +/// the same response family ARE resident). +/// +/// Two shapes, because two things could be rejecting the item: +/// +/// * `6300007` MINIMAL — exactly the field set a STAFF item carries, which is +/// known to ingest, plus `cardsubtypeid` 9. If only this one appears, one of +/// the kit-only extras (`assetId`, `cardassetid`, `teamid`, `category`, +/// `year`) is what makes the client discard the item. +/// * `6300008` NAMED — the full kit shape plus `name`/`localizedName`/ +/// `description`. The cardtype-7 parse arm is documented to copy exactly those +/// three, and OpenFUT sends none of them. If only this one appears, they are +/// required rather than optional. +/// +/// If NEITHER appears, `?type=kit` is not the route that populates the +/// collection `FUN_1800d73d0` scans, and the search moves to which route does. +/// +/// Both ids are real `fcc_kitcards` carddbids for team 21 (`6300007` year 1972 +/// category 2, `6300008` year 0 category 5), so nothing here invents an id the +/// client cannot resolve. They are served `free`, never active, so they cannot +/// disturb the real active-kit assignment. Instance ids are outside Core's +/// range so they can never collide with an owned row. +/// +/// Deliberately small: the one response that has ever crashed this client was 30 +/// items across five families (2026-08-05). This is two items in one family. +fn append_kit_shape_probe(label: &str, items: &mut Vec) { + if label != "kit" || !kit_probe_enabled() { + return; + } + items.push(json!({ + "id": 100009007, + "resourceId": 6300007, + "cardsubtypeid": 9, + "itemType": "kit", + "itemState": "free", + "owners": 1, + "untradeable": false, + })); + items.push(json!({ + "id": 100009008, + "resourceId": 6300008, + "assetId": 6300008, + "cardassetid": 35, + "cardsubtypeid": 9, + "itemType": "kit", + "itemState": "free", + "owners": 1, + "untradeable": false, + "teamid": 21, + "category": 5, + "year": 0, + "name": "OpenFUT Probe Kit", + "localizedName": "OpenFUT Probe Kit", + "description": "kit ingest probe", + })); + eprintln!( + "utas-host owner=RUST route=club KIT_PROBE armed: appended 6300007 (minimal) \ + and 6300008 (named) — diagnostic only" + ); +} + /// Filter already-shaped `/club` items to SPECIALS (`rareflag > 1`) and paginate /// the filtered set locally. Returns `(page, total_specials)`. Pure — the whole /// point is that "special" pagination is over the filtered set, never Core's @@ -2548,11 +2615,12 @@ pub fn handle_club(query: &str, deps: &ClubDeps<'_>) -> (WireResponse, ClubLog) }; let (body, stats) = shape_club_response_with_kits(&visible, deps.entities, deps.assets, active); - let all = body + let mut all = body .get("itemData") .and_then(Value::as_array) .cloned() .unwrap_or_default(); + append_kit_shape_probe(filter_arm.label, &mut all); let (paged, total) = if core_q.special { special_filter_page(&all, offset, limit) } else { @@ -5601,6 +5669,15 @@ fn equippables_enabled() -> bool { *ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_EQUIPPABLES").as_deref() == Ok("1")) } +/// Append the two diagnostic kits described on [`append_kit_shape_probe`]. +/// +/// Default OFF, staging only. Flip the env var off to revert with a restart and +/// no rebuild. +fn kit_probe_enabled() -> bool { + static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); + *ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_KIT_PROBE").as_deref() == Ok("1")) +} + /// A JSON response with an explicit status. fn json_status(status: u16, v: &Value) -> WireResponse { let body = serde_json::to_vec(v).unwrap_or_default();