diff --git a/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs b/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs index d1b2685..4c75fee 100644 --- a/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs +++ b/crates/rrt-cli/src/app/dispatch/runtime/inspect.rs @@ -5,8 +5,8 @@ use crate::app::runtime_inspect::{ inspect_car, inspect_cargo_economy_sources, inspect_cargo_price_selector, inspect_cargo_production_selector, inspect_cargo_skins, inspect_cargo_types, inspect_cct, inspect_cgo, inspect_compact_event_dispatch_cluster, - inspect_compact_event_dispatch_cluster_counts, inspect_infrastructure_asset_trace, - inspect_engine_types, inspect_imb, inspect_lco, inspect_lng, inspect_map_title_hints, + inspect_compact_event_dispatch_cluster_counts, inspect_engine_types, inspect_imb, + inspect_infrastructure_asset_trace, inspect_lco, inspect_lng, inspect_map_title_hints, inspect_periodic_company_service_trace, inspect_pk4, inspect_placed_structure_dynamic_side_buffer, inspect_region_service_trace, inspect_save_company_chairman, inspect_save_placed_structure_triplets, diff --git a/crates/rrt-runtime/src/inspect/engine_types.rs b/crates/rrt-runtime/src/inspect/engine_types.rs index 9b3d347..150b215 100644 --- a/crates/rrt-runtime/src/inspect/engine_types.rs +++ b/crates/rrt-runtime/src/inspect/engine_types.rs @@ -7,7 +7,15 @@ use serde::{Deserialize, Serialize}; const CAR_PRIMARY_DISPLAY_NAME_OFFSET: usize = 0x0c; const CAR_CONTENT_NAME_OFFSET: usize = 0x48; const CAR_INTERNAL_STEM_OFFSET: usize = 0x84; +const CAR_AUXILIARY_STEM_OFFSET: usize = 0xa2; +const CAR_AUXILIARY_STEM_LEN: usize = 0x1e; +const CAR_SIDE_VIEW_RESOURCE_OFFSET: usize = 0xc0; +const CAR_SIDE_VIEW_RESOURCE_LEN: usize = 0x20; const LCO_INTERNAL_STEM_OFFSET: usize = 0x04; +const LCO_COMPANION_STEM_OFFSET: usize = 0x0c; +const LCO_COMPANION_STEM_LEN: usize = 0x06; +const LCO_BODY_TYPE_LABEL_OFFSET: usize = 0x12; +const LCO_BODY_TYPE_LABEL_LEN: usize = 0x06; const UNMATCHED_LOCOMOTIVE_DISPLAY_NAMES: [&str; 5] = ["242 A1", "Class 460", "Class A1", "Class P8", "Class QJ"]; const LCO_EARLY_LANE_OFFSETS: [usize; 14] = [ @@ -24,6 +32,8 @@ pub struct EngineTypeCarInspectionReport { pub primary_display_name: Option, pub content_name: Option, pub internal_stem: Option, + pub auxiliary_stem: Option, + pub side_view_resource: Option, pub notes: Vec, } @@ -42,6 +52,8 @@ pub struct EngineTypeLcoInspectionReport { pub header_magic: Option, pub header_magic_hex: Option, pub internal_stem: Option, + pub companion_stem: Option, + pub body_type_label: Option, pub early_lanes: Vec, pub notes: Vec, } @@ -110,6 +122,10 @@ pub struct EngineTypeFamilyEntry { pub primary_display_name: Option, pub content_name: Option, pub internal_stem: Option, + pub auxiliary_stem: Option, + pub side_view_resource: Option, + pub companion_stem: Option, + pub body_type_label: Option, pub cct_identifier: Option, pub cct_value: Option, pub has_matched_locomotive_pair: bool, @@ -151,8 +167,18 @@ pub fn inspect_car_bytes( primary_display_name: read_ascii_field(bytes, CAR_PRIMARY_DISPLAY_NAME_OFFSET), content_name: read_ascii_field(bytes, CAR_CONTENT_NAME_OFFSET), internal_stem: read_ascii_field(bytes, CAR_INTERNAL_STEM_OFFSET), + auxiliary_stem: read_ascii_slot( + bytes, + CAR_AUXILIARY_STEM_OFFSET, + CAR_AUXILIARY_STEM_LEN, + ), + side_view_resource: read_ascii_slot( + bytes, + CAR_SIDE_VIEW_RESOURCE_OFFSET, + CAR_SIDE_VIEW_RESOURCE_LEN, + ), notes: vec![ - "The current .car parser exposes the fixed header fields already grounded by the checked locomotive display census.".to_string(), + "The current .car parser exposes the fixed header strings already grounded by the checked locomotive display census, plus the auxiliary stem slot at 0xa2 and the trailing side-view resource name at 0xc0.".to_string(), ], }) } @@ -185,9 +211,11 @@ pub fn inspect_lco_bytes( header_magic: read_u32_le(bytes, 0), header_magic_hex: read_u32_le(bytes, 0).map(|value| format!("0x{value:08x}")), internal_stem: read_ascii_field(bytes, LCO_INTERNAL_STEM_OFFSET), + companion_stem: read_lco_companion_stem(bytes), + body_type_label: read_lco_body_type_label(bytes), early_lanes, notes: vec![ - "The current .lco parser exposes the fixed stem at 0x04 plus the early raw lane block without asserting gameplay semantics for those numeric fields.".to_string(), + "The current .lco parser exposes the fixed-width stem slots at 0x04, 0x0c, and 0x12 plus the early raw lane block without asserting gameplay semantics for those numeric fields.".to_string(), ], }) } @@ -226,8 +254,14 @@ pub fn inspect_cct_bytes( bytes: &[u8], ) -> Result> { let text = decode_windows_1252(bytes); - let raw_lines = text.lines().map(|line| line.to_string()).collect::>(); - let first_nonblank = raw_lines.iter().find(|line| !line.trim().is_empty()).cloned(); + let raw_lines = text + .lines() + .map(|line| line.to_string()) + .collect::>(); + let first_nonblank = raw_lines + .iter() + .find(|line| !line.trim().is_empty()) + .cloned(); let (identifier, value) = first_nonblank .as_deref() .map(parse_cct_row) @@ -298,7 +332,7 @@ pub fn inspect_engine_types_dir( let family_entries = families .values() - .map(|family| build_family_entry(family, &car_reports, &cct_reports)) + .map(|family| build_family_entry(family, &car_reports, &lco_reports, &cct_reports)) .collect::>(); let matched_locomotive_pair_count = family_entries .iter() @@ -310,10 +344,22 @@ pub fn inspect_engine_types_dir( Ok(EngineTypesInspectionReport { source_root: path.display().to_string(), family_count: family_entries.len(), - car_file_count: family_entries.iter().filter(|entry| entry.car_file.is_some()).count(), - lco_file_count: family_entries.iter().filter(|entry| entry.lco_file.is_some()).count(), - cgo_file_count: family_entries.iter().filter(|entry| entry.cgo_file.is_some()).count(), - cct_file_count: family_entries.iter().filter(|entry| entry.cct_file.is_some()).count(), + car_file_count: family_entries + .iter() + .filter(|entry| entry.car_file.is_some()) + .count(), + lco_file_count: family_entries + .iter() + .filter(|entry| entry.lco_file.is_some()) + .count(), + cgo_file_count: family_entries + .iter() + .filter(|entry| entry.cgo_file.is_some()) + .count(), + cct_file_count: family_entries + .iter() + .filter(|entry| entry.cct_file.is_some()) + .count(), matched_locomotive_pair_count, unmatched_car_file_count: family_entries .iter() @@ -325,11 +371,15 @@ pub fn inspect_engine_types_dir( .count(), unmatched_cgo_file_count: family_entries .iter() - .filter(|entry| entry.cgo_file.is_some() && !(entry.car_file.is_some() || entry.lco_file.is_some())) + .filter(|entry| { + entry.cgo_file.is_some() && !(entry.car_file.is_some() || entry.lco_file.is_some()) + }) .count(), unmatched_cct_file_count: family_entries .iter() - .filter(|entry| entry.cct_file.is_some() && !(entry.car_file.is_some() || entry.lco_file.is_some())) + .filter(|entry| { + entry.cct_file.is_some() && !(entry.car_file.is_some() || entry.lco_file.is_some()) + }) .count(), locomotive_display_census, families: family_entries, @@ -348,12 +398,17 @@ struct EngineTypeFamilyBuilder { fn build_family_entry( family: &EngineTypeFamilyBuilder, car_reports: &BTreeMap, + lco_reports: &BTreeMap, cct_reports: &BTreeMap, ) -> EngineTypeFamilyEntry { let car_report = family .car_file .as_ref() .and_then(|file_name| car_reports.get(file_name)); + let lco_report = family + .lco_file + .as_ref() + .and_then(|file_name| lco_reports.get(file_name)); let cct_report = family .cct_file .as_ref() @@ -367,6 +422,10 @@ fn build_family_entry( 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()), + auxiliary_stem: car_report.and_then(|report| report.auxiliary_stem.clone()), + side_view_resource: car_report.and_then(|report| report.side_view_resource.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()), 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(), @@ -414,7 +473,10 @@ fn build_locomotive_display_census( .count(); let mut car_header_layout = BTreeMap::new(); - car_header_layout.insert("format_version_dword_offset".to_string(), "0x00".to_string()); + car_header_layout.insert( + "format_version_dword_offset".to_string(), + "0x00".to_string(), + ); car_header_layout.insert("record_kind_dword_offset".to_string(), "0x04".to_string()); car_header_layout.insert( "primary_display_name_offset".to_string(), @@ -464,6 +526,37 @@ fn read_ascii_field(bytes: &[u8], offset: usize) -> Option { (!value.is_empty()).then_some(value) } +fn read_ascii_slot(bytes: &[u8], offset: usize, len: usize) -> Option { + let slot = bytes.get(offset..offset + len)?; + let end = slot + .iter() + .position(|byte| *byte == 0 || !byte.is_ascii() || *byte == 0xcd) + .unwrap_or(slot.len()); + let value = String::from_utf8(slot[..end].to_vec()).ok()?; + (!value.is_empty()).then_some(value) +} + +fn slot_is_padded(bytes: &[u8], offset: usize, len: usize) -> bool { + bytes + .get(offset..offset + len) + .map(|slot| slot.contains(&0)) + .unwrap_or(false) +} + +fn read_lco_companion_stem(bytes: &[u8]) -> Option { + slot_is_padded(bytes, LCO_INTERNAL_STEM_OFFSET, 0x08) + .then(|| read_ascii_slot(bytes, LCO_COMPANION_STEM_OFFSET, LCO_COMPANION_STEM_LEN)) + .flatten() +} + +fn read_lco_body_type_label(bytes: &[u8]) -> Option { + let companion_slot_is_padded = + slot_is_padded(bytes, LCO_COMPANION_STEM_OFFSET, LCO_COMPANION_STEM_LEN); + companion_slot_is_padded + .then(|| read_ascii_slot(bytes, LCO_BODY_TYPE_LABEL_OFFSET, LCO_BODY_TYPE_LABEL_LEN)) + .flatten() +} + fn parse_cct_row(line: &str) -> (Option, Option) { let mut parts = line.split_whitespace(); let identifier = parts.next().map(|value| value.to_string()); @@ -472,7 +565,10 @@ fn parse_cct_row(line: &str) -> (Option, Option) { } fn decode_windows_1252(bytes: &[u8]) -> String { - bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect() + bytes + .iter() + .map(|byte| decode_windows_1252_byte(*byte)) + .collect() } fn decode_windows_1252_byte(byte: u8) -> char { @@ -514,36 +610,57 @@ mod tests { #[test] fn parses_car_header_fields() { - let mut bytes = vec![0u8; 0x90]; + let mut bytes = vec![0u8; 0xe0]; bytes[0..4].copy_from_slice(&0x03eau32.to_le_bytes()); bytes[4..8].copy_from_slice(&2u32.to_le_bytes()); bytes[0x0c..0x0c + 6].copy_from_slice(b"2-D-2\0"); bytes[0x48..0x48 + 5].copy_from_slice(b"2D2L\0"); bytes[0x84..0x84 + 5].copy_from_slice(b"2D2L\0"); + bytes[0xa2..0xa2 + 5].copy_from_slice(b"2D2L\0"); + bytes[0xc0..0xc0 + 18].copy_from_slice(b"CarSideView_2.imb\0"); let report = inspect_car_bytes(&bytes).expect("car should parse"); assert_eq!(report.header_magic, Some(0x03ea)); assert_eq!(report.primary_display_name.as_deref(), Some("2-D-2")); assert_eq!(report.internal_stem.as_deref(), Some("2D2L")); + assert_eq!(report.auxiliary_stem.as_deref(), Some("2D2L")); + assert_eq!( + report.side_view_resource.as_deref(), + Some("CarSideView_2.imb") + ); } #[test] - fn parses_lco_header_and_lanes() { + fn parses_lco_header_slots_and_lanes() { let mut bytes = vec![0u8; 0x58]; bytes[0..4].copy_from_slice(&0x07d5u32.to_le_bytes()); - bytes[4..4 + 5].copy_from_slice(b"2D2L\0"); + bytes[4..4 + 5].copy_from_slice(b"GP7L\0"); + bytes[0x0c..0x0c + 6].copy_from_slice(b"VL80T\0"); + bytes[0x12..0x12 + 5].copy_from_slice(b"Loco\0"); bytes[0x20..0x24].copy_from_slice(&100u32.to_le_bytes()); let report = inspect_lco_bytes(&bytes).expect("lco should parse"); assert_eq!(report.header_magic, Some(0x07d5)); - assert_eq!(report.internal_stem.as_deref(), Some("2D2L")); + assert_eq!(report.internal_stem.as_deref(), Some("GP7L")); + assert_eq!(report.companion_stem.as_deref(), Some("VL80T")); + assert_eq!(report.body_type_label.as_deref(), Some("Loco")); assert_eq!(report.early_lanes[0].raw_u32, 100); } + #[test] + fn does_not_misclassify_long_lco_stems_as_companion_slots() { + let mut bytes = vec![0u8; 0x20]; + bytes[4..4 + 9].copy_from_slice(b"AtlanticL"); + + let report = inspect_lco_bytes(&bytes).expect("lco should parse"); + assert_eq!(report.internal_stem.as_deref(), Some("AtlanticL")); + assert_eq!(report.companion_stem, None); + assert_eq!(report.body_type_label, None); + } + #[test] fn parses_cgo_and_cct_files() { - let cgo = inspect_cgo_bytes(b"\x00\x00\\BAuto_Carrier\0") - .expect("cgo should parse"); + let cgo = inspect_cgo_bytes(b"\x00\x00\\BAuto_Carrier\0").expect("cgo should parse"); assert_eq!(cgo.content_stem.as_deref(), Some("Auto_Carrier")); let cct = inspect_cct_bytes(b"Auto_Carrier 13\n").expect("cct should parse"); @@ -551,6 +668,67 @@ mod tests { assert_eq!(cct.value, Some(13)); } + #[test] + fn builds_family_entry_with_extended_car_and_lco_slots() { + let family = EngineTypeFamilyBuilder { + canonical_stem: "gp7".to_string(), + car_file: Some("GP7.car".to_string()), + lco_file: Some("GP7.lco".to_string()), + cgo_file: Some("GP7.cgo".to_string()), + cct_file: Some("GP7.cct".to_string()), + }; + let car_reports = BTreeMap::from([( + "GP7.car".to_string(), + EngineTypeCarInspectionReport { + file_size: 0, + header_magic: None, + header_magic_hex: None, + record_kind: None, + record_kind_hex: None, + primary_display_name: Some("GP7".to_string()), + content_name: Some("GP7".to_string()), + internal_stem: Some("GP7L".to_string()), + auxiliary_stem: Some("GP7L".to_string()), + side_view_resource: Some("CarSideView_1.imb".to_string()), + notes: Vec::new(), + }, + )]); + let lco_reports = BTreeMap::from([( + "GP7.lco".to_string(), + EngineTypeLcoInspectionReport { + file_size: 0, + header_magic: None, + header_magic_hex: None, + internal_stem: Some("GP7L".to_string()), + companion_stem: Some("VL80T".to_string()), + body_type_label: Some("Loco".to_string()), + early_lanes: Vec::new(), + notes: Vec::new(), + }, + )]); + let cct_reports = BTreeMap::from([( + "GP7.cct".to_string(), + EngineTypeCctInspectionReport { + file_size: 0, + line_count: 1, + identifier: Some("GP7".to_string()), + value: Some(13), + raw_lines: vec!["GP7 13".to_string()], + notes: Vec::new(), + }, + )]); + + let entry = build_family_entry(&family, &car_reports, &lco_reports, &cct_reports); + assert_eq!(entry.auxiliary_stem.as_deref(), Some("GP7L")); + assert_eq!( + entry.side_view_resource.as_deref(), + Some("CarSideView_1.imb") + ); + assert_eq!(entry.companion_stem.as_deref(), Some("VL80T")); + assert_eq!(entry.body_type_label.as_deref(), Some("Loco")); + assert_eq!(entry.cct_identifier.as_deref(), Some("GP7")); + } + #[test] fn builds_locomotive_display_census() { let mut car_reports = BTreeMap::new(); @@ -565,6 +743,8 @@ mod tests { primary_display_name: Some("2-D-2".to_string()), content_name: Some("2D2L".to_string()), internal_stem: Some("2D2L".to_string()), + auxiliary_stem: Some("2D2L".to_string()), + side_view_resource: Some("CarSideView_2.imb".to_string()), notes: Vec::new(), }, ); @@ -577,6 +757,10 @@ mod tests { primary_display_name: Some("2-D-2".to_string()), content_name: Some("2D2L".to_string()), internal_stem: Some("2D2L".to_string()), + auxiliary_stem: Some("2D2L".to_string()), + side_view_resource: Some("CarSideView_2.imb".to_string()), + companion_stem: None, + body_type_label: None, cct_identifier: None, cct_value: None, has_matched_locomotive_pair: true, diff --git a/crates/rrt-runtime/src/inspect/imb.rs b/crates/rrt-runtime/src/inspect/imb.rs index 4560047..c03776f 100644 --- a/crates/rrt-runtime/src/inspect/imb.rs +++ b/crates/rrt-runtime/src/inspect/imb.rs @@ -93,7 +93,10 @@ fn parse_f64_tokens(tokens: &[String]) -> Option> { } fn decode_windows_1252(bytes: &[u8]) -> String { - bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect() + bytes + .iter() + .map(|byte| decode_windows_1252_byte(*byte)) + .collect() } fn decode_windows_1252_byte(byte: u8) -> char { @@ -135,10 +138,8 @@ mod tests { #[test] fn parses_scalar_and_tuple_lines() { - let report = inspect_imb_bytes( - b"TGAName ICE_Profile\nTGAWidth 256\nImageWH 0 0 138 32\n", - ) - .expect("imb should parse"); + let report = inspect_imb_bytes(b"TGAName ICE_Profile\nTGAWidth 256\nImageWH 0 0 138 32\n") + .expect("imb should parse"); assert_eq!(report.entry_count, 3); assert_eq!(report.entries[0].key, "TGAName"); diff --git a/crates/rrt-runtime/src/inspect/lng.rs b/crates/rrt-runtime/src/inspect/lng.rs index 06bbf8e..515ef76 100644 --- a/crates/rrt-runtime/src/inspect/lng.rs +++ b/crates/rrt-runtime/src/inspect/lng.rs @@ -97,7 +97,10 @@ pub fn inspect_lng_bytes(bytes: &[u8]) -> Result>(); - let format_family = match (format_kinds.contains("string"), format_kinds.contains("styled")) { + let format_family = match ( + format_kinds.contains("string"), + format_kinds.contains("styled"), + ) { (true, false) => "quoted-string-table".to_string(), (false, true) => "styled-credits-lines".to_string(), (true, true) => "mixed-language-table".to_string(), @@ -189,7 +192,10 @@ fn normalize_lng_text(text: &str) -> String { } fn decode_windows_1252(bytes: &[u8]) -> String { - bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect() + bytes + .iter() + .map(|byte| decode_windows_1252_byte(*byte)) + .collect() } fn decode_windows_1252_byte(byte: u8) -> char { diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 27c94a9..5ba2770 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -10,11 +10,10 @@ This file is the short active queue for the current runtime and reverse-engineer ## Current Active Items -- No active repo-local non-dynamic items remain. - The last local static head was the locomotives-page tail, and the checked [locomotive catalog tail census](../artifacts/exports/rt3-1.06/locomotive-catalog-tail-census.json) now exhausts the full local `.gms + .gmx` corpus under `rt3_wineprefix/drive_c`: `29` candidate saves found, `26` parsed samples, `5` catalog-bearing saves, one save-stable ordinal prefix through `58` (`VL80T`), two observed `59+` tail clusters (`g.gms` with `242 A1 / Class 460 / Class A1 / Class P8 / U1`, and the four classic 1.05 saves with `GP 35 / U1 / Zephyr`), zero observed `Class QJ`, and zero packed-event carriers for descriptor `452` or the upper bands `457..474` / `475..502`. - The added `18` `.gmx` sandbox saves widen the local corpus and packed-event coverage, but they still contribute no named locomotive table and no derived `locomotive_catalog`, so they do not move the save-native tail evidence beyond the same five catalog-bearing `.gms` saves. - That means the remaining locomotive questions are no longer repo-local static work. They now require either a broader save corpus or dynamic tracing. - Preserved checked locomotive blocker detail now lives in [Locomotive descriptor tails](rehost-queue/locomotive-descriptor-tails-2026-04-21.md). +- The active static parser head is now the `engine_types` semantics frontier. + The repo now has structural inspectors for `.car`, `.lco`, `.cgo`, and `.cct`, but the binary side is still only partially semantic: the checked 1.05 corpus grounds `.car` fixed strings at `0x0c / 0x48 / 0x84` plus a second fixed stem slot at `0xa2` and a side-view resource name at `0xc0`, while `.lco` carries a stable primary stem at `0x04` and only conditional companion/body slots at `0x0c` and `0x12` when the leading stem slot is padded. + The next honest static work is to keep promoting those fixed lanes into stable parser fields and decide how far `.cgo` and the remaining `EngineTypes` sidecars 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). ## Preserved External And Dynamic Blockers @@ -34,6 +33,7 @@ This file is the short active queue for the current runtime and reverse-engineer ## Preserved Detail - [Archive snapshot](rehost-queue/archive-2026-04-19.md) +- [EngineTypes parser semantics](rehost-queue/engine-types-parser-semantics-2026-04-21.md) - [RT3 format inventory](rehost-queue/format-inventory-2026-04-21.md) - [Locomotive descriptor tails](rehost-queue/locomotive-descriptor-tails-2026-04-21.md) - [Periodic company control lane](rehost-queue/periodic-company-control-lane-2026-04-21.md) diff --git a/docs/rehost-queue/README.md b/docs/rehost-queue/README.md index 1a3c1bc..32f70d0 100644 --- a/docs/rehost-queue/README.md +++ b/docs/rehost-queue/README.md @@ -4,6 +4,9 @@ This directory preserves older queue snapshots and long-form implementation note useful as evidence, but should not stay in the short active queue file. - `archive-2026-04-19.md`: preserved detailed queue snapshot from the pre-index cleanup. +- `engine-types-parser-semantics-2026-04-21.md`: current static parser frontier for the + `engine_types` family, including the grounded `.car` fixed slots, guarded `.lco` companion/body + slots, and the remaining semantic questions around `.cgo`. - `format-inventory-2026-04-21.md`: current file-format inventory under `rt3/` and `rt3_105/`, including the RT3-native families we still do not parse. - `locomotive-descriptor-tails-2026-04-21.md`: checked `.gms + .gmx` local locomotive catalog 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 new file mode 100644 index 0000000..9538efe --- /dev/null +++ b/docs/rehost-queue/engine-types-parser-semantics-2026-04-21.md @@ -0,0 +1,70 @@ +# EngineTypes Parser Semantics (2026-04-21) + +This note preserves the current static parser frontier for the `engine_types` family after the +first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed. + +## Grounded Fixed Lanes + +- `.car` is no longer just a three-string header: + - `0x0c`: primary display name + - `0x48`: content name + - `0x84`: internal stem + - `0xa2`: second fixed stem slot + - `0xc0`: side-view resource name such as `CarSideView_1.imb` +- The checked 1.05 corpus (`145` `.car` files) carries all five of those `.car` slots on every + file inspected so far. +- `.lco` carries one always-present primary stem at `0x04`. +- `.lco` only carries meaningful secondary slots when that leading stem slot is padded: + - `0x0c`: conditional companion stem such as `VL80T` or `Zephyr` + - `0x12`: conditional body label such as `Loco` +- The checked 1.05 corpus (`66` `.lco` files) shows why the guard matters: long primary stems + such as `AtlanticL` naturally spill across `0x0c`, so `0x0c` and `0x12` are not independent + fixed fields unless the earlier slot is actually zero-padded. +- `.cgo` looks structurally narrow right now: the checked 1.05 corpus has `37` files, all exactly + `25` bytes long, each carrying one leading scalar lane plus an inline content stem at `0x04`. +- `.cct` remains the least ambiguous sidecar: current shipped files still look like narrow one-row + text metadata. + +## What The Current Parser Now Owns + +- `.car` + - primary display name + - content name + - internal stem + - auxiliary stem slot + - side-view resource name +- `.lco` + - full internal stem + - conditional companion stem slot + - conditional body-type label + - early raw numeric lane block `0x20..0x54` +- `.cgo` + - leading scalar lane + - content stem +- `.cct` + - tokenized identifier/value row + +## Remaining Static Questions + +- `.car` + - what the `0xa2` auxiliary stem really represents across locomotive, tender, and freight-car + families: alias root, image key, or alternate content stem + - whether the trailing side-view resource can be tied cleanly to `.imb` metadata without + inventing frontend semantics +- `.lco` + - whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse + key, or only a subset authoring convenience + - how much of the early numeric lane block can be promoted from raw `u32/f32` views into stable + typed semantics without dynamic evidence +- `.cgo` + - whether the leading scalar is enough to justify a named typed field, or whether it should stay + a conservative raw scalar until more binary/code correlation exists + +## Next Static Parser Work + +- keep extending `engine_types` instead of creating a parallel parser family +- prefer fixed-slot promotion only when the corpus proves the slot is independent rather than a + spillover from an earlier variable-width stem +- treat `.cgo` as parser-complete structurally unless a clearer gameplay consumer appears +- keep the broader remaining unparsed-family list in [RT3 format inventory](format-inventory-2026-04-21.md) + rather than duplicating it here