diff --git a/crates/rrt-runtime/src/inspect/engine_types.rs b/crates/rrt-runtime/src/inspect/engine_types.rs index db3d73f..817e499 100644 --- a/crates/rrt-runtime/src/inspect/engine_types.rs +++ b/crates/rrt-runtime/src/inspect/engine_types.rs @@ -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, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EngineTypeImbProfileSummary { + pub tga_name: Option, + pub texture_width: Option, + pub texture_height: Option, + pub target_screen_width: Option, + pub target_screen_height: Option, + pub horizontal_scale_modifier: Option, + pub max_percent_of_interface_vram: Option, + 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, @@ -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, pub car_side_view_resource_counts: BTreeMap, 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, + pub internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap, + pub internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap, pub lco_companion_stem_counts: BTreeMap, pub lco_body_type_label_counts: BTreeMap, pub lco_low_cardinality_lane_counts: BTreeMap>, @@ -310,7 +327,7 @@ pub fn inspect_engine_types_dir( let mut lco_reports = BTreeMap::::new(); let mut cgo_reports = BTreeMap::::new(); let mut cct_reports = BTreeMap::::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::>(); @@ -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, } +struct SideViewImbPk4Lookup { + path: String, + entry_names: BTreeSet, + car_side_view_profile_summaries: BTreeMap, + internal_ne_profile_texture_size_counts: BTreeMap, + internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap, + internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap, +} + fn build_family_entry( family: &EngineTypeFamilyBuilder, car_reports: &BTreeMap, @@ -581,22 +639,65 @@ fn build_family_entry( fn load_side_view_imb_pk4_lookup( engine_types_dir: &Path, -) -> Result<(Option, Option>), Box> { +) -> Result, Box> { 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::>(); - 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 { @@ -614,6 +715,19 @@ fn find_case_insensitive_file(dir: &Path, expected_name: &str) -> Option 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], diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index a2bb805..2088a54 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -16,6 +16,7 @@ This file is the short active queue for the current runtime and reverse-engineer The early `.lco` lane block is now partially partitioned too: only offsets `0x20`, `0x34`, `0x38`, `0x3c`, `0x44`, `0x48`, and `0x54` behave like low-cardinality buckets, while the other early lanes still look high-variance. The side-view resource path is now grounded into `Data/2D/rt3_2IMB.PK4`, and the `.imb` parser now decodes shipped comment-suffixed numeric rows plus `_NE` profile fields such as `HorizontalScaleModifier` and `ImageWHScaled`. The checked PK4 linkage split is now explicit too: `132 / 145` side-view resource names resolve directly, but the remaining `13` are the missing `CarSideView_3.imb` cohort and that hole exists in both checked installs, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs. + The packaged profile metadata is stable enough to summarize: `CarSideView_1` is `512x512` at `0.04` VRAM, `CarSideView_2` is `512x256` at `0.02`, and every packaged `_NE` profile is `512x128` with `HorizontalScaleModifier = 0.75` and `MaxPercentOfInterfaceVRAM = 0.09`. The next honest static work is to keep promoting those fixed lanes into stable parser fields, explain the five remaining distinct auxiliary-stem cases, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. Preserved checked parser detail now lives in [EngineTypes parser semantics](rehost-queue/engine-types-parser-semantics-2026-04-21.md). Preserved checked format inventory detail now lives in [RT3 format inventory](rehost-queue/format-inventory-2026-04-21.md). diff --git a/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md b/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md index cff9555..e351748 100644 --- a/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md +++ b/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md @@ -66,6 +66,11 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed. - no freight-car or tender family currently resolves to a packaged `{internal_stem}_NE.imb` - That `CarSideView_3.imb` hole is now confirmed across both checked installs (`rt3` and `rt3_105`): neither shipped `rt3_2IMB.PK4` contains a `CarSideView_3.imb` entry. +- The packaged profile metadata is now bounded too: + - `CarSideView_1.imb`: `512x512`, `MaxPercentOfInterfaceVRAM = 0.04` + - `CarSideView_2.imb`: `512x256`, `MaxPercentOfInterfaceVRAM = 0.02` + - every packaged `_NE.imb` profile: `512x128`, `HorizontalScaleModifier = 0.75`, + `MaxPercentOfInterfaceVRAM = 0.09`, and `ImageWHScaled` present ## What The Current Parser Now Owns @@ -94,6 +99,9 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed. - comment-aware typed numeric extraction for shipped profile rows - `HorizontalScaleModifier` - `ImageWHScaled` +- `rt3_2IMB.PK4` link surface + - packaged side-view profile summaries for `CarSideView_*` + - packaged `_NE.imb` texture-size and scalar-family counts ## Remaining Static Questions