Link engine types to packaged side views

This commit is contained in:
Jan Petykiewicz 2026-04-21 23:14:50 -07:00
commit 8387008728
3 changed files with 144 additions and 6 deletions

View file

@ -1,9 +1,11 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::pk4::inspect_pk4_file;
const CAR_PRIMARY_DISPLAY_NAME_OFFSET: usize = 0x0c;
const CAR_CONTENT_NAME_OFFSET: usize = 0x48;
const CAR_INTERNAL_STEM_OFFSET: usize = 0x84;
@ -124,8 +126,11 @@ pub struct EngineTypeFamilyEntry {
pub internal_stem: Option<String>,
pub auxiliary_stem: Option<String>,
pub side_view_resource: Option<String>,
pub side_view_resource_found_in_pk4: Option<bool>,
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 cct_identifier: Option<String>,
pub cct_value: Option<i64>,
pub has_matched_locomotive_pair: bool,
@ -134,6 +139,7 @@ pub struct EngineTypeFamilyEntry {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypesInspectionReport {
pub source_root: String,
pub side_view_imb_pk4_path: Option<String>,
pub family_count: usize,
pub car_file_count: usize,
pub lco_file_count: usize,
@ -145,8 +151,14 @@ pub struct EngineTypesInspectionReport {
pub unmatched_cgo_file_count: usize,
pub unmatched_cct_file_count: usize,
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_auxiliary_stem_counts: BTreeMap<String, usize>,
pub car_auxiliary_stem_relation_counts: BTreeMap<String, usize>,
pub internal_ne_profile_pk4_match_count: usize,
pub internal_ne_profile_pk4_missing_count: usize,
pub locomotive_pair_internal_ne_profile_pk4_match_count: usize,
pub locomotive_pair_internal_ne_profile_pk4_missing_count: usize,
pub lco_companion_stem_counts: BTreeMap<String, usize>,
pub lco_body_type_label_counts: BTreeMap<String, usize>,
pub lco_low_cardinality_lane_counts: BTreeMap<String, BTreeMap<String, usize>>,
@ -297,6 +309,7 @@ pub fn inspect_engine_types_dir(
let mut lco_reports = BTreeMap::<String, EngineTypeLcoInspectionReport>::new();
let mut cgo_reports = BTreeMap::<String, EngineTypeCgoInspectionReport>::new();
let mut cct_reports = BTreeMap::<String, EngineTypeCctInspectionReport>::new();
let (side_view_imb_pk4_path, side_view_imb_entry_names) = load_side_view_imb_pk4_lookup(path)?;
for entry in fs::read_dir(path)? {
let entry = entry?;
@ -343,7 +356,15 @@ pub fn inspect_engine_types_dir(
let family_entries = families
.values()
.map(|family| build_family_entry(family, &car_reports, &lco_reports, &cct_reports))
.map(|family| {
build_family_entry(
family,
&car_reports,
&lco_reports,
&cct_reports,
side_view_imb_entry_names.as_ref(),
)
})
.collect::<Vec<_>>();
let matched_locomotive_pair_count = family_entries
.iter()
@ -354,6 +375,14 @@ pub fn inspect_engine_types_dir(
.iter()
.filter_map(|family| family.side_view_resource.as_deref()),
);
let car_side_view_resource_pk4_match_count = family_entries
.iter()
.filter(|family| family.side_view_resource_found_in_pk4 == Some(true))
.count();
let car_side_view_resource_pk4_missing_count = family_entries
.iter()
.filter(|family| family.side_view_resource_found_in_pk4 == Some(false))
.count();
let car_auxiliary_stem_counts = count_named_values(
family_entries
.iter()
@ -369,6 +398,28 @@ pub fn inspect_engine_types_dir(
.iter()
.filter_map(|family| family.companion_stem.as_deref()),
);
let internal_ne_profile_pk4_match_count = family_entries
.iter()
.filter(|family| family.internal_ne_profile_found_in_pk4 == Some(true))
.count();
let internal_ne_profile_pk4_missing_count = family_entries
.iter()
.filter(|family| family.internal_ne_profile_found_in_pk4 == Some(false))
.count();
let locomotive_pair_internal_ne_profile_pk4_match_count = family_entries
.iter()
.filter(|family| {
family.has_matched_locomotive_pair
&& family.internal_ne_profile_found_in_pk4 == Some(true)
})
.count();
let locomotive_pair_internal_ne_profile_pk4_missing_count = family_entries
.iter()
.filter(|family| {
family.has_matched_locomotive_pair
&& family.internal_ne_profile_found_in_pk4 == Some(false)
})
.count();
let lco_body_type_label_counts = count_named_values(
family_entries
.iter()
@ -400,6 +451,7 @@ pub fn inspect_engine_types_dir(
Ok(EngineTypesInspectionReport {
source_root: path.display().to_string(),
side_view_imb_pk4_path,
family_count: family_entries.len(),
car_file_count: family_entries
.iter()
@ -439,8 +491,14 @@ pub fn inspect_engine_types_dir(
})
.count(),
car_side_view_resource_counts,
car_side_view_resource_pk4_match_count,
car_side_view_resource_pk4_missing_count,
car_auxiliary_stem_counts,
car_auxiliary_stem_relation_counts,
internal_ne_profile_pk4_match_count,
internal_ne_profile_pk4_missing_count,
locomotive_pair_internal_ne_profile_pk4_match_count,
locomotive_pair_internal_ne_profile_pk4_missing_count,
lco_companion_stem_counts,
lco_body_type_label_counts,
lco_low_cardinality_lane_counts,
@ -468,6 +526,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>>,
) -> EngineTypeFamilyEntry {
let car_report = family
.car_file
@ -481,6 +540,11 @@ fn build_family_entry(
.cct_file
.as_ref()
.and_then(|file_name| cct_reports.get(file_name));
let side_view_resource = car_report.and_then(|report| report.side_view_resource.clone());
let internal_stem = car_report.and_then(|report| report.internal_stem.clone());
let internal_ne_profile_name = internal_stem
.as_ref()
.map(|internal_stem| format!("{internal_stem}_NE.imb"));
EngineTypeFamilyEntry {
canonical_stem: family.canonical_stem.clone(),
car_file: family.car_file.clone(),
@ -489,17 +553,59 @@ fn build_family_entry(
cct_file: family.cct_file.clone(),
primary_display_name: car_report.and_then(|report| report.primary_display_name.clone()),
content_name: car_report.and_then(|report| report.content_name.clone()),
internal_stem: car_report.and_then(|report| report.internal_stem.clone()),
internal_stem,
auxiliary_stem: car_report.and_then(|report| report.auxiliary_stem.clone()),
side_view_resource: car_report.and_then(|report| report.side_view_resource.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))
}),
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)),
),
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(),
}
}
fn load_side_view_imb_pk4_lookup(
engine_types_dir: &Path,
) -> Result<(Option<String>, Option<BTreeSet<String>>), Box<dyn std::error::Error>> {
let Some(data_dir) = engine_types_dir.parent() else {
return Ok((None, None));
};
let pk4_path = find_case_insensitive_file(&data_dir.join("2D"), "rt3_2imb.pk4");
let Some(pk4_path) = pk4_path else {
return Ok((None, None));
};
let inspection = inspect_pk4_file(&pk4_path)?;
let entry_names = inspection
.entries
.into_iter()
.map(|entry| entry.name)
.collect::<BTreeSet<_>>();
Ok((Some(pk4_path.display().to_string()), Some(entry_names)))
}
fn find_case_insensitive_file(dir: &Path, expected_name: &str) -> Option<PathBuf> {
let expected_lower = expected_name.to_ascii_lowercase();
fs::read_dir(dir)
.ok()?
.filter_map(Result::ok)
.find(|entry| {
entry
.file_name()
.to_str()
.map(|name| name.to_ascii_lowercase() == expected_lower)
.unwrap_or(false)
})
.map(|entry| entry.path())
}
fn build_locomotive_display_census(
path: &Path,
families: &[EngineTypeFamilyEntry],
@ -894,14 +1000,28 @@ mod tests {
},
)]);
let entry = build_family_entry(&family, &car_reports, &lco_reports, &cct_reports);
let pk4_entry_names =
BTreeSet::from(["CarSideView_1.imb".to_string(), "GP7L_NE.imb".to_string()]);
let entry = build_family_entry(
&family,
&car_reports,
&lco_reports,
&cct_reports,
Some(&pk4_entry_names),
);
assert_eq!(entry.auxiliary_stem.as_deref(), Some("GP7L"));
assert_eq!(
entry.side_view_resource.as_deref(),
Some("CarSideView_1.imb")
);
assert_eq!(entry.side_view_resource_found_in_pk4, Some(true));
assert_eq!(entry.companion_stem.as_deref(), Some("VL80T"));
assert_eq!(entry.body_type_label.as_deref(), Some("Loco"));
assert_eq!(
entry.internal_ne_profile_name.as_deref(),
Some("GP7L_NE.imb")
);
assert_eq!(entry.internal_ne_profile_found_in_pk4, Some(true));
assert_eq!(entry.cct_identifier.as_deref(), Some("GP7"));
}
@ -963,8 +1083,11 @@ mod tests {
internal_stem: Some("GP7L".to_string()),
auxiliary_stem: Some("GP7L".to_string()),
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,
@ -1129,8 +1252,11 @@ mod tests {
internal_stem: Some("2D2L".to_string()),
auxiliary_stem: Some("2D2L".to_string()),
side_view_resource: Some("CarSideView_2.imb".to_string()),
side_view_resource_found_in_pk4: Some(true),
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),
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: true,