Add parsers for RT3 language and engine type assets

This commit is contained in:
Jan Petykiewicz 2026-04-21 22:10:04 -07:00
commit 61472bf72d
17 changed files with 32835 additions and 9 deletions

View file

@ -31,6 +31,9 @@ Canonical derived outputs for the patch 1.06 executable.
- `event-effects-building-bindings.json`
- `economy-cargo-sources.json`
- `building-type-sources.json`
- `rt3-language-catalog.json`
- `engine-type-locomotive-display-census.json`
- `locomotive-catalog-tail-census.json`
- `candidate-table-header-clusters.json`
- `candidate-table-named-runs.json`
- `compact-event-dispatch-cluster-counts.json`

View file

@ -0,0 +1,589 @@
{
"format_version": 1,
"semantic_family": "engine-type-locomotive-display-census",
"source_root": "rt3_wineprefix/drive_c/rt3_105/Data/EngineTypes",
"car_header_layout": {
"content_name_offset": "0x48",
"format_version_dword_offset": "0x00",
"internal_stem_offset": "0x84",
"primary_display_name_offset": "0x0c",
"record_kind_dword_offset": "0x04"
},
"observed_locomotive_pair_count": 66,
"grounded_prefix_count": 61,
"grounded_prefix_match_count": 61,
"unmatched_display_family_count": 5,
"unmatched_display_families": [
{
"car_file": "242_A1_L.car",
"lco_file": "242_A1_L.lco",
"primary_display_name": "242 A1",
"content_name": "242_A1_L",
"internal_stem": "242_A1L"
},
{
"car_file": "Class_460.car",
"lco_file": "Class_460.lco",
"primary_display_name": "Class 460",
"content_name": "Class_460",
"internal_stem": "Class460L"
},
{
"car_file": "Class_A1L.car",
"lco_file": "Class_A1L.lco",
"primary_display_name": "Class A1",
"content_name": "Class_A1L",
"internal_stem": "ClassA1L"
},
{
"car_file": "Class_P8L.car",
"lco_file": "Class_P8L.lco",
"primary_display_name": "Class P8",
"content_name": "Class_P8L",
"internal_stem": "ClassP8L"
},
{
"car_file": "Class_QJL.car",
"lco_file": "Class_QJL.lco",
"primary_display_name": "Class QJ",
"content_name": "Class_QJL",
"internal_stem": "classqjl"
}
],
"entries": [
{
"car_file": "242_A1_L.car",
"lco_file": "242_A1_L.lco",
"primary_display_name": "242 A1",
"content_name": "242_A1_L",
"internal_stem": "242_A1L",
"matches_grounded_prefix_name": false
},
{
"car_file": "2D2L.car",
"lco_file": "2D2L.lco",
"primary_display_name": "2-D-2",
"content_name": "2D2L",
"internal_stem": "2D2L",
"matches_grounded_prefix_name": true
},
{
"car_file": "88L.car",
"lco_file": "88L.lco",
"primary_display_name": "E-88",
"content_name": "88L",
"internal_stem": "88L",
"matches_grounded_prefix_name": true
},
{
"car_file": "AMD103.car",
"lco_file": "AMD103.lco",
"primary_display_name": "USA 103",
"content_name": "AMD103",
"internal_stem": "AMD103L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Adler 2-2-2 Loco.car",
"lco_file": "Adler 2-2-2 Loco.lco",
"primary_display_name": "Adler 2-2-2",
"content_name": "Adler 2-2-2 Loco",
"internal_stem": "AdlerL",
"matches_grounded_prefix_name": true
},
{
"car_file": "American 4-4-0 Loco.car",
"lco_file": "American 4-4-0 Loco.lco",
"primary_display_name": "American 4-4-0",
"content_name": "American 4-4-0 Loco",
"internal_stem": "AMER440L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Atlantic Class 4-4-2 Loco.car",
"lco_file": "Atlantic Class 4-4-2 Loco.lco",
"primary_display_name": "Atlantic 4-4-2",
"content_name": "Atlantic Class 4-4-2 Loco",
"internal_stem": "AtlanticL",
"matches_grounded_prefix_name": true
},
{
"car_file": "BE 5-7.car",
"lco_file": "BE 5-7.lco",
"primary_display_name": "Be 5/7",
"content_name": "BE 5-7",
"internal_stem": "BE57L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Baldwin 060 Loco.car",
"lco_file": "Baldwin 060 Loco.lco",
"primary_display_name": "Baldwin 0-6-0",
"content_name": "Baldwin 060 Loco",
"internal_stem": "Baldwin060L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Beuth 222 Loco.car",
"lco_file": "Beuth 222 Loco.lco",
"primary_display_name": "Beuth 2-2-2",
"content_name": "Beuth 222 Loco",
"internal_stem": "beuth222l",
"matches_grounded_prefix_name": true
},
{
"car_file": "Big Boy.car",
"lco_file": "Big Boy.lco",
"primary_display_name": "Big Boy 4-8-8-4",
"content_name": "Big Boy",
"internal_stem": "BigBoyL",
"matches_grounded_prefix_name": true
},
{
"car_file": "C55 Deltic.car",
"lco_file": "C55 Deltic.lco",
"primary_display_name": "C55 Deltic",
"content_name": "C55 Deltic",
"internal_stem": "c55DelticL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Camelback Loco.car",
"lco_file": "Camelback Loco.lco",
"primary_display_name": "Camelback 0-6-0",
"content_name": "Camelback Loco",
"internal_stem": "CamelBackL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Challenger Loco.car",
"lco_file": "Challenger Loco.lco",
"primary_display_name": "Challenger 4-6-6-4",
"content_name": "Challenger Loco",
"internal_stem": "CHALLENGERL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class 01 Loco.car",
"lco_file": "Class 01 Loco.lco",
"primary_display_name": "Class 01 4-6-2",
"content_name": "Class 01 Loco",
"internal_stem": "Class01L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class 103.car",
"lco_file": "Class 103.lco",
"primary_display_name": "Class 103",
"content_name": "Class 103",
"internal_stem": "C103L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class 132 Loco.car",
"lco_file": "Class 132 Loco.lco",
"primary_display_name": "Class 132",
"content_name": "Class 132 Loco",
"internal_stem": "C132L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class 500 Loco.car",
"lco_file": "Class 500 Loco.lco",
"primary_display_name": "Class 500 4-6-0",
"content_name": "Class 500 Loco",
"internal_stem": "Class500L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class 9100.car",
"lco_file": "Class 9100.lco",
"primary_display_name": "Class 9100",
"content_name": "Class 9100",
"internal_stem": "Class9100L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class EF 66.car",
"lco_file": "Class EF 66.lco",
"primary_display_name": "Class EF 66",
"content_name": "Class EF 66",
"internal_stem": "EF66L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class6EL.car",
"lco_file": "Class6EL.lco",
"primary_display_name": "Class 6E",
"content_name": "Class6EL",
"internal_stem": "Class6EL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Class_460.car",
"lco_file": "Class_460.lco",
"primary_display_name": "Class 460",
"content_name": "Class_460",
"internal_stem": "Class460L",
"matches_grounded_prefix_name": false
},
{
"car_file": "Class_A1L.car",
"lco_file": "Class_A1L.lco",
"primary_display_name": "Class A1",
"content_name": "Class_A1L",
"internal_stem": "ClassA1L",
"matches_grounded_prefix_name": false
},
{
"car_file": "Class_P8L.car",
"lco_file": "Class_P8L.lco",
"primary_display_name": "Class P8",
"content_name": "Class_P8L",
"internal_stem": "ClassP8L",
"matches_grounded_prefix_name": false
},
{
"car_file": "Class_QJL.car",
"lco_file": "Class_QJL.lco",
"primary_display_name": "Class QJ",
"content_name": "Class_QJL",
"internal_stem": "classqjl",
"matches_grounded_prefix_name": false
},
{
"car_file": "Consolidation Loco.car",
"lco_file": "Consolidation Loco.lco",
"primary_display_name": "Consolidation 2-8-0",
"content_name": "Consolidation Loco",
"internal_stem": "CONSOLIDATIONL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Crampton 4-2-0 Locomotive.car",
"lco_file": "Crampton 4-2-0 Locomotive.lco",
"primary_display_name": "Crampton 4-2-0",
"content_name": "Crampton 4-2-0 Locomotive",
"internal_stem": "CramptonL",
"matches_grounded_prefix_name": true
},
{
"car_file": "DD080-X.car",
"lco_file": "DD080-X.lco",
"primary_display_name": "DD 080-X",
"content_name": "DD080-X",
"internal_stem": "FutureL",
"matches_grounded_prefix_name": true
},
{
"car_file": "DD40AXL.car",
"lco_file": "DD40AXL.lco",
"primary_display_name": "DD40AX",
"content_name": "DD40AXL",
"internal_stem": "DD40AXL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Duke Class 4-4-0 Loco.car",
"lco_file": "Duke Class 4-4-0 Loco.lco",
"primary_display_name": "Duke Class 4-4-0",
"content_name": "Duke Class 4-4-0 Loco",
"internal_stem": "DukeL",
"matches_grounded_prefix_name": true
},
{
"car_file": "E 18.car",
"lco_file": "E 18.lco",
"primary_display_name": "E18",
"content_name": "E 18",
"internal_stem": "E18L",
"matches_grounded_prefix_name": true
},
{
"car_file": "E 428L.car",
"lco_file": "E 428L.lco",
"primary_display_name": "E428",
"content_name": "E 428L",
"internal_stem": "E428L",
"matches_grounded_prefix_name": true
},
{
"car_file": "E412L.car",
"lco_file": "E412L.lco",
"primary_display_name": "Brenner E412",
"content_name": "E412L",
"internal_stem": "E412L",
"matches_grounded_prefix_name": true
},
{
"car_file": "E60CP.car",
"lco_file": "E60CP.lco",
"primary_display_name": "E60CP",
"content_name": "E60CP",
"internal_stem": "E60CPL",
"matches_grounded_prefix_name": true
},
{
"car_file": "EP2 Bipolar.car",
"lco_file": "EP2 Bipolar.lco",
"primary_display_name": "EP-2 Bipolar",
"content_name": "EP2 Bipolar",
"internal_stem": "EP2BipolarL",
"matches_grounded_prefix_name": true
},
{
"car_file": "ET-22.car",
"lco_file": "ET-22.lco",
"primary_display_name": "ET22",
"content_name": "ET-22",
"internal_stem": "ET22L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Eight Wheeler 4-4-0 Loco.car",
"lco_file": "Eight Wheeler 4-4-0 Loco.lco",
"primary_display_name": "Eight Wheeler 4-4-0",
"content_name": "Eight Wheeler 4-4-0 Loco",
"internal_stem": "No999L",
"matches_grounded_prefix_name": true
},
{
"car_file": "F3 Loco.car",
"lco_file": "F3 Loco.lco",
"primary_display_name": "F3",
"content_name": "F3 Loco",
"internal_stem": "F3L",
"matches_grounded_prefix_name": true
},
{
"car_file": "FP45L.car",
"lco_file": "FP45L.lco",
"primary_display_name": "FP45",
"content_name": "FP45L",
"internal_stem": "FP45L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Fairlie Loco.car",
"lco_file": "Fairlie Loco.lco",
"primary_display_name": "Fairlie 0-6-6-0",
"content_name": "Fairlie Loco",
"internal_stem": "FairlieL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Firefly Loco.car",
"lco_file": "Firefly Loco.lco",
"primary_display_name": "Firefly 2-2-2",
"content_name": "Firefly Loco",
"internal_stem": "FireflyL",
"matches_grounded_prefix_name": true
},
{
"car_file": "GG1.car",
"lco_file": "GG1.lco",
"primary_display_name": "GG1",
"content_name": "GG1",
"internal_stem": "GG1L",
"matches_grounded_prefix_name": true
},
{
"car_file": "GP35L.car",
"lco_file": "GP35L.lco",
"primary_display_name": "GP 35",
"content_name": "GP35L",
"internal_stem": "GP35l",
"matches_grounded_prefix_name": true
},
{
"car_file": "GP7.car",
"lco_file": "GP7.lco",
"primary_display_name": "GP7",
"content_name": "GP7",
"internal_stem": "GP7L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Ge 66 Crocodile.car",
"lco_file": "Ge 66 Crocodile.lco",
"primary_display_name": "Ge 6/6 Crocodile",
"content_name": "Ge 66 Crocodile",
"internal_stem": "Ge66CrocodileL",
"matches_grounded_prefix_name": true
},
{
"car_file": "H10 282.car",
"lco_file": "H10 282.lco",
"primary_display_name": "H10 2-8-2",
"content_name": "H10 282",
"internal_stem": "H10282L",
"matches_grounded_prefix_name": true
},
{
"car_file": "HST 125 Loco.car",
"lco_file": "HST 125 Loco.lco",
"primary_display_name": "HST 125",
"content_name": "HST 125 Loco",
"internal_stem": "HST125L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Kriegslok Loco.car",
"lco_file": "Kriegslok Loco.lco",
"primary_display_name": "Kriegslok 2-10-0",
"content_name": "Kriegslok Loco",
"internal_stem": "KriegslokL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Mallard Loco.car",
"lco_file": "Mallard Loco.lco",
"primary_display_name": "Mallard 4-6-2",
"content_name": "Mallard Loco",
"internal_stem": "MallardL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Norris Loco.car",
"lco_file": "Norris Loco.lco",
"primary_display_name": "Norris 4-2-0",
"content_name": "Norris Loco",
"internal_stem": "NorrisL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Northern 4-8-4 Loco.car",
"lco_file": "Northern 4-8-4 Loco.lco",
"primary_display_name": "Northern 4-8-4",
"content_name": "Northern 4-8-4 Loco",
"internal_stem": "Northern484l",
"matches_grounded_prefix_name": true
},
{
"car_file": "Orca NX462 Loco.car",
"lco_file": "Orca NX462 Loco.lco",
"primary_display_name": "Orca NX462",
"content_name": "Orca NX462 Loco",
"internal_stem": "WhaleL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Pacific 4-6-2 Loco.car",
"lco_file": "Pacific 4-6-2 Loco.lco",
"primary_display_name": "Pacific 4-6-2",
"content_name": "Pacific 4-6-2 Loco",
"internal_stem": "Penn462L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Planet Loco.car",
"lco_file": "Planet Loco.lco",
"primary_display_name": "Planet 2-2-0",
"content_name": "Planet Loco",
"internal_stem": "PlanetL",
"matches_grounded_prefix_name": true
},
{
"car_file": "RE66.car",
"lco_file": "RE66.lco",
"primary_display_name": "Re 6/6",
"content_name": "RE66",
"internal_stem": "RE66L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Red Devil 4-8-4 Loco.car",
"lco_file": "Red Devil 4-8-4 Loco.lco",
"primary_display_name": "Red Devil 4-8-4",
"content_name": "Red Devil 4-8-4 Loco",
"internal_stem": "ReddevilL",
"matches_grounded_prefix_name": true
},
{
"car_file": "S3 Loco.car",
"lco_file": "S3 Loco.lco",
"primary_display_name": "S3 4-4-0",
"content_name": "S3 Loco",
"internal_stem": "S3L",
"matches_grounded_prefix_name": true
},
{
"car_file": "SD90Mac Loco.car",
"lco_file": "SD90Mac Loco.lco",
"primary_display_name": "NA-90D",
"content_name": "SD90Mac Loco",
"internal_stem": "SD90MacL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Shay Loco.car",
"lco_file": "Shay Loco.lco",
"primary_display_name": "Shay (2-Truck)",
"content_name": "Shay Loco",
"internal_stem": "ShayL",
"matches_grounded_prefix_name": true
},
{
"car_file": "Shinkansen Series.car",
"lco_file": "Shinkansen Series.lco",
"primary_display_name": "Shinkansen Series 0",
"content_name": "Shinkansen Series",
"internal_stem": "ShinkansenSeries0L",
"matches_grounded_prefix_name": true
},
{
"car_file": "Stirling422 Loco.car",
"lco_file": "Stirling422 Loco.lco",
"primary_display_name": "Stirling 4-2-2",
"content_name": "Stirling422 Loco",
"internal_stem": "Stirling422L",
"matches_grounded_prefix_name": true
},
{
"car_file": "TransEuro.car",
"lco_file": "TransEuro.lco",
"primary_display_name": "Trans-Euro",
"content_name": "TransEuro",
"internal_stem": "TransEuroL",
"matches_grounded_prefix_name": true
},
{
"car_file": "U1L.car",
"lco_file": "U1L.lco",
"primary_display_name": "U1",
"content_name": "U1L",
"internal_stem": "u1l",
"matches_grounded_prefix_name": true
},
{
"car_file": "V200 Loco.car",
"lco_file": "V200 Loco.lco",
"primary_display_name": "V200",
"content_name": "V200 Loco",
"internal_stem": "V200L",
"matches_grounded_prefix_name": true
},
{
"car_file": "VL80T Loco.car",
"lco_file": "VL80T Loco.lco",
"primary_display_name": "VL80T",
"content_name": "VL80T Loco",
"internal_stem": "VL80TL",
"matches_grounded_prefix_name": true
},
{
"car_file": "ZephyrL.car",
"lco_file": "ZephyrL.lco",
"primary_display_name": "Zephyr",
"content_name": "ZephyrL",
"internal_stem": "zephyrl",
"matches_grounded_prefix_name": true
}
],
"notes": [
"Each row comes from one shipped .car/.lco locomotive engine-type pair under Data/EngineTypes.",
"The primary display string is parsed directly from the .car header at 0x0c rather than inferred from strings output.",
"The five unmatched display families are shipped named locomotive assets whose names do not appear in the current 61-name grounded descriptor prefix.",
"This export grounds the extra shipped locomotive-name cohort, but it does not by itself prove where those names land in the live ordinal catalog or descriptor bands."
]
}

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ pub(crate) use model::{
ScanCommand,
};
const USAGE: &str = "usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime snapshot-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime inspect-candidate-table <file.smp> | runtime inspect-compact-event-dispatch-cluster <maps-dir> | runtime inspect-compact-event-dispatch-cluster-counts <maps-dir> | runtime inspect-map-title-hints <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime snapshot-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-building-type-sources <BuildingTypes-dir> [building-bindings.json] | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-candidate-table-named-runs <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]";
const USAGE: &str = "usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime snapshot-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime inspect-candidate-table <file.smp> | runtime inspect-compact-event-dispatch-cluster <maps-dir> | runtime inspect-compact-event-dispatch-cluster-counts <maps-dir> | runtime inspect-map-title-hints <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime snapshot-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-building-type-sources <BuildingTypes-dir> [building-bindings.json] | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-lng <file.lng> | runtime inspect-car <file.car> | runtime inspect-lco <file.lco> | runtime inspect-engine-types <EngineTypes-dir> | runtime inspect-imb <file.imb> | runtime inspect-cct <file.cct> | runtime inspect-cgo <file.cgo> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-candidate-table-named-runs <root-dir> | runtime scan-locomotive-catalog-tail <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]";
pub(super) fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
@ -134,6 +134,58 @@ mod tests {
);
}
#[test]
fn parses_runtime_lng_inspect_command() {
assert_eq!(
parse(&["runtime", "inspect-lng", "RT3.lng"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectLng {
lng_path: PathBuf::from("RT3.lng"),
}))
);
}
#[test]
fn parses_runtime_engine_type_inspect_commands() {
assert_eq!(
parse(&["runtime", "inspect-car", "Class_QJL.car"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectCar {
car_path: PathBuf::from("Class_QJL.car"),
}))
);
assert_eq!(
parse(&["runtime", "inspect-lco", "Class_QJL.lco"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectLco {
lco_path: PathBuf::from("Class_QJL.lco"),
}))
);
assert_eq!(
parse(&["runtime", "inspect-engine-types", "Data/EngineTypes"]),
Command::Runtime(RuntimeCommand::Inspect(
InspectCommand::InspectEngineTypes {
engine_types_dir: PathBuf::from("Data/EngineTypes"),
}
))
);
assert_eq!(
parse(&["runtime", "inspect-imb", "ice_profile.imb"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectImb {
imb_path: PathBuf::from("ice_profile.imb"),
}))
);
assert_eq!(
parse(&["runtime", "inspect-cct", "Auto_Carrier.cct"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectCct {
cct_path: PathBuf::from("Auto_Carrier.cct"),
}))
);
assert_eq!(
parse(&["runtime", "inspect-cgo", "AutoA.cgo"]),
Command::Runtime(RuntimeCommand::Inspect(InspectCommand::InspectCgo {
cgo_path: PathBuf::from("AutoA.cgo"),
}))
);
}
#[test]
fn parses_runtime_compare_command() {
assert_eq!(
@ -155,4 +207,16 @@ mod tests {
}))
);
}
#[test]
fn parses_runtime_scan_locomotive_catalog_tail_command() {
assert_eq!(
parse(&["runtime", "scan-locomotive-catalog-tail", "root"]),
Command::Runtime(RuntimeCommand::Scan(
ScanCommand::ScanLocomotiveCatalogTail {
root_path: PathBuf::from("root"),
}
))
);
}
}

