Link engine types to packaged side views
This commit is contained in:
parent
d45a84038e
commit
8387008728
3 changed files with 144 additions and 6 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ This file is the short active queue for the current runtime and reverse-engineer
|
|||
The checked 1.05 corpus now also splits `.car` auxiliary stems into `126` direct matches, `14` role-neutral roots, and only `5` truly distinct cases, while `.cgo` 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 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, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs.
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -57,6 +57,13 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
|
|||
- the `.imb` parser now preserves comment-suffixed numeric rows in those shipped files, so
|
||||
`MaxPercentOfInterfaceVRAM`, `HorizontalScaleModifier`, `ImageWH`, and `ImageWHScaled` can
|
||||
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`
|
||||
and the packaged `.imb` resources:
|
||||
- `132 / 145` `.car` side-view resource names resolve directly inside `rt3_2IMB.PK4`
|
||||
- the `13` missing side-view resource names are exactly the `CarSideView_3.imb` cohort
|
||||
- `43 / 145` derived `{internal_stem}_NE.imb` names resolve directly inside `rt3_2IMB.PK4`
|
||||
- every one of those `43` `_NE` hits belongs to a matched locomotive `.car/.lco` pair
|
||||
- no freight-car or tender family currently resolves to a packaged `{internal_stem}_NE.imb`
|
||||
|
||||
## What The Current Parser Now Owns
|
||||
|
||||
|
|
@ -67,6 +74,8 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
|
|||
- auxiliary stem slot
|
||||
- side-view resource name
|
||||
- auxiliary-stem relation counts across the shipped corpus
|
||||
- PK4-backed side-view resource resolution status
|
||||
- derived `{internal_stem}_NE.imb` resolution status
|
||||
- `.lco`
|
||||
- full internal stem
|
||||
- conditional companion stem slot
|
||||
|
|
@ -91,6 +100,8 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
|
|||
alternate content root, paired tender/loco image root, or a narrower foreign-display alias
|
||||
- whether the trailing side-view resource can be tied cleanly to the PK4-backed `CarSideView_*`
|
||||
metadata and the engine-specific `_NE.imb` profiles without inventing frontend semantics
|
||||
- whether the `CarSideView_3.imb` missing cohort is a classic-only asset dependency, an omitted
|
||||
1.05 package, or just a dead resource reference in the shipped `EngineTypes` data
|
||||
- `.lco`
|
||||
- whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse
|
||||
key, or only a subset authoring convenience
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue