From 1aeb5cf6634cf2fd4a1b0c73920d0cb67ae6190a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 21 Apr 2026 22:53:08 -0700 Subject: [PATCH] Promote engine type and imb parser fields --- .../rrt-runtime/src/inspect/engine_types.rs | 124 ++++++++++++++++++ crates/rrt-runtime/src/inspect/imb.rs | 75 +++++++++++ 2 files changed, 199 insertions(+) diff --git a/crates/rrt-runtime/src/inspect/engine_types.rs b/crates/rrt-runtime/src/inspect/engine_types.rs index 150b215..2815e02 100644 --- a/crates/rrt-runtime/src/inspect/engine_types.rs +++ b/crates/rrt-runtime/src/inspect/engine_types.rs @@ -144,6 +144,12 @@ pub struct EngineTypesInspectionReport { pub unmatched_lco_file_count: usize, pub unmatched_cgo_file_count: usize, pub unmatched_cct_file_count: usize, + pub car_side_view_resource_counts: BTreeMap, + pub car_auxiliary_stem_counts: BTreeMap, + pub lco_companion_stem_counts: BTreeMap, + pub lco_body_type_label_counts: BTreeMap, + pub cgo_scalar_value_counts: BTreeMap, + pub cgo_scalar_values_by_content_stem: BTreeMap>, pub locomotive_display_census: EngineTypeLocomotiveDisplayCensusReport, pub families: Vec, } @@ -338,6 +344,33 @@ pub fn inspect_engine_types_dir( .iter() .filter(|family| family.has_matched_locomotive_pair) .count(); + let car_side_view_resource_counts = count_named_values( + family_entries + .iter() + .filter_map(|family| family.side_view_resource.as_deref()), + ); + let car_auxiliary_stem_counts = count_named_values( + family_entries + .iter() + .filter_map(|family| family.auxiliary_stem.as_deref()), + ); + let lco_companion_stem_counts = count_named_values( + family_entries + .iter() + .filter_map(|family| family.companion_stem.as_deref()), + ); + let lco_body_type_label_counts = count_named_values( + family_entries + .iter() + .filter_map(|family| family.body_type_label.as_deref()), + ); + let cgo_scalar_value_counts = count_owned_values( + cgo_reports + .values() + .filter_map(|report| report.leading_f32.map(|value| format!("{value:.6}"))), + ); + let cgo_scalar_values_by_content_stem = + build_cgo_scalar_values_by_content_stem(cgo_reports.values()); let locomotive_display_census = build_locomotive_display_census(path, &family_entries, &car_reports)?; @@ -381,6 +414,12 @@ pub fn inspect_engine_types_dir( entry.cct_file.is_some() && !(entry.car_file.is_some() || entry.lco_file.is_some()) }) .count(), + car_side_view_resource_counts, + car_auxiliary_stem_counts, + lco_companion_stem_counts, + lco_body_type_label_counts, + cgo_scalar_value_counts, + cgo_scalar_values_by_content_stem, locomotive_display_census, families: family_entries, }) @@ -604,6 +643,45 @@ fn decode_windows_1252_byte(byte: u8) -> char { } } +fn count_named_values<'a>(values: impl Iterator) -> BTreeMap { + let mut counts = BTreeMap::new(); + for value in values { + *counts.entry(value.to_string()).or_insert(0) += 1; + } + counts +} + +fn count_owned_values(values: impl Iterator) -> BTreeMap { + let mut counts = BTreeMap::new(); + for value in values { + *counts.entry(value).or_insert(0) += 1; + } + counts +} + +fn build_cgo_scalar_values_by_content_stem<'a>( + reports: impl Iterator, +) -> BTreeMap> { + let mut grouped = BTreeMap::>::new(); + for report in reports { + let Some(content_stem) = report.content_stem.as_ref() else { + continue; + }; + let Some(leading_f32) = report.leading_f32 else { + continue; + }; + grouped + .entry(content_stem.clone()) + .or_default() + .push(format!("{leading_f32:.6}")); + } + for values in grouped.values_mut() { + values.sort(); + values.dedup(); + } + grouped +} + #[cfg(test)] mod tests { use super::*; @@ -729,6 +807,52 @@ mod tests { assert_eq!(entry.cct_identifier.as_deref(), Some("GP7")); } + #[test] + fn counts_directory_level_slot_values() { + let counts = count_named_values( + ["CarSideView_1.imb", "CarSideView_1.imb", "VL80T"].into_iter(), + ); + assert_eq!(counts.get("CarSideView_1.imb"), Some(&2)); + assert_eq!(counts.get("VL80T"), Some(&1)); + } + + #[test] + fn groups_cgo_scalar_values_by_content_stem() { + let grouped = build_cgo_scalar_values_by_content_stem( + [ + EngineTypeCgoInspectionReport { + file_size: 25, + leading_u32: Some(0), + leading_u32_hex: Some("0x00000000".to_string()), + leading_f32: Some(10.0), + content_stem: Some("Box".to_string()), + notes: Vec::new(), + }, + EngineTypeCgoInspectionReport { + file_size: 25, + leading_u32: Some(0), + leading_u32_hex: Some("0x00000000".to_string()), + leading_f32: Some(20.0), + content_stem: Some("Box".to_string()), + notes: Vec::new(), + }, + EngineTypeCgoInspectionReport { + file_size: 25, + leading_u32: Some(0), + leading_u32_hex: Some("0x00000000".to_string()), + leading_f32: Some(20.0), + content_stem: Some("Box".to_string()), + notes: Vec::new(), + }, + ] + .iter(), + ); + assert_eq!( + grouped.get("Box"), + Some(&vec!["10.000000".to_string(), "20.000000".to_string()]) + ); + } + #[test] fn builds_locomotive_display_census() { let mut car_reports = BTreeMap::new(); diff --git a/crates/rrt-runtime/src/inspect/imb.rs b/crates/rrt-runtime/src/inspect/imb.rs index c03776f..0ef63e0 100644 --- a/crates/rrt-runtime/src/inspect/imb.rs +++ b/crates/rrt-runtime/src/inspect/imb.rs @@ -19,6 +19,14 @@ pub struct ImbInspectionReport { pub entry_count: usize, pub blank_line_count: usize, pub malformed_line_count: usize, + pub tga_name: Option, + pub texture_width: Option, + pub texture_height: Option, + pub target_screen_width: Option, + pub target_screen_height: Option, + pub scaleable: Option, + pub max_percent_of_interface_vram: Option, + pub image_rect: Option<[i64; 4]>, pub notes: Vec, pub entries: Vec, pub malformed_lines: Vec, @@ -64,14 +72,33 @@ pub fn inspect_imb_bytes(bytes: &[u8]) -> Result Option> { .collect::>>() } +fn find_scalar_string(entries: &[ImbInspectionEntry], key: &str) -> Option { + entries + .iter() + .find(|entry| entry.key == key && entry.tokens.len() == 1) + .map(|entry| entry.tokens[0].clone()) +} + +fn find_scalar_i64(entries: &[ImbInspectionEntry], key: &str) -> Option { + entries + .iter() + .find(|entry| entry.key == key) + .and_then(|entry| entry.integer_values.as_ref()) + .and_then(|values| (values.len() == 1).then_some(values[0])) +} + +fn find_scalar_f64(entries: &[ImbInspectionEntry], key: &str) -> Option { + entries + .iter() + .find(|entry| entry.key == key) + .and_then(|entry| entry.float_values.as_ref()) + .and_then(|values| (values.len() == 1).then_some(values[0])) +} + +fn find_i64_quad(entries: &[ImbInspectionEntry], key: &str) -> Option<[i64; 4]> { + let values = entries + .iter() + .find(|entry| entry.key == key) + .and_then(|entry| entry.integer_values.as_ref())?; + (values.len() == 4).then_some([values[0], values[1], values[2], values[3]]) +} + fn decode_windows_1252(bytes: &[u8]) -> String { bytes .iter() @@ -145,5 +203,22 @@ mod tests { assert_eq!(report.entries[0].key, "TGAName"); assert_eq!(report.entries[1].integer_values, Some(vec![256])); assert_eq!(report.entries[2].integer_values, Some(vec![0, 0, 138, 32])); + assert_eq!(report.tga_name.as_deref(), Some("ICE_Profile")); + assert_eq!(report.texture_width, Some(256)); + assert_eq!(report.image_rect, Some([0, 0, 138, 32])); + } + + #[test] + fn promotes_known_profile_keys_into_typed_fields() { + let report = inspect_imb_bytes( + b"TGAName ICE_Profile\nTGAWidth 256\nTGAHeight 32\nTGATargetScreenWidth 1600\nTGATargetScreenHeight 1200\nScaleable 1\nMaxPercentOfInterfaceVRAM 0.06\nImageWH 0 0 138 32\n", + ) + .expect("imb should parse"); + + assert_eq!(report.texture_height, Some(32)); + assert_eq!(report.target_screen_width, Some(1600)); + assert_eq!(report.target_screen_height, Some(1200)); + assert_eq!(report.scaleable, Some(true)); + assert_eq!(report.max_percent_of_interface_vram, Some(0.06)); } }