View file

@ -136,6 +136,27 @@ pub(crate) enum InspectCommand {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectLng {
lng_path: PathBuf,
},
InspectCar {
car_path: PathBuf,
},
InspectLco {
lco_path: PathBuf,
},
InspectEngineTypes {
engine_types_dir: PathBuf,
},
InspectImb {
imb_path: PathBuf,
},
InspectCct {
cct_path: PathBuf,
},
InspectCgo {
cgo_path: PathBuf,
},
InspectWin {
win_path: PathBuf,
},
@ -186,6 +207,7 @@ pub(crate) enum CompareCommand {
pub(crate) enum ScanCommand {
ScanCandidateTableHeaders { root_path: PathBuf },
ScanCandidateTableNamedRuns { root_path: PathBuf },
ScanLocomotiveCatalogTail { root_path: PathBuf },
ScanSpecialConditions { root_path: PathBuf },
ScanAlignedRuntimeRuleBand { root_path: PathBuf },
ScanPostSpecialConditionsScalars { root_path: PathBuf },

View file

@ -120,6 +120,29 @@ pub(super) fn parse_inspect_command(
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, lng_path] if subcommand == "inspect-lng" => Ok(InspectCommand::InspectLng {
lng_path: lng_path.into(),
}),
[subcommand, car_path] if subcommand == "inspect-car" => Ok(InspectCommand::InspectCar {
car_path: car_path.into(),
}),
[subcommand, lco_path] if subcommand == "inspect-lco" => Ok(InspectCommand::InspectLco {
lco_path: lco_path.into(),
}),
[subcommand, engine_types_dir] if subcommand == "inspect-engine-types" => {
Ok(InspectCommand::InspectEngineTypes {
engine_types_dir: engine_types_dir.into(),
})
}
[subcommand, imb_path] if subcommand == "inspect-imb" => Ok(InspectCommand::InspectImb {
imb_path: imb_path.into(),
}),
[subcommand, cct_path] if subcommand == "inspect-cct" => Ok(InspectCommand::InspectCct {
cct_path: cct_path.into(),
}),
[subcommand, cgo_path] if subcommand == "inspect-cgo" => Ok(InspectCommand::InspectCgo {
cgo_path: cgo_path.into(),
}),
[subcommand, win_path] if subcommand == "inspect-win" => Ok(InspectCommand::InspectWin {
win_path: win_path.into(),
}),

View file

@ -46,6 +46,13 @@ pub(super) fn parse_runtime_command(
| "inspect-cargo-economy-sources"
| "inspect-cargo-production-selector"
| "inspect-cargo-price-selector"
| "inspect-lng"
| "inspect-car"
| "inspect-lco"
| "inspect-engine-types"
| "inspect-imb"
| "inspect-cct"
| "inspect-cgo"
| "inspect-win"
| "extract-pk4-entry"
| "inspect-campaign-exe"
@ -62,6 +69,7 @@ pub(super) fn parse_runtime_command(
}
"scan-candidate-table-headers"
| "scan-candidate-table-named-runs"
| "scan-locomotive-catalog-tail"
| "scan-special-conditions"
| "scan-aligned-runtime-rule-band"
| "scan-post-special-conditions-scalars"

View file

@ -2,10 +2,12 @@ use crate::app::command::InspectCommand;
use crate::app::runtime_compare::inspect_candidate_table;
use crate::app::runtime_inspect::{
export_profile_block, extract_pk4_entry, inspect_building_type_sources, inspect_campaign_exe,
inspect_cargo_economy_sources, inspect_cargo_price_selector, inspect_cargo_production_selector,
inspect_cargo_skins, inspect_cargo_types, inspect_compact_event_dispatch_cluster,
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_map_title_hints, inspect_periodic_company_service_trace, inspect_pk4,
inspect_engine_types, inspect_imb, 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,
inspect_save_region_queued_notice_records, inspect_smp, inspect_unclassified_save_collections,
@ -70,6 +72,15 @@ pub(super) fn dispatch_inspect(command: InspectCommand) -> Result<(), Box<dyn st
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_price_selector(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectLng { lng_path } => inspect_lng(&lng_path),
InspectCommand::InspectCar { car_path } => inspect_car(&car_path),
InspectCommand::InspectLco { lco_path } => inspect_lco(&lco_path),
InspectCommand::InspectEngineTypes { engine_types_dir } => {
inspect_engine_types(&engine_types_dir)
}
InspectCommand::InspectImb { imb_path } => inspect_imb(&imb_path),
InspectCommand::InspectCct { cct_path } => inspect_cct(&cct_path),
InspectCommand::InspectCgo { cgo_path } => inspect_cgo(&cgo_path),
InspectCommand::InspectWin { win_path } => inspect_win(&win_path),
InspectCommand::ExtractPk4Entry {
pk4_path,

View file

@ -7,6 +7,12 @@ use rrt_runtime::inspect::{
CargoEconomySourceReport, CargoSelectorReport, CargoSkinInspectionReport,
CargoTypeInspectionReport,
},
engine_types::{
EngineTypeCarInspectionReport, EngineTypeCctInspectionReport,
EngineTypeCgoInspectionReport, EngineTypeLcoInspectionReport, EngineTypesInspectionReport,
},
imb::ImbInspectionReport,
lng::LngInspectionReport,
pk4::{Pk4ExtractionReport, Pk4InspectionReport},
smp::{
bundle::SmpInspectionReport,
@ -236,6 +242,48 @@ pub(crate) struct RuntimeCargoSelectorInspectionOutput {
pub(crate) selector: CargoSelectorReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeLngInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: LngInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCarInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: EngineTypeCarInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeLcoInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: EngineTypeLcoInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeImbInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: ImbInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCctInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: EngineTypeCctInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCgoInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: EngineTypeCgoInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeEngineTypesInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: EngineTypesInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeWinInspectionOutput {
pub(crate) path: String,

View file

@ -4,9 +4,12 @@ use std::path::Path;
use crate::app::helpers::inspect::build_profile_block_export_document;
use crate::app::reports::inspect::{
RuntimeBuildingTypeInspectionOutput, RuntimeCampaignExeInspectionOutput,
RuntimeCargoEconomyInspectionOutput, RuntimeCargoSelectorInspectionOutput,
RuntimeCargoSkinInspectionOutput, RuntimeCargoTypeInspectionOutput, RuntimePk4ExtractionOutput,
RuntimePk4InspectionOutput, RuntimeProfileBlockExportReport, RuntimeWinInspectionOutput,
RuntimeCarInspectionOutput, RuntimeCargoEconomyInspectionOutput,
RuntimeCargoSelectorInspectionOutput, RuntimeCargoSkinInspectionOutput,
RuntimeCargoTypeInspectionOutput, RuntimeCctInspectionOutput, RuntimeCgoInspectionOutput,
RuntimeEngineTypesInspectionOutput, RuntimeImbInspectionOutput, RuntimeLcoInspectionOutput,
RuntimeLngInspectionOutput, RuntimePk4ExtractionOutput, RuntimePk4InspectionOutput,
RuntimeProfileBlockExportReport, RuntimeWinInspectionOutput,
};
use rrt_runtime::inspect::{
building::inspect_building_types_dir_with_bindings,
@ -15,6 +18,12 @@ use rrt_runtime::inspect::{
inspect_cargo_economy_sources_with_bindings, inspect_cargo_skin_pk4,
inspect_cargo_types_dir,
},
engine_types::{
inspect_car_file, inspect_cct_file, inspect_cgo_file, inspect_engine_types_dir,
inspect_lco_file,
},
imb::inspect_imb_file,
lng::inspect_lng_file,
pk4::{extract_pk4_entry_file, inspect_pk4_file},
smp::bundle::inspect_smp_file,
win::inspect_win_file,
@ -125,6 +134,71 @@ pub(crate) fn inspect_cargo_price_selector(
Ok(())
}
pub(crate) fn inspect_lng(lng_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeLngInspectionOutput {
path: lng_path.display().to_string(),
inspection: inspect_lng_file(lng_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_car(car_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCarInspectionOutput {
path: car_path.display().to_string(),
inspection: inspect_car_file(car_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_lco(lco_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeLcoInspectionOutput {
path: lco_path.display().to_string(),
inspection: inspect_lco_file(lco_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_engine_types(
engine_types_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeEngineTypesInspectionOutput {
path: engine_types_dir.display().to_string(),
inspection: inspect_engine_types_dir(engine_types_dir)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_imb(imb_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeImbInspectionOutput {
path: imb_path.display().to_string(),
inspection: inspect_imb_file(imb_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cct(cct_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCctInspectionOutput {
path: cct_path.display().to_string(),
inspection: inspect_cct_file(cct_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cgo(cgo_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCgoInspectionOutput {
path: cgo_path.display().to_string(),
inspection: inspect_cgo_file(cgo_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_win(win_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeWinInspectionOutput {
path: win_path.display().to_string(),

View file

@ -4,8 +4,10 @@ mod smp;
pub(crate) use assets::{
export_profile_block, extract_pk4_entry, inspect_building_type_sources, inspect_campaign_exe,
inspect_cargo_economy_sources, inspect_cargo_price_selector, inspect_cargo_production_selector,
inspect_cargo_skins, inspect_cargo_types, inspect_pk4, inspect_win,
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_engine_types, inspect_imb, inspect_lco, inspect_lng, inspect_pk4,
inspect_win,
};
pub(crate) use maps::{
inspect_compact_event_dispatch_cluster, inspect_compact_event_dispatch_cluster_counts,

View file

@ -35,6 +35,9 @@ pub const REQUIRED_EXPORTS: &[&str] = &[
"artifacts/exports/rt3-1.06/event-effects-building-bindings.json",
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
"artifacts/exports/rt3-1.06/building-type-sources.json",
"artifacts/exports/rt3-1.06/rt3-language-catalog.json",
"artifacts/exports/rt3-1.06/engine-type-locomotive-display-census.json",
"artifacts/exports/rt3-1.06/locomotive-catalog-tail-census.json",
"artifacts/exports/rt3-1.06/candidate-table-header-clusters.json",
"artifacts/exports/rt3-1.06/candidate-table-named-runs.json",
"artifacts/exports/rt3-1.06/compact-event-dispatch-cluster-counts.json",

View file

@ -1,6 +1,9 @@
pub mod building;
pub mod campaign;
pub mod cargo;
pub mod engine_types;
pub mod imb;
pub mod lng;
pub mod pk4;
pub mod smp;
pub mod win;

View file

@ -0,0 +1,592 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
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 LCO_INTERNAL_STEM_OFFSET: usize = 0x04;
const UNMATCHED_LOCOMOTIVE_DISPLAY_NAMES: [&str; 5] =
["242 A1", "Class 460", "Class A1", "Class P8", "Class QJ"];
const LCO_EARLY_LANE_OFFSETS: [usize; 14] = [
0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, 0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c, 0x50, 0x54,
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeCarInspectionReport {
pub file_size: usize,
pub header_magic: Option<u32>,
pub header_magic_hex: Option<String>,
pub record_kind: Option<u32>,
pub record_kind_hex: Option<String>,
pub primary_display_name: Option<String>,
pub content_name: Option<String>,
pub internal_stem: Option<String>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EngineTypeRawLane {
pub offset: usize,
pub offset_hex: String,
pub raw_u32: u32,
pub raw_u32_hex: String,
pub raw_f32: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EngineTypeLcoInspectionReport {
pub file_size: usize,
pub header_magic: Option<u32>,
pub header_magic_hex: Option<String>,
pub internal_stem: Option<String>,
pub early_lanes: Vec<EngineTypeRawLane>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EngineTypeCgoInspectionReport {
pub file_size: usize,
pub leading_u32: Option<u32>,
pub leading_u32_hex: Option<String>,
pub leading_f32: Option<f32>,
pub content_stem: Option<String>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeCctInspectionReport {
pub file_size: usize,
pub line_count: usize,
pub identifier: Option<String>,
pub value: Option<i64>,
pub raw_lines: Vec<String>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeLocomotiveDisplayEntry {
pub car_file: String,
pub lco_file: String,
pub primary_display_name: String,
pub content_name: String,
pub internal_stem: String,
pub matches_grounded_prefix_name: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeLocomotiveDisplayFamily {
pub car_file: String,
pub lco_file: String,
pub primary_display_name: String,
pub content_name: String,
pub internal_stem: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeLocomotiveDisplayCensusReport {
pub format_version: u32,
pub semantic_family: String,
pub source_root: String,
pub car_header_layout: BTreeMap<String, String>,
pub observed_locomotive_pair_count: usize,
pub grounded_prefix_count: usize,
pub grounded_prefix_match_count: usize,
pub unmatched_display_family_count: usize,
pub unmatched_display_families: Vec<EngineTypeLocomotiveDisplayFamily>,
pub entries: Vec<EngineTypeLocomotiveDisplayEntry>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypeFamilyEntry {
pub canonical_stem: String,
pub car_file: Option<String>,
pub lco_file: Option<String>,
pub cgo_file: Option<String>,
pub cct_file: Option<String>,
pub primary_display_name: Option<String>,
pub content_name: Option<String>,
pub internal_stem: Option<String>,
pub cct_identifier: Option<String>,
pub cct_value: Option<i64>,
pub has_matched_locomotive_pair: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineTypesInspectionReport {
pub source_root: String,
pub family_count: usize,
pub car_file_count: usize,
pub lco_file_count: usize,
pub cgo_file_count: usize,
pub cct_file_count: usize,
pub matched_locomotive_pair_count: usize,
pub unmatched_car_file_count: usize,
pub unmatched_lco_file_count: usize,
pub unmatched_cgo_file_count: usize,
pub unmatched_cct_file_count: usize,
pub locomotive_display_census: EngineTypeLocomotiveDisplayCensusReport,
pub families: Vec<EngineTypeFamilyEntry>,
}
pub fn inspect_car_file(
path: &Path,
) -> Result<EngineTypeCarInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_car_bytes(&bytes)
}
pub fn inspect_car_bytes(
bytes: &[u8],
) -> Result<EngineTypeCarInspectionReport, Box<dyn std::error::Error>> {
Ok(EngineTypeCarInspectionReport {
file_size: bytes.len(),
header_magic: read_u32_le(bytes, 0),
header_magic_hex: read_u32_le(bytes, 0).map(|value| format!("0x{value:08x}")),
record_kind: read_u32_le(bytes, 4),
record_kind_hex: read_u32_le(bytes, 4).map(|value| format!("0x{value:08x}")),
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),
notes: vec![
"The current .car parser exposes the fixed header fields already grounded by the checked locomotive display census.".to_string(),
],
})
}
pub fn inspect_lco_file(
path: &Path,
) -> Result<EngineTypeLcoInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_lco_bytes(&bytes)
}
pub fn inspect_lco_bytes(
bytes: &[u8],
) -> Result<EngineTypeLcoInspectionReport, Box<dyn std::error::Error>> {
let early_lanes = LCO_EARLY_LANE_OFFSETS
.iter()
.filter_map(|offset| {
let raw_u32 = read_u32_le(bytes, *offset)?;
Some(EngineTypeRawLane {
offset: *offset,
offset_hex: format!("0x{offset:04x}"),
raw_u32,
raw_u32_hex: format!("0x{raw_u32:08x}"),
raw_f32: f32::from_bits(raw_u32),
})
})
.collect::<Vec<_>>();
Ok(EngineTypeLcoInspectionReport {
file_size: bytes.len(),
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),
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(),
],
})
}
pub fn inspect_cgo_file(
path: &Path,
) -> Result<EngineTypeCgoInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_cgo_bytes(&bytes)
}
pub fn inspect_cgo_bytes(
bytes: &[u8],
) -> Result<EngineTypeCgoInspectionReport, Box<dyn std::error::Error>> {
let leading_u32 = read_u32_le(bytes, 0);
Ok(EngineTypeCgoInspectionReport {
file_size: bytes.len(),
leading_u32,
leading_u32_hex: leading_u32.map(|value| format!("0x{value:08x}")),
leading_f32: leading_u32.map(f32::from_bits),
content_stem: read_ascii_field(bytes, 4),
notes: vec![
"The current .cgo parser is intentionally conservative: it exposes the leading scalar lane plus the inline content stem without overclaiming the remaining payload layout.".to_string(),
],
})
}
pub fn inspect_cct_file(
path: &Path,
) -> Result<EngineTypeCctInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_cct_bytes(&bytes)
}
pub fn inspect_cct_bytes(
bytes: &[u8],
) -> Result<EngineTypeCctInspectionReport, Box<dyn std::error::Error>> {
let text = decode_windows_1252(bytes);
let raw_lines = text.lines().map(|line| line.to_string()).collect::<Vec<_>>();
let first_nonblank = raw_lines.iter().find(|line| !line.trim().is_empty()).cloned();
let (identifier, value) = first_nonblank
.as_deref()
.map(parse_cct_row)
.unwrap_or((None, None));
Ok(EngineTypeCctInspectionReport {
file_size: bytes.len(),
line_count: raw_lines.len(),
identifier,
value,
raw_lines,
notes: vec![
"The current .cct parser preserves the first observed identifier/value row and the raw text lines without claiming wider semantics yet.".to_string(),
],
})
}
pub fn inspect_engine_types_dir(
path: &Path,
) -> Result<EngineTypesInspectionReport, Box<dyn std::error::Error>> {
let mut families = BTreeMap::<String, EngineTypeFamilyBuilder>::new();
let mut car_reports = BTreeMap::<String, EngineTypeCarInspectionReport>::new();
let mut lco_reports = BTreeMap::<String, EngineTypeLcoInspectionReport>::new();
let mut cgo_reports = BTreeMap::<String, EngineTypeCgoInspectionReport>::new();
let mut cct_reports = BTreeMap::<String, EngineTypeCctInspectionReport>::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let file_name = entry.file_name().to_string_lossy().into_owned();
let Some(stem) = Path::new(&file_name)
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| stem.to_string())
else {
continue;
};
let Some(extension) = Path::new(&file_name)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
else {
continue;
};
let family = families.entry(stem.to_ascii_lowercase()).or_default();
family.canonical_stem = stem.to_ascii_lowercase();
match extension.as_str() {
"car" => {
family.car_file = Some(file_name.clone());
car_reports.insert(file_name.clone(), inspect_car_file(&entry.path())?);
}
"lco" => {
family.lco_file = Some(file_name.clone());
lco_reports.insert(file_name.clone(), inspect_lco_file(&entry.path())?);
}
"cgo" => {
family.cgo_file = Some(file_name.clone());
cgo_reports.insert(file_name.clone(), inspect_cgo_file(&entry.path())?);
}
"cct" => {
family.cct_file = Some(file_name.clone());
cct_reports.insert(file_name.clone(), inspect_cct_file(&entry.path())?);
}
_ => {}
}
}
let family_entries = families
.values()
.map(|family| build_family_entry(family, &car_reports, &cct_reports))
.collect::<Vec<_>>();
let matched_locomotive_pair_count = family_entries
.iter()
.filter(|family| family.has_matched_locomotive_pair)
.count();
let locomotive_display_census =
build_locomotive_display_census(path, &family_entries, &car_reports)?;
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(),
matched_locomotive_pair_count,
unmatched_car_file_count: family_entries
.iter()
.filter(|entry| entry.car_file.is_some() && entry.lco_file.is_none())
.count(),
unmatched_lco_file_count: family_entries
.iter()
.filter(|entry| entry.car_file.is_none() && entry.lco_file.is_some())
.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()))
.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()))
.count(),
locomotive_display_census,
families: family_entries,
})
}
#[derive(Default)]
struct EngineTypeFamilyBuilder {
canonical_stem: String,
car_file: Option<String>,
lco_file: Option<String>,
cgo_file: Option<String>,
cct_file: Option<String>,
}
fn build_family_entry(
family: &EngineTypeFamilyBuilder,
car_reports: &BTreeMap<String, EngineTypeCarInspectionReport>,
cct_reports: &BTreeMap<String, EngineTypeCctInspectionReport>,
) -> EngineTypeFamilyEntry {
let car_report = family
.car_file
.as_ref()
.and_then(|file_name| car_reports.get(file_name));
let cct_report = family
.cct_file
.as_ref()
.and_then(|file_name| cct_reports.get(file_name));
EngineTypeFamilyEntry {
canonical_stem: family.canonical_stem.clone(),
car_file: family.car_file.clone(),
lco_file: family.lco_file.clone(),
cgo_file: family.cgo_file.clone(),
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()),
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 build_locomotive_display_census(
path: &Path,
families: &[EngineTypeFamilyEntry],
car_reports: &BTreeMap<String, EngineTypeCarInspectionReport>,
) -> Result<EngineTypeLocomotiveDisplayCensusReport, Box<dyn std::error::Error>> {
let mut entries = families
.iter()
.filter_map(|family| {
let car_file = family.car_file.clone()?;
let lco_file = family.lco_file.clone()?;
let car_report = car_reports.get(&car_file)?;
Some(EngineTypeLocomotiveDisplayEntry {
car_file: car_file.clone(),
lco_file,
primary_display_name: car_report.primary_display_name.clone().unwrap_or_default(),
content_name: car_report.content_name.clone().unwrap_or_default(),
internal_stem: car_report.internal_stem.clone().unwrap_or_default(),
matches_grounded_prefix_name: !UNMATCHED_LOCOMOTIVE_DISPLAY_NAMES
.contains(&car_report.primary_display_name.as_deref().unwrap_or("")),
})
})
.collect::<Vec<_>>();
entries.sort_by(|left, right| left.car_file.cmp(&right.car_file));
let unmatched_display_families = entries
.iter()
.filter(|entry| !entry.matches_grounded_prefix_name)
.map(|entry| EngineTypeLocomotiveDisplayFamily {
car_file: entry.car_file.clone(),
lco_file: entry.lco_file.clone(),
primary_display_name: entry.primary_display_name.clone(),
content_name: entry.content_name.clone(),
internal_stem: entry.internal_stem.clone(),
})
.collect::<Vec<_>>();
let grounded_prefix_count = entries
.iter()
.filter(|entry| entry.matches_grounded_prefix_name)
.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("record_kind_dword_offset".to_string(), "0x04".to_string());
car_header_layout.insert(
"primary_display_name_offset".to_string(),
format!("0x{CAR_PRIMARY_DISPLAY_NAME_OFFSET:02x}"),
);
car_header_layout.insert(
"content_name_offset".to_string(),
format!("0x{CAR_CONTENT_NAME_OFFSET:02x}"),
);
car_header_layout.insert(
"internal_stem_offset".to_string(),
format!("0x{CAR_INTERNAL_STEM_OFFSET:02x}"),
);
Ok(EngineTypeLocomotiveDisplayCensusReport {
format_version: 1,
semantic_family: "engine-type-locomotive-display-census".to_string(),
source_root: path.display().to_string(),
car_header_layout,
observed_locomotive_pair_count: entries.len(),
grounded_prefix_count,
grounded_prefix_match_count: grounded_prefix_count,
unmatched_display_family_count: unmatched_display_families.len(),
unmatched_display_families,
entries,
notes: vec![
"Each row comes from one shipped .car/.lco locomotive engine-type pair under Data/EngineTypes.".to_string(),
"The primary display string is parsed directly from the .car header at 0x0c rather than inferred from strings output.".to_string(),
"The five unmatched display families are shipped named locomotive assets whose names do not appear in the current 61-name grounded descriptor prefix.".to_string(),
"This export grounds the extra shipped locomotive-name cohort, but it does not by itself prove where those names land in the live ordinal catalog or descriptor bands.".to_string(),
],
})
}
fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
let slice = bytes.get(offset..offset + 4)?;
Some(u32::from_le_bytes(slice.try_into().ok()?))
}
fn read_ascii_field(bytes: &[u8], offset: usize) -> Option<String> {
let tail = bytes.get(offset..)?;
let end = tail
.iter()
.position(|byte| *byte == 0 || !byte.is_ascii() || *byte == 0xcd)
.unwrap_or(tail.len());
let value = String::from_utf8(tail[..end].to_vec()).ok()?;
(!value.is_empty()).then_some(value)
}
fn parse_cct_row(line: &str) -> (Option<String>, Option<i64>) {
let mut parts = line.split_whitespace();
let identifier = parts.next().map(|value| value.to_string());
let value = parts.next().and_then(|value| value.parse().ok());
(identifier, value)
}
fn decode_windows_1252(bytes: &[u8]) -> String {
bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect()
}
fn decode_windows_1252_byte(byte: u8) -> char {
match byte {
0x80 => '\u{20AC}',
0x82 => '\u{201A}',
0x83 => '\u{0192}',
0x84 => '\u{201E}',
0x85 => '\u{2026}',
0x86 => '\u{2020}',
0x87 => '\u{2021}',
0x88 => '\u{02C6}',
0x89 => '\u{2030}',
0x8A => '\u{0160}',
0x8B => '\u{2039}',
0x8C => '\u{0152}',
0x8E => '\u{017D}',
0x91 => '\u{2018}',
0x92 => '\u{2019}',
0x93 => '\u{201C}',
0x94 => '\u{201D}',
0x95 => '\u{2022}',
0x96 => '\u{2013}',
0x97 => '\u{2014}',
0x98 => '\u{02DC}',
0x99 => '\u{2122}',
0x9A => '\u{0161}',
0x9B => '\u{203A}',
0x9C => '\u{0153}',
0x9E => '\u{017E}',
0x9F => '\u{0178}',
_ => byte as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_car_header_fields() {
let mut bytes = vec![0u8; 0x90];
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");
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"));
}
#[test]
fn parses_lco_header_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[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.early_lanes[0].raw_u32, 100);
}
#[test]
fn parses_cgo_and_cct_files() {
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");
assert_eq!(cct.identifier.as_deref(), Some("Auto_Carrier"));
assert_eq!(cct.value, Some(13));
}
#[test]
fn builds_locomotive_display_census() {
let mut car_reports = BTreeMap::new();
car_reports.insert(
"2D2L.car".to_string(),
EngineTypeCarInspectionReport {
file_size: 0,
header_magic: Some(0x03ea),
header_magic_hex: Some("0x000003ea".to_string()),
record_kind: Some(2),
record_kind_hex: Some("0x00000002".to_string()),
primary_display_name: Some("2-D-2".to_string()),
content_name: Some("2D2L".to_string()),
internal_stem: Some("2D2L".to_string()),
notes: Vec::new(),
},
);
let families = vec![EngineTypeFamilyEntry {
canonical_stem: "2d2l".to_string(),
car_file: Some("2D2L.car".to_string()),
lco_file: Some("2D2L.lco".to_string()),
cgo_file: None,
cct_file: None,
primary_display_name: Some("2-D-2".to_string()),
content_name: Some("2D2L".to_string()),
internal_stem: Some("2D2L".to_string()),
cct_identifier: None,
cct_value: None,
has_matched_locomotive_pair: true,
}];
let report =
build_locomotive_display_census(Path::new("EngineTypes"), &families, &car_reports)
.expect("census should build");
assert_eq!(report.observed_locomotive_pair_count, 1);
assert_eq!(report.entries[0].primary_display_name, "2-D-2");
assert!(report.entries[0].matches_grounded_prefix_name);
}
}

View file

@ -0,0 +1,148 @@
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImbInspectionEntry {
pub line_number: usize,
pub key: String,
pub raw_value: String,
pub tokens: Vec<String>,
pub integer_values: Option<Vec<i64>>,
pub float_values: Option<Vec<f64>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImbInspectionReport {
pub line_count: usize,
pub entry_count: usize,
pub blank_line_count: usize,
pub malformed_line_count: usize,
pub notes: Vec<String>,
pub entries: Vec<ImbInspectionEntry>,
pub malformed_lines: Vec<String>,
}
pub fn inspect_imb_file(path: &Path) -> Result<ImbInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_imb_bytes(&bytes)
}
pub fn inspect_imb_bytes(bytes: &[u8]) -> Result<ImbInspectionReport, Box<dyn std::error::Error>> {
let text = decode_windows_1252(bytes);
let mut entries = Vec::new();
let mut malformed_lines = Vec::new();
let mut blank_line_count = 0usize;
for (index, raw_line) in text.lines().enumerate() {
let line_number = index + 1;
let trimmed = raw_line.trim();
if trimmed.is_empty() {
blank_line_count += 1;
continue;
}
let mut parts = trimmed.split_whitespace();
let Some(key) = parts.next() else {
blank_line_count += 1;
continue;
};
let tokens = parts.map(|token| token.to_string()).collect::<Vec<_>>();
if tokens.is_empty() {
malformed_lines.push(raw_line.to_string());
continue;
}
let integer_values = parse_i64_tokens(&tokens);
let float_values = parse_f64_tokens(&tokens);
entries.push(ImbInspectionEntry {
line_number,
key: key.to_string(),
raw_value: tokens.join(" "),
tokens,
integer_values,
float_values,
});
}
Ok(ImbInspectionReport {
line_count: text.lines().count(),
entry_count: entries.len(),
blank_line_count,
malformed_line_count: malformed_lines.len(),
notes: vec![
"The current .imb parser preserves one whitespace-delimited key plus the remaining token list per line.".to_string(),
"Integer and float projections are only populated when every token in the value lane parses cleanly.".to_string(),
],
entries,
malformed_lines,
})
}
fn parse_i64_tokens(tokens: &[String]) -> Option<Vec<i64>> {
tokens
.iter()
.map(|token| token.parse::<i64>().ok())
.collect::<Option<Vec<_>>>()
}
fn parse_f64_tokens(tokens: &[String]) -> Option<Vec<f64>> {
tokens
.iter()
.map(|token| token.parse::<f64>().ok())
.collect::<Option<Vec<_>>>()
}
fn decode_windows_1252(bytes: &[u8]) -> String {
bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect()
}
fn decode_windows_1252_byte(byte: u8) -> char {
match byte {
0x80 => '\u{20AC}',
0x82 => '\u{201A}',
0x83 => '\u{0192}',
0x84 => '\u{201E}',
0x85 => '\u{2026}',
0x86 => '\u{2020}',
0x87 => '\u{2021}',
0x88 => '\u{02C6}',
0x89 => '\u{2030}',
0x8A => '\u{0160}',
0x8B => '\u{2039}',
0x8C => '\u{0152}',
0x8E => '\u{017D}',
0x91 => '\u{2018}',
0x92 => '\u{2019}',
0x93 => '\u{201C}',
0x94 => '\u{201D}',
0x95 => '\u{2022}',
0x96 => '\u{2013}',
0x97 => '\u{2014}',
0x98 => '\u{02DC}',
0x99 => '\u{2122}',
0x9A => '\u{0161}',
0x9B => '\u{203A}',
0x9C => '\u{0153}',
0x9E => '\u{017E}',
0x9F => '\u{0178}',
_ => byte as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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");
assert_eq!(report.entry_count, 3);
assert_eq!(report.entries[0].key, "TGAName");
assert_eq!(report.entries[1].integer_values, Some(vec![256]));
assert_eq!(report.entries[2].integer_values, Some(vec![0, 0, 138, 32]));
}
}

View file

@ -0,0 +1,270 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LngInspectionEntry {
pub line_number: usize,
pub kind: String,
pub string_id: Option<u32>,
pub style_level: Option<u32>,
pub raw_text: String,
pub normalized_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LngMalformedLine {
pub line_number: usize,
pub raw_line: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LngInspectionReport {
pub format_family: String,
pub line_count: usize,
pub entry_count: usize,
pub string_entry_count: usize,
pub styled_entry_count: usize,
pub comment_count: usize,
pub blank_line_count: usize,
pub duplicate_id_count: usize,
pub duplicate_ids: Vec<u32>,
pub malformed_line_count: usize,
pub highest_string_id: Option<u32>,
pub notes: Vec<String>,
pub entries: Vec<LngInspectionEntry>,
pub malformed_lines: Vec<LngMalformedLine>,
}
pub fn inspect_lng_file(path: &Path) -> Result<LngInspectionReport, Box<dyn std::error::Error>> {
let bytes = fs::read(path)?;
inspect_lng_bytes(&bytes)
}
pub fn inspect_lng_bytes(bytes: &[u8]) -> Result<LngInspectionReport, Box<dyn std::error::Error>> {
let text = decode_windows_1252(bytes);
let mut entries = Vec::new();
let mut malformed_lines = Vec::new();
let mut string_id_counts = BTreeMap::<u32, usize>::new();
let mut comment_count = 0usize;
let mut blank_line_count = 0usize;
let mut string_entry_count = 0usize;
let mut styled_entry_count = 0usize;
for (index, raw_line) in text.lines().enumerate() {
let line_number = index + 1;
let trimmed = raw_line.trim();
if trimmed.is_empty() {
blank_line_count += 1;
continue;
}
if trimmed.starts_with(';') {
comment_count += 1;
continue;
}
if let Some(entry) = parse_string_entry(line_number, raw_line) {
string_entry_count += 1;
if let Some(string_id) = entry.string_id {
*string_id_counts.entry(string_id).or_default() += 1;
}
entries.push(entry);
continue;
}
if let Some(entry) = parse_styled_entry(line_number, raw_line) {
styled_entry_count += 1;
entries.push(entry);
continue;
}
malformed_lines.push(LngMalformedLine {
line_number,
raw_line: raw_line.to_string(),
reason: "line is neither a quoted string-id row nor a styled credits row".to_string(),
});
}
let duplicate_ids = string_id_counts
.into_iter()
.filter_map(|(string_id, count)| (count > 1).then_some(string_id))
.collect::<Vec<_>>();
let highest_string_id = entries.iter().filter_map(|entry| entry.string_id).max();
let format_kinds = entries
.iter()
.map(|entry| entry.kind.as_str())
.collect::<BTreeSet<_>>();
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(),
(false, false) => "unclassified-language-text".to_string(),
};
let mut notes = Vec::new();
notes.push(
"Quoted string rows preserve both the raw escape spelling and a normalized text view where `\\n` becomes a line break.".to_string(),
);
if format_kinds.contains("styled") {
notes.push(
"Styled rows use the observed `*<level>` credits format and preserve the style level separately from the rendered text.".to_string(),
);
}
if !duplicate_ids.is_empty() {
notes.push("Duplicate string ids are preserved explicitly instead of silently overwriting earlier rows.".to_string());
}
Ok(LngInspectionReport {
format_family,
line_count: text.lines().count(),
entry_count: entries.len(),
string_entry_count,
styled_entry_count,
comment_count,
blank_line_count,
duplicate_id_count: duplicate_ids.len(),
duplicate_ids,
malformed_line_count: malformed_lines.len(),
highest_string_id,
notes,
entries,
malformed_lines,
})
}
fn parse_string_entry(line_number: usize, raw_line: &str) -> Option<LngInspectionEntry> {
let trimmed = raw_line.trim_start();
let digit_len = trimmed.chars().take_while(|ch| ch.is_ascii_digit()).count();
if digit_len == 0 {
return None;
}
let string_id = trimmed[..digit_len].parse().ok()?;
let remainder = trimmed[digit_len..].trim_start();
let raw_text = parse_quoted_payload(remainder)?;
Some(LngInspectionEntry {
line_number,
kind: "string".to_string(),
string_id: Some(string_id),
style_level: None,
normalized_text: normalize_lng_text(&raw_text),
raw_text,
})
}
fn parse_styled_entry(line_number: usize, raw_line: &str) -> Option<LngInspectionEntry> {
let trimmed = raw_line.trim_start();
let remainder = trimmed.strip_prefix('*')?;
let digit_len = remainder
.chars()
.take_while(|ch| ch.is_ascii_digit())
.count();
if digit_len == 0 {
return None;
}
let style_level = remainder[..digit_len].parse().ok()?;
let raw_text = remainder[digit_len..].trim_start().to_string();
Some(LngInspectionEntry {
line_number,
kind: "styled".to_string(),
string_id: None,
style_level: Some(style_level),
normalized_text: normalize_lng_text(&raw_text),
raw_text,
})
}
fn parse_quoted_payload(text: &str) -> Option<String> {
let trimmed = text.trim();
if !(trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2) {
return None;
}
Some(trimmed[1..trimmed.len() - 1].to_string())
}
fn normalize_lng_text(text: &str) -> String {
text.replace("\\n", "\n")
}
fn decode_windows_1252(bytes: &[u8]) -> String {
bytes.iter().map(|byte| decode_windows_1252_byte(*byte)).collect()
}
fn decode_windows_1252_byte(byte: u8) -> char {
match byte {
0x80 => '\u{20AC}',
0x82 => '\u{201A}',
0x83 => '\u{0192}',
0x84 => '\u{201E}',
0x85 => '\u{2026}',
0x86 => '\u{2020}',
0x87 => '\u{2021}',
0x88 => '\u{02C6}',
0x89 => '\u{2030}',
0x8A => '\u{0160}',
0x8B => '\u{2039}',
0x8C => '\u{0152}',
0x8E => '\u{017D}',
0x91 => '\u{2018}',
0x92 => '\u{2019}',
0x93 => '\u{201C}',
0x94 => '\u{201D}',
0x95 => '\u{2022}',
0x96 => '\u{2013}',
0x97 => '\u{2014}',
0x98 => '\u{02DC}',
0x99 => '\u{2122}',
0x9A => '\u{0161}',
0x9B => '\u{203A}',
0x9C => '\u{0153}',
0x9E => '\u{017E}',
0x9F => '\u{0178}',
_ => byte as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_standard_string_rows_and_comments() {
let report = inspect_lng_bytes(b"; comment\n 10 \"Cancel\"\n11\t\"Line\\nBreak\"\n")
.expect("lng should parse");
assert_eq!(report.format_family, "quoted-string-table");
assert_eq!(report.comment_count, 1);
assert_eq!(report.string_entry_count, 2);
assert_eq!(report.highest_string_id, Some(11));
assert_eq!(report.entries[1].normalized_text, "Line\nBreak");
}
#[test]
fn parses_styled_credit_rows() {
let report = inspect_lng_bytes(b"*3Railroad Tycoon 3\n*2Development\nPopTop\n")
.expect("lng should parse");
assert_eq!(report.format_family, "styled-credits-lines");
assert_eq!(report.styled_entry_count, 2);
assert_eq!(report.malformed_line_count, 1);
assert_eq!(report.entries[0].style_level, Some(3));
assert_eq!(report.entries[0].raw_text, "Railroad Tycoon 3");
}
#[test]
fn reports_duplicate_string_ids() {
let report = inspect_lng_bytes(b"1 \"A\"\n1 \"B\"\n").expect("lng should parse");
assert_eq!(report.duplicate_id_count, 1);
assert_eq!(report.duplicate_ids, vec![1]);
}
#[test]
fn decodes_windows_1252_text() {
let report = inspect_lng_bytes(b"1 \"Wait\x85\"\n").expect("lng should parse");
assert_eq!(report.entries[0].raw_text, "Wait…");
}
}

View file

@ -0,0 +1,51 @@
# RT3 Format Inventory (2026-04-21)
This note preserves the current file-format inventory under `rt3_wineprefix/drive_c/rt3` and
`rt3_wineprefix/drive_c/rt3_105`, so future queue work can distinguish parser gaps from ordinary
generic media/support files.
## Parsed Game-Native Families
These formats already have checked loader or inspection support in the current repo:
- `.gmp`, `.gms`, `.gmx`: SMP/map/save/sandbox container inspection and save-slice loading
- `.pk4`: pack4 archive inspection and entry extraction
- `.bca`, `.bty`: building-source inspection
- `.cty`: cargo-type inspection
- `.lng`: language-table inspection
- `.car`, `.lco`, `.cgo`, `.cct`: engine-type inspection
- `.imb`: inline resource-descriptor inspection
- `.win`: window-resource inspection
- `.exe`: campaign-oriented PE inspection for `RT3.exe`
## RT3-Native Or RT3-Adjacent Unparsed Families
These formats are present in-tree and look like future RE/parser candidates, but the repo does not
yet have a dedicated structured parser for them:
- `.105`: version-suffixed executable copies such as `RT3.exe.105`
- `.dat`: opaque game-data blobs such as `emitters.dat`
- `.g`: shader text sources
- `.cfg`: engine/game configuration files
## Generic Media And Support Families
These files are present under the game trees, but they are generic media/resource/support formats
rather than RT3-specific parser targets:
- PE/DLL-style binaries: `.asi`, `.dll`, `.flt`, `.m3d`
- media/resources: `.bik`, `.bmp`, `.cur`, `.dds`, `.ico`, `.mp3`, `.scc`, `.tga`, `.wav`
- support/docs/scripts: `.asm`, `.bak`, `.bat`, `.c`, `.css`, `.html`, `.js`, `.json`, `.log`,
`.nsi`, `.pdf`, `.rtf`, `.txt`
## Current Counts
Normalized lowercase extension counts across both trees:
- parsed game-native: `gmp 86`, `gms 8`, `gmx 21`, `pk4 55`, `bca 173`, `bty 390`, `cty 92`,
`lng 5`, `car 342`, `lco 170`, `cgo 74`, `cct 22`, `imb 2`, `win 3`
- RT3-native or RT3-adjacent unparsed: `105 2`, `dat 5`, `g 10`, `cfg 4`
The immediate queue consequence is narrow: `.gmx` is already a known parsed container family, so
it belongs in local save-corpus scans by default. The remaining unparsed RT3-native families above
are preserved here for future parser or RE passes, but they are not active queue heads today.