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

View file

@ -17,14 +17,15 @@ This file is the short active queue for the current runtime and reverse-engineer
On the cargo side, `.cgo` now collapses into five stable scalar ladders instead of arbitrary floats. On the cargo side, `.cgo` now collapses into five stable scalar ladders instead of arbitrary floats.
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 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 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 in 1.05, 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 checked PK4 linkage split is now explicit too: the side-view slot at `0xc0` is a shipped multi-package seam, not just a single `rt3_2IMB.PK4` lookup.
The parser now preserves that `CarSideView_3` miss cohort exactly, and it also preserves the tiny conditional `.lco` companion-profile seam directly: in 1.05 the padded `.lco` companion/body slots collapse to `Zephyr / (none) / CarSideView_3` for `242_a1_l`, `gp35l`, `u1l`, and `zephyrl`, and to `VL80T / Loco / CarSideView_{1,2}` for `be 5-7`, `f3 loco`, and `gp7`. In 1.05, `132 / 145` side-view resource names still resolve through `rt3_2IMB.PK4`, but the remaining `13` `CarSideView_3.imb` families resolve through `Data/2D/RT3_CoastToCoast.PK4`, and the same CoastToCoast package also contributes `4` extra `_NE.imb` hits for the unmatched display tail (`242 A1`, `Class 460`, `Class A1`, `Class P8`) while `Class QJ` remains the only unmatched display family without a packaged `_NE` profile.
The classic install widens the same `0xc0` seam further: the side-view slot is not only `CarSideView_*` but also a larger unresolved `*_Profile.imb` family, and every checked classic `*_Profile.imb` reference currently misses the packaged `rt3_2IMB.PK4` surface too. The parser now preserves that `CarSideView_3` CoastToCoast cohort exactly, and it also preserves the tiny conditional `.lco` companion-profile seam directly: in 1.05 the padded `.lco` companion/body slots collapse to `Zephyr / (none) / CarSideView_3` for `242_a1_l`, `gp35l`, `u1l`, and `zephyrl`, and to `VL80T / Loco / CarSideView_{1,2}` for `be 5-7`, `f3 loco`, and `gp7`.
A whole-tree file census now rules out the simple fallback too: none of those `*_Profile.imb` names, and no `CarSideView_3.imb`, exist as loose files anywhere in the checked `rt3/` or `rt3_105/` trees. The classic install widens the same `0xc0` seam further: the side-view slot is not only `CarSideView_*` but also a larger `*_Profile.imb` family, and those profile references are now grounded into shipped `Data/PopTopExtraContent/*.pk4` packages instead of staying unresolved. Only `Eurostar_Profile.imb` and `ICE_Profile.imb` remain missing from the checked classic shipped PK4 set.
A whole-tree file census also rules out the old loose-file fallback: none of those `*_Profile.imb` names, and no `CarSideView_3.imb`, exists as a loose file anywhere in the checked `rt3/` or `rt3_105/` trees.
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 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 `_NE` split is now aligned with the locomotive display census too: all `43` packaged `_NE` hits live inside the grounded display prefix, and all `5` unmatched display-tail families are still missing packaged `_NE` profiles. The `_NE` split is now aligned with the locomotive display census too: all `43` packaged `_NE` hits live inside the grounded display prefix, and all `5` unmatched display-tail families are still missing packaged `_NE` profiles.
The cargo side is partially linked now as well: the `.cgo` ladder families and `.cct` sidecar identifiers share the same cargo-family keys for ten checked families, with `Troop` left as the only `.cct`-only outlier. The cargo side is partially linked now as well: the `.cgo` ladder families and `.cct` sidecar identifiers share the same cargo-family keys for ten checked families, with `Troop` left as the only `.cct`-only outlier.
The next honest static work is to decide whether the two remaining reordered `QJ` auxiliary roots are just alternate content aliases or evidence of a narrower foreign-display/image root, decide whether the classic `*_Profile.imb` side-view references are dead loose-file dependencies or a still-unmapped package family, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting. The next honest static work is to decide whether the two remaining reordered `QJ` auxiliary roots are just alternate content aliases or evidence of a narrower foreign-display/image root, explain the residual classic `Eurostar_Profile.imb` and `ICE_Profile.imb` misses, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting.
Preserved checked parser detail now lives in [EngineTypes parser semantics](rehost-queue/engine-types-parser-semantics-2026-04-21.md). 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). Preserved checked format inventory detail now lives in [RT3 format inventory](rehost-queue/format-inventory-2026-04-21.md).

View file

