Promote engine type and imb parser fields

This commit is contained in:
Jan Petykiewicz 2026-04-21 22:53:08 -07:00
commit 1aeb5cf663
2 changed files with 199 additions and 0 deletions

View file

@ -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<String, usize>,
pub car_auxiliary_stem_counts: BTreeMap<String, usize>,
pub lco_companion_stem_counts: BTreeMap<String, usize>,
pub lco_body_type_label_counts: BTreeMap<String, usize>,
pub cgo_scalar_value_counts: BTreeMap<String, usize>,
pub cgo_scalar_values_by_content_stem: BTreeMap<String, Vec<String>>,
pub locomotive_display_census: EngineTypeLocomotiveDisplayCensusReport,
pub families: Vec<EngineTypeFamilyEntry>,
}
@ -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<Item = &'a str>) -> BTreeMap<String, usize> {
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<Item = String>) -> BTreeMap<String, usize> {
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<Item = &'a EngineTypeCgoInspectionReport>,
) -> BTreeMap<String, Vec<String>> {
let mut grouped = BTreeMap::<String, Vec<String>>::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();

View file

@ -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<String>,
pub texture_width: Option<i64>,
pub texture_height: Option<i64>,
pub target_screen_width: Option<i64>,
pub target_screen_height: Option<i64>,
pub scaleable: Option<bool>,
pub max_percent_of_interface_vram: Option<f64>,
pub image_rect: Option<[i64; 4]>,
pub notes: Vec<String>,
pub entries: Vec<ImbInspectionEntry>,
pub malformed_lines: Vec<String>,
@ -64,14 +72,33 @@ pub fn inspect_imb_bytes(bytes: &[u8]) -> Result<ImbInspectionReport, Box<dyn st
});
}
let tga_name = find_scalar_string(&entries, "TGAName");
let texture_width = find_scalar_i64(&entries, "TGAWidth");
let texture_height = find_scalar_i64(&entries, "TGAHeight");
let target_screen_width = find_scalar_i64(&entries, "TGATargetScreenWidth");
let target_screen_height = find_scalar_i64(&entries, "TGATargetScreenHeight");
let scaleable = find_scalar_i64(&entries, "Scaleable").map(|value| value != 0);
let max_percent_of_interface_vram =
find_scalar_f64(&entries, "MaxPercentOfInterfaceVRAM");
let image_rect = find_i64_quad(&entries, "ImageWH");
Ok(ImbInspectionReport {
line_count: text.lines().count(),
entry_count: entries.len(),
blank_line_count,
malformed_line_count: malformed_lines.len(),
tga_name,
texture_width,
texture_height,
target_screen_width,
target_screen_height,
scaleable,
max_percent_of_interface_vram,
image_rect,
notes: vec![
"The current .imb parser preserves one whitespace-delimited key plus the remaining token list per line.".to_string(),
"Integer and float projections are only populated when every token in the value lane parses cleanly.".to_string(),
"Known profile-style keys such as `TGAName`, `TGAWidth`, `TGAHeight`, `TGATargetScreenWidth`, `TGATargetScreenHeight`, `Scaleable`, `MaxPercentOfInterfaceVRAM`, and `ImageWH` are promoted into typed report fields when present.".to_string(),
],
entries,
malformed_lines,
@ -92,6 +119,37 @@ fn parse_f64_tokens(tokens: &[String]) -> Option<Vec<f64>> {
.collect::<Option<Vec<_>>>()
}
fn find_scalar_string(entries: &[ImbInspectionEntry], key: &str) -> Option<String> {
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<i64> {
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<f64> {
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));
}
}