diff --git a/crates/rrt-runtime/src/inspect/engine_types.rs b/crates/rrt-runtime/src/inspect/engine_types.rs index 9ecd171..c5d555b 100644 --- a/crates/rrt-runtime/src/inspect/engine_types.rs +++ b/crates/rrt-runtime/src/inspect/engine_types.rs @@ -168,6 +168,7 @@ pub struct EngineTypesInspectionReport { pub car_side_view_resource_pk4_match_count: usize, pub car_side_view_resource_pk4_missing_count: usize, pub car_side_view_resource_pk4_missing_counts: BTreeMap, + pub car_side_view_resource_pk4_missing_families: Vec, pub car_auxiliary_stem_counts: BTreeMap, pub car_auxiliary_stem_relation_counts: BTreeMap, pub car_auxiliary_stem_distinct_pair_counts: BTreeMap, @@ -184,6 +185,7 @@ pub struct EngineTypesInspectionReport { pub internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap, pub lco_companion_stem_counts: BTreeMap, pub lco_body_type_label_counts: BTreeMap, + pub lco_companion_profile_family_stems: BTreeMap>, pub lco_low_cardinality_lane_counts: BTreeMap>, pub cgo_scalar_value_counts: BTreeMap, pub cgo_scalar_ladder_counts: BTreeMap, @@ -404,7 +406,7 @@ pub fn inspect_engine_types_dir( ); let car_side_view_resource_profile_summaries = side_view_imb_pk4_lookup .as_ref() - .map(|lookup| lookup.car_side_view_profile_summaries.clone()) + .map(|lookup| build_side_view_resource_profile_summaries(&family_entries, lookup)) .unwrap_or_default(); let car_side_view_resource_pk4_match_count = family_entries .iter() @@ -420,6 +422,8 @@ pub fn inspect_engine_types_dir( .filter(|family| family.side_view_resource_found_in_pk4 == Some(false)) .filter_map(|family| family.side_view_resource.as_deref()), ); + let car_side_view_resource_pk4_missing_families = + build_car_side_view_resource_pk4_missing_families(&family_entries); let car_auxiliary_stem_counts = count_named_values( family_entries .iter() @@ -523,6 +527,8 @@ pub fn inspect_engine_types_dir( .iter() .filter_map(|family| family.body_type_label.as_deref()), ); + let lco_companion_profile_family_stems = + build_lco_companion_profile_family_stems(&family_entries); let lco_low_cardinality_lane_counts = build_lco_low_cardinality_lane_counts(lco_reports.values()); let cgo_scalar_value_counts = count_owned_values( @@ -601,6 +607,7 @@ pub fn inspect_engine_types_dir( car_side_view_resource_pk4_match_count, car_side_view_resource_pk4_missing_count, car_side_view_resource_pk4_missing_counts, + car_side_view_resource_pk4_missing_families, car_auxiliary_stem_counts, car_auxiliary_stem_relation_counts, car_auxiliary_stem_distinct_pair_counts, @@ -617,6 +624,7 @@ pub fn inspect_engine_types_dir( internal_ne_profile_max_percent_of_interface_vram_counts, lco_companion_stem_counts, lco_body_type_label_counts, + lco_companion_profile_family_stems, lco_low_cardinality_lane_counts, cgo_scalar_value_counts, cgo_scalar_ladder_counts, @@ -642,7 +650,7 @@ struct EngineTypeFamilyBuilder { struct SideViewImbPk4Lookup { path: String, entry_names: BTreeSet, - car_side_view_profile_summaries: BTreeMap, + imb_profile_summaries_by_entry_name: BTreeMap, internal_ne_profile_texture_size_counts: BTreeMap, internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap, internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap, @@ -712,24 +720,22 @@ fn load_side_view_imb_pk4_lookup( let bytes = fs::read(&pk4_path)?; let inspection = inspect_pk4_file(&pk4_path)?; let mut entry_names = BTreeSet::new(); - let mut car_side_view_profile_summaries = BTreeMap::new(); + let mut imb_profile_summaries_by_entry_name = BTreeMap::new(); let mut internal_ne_profile_texture_size_counts = BTreeMap::new(); let mut internal_ne_profile_horizontal_scale_modifier_counts = BTreeMap::new(); let mut internal_ne_profile_max_percent_of_interface_vram_counts = BTreeMap::new(); for entry in inspection.entries { entry_names.insert(entry.name.clone()); - let is_car_side_view = entry.name.starts_with("CarSideView_"); + let is_imb_profile = entry.name.ends_with(".imb"); let is_internal_ne = entry.name.ends_with("_NE.imb"); - if !(is_car_side_view || is_internal_ne) { + if !is_imb_profile { continue; } let payload = &bytes[entry.payload_absolute_offset..entry.payload_end_offset]; let imb_profile = summarize_imb_profile(&inspect_imb_bytes(payload)?); - if is_car_side_view { - car_side_view_profile_summaries.insert(entry.name.clone(), imb_profile.clone()); - } + imb_profile_summaries_by_entry_name.insert(entry.name.clone(), imb_profile.clone()); if is_internal_ne { if let (Some(width), Some(height)) = (imb_profile.texture_width, imb_profile.texture_height) @@ -754,7 +760,7 @@ fn load_side_view_imb_pk4_lookup( Ok(Some(SideViewImbPk4Lookup { path: pk4_path.display().to_string(), entry_names, - car_side_view_profile_summaries, + imb_profile_summaries_by_entry_name, internal_ne_profile_texture_size_counts, internal_ne_profile_horizontal_scale_modifier_counts, internal_ne_profile_max_percent_of_interface_vram_counts, @@ -1110,6 +1116,61 @@ fn build_lco_low_cardinality_lane_counts<'a>( .collect() } +fn build_car_side_view_resource_pk4_missing_families( + families: &[EngineTypeFamilyEntry], +) -> Vec { + let mut stems = families + .iter() + .filter(|family| family.side_view_resource_found_in_pk4 == Some(false)) + .map(|family| family.canonical_stem.clone()) + .collect::>(); + stems.sort(); + stems +} + +fn build_lco_companion_profile_family_stems( + families: &[EngineTypeFamilyEntry], +) -> BTreeMap> { + let mut grouped = BTreeMap::>::new(); + for family in families { + let Some(companion_stem) = family.companion_stem.as_deref() else { + continue; + }; + let body_type_label = family.body_type_label.as_deref().unwrap_or("(none)"); + let side_view_resource = family.side_view_resource.as_deref().unwrap_or("(none)"); + grouped + .entry(format!( + "companion={companion_stem} / body={body_type_label} / side_view={side_view_resource}" + )) + .or_default() + .push(family.canonical_stem.clone()); + } + for stems in grouped.values_mut() { + stems.sort(); + } + grouped +} + +fn build_side_view_resource_profile_summaries( + families: &[EngineTypeFamilyEntry], + lookup: &SideViewImbPk4Lookup, +) -> BTreeMap { + let mut summaries = BTreeMap::new(); + for resource_name in families + .iter() + .filter_map(|family| family.side_view_resource.as_deref()) + { + let Some(summary) = lookup + .imb_profile_summaries_by_entry_name + .get(resource_name) + else { + continue; + }; + summaries.insert(resource_name.to_string(), summary.clone()); + } + summaries +} + #[cfg(test)] mod tests { use super::*; @@ -1438,6 +1499,122 @@ mod tests { ); } + #[test] + fn collects_missing_side_view_resource_family_stems() { + let missing = build_car_side_view_resource_pk4_missing_families(&[ + EngineTypeFamilyEntry { + canonical_stem: "zephyrl".to_string(), + side_view_resource_found_in_pk4: Some(false), + ..minimal_family_entry() + }, + EngineTypeFamilyEntry { + canonical_stem: "gp35l".to_string(), + side_view_resource_found_in_pk4: Some(false), + ..minimal_family_entry() + }, + EngineTypeFamilyEntry { + canonical_stem: "gp7".to_string(), + side_view_resource_found_in_pk4: Some(true), + ..minimal_family_entry() + }, + ]); + + assert_eq!(missing, vec!["gp35l".to_string(), "zephyrl".to_string()]); + } + + #[test] + fn groups_lco_companion_profiles_by_companion_body_and_side_view() { + let grouped = build_lco_companion_profile_family_stems(&[ + EngineTypeFamilyEntry { + canonical_stem: "gp35l".to_string(), + companion_stem: Some("Zephyr".to_string()), + side_view_resource: Some("CarSideView_3.imb".to_string()), + ..minimal_family_entry() + }, + EngineTypeFamilyEntry { + canonical_stem: "u1l".to_string(), + companion_stem: Some("Zephyr".to_string()), + side_view_resource: Some("CarSideView_3.imb".to_string()), + ..minimal_family_entry() + }, + EngineTypeFamilyEntry { + canonical_stem: "gp7".to_string(), + companion_stem: Some("VL80T".to_string()), + body_type_label: Some("Loco".to_string()), + side_view_resource: Some("CarSideView_1.imb".to_string()), + ..minimal_family_entry() + }, + ]); + + assert_eq!( + grouped.get("companion=Zephyr / body=(none) / side_view=CarSideView_3.imb"), + Some(&vec!["gp35l".to_string(), "u1l".to_string()]) + ); + assert_eq!( + grouped.get("companion=VL80T / body=Loco / side_view=CarSideView_1.imb"), + Some(&vec!["gp7".to_string()]) + ); + } + + #[test] + fn keeps_profile_summaries_for_all_referenced_side_view_resources() { + let summaries = build_side_view_resource_profile_summaries( + &[ + EngineTypeFamilyEntry { + side_view_resource: Some("CarSideView_1.imb".to_string()), + ..minimal_family_entry() + }, + EngineTypeFamilyEntry { + side_view_resource: Some("GP7D_Profile.imb".to_string()), + ..minimal_family_entry() + }, + ], + &SideViewImbPk4Lookup { + path: "rt3_2IMB.PK4".to_string(), + entry_names: BTreeSet::new(), + imb_profile_summaries_by_entry_name: BTreeMap::from([ + ( + "CarSideView_1.imb".to_string(), + EngineTypeImbProfileSummary { + tga_name: Some("CarSideView_1".to_string()), + texture_width: Some(512), + texture_height: Some(512), + target_screen_width: None, + target_screen_height: None, + horizontal_scale_modifier: None, + max_percent_of_interface_vram: Some(0.04), + image_rect_scaled: None, + }, + ), + ( + "GP7D_Profile.imb".to_string(), + EngineTypeImbProfileSummary { + tga_name: Some("GP7D_Profile".to_string()), + texture_width: Some(256), + texture_height: Some(128), + target_screen_width: None, + target_screen_height: None, + horizontal_scale_modifier: Some(0.75), + max_percent_of_interface_vram: Some(0.09), + image_rect_scaled: None, + }, + ), + ]), + internal_ne_profile_texture_size_counts: BTreeMap::new(), + internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap::new(), + internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap::new(), + }, + ); + + assert_eq!(summaries.len(), 2); + assert_eq!( + summaries + .get("GP7D_Profile.imb") + .and_then(|summary| summary.tga_name.as_deref()), + Some("GP7D_Profile") + ); + } + #[test] fn maps_cgo_content_stems_to_matching_cct_values() { let families = vec![ @@ -1559,4 +1736,27 @@ mod tests { assert_eq!(report.entries[0].primary_display_name, "2-D-2"); assert!(report.entries[0].matches_grounded_prefix_name); } + + fn minimal_family_entry() -> EngineTypeFamilyEntry { + EngineTypeFamilyEntry { + canonical_stem: "family".to_string(), + car_file: None, + lco_file: None, + cgo_file: None, + cct_file: None, + primary_display_name: None, + content_name: None, + internal_stem: None, + auxiliary_stem: None, + side_view_resource: None, + side_view_resource_found_in_pk4: None, + companion_stem: None, + body_type_label: None, + internal_ne_profile_name: None, + internal_ne_profile_found_in_pk4: None, + cct_identifier: None, + cct_value: None, + has_matched_locomotive_pair: false, + } + } } diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index a6a3f30..c361792 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -15,11 +15,13 @@ This file is the short active queue for the current runtime and reverse-engineer The checked 1.05 corpus now also splits `.car` auxiliary stems into `126` direct matches, `14` role-neutral roots, and only `5` truly distinct cases, with those five exact internal-to-auxiliary pairs now preserved directly in the report surface, while `.cgo` collapses into five stable scalar ladders instead of arbitrary floats. The early `.lco` lane block is now partially partitioned too: only offsets `0x20`, `0x34`, `0x38`, `0x3c`, `0x44`, `0x48`, and `0x54` behave like low-cardinality buckets, while the other early lanes still look high-variance. The side-view resource path is now grounded into `Data/2D/rt3_2IMB.PK4`, and the `.imb` parser now decodes shipped comment-suffixed numeric rows plus `_NE` profile fields such as `HorizontalScaleModifier` and `ImageWHScaled`. - The checked PK4 linkage split is now explicit too: `132 / 145` side-view resource names resolve directly, but the remaining `13` are the missing `CarSideView_3.imb` cohort and that hole exists in both checked installs, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs. + The checked PK4 linkage split is now explicit too: `132 / 145` side-view resource names resolve directly in 1.05, but the remaining `13` are the missing `CarSideView_3.imb` cohort and that hole exists in both checked installs, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs. + The parser now preserves that `CarSideView_3` miss cohort exactly, and it also preserves the tiny conditional `.lco` companion-profile seam directly: in 1.05 the padded `.lco` companion/body slots collapse to `Zephyr / (none) / CarSideView_3` for `242_a1_l`, `gp35l`, `u1l`, and `zephyrl`, and to `VL80T / Loco / CarSideView_{1,2}` for `be 5-7`, `f3 loco`, and `gp7`. + The classic install widens the same `0xc0` seam further: the side-view slot is not only `CarSideView_*` but also a larger unresolved `*_Profile.imb` family, and every checked classic `*_Profile.imb` reference currently misses the packaged `rt3_2IMB.PK4` surface too. The packaged profile metadata is stable enough to summarize: `CarSideView_1` is `512x512` at `0.04` VRAM, `CarSideView_2` is `512x256` at `0.02`, and every packaged `_NE` profile is `512x128` with `HorizontalScaleModifier = 0.75` and `MaxPercentOfInterfaceVRAM = 0.09`. The `_NE` split is now aligned with the locomotive display census too: all `43` packaged `_NE` hits live inside the grounded display prefix, and all `5` unmatched display-tail families are still missing packaged `_NE` profiles. The cargo side is partially linked now as well: the `.cgo` ladder families and `.cct` sidecar identifiers share the same cargo-family keys for ten checked families, with `Troop` left as the only `.cct`-only outlier. - The next honest static work is to keep promoting those fixed lanes into stable parser fields, explain the five remaining distinct auxiliary-stem cases, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting. + The next honest static work is to keep promoting those fixed lanes into stable parser fields, explain the five remaining distinct auxiliary-stem cases, decide whether the classic `*_Profile.imb` side-view references are dead loose-file dependencies or a still-unmapped package family, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting. Preserved checked parser detail now lives in [EngineTypes parser semantics](rehost-queue/engine-types-parser-semantics-2026-04-21.md). Preserved checked format inventory detail now lives in [RT3 format inventory](rehost-queue/format-inventory-2026-04-21.md). diff --git a/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md b/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md index f04ac91..da811fa 100644 --- a/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md +++ b/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md @@ -71,6 +71,54 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed. - no freight-car or tender family currently resolves to a packaged `{internal_stem}_NE.imb` - That `CarSideView_3.imb` hole is now confirmed across both checked installs (`rt3` and `rt3_105`): neither shipped `rt3_2IMB.PK4` contains a `CarSideView_3.imb` entry. +- The parser now preserves the exact 1.05 `CarSideView_3.imb` miss cohort directly: + - `242_a1_l` + - `242_a1_t` + - `class_460` + - `class_a1l` + - `class_a1t` + - `class_p8l` + - `class_p8t` + - `class_qjl` + - `class_qjt` + - `gp35l` + - `u1l` + - `u1t` + - `zephyrl` +- The conditional `.lco` companion/body seam is also narrow enough to preserve exactly in 1.05: + - `companion=Zephyr / body=(none) / side_view=CarSideView_3.imb` + - `242_a1_l` + - `gp35l` + - `u1l` + - `zephyrl` + - `companion=VL80T / body=Loco / side_view=CarSideView_1.imb` + - `f3 loco` + - `gp7` + - `companion=VL80T / body=Loco / side_view=CarSideView_2.imb` + - `be 5-7` +- The classic install widens the `0xc0` side-view seam beyond `CarSideView_*`. + The parser now confirms a larger checked classic `*_Profile.imb` reference family at `0xc0`, + and every one of those checked classic profile names currently misses the packaged + `rt3_2IMB.PK4` entry set too: + - `264T_Profile.imb` + - `Black5_Profile.imb` + - `BR39_Profile.imb` + - `ClassS_Profile.imb` + - `Daylight484_Profile.imb` + - `E111_Profile.imb` + - `Eurostar_Profile.imb` + - `G10_Profile.imb` + - `G4_Profile.imb` + - `Goods_Profile.imb` + - `GP40-2_Profile.imb` + - `GP7D_Profile.imb` + - `ICE_Profile.imb` + - `Mogul_Profile.imb` + - `P-2_Profile.imb` + - `Sm2_Profile.imb` + - `TenWheeler_Profile.imb` + - `V2Class_Profile.imb` + - `Vittorio_Profile.imb` - The packaged profile metadata is now bounded too: - `CarSideView_1.imb`: `512x512`, `MaxPercentOfInterfaceVRAM = 0.04` - `CarSideView_2.imb`: `512x256`, `MaxPercentOfInterfaceVRAM = 0.02` @@ -123,9 +171,12 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed. - what the `0xa2` auxiliary stem really represents in the five remaining distinct cases: alternate content root, paired tender/loco image root, or a narrower foreign-display alias - whether the trailing side-view resource can be tied cleanly to the PK4-backed `CarSideView_*` - metadata and the engine-specific `_NE.imb` profiles without inventing frontend semantics + metadata, the classic `*_Profile.imb` reference family, and the engine-specific `_NE.imb` + profiles without inventing frontend semantics - whether the `CarSideView_3.imb` missing cohort is a classic-only asset dependency, an omitted 1.05 package, or just a dead resource reference in the shipped `EngineTypes` data + - whether the classic `*_Profile.imb` references are dead loose-file dependencies, a separate + package family outside `rt3_2IMB.PK4`, or a still-unmapped authoring convention - `.lco` - whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse key, or only a subset authoring convenience