@ -70,15 +70,21 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- the `.imb` parser now preserves comment-suffixed numeric rows in those shipped files, so - the `.imb` parser now preserves comment-suffixed numeric rows in those shipped files, so
`MaxPercentOfInterfaceVRAM`, `HorizontalScaleModifier`, `ImageWH`, and `ImageWHScaled` can `MaxPercentOfInterfaceVRAM`, `HorizontalScaleModifier`, `ImageWH`, and `ImageWHScaled` can
all surface as typed fields instead of falling back to raw text all surface as typed fields instead of falling back to raw text
- The checked 1.05 PK4 census now gives the first grounded linkage split between `EngineTypes` - The checked PK4 census now gives a broader grounded linkage split between `EngineTypes`
and the packaged `.imb` resources: and the packaged `.imb` resources:
- `132 / 145` `.car` side-view resource names resolve directly inside `rt3_2IMB.PK4` - in `rt3_105`, `132 / 145` `.car` side-view resource names resolve through `Data/2D/rt3_2IMB.PK4`
- the `13` missing side-view resource names are exactly the `CarSideView_3.imb` cohort - the remaining `13 / 145` `CarSideView_3.imb` side-view resource names resolve through
- `43 / 145` derived `{internal_stem}_NE.imb` names resolve directly inside `rt3_2IMB.PK4` `Data/2D/RT3_CoastToCoast.PK4`
- every one of those `43` `_NE` hits belongs to a matched locomotive `.car/.lco` pair - in `rt3_105`, `43 / 145` derived `{internal_stem}_NE.imb` names resolve through
- no freight-car or tender family currently resolves to a packaged `{internal_stem}_NE.imb` `Data/2D/rt3_2IMB.PK4`
- That `CarSideView_3.imb` hole is now confirmed across both checked installs (`rt3` and - the same 1.05 CoastToCoast package contributes `4` more `_NE.imb` hits:
`rt3_105`): neither shipped `rt3_2IMB.PK4` contains a `CarSideView_3.imb` entry. `242_A1L_NE.imb`, `Class460L_NE.imb`, `ClassA1L_NE.imb`, and `ClassP8L_NE.imb`
- that leaves `classqjl_NE.imb` / `Class QJ` as the only unmatched display-tail family still
missing a packaged `_NE` profile in the checked 1.05 install
- in classic `rt3`, the base `rt3_2IMB.PK4` still carries `137` side-view resource hits and
`43` `_NE.imb` hits, `RT3_CoastToCoast.PK4` still carries `15` side-view hits and `4`
unmatched-display `_NE` hits, and the remaining custom side-view / `_NE` families resolve
through shipped `Data/PopTopExtraContent/*.pk4` packages
- The parser now preserves the exact 1.05 `CarSideView_3.imb` miss cohort directly: - The parser now preserves the exact 1.05 `CarSideView_3.imb` miss cohort directly:
- `242_a1_l` - `242_a1_l`
- `242_a1_t` - `242_a1_t`
@ -93,6 +99,8 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- `u1l` - `u1l`
- `u1t` - `u1t`
- `zephyrl` - `zephyrl`
- That cohort is no longer a true package miss: all `13` of those 1.05 `CarSideView_3.imb`
references now resolve specifically to `Data/2D/RT3_CoastToCoast.PK4`.
- The conditional `.lco` companion/body seam is also narrow enough to preserve exactly in 1.05: - The conditional `.lco` companion/body seam is also narrow enough to preserve exactly in 1.05:
- `companion=Zephyr / body=(none) / side_view=CarSideView_3.imb` - `companion=Zephyr / body=(none) / side_view=CarSideView_3.imb`
- `242_a1_l` - `242_a1_l`
@ -106,8 +114,8 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- `be 5-7` - `be 5-7`
- The classic install widens the `0xc0` side-view seam beyond `CarSideView_*`. - The classic install widens the `0xc0` side-view seam beyond `CarSideView_*`.
The parser now confirms a larger checked classic `*_Profile.imb` reference family at `0xc0`, The parser now confirms a larger checked classic `*_Profile.imb` reference family at `0xc0`,
and every one of those checked classic profile names currently misses the packaged and most of those checked profile names now resolve to one matching shipped
`rt3_2IMB.PK4` entry set too: `Data/PopTopExtraContent/*.pk4` package:
- `264T_Profile.imb` - `264T_Profile.imb`
- `Black5_Profile.imb` - `Black5_Profile.imb`
- `BR39_Profile.imb` - `BR39_Profile.imb`
@ -127,6 +135,28 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- `TenWheeler_Profile.imb` - `TenWheeler_Profile.imb`
- `V2Class_Profile.imb` - `V2Class_Profile.imb`
- `Vittorio_Profile.imb` - `Vittorio_Profile.imb`
- That classic package split is now preserved directly in the inspector too:
- `264t.pk4`: `264T_Profile.imb`
- `black5.pk4`: `Black5_Profile.imb`
- `br39.pk4`: `BR39_Profile.imb`
- `classs.pk4`: `ClassS_Profile.imb`
- `daylight484.pk4`: `Daylight484_Profile.imb`
- `dxgoods.pk4`: `Goods_Profile.imb`
- `e111.pk4`: `E111_Profile.imb`
- `g10.pk4`: `G10_Profile.imb`
- `g4.pk4`: `G4_Profile.imb`
- `gp40-2.pk4`: `GP40-2_Profile.imb` and `GP40-2L_NE.imb`
- `GP7D.PK4`: `GP7D_Profile.imb` and `GP7DL_NE.imb`
- `mogul.pk4`: `Mogul_Profile.imb`
- `p-2.pk4`: `P-2_Profile.imb` and `P-2L_NE.imb`
- `sm2.pk4`: `Sm2_Profile.imb` and `Sm2L_NE.imb`
- `ten.pk4`: `TenWheeler_Profile.imb` and `TenWheelersL_NE.imb`
- `v2class.pk4`: `V2Class_Profile.imb` and `V2ClassL_NE.imb`
- `vittorio.pk4`: `Vittorio_Profile.imb` and `VittorioL_NE.imb`
- Only two checked classic side-view profile references remain unresolved after widening the
package scan:
- `Eurostar_Profile.imb`
- `ICE_Profile.imb`
- A whole-tree file census now rules out the simplest fallback for that family: - A whole-tree file census now rules out the simplest fallback for that family:
- none of those checked classic `*_Profile.imb` names exists as a loose file anywhere under the - none of those checked classic `*_Profile.imb` names exists as a loose file anywhere under the
checked `rt3/` or `rt3_105/` trees checked `rt3/` or `rt3_105/` trees
@ -137,11 +167,11 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- every packaged `_NE.imb` profile: `512x128`, `HorizontalScaleModifier = 0.75`, - every packaged `_NE.imb` profile: `512x128`, `HorizontalScaleModifier = 0.75`,
`MaxPercentOfInterfaceVRAM = 0.09`, and `ImageWHScaled` present `MaxPercentOfInterfaceVRAM = 0.09`, and `ImageWHScaled` present
- The `_NE` profile split is now tied back to the locomotive display census: - The `_NE` profile split is now tied back to the locomotive display census:
- `43` matched locomotive display-prefix families resolve `{internal_stem}_NE.imb` - in 1.05, `47` families now resolve `{internal_stem}_NE.imb`
- `18` matched display-prefix families still do not resolve `{internal_stem}_NE.imb` - in 1.05, `4 / 5` unmatched display-tail families (`242 A1`, `Class 460`, `Class A1`,
- `0` unmatched display-tail families resolve `{internal_stem}_NE.imb` `Class P8`) now resolve packaged `_NE` profiles through `RT3_CoastToCoast.PK4`
- all `5` unmatched display-tail families (`242 A1`, `Class 460`, `Class A1`, `Class P8`, - `Class QJ` remains the only checked unmatched display-tail family still missing a packaged
`Class QJ`) are missing packaged `_NE` profiles `_NE` profile
## What The Current Parser Now Owns ## What The Current Parser Now Owns
@ -185,12 +215,12 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
- whether the three opposite-role pairings are best described as direct tender-to-loco image - whether the three opposite-role pairings are best described as direct tender-to-loco image
roots or some slightly broader paired-display fallback convention roots or some slightly broader paired-display fallback convention
- whether the trailing side-view resource can be tied cleanly to the PK4-backed `CarSideView_*` - whether the trailing side-view resource can be tied cleanly to the PK4-backed `CarSideView_*`
metadata, the classic `*_Profile.imb` reference family, and the engine-specific `_NE.imb` metadata, the classic `*_Profile.imb` package family, and the engine-specific `_NE.imb`
profiles without inventing frontend semantics profiles without inventing frontend semantics
- whether the `CarSideView_3.imb` missing cohort is a classic-only asset dependency, an omitted - whether the `CarSideView_3.imb` CoastToCoast cohort should be described as a scenario /
1.05 package, or just a dead resource reference in the shipped `EngineTypes` data expansion-side view family or a more general third packaged side-view profile set
- whether the classic `*_Profile.imb` references are dead loose-file dependencies, a separate - why `Eurostar_Profile.imb` and `ICE_Profile.imb` remain unresolved when the other classic
package family outside `rt3_2IMB.PK4`, or a still-unmapped authoring convention `*_Profile.imb` references now map cleanly to shipped `PopTopExtraContent` packages
- `.lco` - `.lco`
- whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse - whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse
key, or only a subset authoring convenience key, or only a subset authoring convenience