Resolve engine type side view pk4 owners

This commit is contained in:
Jan Petykiewicz 2026-04-21 23:51:05 -07:00
commit 3f0b99b0e6
3 changed files with 207 additions and 70 deletions

View file

@ -140,10 +140,12 @@ pub struct EngineTypeFamilyEntry {
pub auxiliary_stem: Option<String>,
pub side_view_resource: Option<String>,
pub side_view_resource_found_in_pk4: Option<bool>,
pub side_view_resource_pk4_path: Option<String>,
pub companion_stem: Option<String>,
pub body_type_label: Option<String>,
pub internal_ne_profile_name: Option<String>,
pub internal_ne_profile_found_in_pk4: Option<bool>,
pub internal_ne_profile_pk4_path: Option<String>,
pub cct_identifier: Option<String>,
pub cct_value: Option<i64>,
pub has_matched_locomotive_pair: bool,
@ -153,6 +155,7 @@ pub struct EngineTypeFamilyEntry {
pub struct EngineTypesInspectionReport {
pub source_root: String,
pub side_view_imb_pk4_path: Option<String>,
pub side_view_imb_pk4_paths: Vec<String>,
pub family_count: usize,
pub car_file_count: usize,
pub lco_file_count: usize,
@ -167,6 +170,7 @@ pub struct EngineTypesInspectionReport {
pub car_side_view_resource_counts: BTreeMap<String, usize>,
pub car_side_view_resource_pk4_match_count: usize,
pub car_side_view_resource_pk4_missing_count: usize,
pub car_side_view_resource_pk4_path_counts: BTreeMap<String, 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>,
@ -176,6 +180,7 @@ pub struct EngineTypesInspectionReport {
pub car_auxiliary_stem_distinct_pair_family_stems: BTreeMap<String, Vec<String>>,
pub internal_ne_profile_pk4_match_count: usize,
pub internal_ne_profile_pk4_missing_count: usize,
pub internal_ne_profile_pk4_path_counts: BTreeMap<String, usize>,
pub locomotive_pair_internal_ne_profile_pk4_match_count: usize,
pub locomotive_pair_internal_ne_profile_pk4_missing_count: usize,
pub matched_prefix_internal_ne_profile_pk4_match_count: usize,
@ -391,9 +396,7 @@ pub fn inspect_engine_types_dir(
&car_reports,
&lco_reports,
&cct_reports,
side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| &lookup.entry_names),
side_view_imb_pk4_lookup.as_ref(),
)
})
.collect::<Vec<_>>();
@ -418,6 +421,11 @@ pub fn inspect_engine_types_dir(
.iter()
.filter(|family| family.side_view_resource_found_in_pk4 == Some(false))
.count();
let car_side_view_resource_pk4_path_counts = count_named_values(
family_entries
.iter()
.filter_map(|family| family.side_view_resource_pk4_path.as_deref()),
);
let car_side_view_resource_pk4_missing_counts = count_named_values(
family_entries
.iter()
@ -461,6 +469,11 @@ pub fn inspect_engine_types_dir(
.iter()
.filter(|family| family.internal_ne_profile_found_in_pk4 == Some(false))
.count();
let internal_ne_profile_pk4_path_counts = count_named_values(
family_entries
.iter()
.filter_map(|family| family.internal_ne_profile_pk4_path.as_deref()),
);
let locomotive_pair_internal_ne_profile_pk4_match_count = family_entries
.iter()
.filter(|family| {
@ -572,7 +585,11 @@ pub fn inspect_engine_types_dir(
source_root: path.display().to_string(),
side_view_imb_pk4_path: side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| lookup.path.clone()),
.and_then(|lookup| lookup.primary_path.clone()),
side_view_imb_pk4_paths: side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| lookup.paths.clone())
.unwrap_or_default(),
family_count: family_entries.len(),
car_file_count: family_entries
.iter()
@ -615,6 +632,7 @@ pub fn inspect_engine_types_dir(
car_side_view_resource_counts,
car_side_view_resource_pk4_match_count,
car_side_view_resource_pk4_missing_count,
car_side_view_resource_pk4_path_counts,
car_side_view_resource_pk4_missing_counts,
car_side_view_resource_pk4_missing_families,
car_auxiliary_stem_counts,
@ -624,6 +642,7 @@ pub fn inspect_engine_types_dir(
car_auxiliary_stem_distinct_pair_family_stems,
internal_ne_profile_pk4_match_count,
internal_ne_profile_pk4_missing_count,
internal_ne_profile_pk4_path_counts,
locomotive_pair_internal_ne_profile_pk4_match_count,
locomotive_pair_internal_ne_profile_pk4_missing_count,
matched_prefix_internal_ne_profile_pk4_match_count,
@ -659,8 +678,10 @@ struct EngineTypeFamilyBuilder {
}
struct SideViewImbPk4Lookup {
path: String,
primary_path: Option<String>,
paths: Vec<String>,
entry_names: BTreeSet<String>,
entry_pk4_paths_by_name: BTreeMap<String, String>,
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>,
@ -672,7 +693,7 @@ fn build_family_entry(
car_reports: &BTreeMap<String, EngineTypeCarInspectionReport>,
lco_reports: &BTreeMap<String, EngineTypeLcoInspectionReport>,
cct_reports: &BTreeMap<String, EngineTypeCctInspectionReport>,
side_view_imb_entry_names: Option<&BTreeSet<String>>,
side_view_imb_lookup: Option<&SideViewImbPk4Lookup>,
) -> EngineTypeFamilyEntry {
let car_report = family
.car_file
@ -703,14 +724,26 @@ fn build_family_entry(
auxiliary_stem: car_report.and_then(|report| report.auxiliary_stem.clone()),
side_view_resource: side_view_resource.clone(),
side_view_resource_found_in_pk4: side_view_resource.as_ref().and_then(|resource| {
side_view_imb_entry_names.map(|entries| entries.contains(resource))
side_view_imb_lookup.map(|lookup| lookup.entry_names.contains(resource))
}),
side_view_resource_pk4_path: side_view_resource
.as_ref()
.and_then(|resource| {
side_view_imb_lookup
.and_then(|lookup| lookup.entry_pk4_paths_by_name.get(resource).cloned())
}),
companion_stem: lco_report.and_then(|report| report.companion_stem.clone()),
body_type_label: lco_report.and_then(|report| report.body_type_label.clone()),
internal_ne_profile_name: internal_ne_profile_name.clone(),
internal_ne_profile_found_in_pk4: internal_ne_profile_name.as_ref().and_then(
|entry_name| side_view_imb_entry_names.map(|entries| entries.contains(entry_name)),
|entry_name| {
side_view_imb_lookup.map(|lookup| lookup.entry_names.contains(entry_name))
},
),
internal_ne_profile_pk4_path: internal_ne_profile_name.as_ref().and_then(|entry_name| {
side_view_imb_lookup
.and_then(|lookup| lookup.entry_pk4_paths_by_name.get(entry_name).cloned())
}),
cct_identifier: cct_report.and_then(|report| report.identifier.clone()),
cct_value: cct_report.and_then(|report| report.value),
has_matched_locomotive_pair: family.car_file.is_some() && family.lco_file.is_some(),
@ -723,54 +756,68 @@ fn load_side_view_imb_pk4_lookup(
let Some(data_dir) = engine_types_dir.parent() else {
return Ok(None);
};
let pk4_path = find_case_insensitive_file(&data_dir.join("2D"), "rt3_2imb.pk4");
let Some(pk4_path) = pk4_path else {
let pk4_paths = find_side_view_pk4_paths(data_dir);
if pk4_paths.is_empty() {
return Ok(None);
};
}
let bytes = fs::read(&pk4_path)?;
let inspection = inspect_pk4_file(&pk4_path)?;
let mut entry_names = BTreeSet::new();
let mut entry_pk4_paths_by_name = 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_imb_profile = entry.name.ends_with(".imb");
let is_internal_ne = entry.name.ends_with("_NE.imb");
if !is_imb_profile {
continue;
}
for pk4_path in &pk4_paths {
let bytes = fs::read(pk4_path)?;
let inspection = inspect_pk4_file(pk4_path)?;
for entry in inspection.entries {
entry_names.insert(entry.name.clone());
entry_pk4_paths_by_name
.entry(entry.name.clone())
.or_insert_with(|| pk4_path.display().to_string());
let is_imb_profile = entry.name.ends_with(".imb");
let is_internal_ne = entry.name.ends_with("_NE.imb");
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)?);
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)
{
*internal_ne_profile_texture_size_counts
.entry(format!("{width}x{height}"))
.or_insert(0) += 1;
}
if let Some(horizontal_scale_modifier) = imb_profile.horizontal_scale_modifier {
*internal_ne_profile_horizontal_scale_modifier_counts
.entry(format!("{horizontal_scale_modifier:.6}"))
.or_insert(0) += 1;
}
if let Some(max_percent) = imb_profile.max_percent_of_interface_vram {
*internal_ne_profile_max_percent_of_interface_vram_counts
.entry(format!("{max_percent:.6}"))
.or_insert(0) += 1;
let payload = &bytes[entry.payload_absolute_offset..entry.payload_end_offset];
let imb_profile = summarize_imb_profile(&inspect_imb_bytes(payload)?);
imb_profile_summaries_by_entry_name
.entry(entry.name.clone())
.or_insert_with(|| imb_profile.clone());
if is_internal_ne {
if let (Some(width), Some(height)) =
(imb_profile.texture_width, imb_profile.texture_height)
{
*internal_ne_profile_texture_size_counts
.entry(format!("{width}x{height}"))
.or_insert(0) += 1;
}
if let Some(horizontal_scale_modifier) = imb_profile.horizontal_scale_modifier {
*internal_ne_profile_horizontal_scale_modifier_counts
.entry(format!("{horizontal_scale_modifier:.6}"))
.or_insert(0) += 1;
}
if let Some(max_percent) = imb_profile.max_percent_of_interface_vram {
*internal_ne_profile_max_percent_of_interface_vram_counts
.entry(format!("{max_percent:.6}"))
.or_insert(0) += 1;
}
}
}
}
Ok(Some(SideViewImbPk4Lookup {
path: pk4_path.display().to_string(),
primary_path: find_case_insensitive_file(&data_dir.join("2D"), "rt3_2imb.pk4")
.map(|path| path.display().to_string()),
paths: pk4_paths
.iter()
.map(|path| path.display().to_string())
.collect(),
entry_names,
entry_pk4_paths_by_name,
imb_profile_summaries_by_entry_name,
internal_ne_profile_texture_size_counts,
internal_ne_profile_horizontal_scale_modifier_counts,
@ -793,6 +840,28 @@ fn find_case_insensitive_file(dir: &Path, expected_name: &str) -> Option<PathBuf
.map(|entry| entry.path())
}
fn find_side_view_pk4_paths(data_dir: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
for dir in [data_dir.join("2D"), data_dir.join("PopTopExtraContent")] {
let Ok(entries) = fs::read_dir(dir) else {
continue;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let is_pk4 = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case("pk4"))
.unwrap_or(false);
if is_pk4 {
paths.push(path);
}
}
}
paths.sort();
paths
}
fn summarize_imb_profile(report: &ImbInspectionReport) -> EngineTypeImbProfileSummary {
EngineTypeImbProfileSummary {
tga_name: report.tga_name.clone(),
@ -1325,14 +1394,31 @@ mod tests {
},
)]);
let pk4_entry_names =
BTreeSet::from(["CarSideView_1.imb".to_string(), "GP7L_NE.imb".to_string()]);
let pk4_lookup = SideViewImbPk4Lookup {
primary_path: Some("rt3_2IMB.PK4".to_string()),
paths: vec!["rt3_2IMB.PK4".to_string()],
entry_names: BTreeSet::from([
"CarSideView_1.imb".to_string(),
"GP7L_NE.imb".to_string(),
]),
entry_pk4_paths_by_name: BTreeMap::from([
(
"CarSideView_1.imb".to_string(),
"rt3_2IMB.PK4".to_string(),
),
("GP7L_NE.imb".to_string(), "rt3_2IMB.PK4".to_string()),
]),
imb_profile_summaries_by_entry_name: BTreeMap::new(),
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(),
};
let entry = build_family_entry(
&family,
&car_reports,
&lco_reports,
&cct_reports,
Some(&pk4_entry_names),
Some(&pk4_lookup),
);
assert_eq!(entry.auxiliary_stem.as_deref(), Some("GP7L"));
assert_eq!(
@ -1340,6 +1426,10 @@ mod tests {
Some("CarSideView_1.imb")
);
assert_eq!(entry.side_view_resource_found_in_pk4, Some(true));
assert_eq!(
entry.side_view_resource_pk4_path.as_deref(),
Some("rt3_2IMB.PK4")
);
assert_eq!(entry.companion_stem.as_deref(), Some("VL80T"));
assert_eq!(entry.body_type_label.as_deref(), Some("Loco"));
assert_eq!(
@ -1347,6 +1437,10 @@ mod tests {
Some("GP7L_NE.imb")
);
assert_eq!(entry.internal_ne_profile_found_in_pk4, Some(true));
assert_eq!(
entry.internal_ne_profile_pk4_path.as_deref(),
Some("rt3_2IMB.PK4")
);
assert_eq!(entry.cct_identifier.as_deref(), Some("GP7"));
}
@ -1409,10 +1503,12 @@ mod tests {
auxiliary_stem: Some("GP7L".to_string()),
side_view_resource: None,
side_view_resource_found_in_pk4: None,
side_view_resource_pk4_path: None,
companion_stem: None,
body_type_label: None,
internal_ne_profile_name: None,
internal_ne_profile_found_in_pk4: None,
internal_ne_profile_pk4_path: None,
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: false,
@ -1662,8 +1758,10 @@ mod tests {
},
],
&SideViewImbPk4Lookup {
path: "rt3_2IMB.PK4".to_string(),
primary_path: Some("rt3_2IMB.PK4".to_string()),
paths: vec!["rt3_2IMB.PK4".to_string()],
entry_names: BTreeSet::new(),
entry_pk4_paths_by_name: BTreeMap::new(),
imb_profile_summaries_by_entry_name: BTreeMap::from([
(
"CarSideView_1.imb".to_string(),
@ -1722,10 +1820,12 @@ mod tests {
auxiliary_stem: None,
side_view_resource: None,
side_view_resource_found_in_pk4: None,
side_view_resource_pk4_path: None,
companion_stem: None,
body_type_label: None,
internal_ne_profile_name: None,
internal_ne_profile_found_in_pk4: None,
internal_ne_profile_pk4_path: None,
cct_identifier: Some("Box".to_string()),
cct_value: Some(11),
has_matched_locomotive_pair: false,
@ -1747,10 +1847,12 @@ mod tests {
auxiliary_stem: None,
side_view_resource: None,
side_view_resource_found_in_pk4: None,
side_view_resource_pk4_path: None,
companion_stem: None,
body_type_label: None,
internal_ne_profile_name: None,
internal_ne_profile_found_in_pk4: None,
internal_ne_profile_pk4_path: None,
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: false,
@ -1812,10 +1914,12 @@ mod tests {
auxiliary_stem: Some("2D2L".to_string()),
side_view_resource: Some("CarSideView_2.imb".to_string()),
side_view_resource_found_in_pk4: Some(true),
side_view_resource_pk4_path: Some("rt3_2IMB.PK4".to_string()),
companion_stem: None,
body_type_label: None,
internal_ne_profile_name: Some("2D2L_NE.imb".to_string()),
internal_ne_profile_found_in_pk4: Some(true),
internal_ne_profile_pk4_path: Some("rt3_2IMB.PK4".to_string()),
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: true,
@ -1842,10 +1946,12 @@ mod tests {
auxiliary_stem: None,
side_view_resource: None,
side_view_resource_found_in_pk4: None,
side_view_resource_pk4_path: None,
companion_stem: None,
body_type_label: None,
internal_ne_profile_name: None,
internal_ne_profile_found_in_pk4: None,
internal_ne_profile_pk4_path: None,
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: false,