Classify engine type parser families

This commit is contained in:
Jan Petykiewicz 2026-04-21 23:05:10 -07:00
commit f3c3eb7262
4 changed files with 149 additions and 17 deletions

View file

@ -146,9 +146,11 @@ pub struct EngineTypesInspectionReport {
pub unmatched_cct_file_count: usize,
pub car_side_view_resource_counts: BTreeMap<String, usize>,
pub car_auxiliary_stem_counts: BTreeMap<String, usize>,
pub car_auxiliary_stem_relation_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_ladder_counts: BTreeMap<String, usize>,
pub cgo_scalar_values_by_content_stem: BTreeMap<String, Vec<String>>,
pub cct_identifier_counts: BTreeMap<String, usize>,
pub cct_value_counts: BTreeMap<String, usize>,
@ -356,6 +358,11 @@ pub fn inspect_engine_types_dir(
.iter()
.filter_map(|family| family.auxiliary_stem.as_deref()),
);
let car_auxiliary_stem_relation_counts = count_owned_values(
family_entries
.iter()
.filter_map(classify_car_auxiliary_stem_relation),
);
let lco_companion_stem_counts = count_named_values(
family_entries
.iter()
@ -373,6 +380,8 @@ pub fn inspect_engine_types_dir(
);
let cgo_scalar_values_by_content_stem =
build_cgo_scalar_values_by_content_stem(cgo_reports.values());
let cgo_scalar_ladder_counts =
build_cgo_scalar_ladder_counts(cgo_scalar_values_by_content_stem.values());
let cct_identifier_counts = count_named_values(
family_entries
.iter()
@ -428,9 +437,11 @@ pub fn inspect_engine_types_dir(
.count(),
car_side_view_resource_counts,
car_auxiliary_stem_counts,
car_auxiliary_stem_relation_counts,
lco_companion_stem_counts,
lco_body_type_label_counts,
cgo_scalar_value_counts,
cgo_scalar_ladder_counts,
cgo_scalar_values_by_content_stem,
cct_identifier_counts,
cct_value_counts,
@ -674,10 +685,34 @@ fn count_owned_values(values: impl Iterator<Item = String>) -> BTreeMap<String,
counts
}
fn classify_car_auxiliary_stem_relation(family: &EngineTypeFamilyEntry) -> Option<String> {
let auxiliary_stem = family.auxiliary_stem.as_deref()?;
let internal_stem = family.internal_stem.as_deref()?;
if auxiliary_stem == internal_stem {
return Some("matches_internal_stem".to_string());
}
let internal_without_role_suffix = strip_terminal_role_letter(internal_stem)?;
if auxiliary_stem == internal_without_role_suffix {
return Some("matches_internal_without_role_suffix".to_string());
}
if auxiliary_stem.eq_ignore_ascii_case(internal_without_role_suffix) {
return Some("matches_internal_without_role_suffix_casefolded".to_string());
}
Some("distinct_auxiliary_stem".to_string())
}
fn strip_terminal_role_letter(value: &str) -> Option<&str> {
let last = value.chars().last()?;
matches!(last, 'L' | 'T' | 'l' | 't').then(|| {
let cutoff = value.len() - last.len_utf8();
&value[..cutoff]
})
}
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();
let mut grouped = BTreeMap::<String, Vec<f32>>::new();
for report in reports {
let Some(content_stem) = report.content_stem.as_ref() else {
continue;
@ -688,13 +723,28 @@ fn build_cgo_scalar_values_by_content_stem<'a>(
grouped
.entry(content_stem.clone())
.or_default()
.push(format!("{leading_f32:.6}"));
}
for values in grouped.values_mut() {
values.sort();
values.dedup();
.push(leading_f32);
}
grouped
.into_iter()
.map(|(content_stem, mut values)| {
values.sort_by(f32::total_cmp);
values.dedup();
(
content_stem,
values
.into_iter()
.map(|value| format!("{value:.6}"))
.collect::<Vec<_>>(),
)
})
.collect()
}
fn build_cgo_scalar_ladder_counts<'a>(
ladders: impl Iterator<Item = &'a Vec<String>>,
) -> BTreeMap<String, usize> {
count_owned_values(ladders.map(|ladder| ladder.join(" -> ")))
}
#[cfg(test)]
@ -824,9 +874,8 @@ mod tests {
#[test]
fn counts_directory_level_slot_values() {
let counts = count_named_values(
["CarSideView_1.imb", "CarSideView_1.imb", "VL80T"].into_iter(),
);
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));
}
@ -868,9 +917,73 @@ mod tests {
);
}
#[test]
fn classifies_car_auxiliary_stem_relations() {
let identical = EngineTypeFamilyEntry {
canonical_stem: "gp7".to_string(),
car_file: None,
lco_file: None,
cgo_file: None,
cct_file: None,
primary_display_name: None,
content_name: None,
internal_stem: Some("GP7L".to_string()),
auxiliary_stem: Some("GP7L".to_string()),
side_view_resource: None,
companion_stem: None,
body_type_label: None,
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: false,
};
let stripped = EngineTypeFamilyEntry {
internal_stem: Some("Class01L".to_string()),
auxiliary_stem: Some("Class01".to_string()),
..identical.clone()
};
let stripped_casefolded = EngineTypeFamilyEntry {
internal_stem: Some("classqjt".to_string()),
auxiliary_stem: Some("qjclasst".to_string()),
..identical.clone()
};
let distinct = EngineTypeFamilyEntry {
internal_stem: Some("ClassA1T".to_string()),
auxiliary_stem: Some("ClassA1L".to_string()),
..identical
};
assert_eq!(
classify_car_auxiliary_stem_relation(&stripped),
Some("matches_internal_without_role_suffix".to_string())
);
assert_eq!(
classify_car_auxiliary_stem_relation(&stripped_casefolded),
Some("distinct_auxiliary_stem".to_string())
);
assert_eq!(
classify_car_auxiliary_stem_relation(&distinct),
Some("distinct_auxiliary_stem".to_string())
);
}
#[test]
fn builds_cgo_scalar_ladder_counts() {
let ladders = build_cgo_scalar_ladder_counts(
[
vec!["10.000000".to_string(), "20.000000".to_string()],
vec!["10.000000".to_string(), "20.000000".to_string()],
vec!["55.000000".to_string(), "85.000000".to_string()],
]
.iter(),
);
assert_eq!(ladders.get("10.000000 -> 20.000000"), Some(&2));
assert_eq!(ladders.get("55.000000 -> 85.000000"), Some(&1));
}
#[test]
fn counts_owned_value_strings() {
let counts = count_owned_values(["13".to_string(), "13".to_string(), "4".to_string()].into_iter());
let counts =
count_owned_values(["13".to_string(), "13".to_string(), "4".to_string()].into_iter());
assert_eq!(counts.get("13"), Some(&2));
assert_eq!(counts.get("4"), Some(&1));
}

View file

@ -78,8 +78,7 @@ pub fn inspect_imb_bytes(bytes: &[u8]) -> Result<ImbInspectionReport, Box<dyn st
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 max_percent_of_interface_vram = find_scalar_f64(&entries, "MaxPercentOfInterfaceVRAM");
let image_rect = find_i64_quad(&entries, "ImageWH");
Ok(ImbInspectionReport {