Summarize packaged engine type profiles

This commit is contained in:
Jan Petykiewicz 2026-04-21 23:21:37 -07:00
commit 4801b1a164
3 changed files with 136 additions and 13 deletions

View file

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::imb::{ImbInspectionReport, inspect_imb_bytes};
use super::pk4::inspect_pk4_file;
const CAR_PRIMARY_DISPLAY_NAME_OFFSET: usize = 0x0c;
@ -114,6 +115,18 @@ pub struct EngineTypeLocomotiveDisplayCensusReport {
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EngineTypeImbProfileSummary {
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 horizontal_scale_modifier: Option<f64>,
pub max_percent_of_interface_vram: Option<f64>,
pub image_rect_scaled: Option<[i64; 4]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeFamilyEntry {
pub canonical_stem: String,
@ -136,7 +149,7 @@ pub struct EngineTypeFamilyEntry {
pub has_matched_locomotive_pair: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EngineTypesInspectionReport {
pub source_root: String,
pub side_view_imb_pk4_path: Option<String>,
@ -150,6 +163,7 @@ 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_profile_summaries: BTreeMap<String, EngineTypeImbProfileSummary>,
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,
@ -160,6 +174,9 @@ pub struct EngineTypesInspectionReport {
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 internal_ne_profile_texture_size_counts: BTreeMap<String, usize>,
pub internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap<String, usize>,
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_low_cardinality_lane_counts: BTreeMap<String, BTreeMap<String, usize>>,
@ -310,7 +327,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)?;
let side_view_imb_pk4_lookup = load_side_view_imb_pk4_lookup(path)?;
for entry in fs::read_dir(path)? {
let entry = entry?;
@ -363,7 +380,9 @@ pub fn inspect_engine_types_dir(
&car_reports,
&lco_reports,
&cct_reports,
side_view_imb_entry_names.as_ref(),
side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| &lookup.entry_names),
)
})
.collect::<Vec<_>>();
@ -376,6 +395,10 @@ pub fn inspect_engine_types_dir(
.iter()
.filter_map(|family| family.side_view_resource.as_deref()),
);
let car_side_view_resource_profile_summaries = side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| lookup.car_side_view_profile_summaries.clone())
.unwrap_or_default();
let car_side_view_resource_pk4_match_count = family_entries
.iter()
.filter(|family| family.side_view_resource_found_in_pk4 == Some(true))
@ -427,6 +450,26 @@ pub fn inspect_engine_types_dir(
&& family.internal_ne_profile_found_in_pk4 == Some(false)
})
.count();
let internal_ne_profile_texture_size_counts = side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| lookup.internal_ne_profile_texture_size_counts.clone())
.unwrap_or_default();
let internal_ne_profile_horizontal_scale_modifier_counts = side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| {
lookup
.internal_ne_profile_horizontal_scale_modifier_counts
.clone()
})
.unwrap_or_default();
let internal_ne_profile_max_percent_of_interface_vram_counts = side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| {
lookup
.internal_ne_profile_max_percent_of_interface_vram_counts
.clone()
})
.unwrap_or_default();
let lco_body_type_label_counts = count_named_values(
family_entries
.iter()
@ -458,7 +501,9 @@ pub fn inspect_engine_types_dir(
Ok(EngineTypesInspectionReport {
source_root: path.display().to_string(),
side_view_imb_pk4_path,
side_view_imb_pk4_path: side_view_imb_pk4_lookup
.as_ref()
.map(|lookup| lookup.path.clone()),
family_count: family_entries.len(),
car_file_count: family_entries
.iter()
@ -497,6 +542,7 @@ 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_profile_summaries,
car_side_view_resource_counts,
car_side_view_resource_pk4_match_count,
car_side_view_resource_pk4_missing_count,
@ -507,6 +553,9 @@ pub fn inspect_engine_types_dir(
internal_ne_profile_pk4_missing_count,
locomotive_pair_internal_ne_profile_pk4_match_count,
locomotive_pair_internal_ne_profile_pk4_missing_count,
internal_ne_profile_texture_size_counts,
internal_ne_profile_horizontal_scale_modifier_counts,
internal_ne_profile_max_percent_of_interface_vram_counts,
lco_companion_stem_counts,
lco_body_type_label_counts,
lco_low_cardinality_lane_counts,
@ -529,6 +578,15 @@ struct EngineTypeFamilyBuilder {
cct_file: Option<String>,
}
struct SideViewImbPk4Lookup {
path: String,
entry_names: BTreeSet<String>,
car_side_view_profile_summaries: 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>,
}
fn build_family_entry(
family: &EngineTypeFamilyBuilder,
car_reports: &BTreeMap<String, EngineTypeCarInspectionReport>,
@ -581,22 +639,65 @@ fn build_family_entry(
fn load_side_view_imb_pk4_lookup(
engine_types_dir: &Path,
) -> Result<(Option<String>, Option<BTreeSet<String>>), Box<dyn std::error::Error>> {
) -> Result<Option<SideViewImbPk4Lookup>, Box<dyn std::error::Error>> {
let Some(data_dir) = engine_types_dir.parent() else {
return Ok((None, None));
return Ok(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));
return Ok(None);
};
let bytes = fs::read(&pk4_path)?;
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)))
let mut entry_names = BTreeSet::new();
let mut car_side_view_profile_summaries = 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_internal_ne = entry.name.ends_with("_NE.imb");
if !(is_car_side_view || is_internal_ne) {
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());
}
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(),
entry_names,
car_side_view_profile_summaries,
internal_ne_profile_texture_size_counts,
internal_ne_profile_horizontal_scale_modifier_counts,
internal_ne_profile_max_percent_of_interface_vram_counts,
}))
}
fn find_case_insensitive_file(dir: &Path, expected_name: &str) -> Option<PathBuf> {
@ -614,6 +715,19 @@ fn find_case_insensitive_file(dir: &Path, expected_name: &str) -> Option<PathBuf
.map(|entry| entry.path())
}
fn summarize_imb_profile(report: &ImbInspectionReport) -> EngineTypeImbProfileSummary {
EngineTypeImbProfileSummary {
tga_name: report.tga_name.clone(),
texture_width: report.texture_width,
texture_height: report.texture_height,
target_screen_width: report.target_screen_width,
target_screen_height: report.target_screen_height,
horizontal_scale_modifier: report.horizontal_scale_modifier,
max_percent_of_interface_vram: report.max_percent_of_interface_vram,
image_rect_scaled: report.image_rect_scaled,
}
}
fn build_locomotive_display_census(
path: &Path,
families: &[EngineTypeFamilyEntry],