Add parsers for RT3 language and engine type assets
This commit is contained in:
parent
8ebced08c0
commit
61472bf72d
17 changed files with 32835 additions and 9 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
]
|
||||
}
|
||||
30915
artifacts/exports/rt3-1.06/rt3-language-catalog.json
Normal file
30915
artifacts/exports/rt3-1.06/rt3-language-catalog.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"),
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
592
crates/rrt-runtime/src/inspect/engine_types.rs
Normal file
592
crates/rrt-runtime/src/inspect/engine_types.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
148
crates/rrt-runtime/src/inspect/imb.rs
Normal file
148
crates/rrt-runtime/src/inspect/imb.rs
Normal 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]));
|
||||
}
|
||||
}
|
||||
270
crates/rrt-runtime/src/inspect/lng.rs
Normal file
270
crates/rrt-runtime/src/inspect/lng.rs
Normal 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…");
|
||||
}
|
||||
}
|
||||
51
docs/rehost-queue/format-inventory-2026-04-21.md
Normal file
51
docs/rehost-queue/format-inventory-2026-04-21.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue