Preserve engine type side view cohorts

This commit is contained in:
Jan Petykiewicz 2026-04-21 23:42:20 -07:00
commit 7741dc2087
3 changed files with 265 additions and 12 deletions

View file

@ -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<String, usize>,
pub car_side_view_resource_pk4_missing_families: Vec<String>,
pub car_auxiliary_stem_counts: BTreeMap<String, usize>,
pub car_auxiliary_stem_relation_counts: BTreeMap<String, usize>,
pub car_auxiliary_stem_distinct_pair_counts: BTreeMap<String, usize>,
@ -184,6 +185,7 @@ pub struct EngineTypesInspectionReport {
pub internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap<String, usize>,
pub lco_companion_stem_counts: BTreeMap<String, usize>,
pub lco_body_type_label_counts: BTreeMap<String, usize>,
pub lco_companion_profile_family_stems: BTreeMap<String, Vec<String>>,
pub lco_low_cardinality_lane_counts: BTreeMap<String, BTreeMap<String, usize>>,
pub cgo_scalar_value_counts: BTreeMap<String, usize>,
pub cgo_scalar_ladder_counts: BTreeMap<String, usize>,
@ -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<String>,
car_side_view_profile_summaries: BTreeMap<String, EngineTypeImbProfileSummary>,
imb_profile_summaries_by_entry_name: BTreeMap<String, EngineTypeImbProfileSummary>,
internal_ne_profile_texture_size_counts: BTreeMap<String, usize>,
internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap<String, usize>,
internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap<String, usize>,
@ -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<String> {
let mut stems = families
.iter()
.filter(|family| family.side_view_resource_found_in_pk4 == Some(false))
.map(|family| family.canonical_stem.clone())
.collect::<Vec<_>>();
stems.sort();
stems
}
fn build_lco_companion_profile_family_stems(
families: &[EngineTypeFamilyEntry],
) -> BTreeMap<String, Vec<String>> {
let mut grouped = BTreeMap::<String, Vec<String>>::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<String, EngineTypeImbProfileSummary> {
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,
}
}
}