Rehost offline cargo economy sources
This commit is contained in:
parent
b06abb24a7
commit
517b01cd35
8 changed files with 1991 additions and 5 deletions
|
|
@ -63,7 +63,12 @@ remains explicit `blocked_evidence_blocked_descriptor` parity until descriptor o
|
||||||
more strongly, but the semantic catalog now gives that band stable `Named Cargo Price Slot N`
|
more strongly, but the semantic catalog now gives that band stable `Named Cargo Price Slot N`
|
||||||
labels instead of anonymous `Unknown Cargo Price` residue. The checked-in static corpora now make
|
labels instead of anonymous `Unknown Cargo Price` residue. The checked-in static corpora now make
|
||||||
that boundary more explicit too: the broader 1.06 CargoTypes corpus has `51` names and the 1.05
|
that boundary more explicit too: the broader 1.06 CargoTypes corpus has `51` names and the 1.05
|
||||||
corpus has `41`, so neither static set closes the `71`-row named price strip on its own. The
|
corpus has `41`, so neither static set closes the `71`-row named price strip on its own. A new
|
||||||
|
offline cargo-source inspector now pushes that groundwork further in rehosted code: the checked-in
|
||||||
|
`artifacts/exports/rt3-1.06/economy-cargo-sources.json` report parses both `CargoTypes` and the
|
||||||
|
`Cargo106.PK4` `cargoSkin` descriptors, normalizes localized `~####Name` tokens into visible
|
||||||
|
names, and shows that the current 1.06 visible-name union is `80`, not `71`, so source recovery
|
||||||
|
alone still does not prove the live price-selector ordering. The
|
||||||
add-building strip `503..519` is now explicitly classified as recovered
|
add-building strip `503..519` is now explicitly classified as recovered
|
||||||
shell-owned descriptor parity rather than generic unresolved residue. The first grounded
|
shell-owned descriptor parity rather than generic unresolved residue. The first grounded
|
||||||
condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and
|
condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and
|
||||||
|
|
|
||||||
1444
artifacts/exports/rt3-1.06/economy-cargo-sources.json
Normal file
1444
artifacts/exports/rt3-1.06/economy-cargo-sources.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -17,13 +17,15 @@ use rrt_model::{
|
||||||
load_binary_summary, load_function_map,
|
load_binary_summary, load_function_map,
|
||||||
};
|
};
|
||||||
use rrt_runtime::{
|
use rrt_runtime::{
|
||||||
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES,
|
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CargoEconomySourceReport,
|
||||||
|
CargoSkinInspectionReport, CargoTypeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES,
|
||||||
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, Pk4InspectionReport,
|
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, Pk4InspectionReport,
|
||||||
RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument,
|
RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument,
|
||||||
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
|
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
|
||||||
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
|
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
|
||||||
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
|
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
|
||||||
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
|
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
|
||||||
|
inspect_cargo_economy_sources, inspect_cargo_skin_pk4, inspect_cargo_types_dir,
|
||||||
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
|
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
|
||||||
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
|
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
|
||||||
save_runtime_overlay_import_document, save_runtime_save_slice_document,
|
save_runtime_overlay_import_document, save_runtime_save_slice_document,
|
||||||
|
|
@ -139,6 +141,16 @@ enum Command {
|
||||||
RuntimeInspectPk4 {
|
RuntimeInspectPk4 {
|
||||||
pk4_path: PathBuf,
|
pk4_path: PathBuf,
|
||||||
},
|
},
|
||||||
|
RuntimeInspectCargoTypes {
|
||||||
|
cargo_types_dir: PathBuf,
|
||||||
|
},
|
||||||
|
RuntimeInspectCargoSkins {
|
||||||
|
cargo_skin_pk4_path: PathBuf,
|
||||||
|
},
|
||||||
|
RuntimeInspectCargoEconomySources {
|
||||||
|
cargo_types_dir: PathBuf,
|
||||||
|
cargo_skin_pk4_path: PathBuf,
|
||||||
|
},
|
||||||
RuntimeInspectWin {
|
RuntimeInspectWin {
|
||||||
win_path: PathBuf,
|
win_path: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
@ -272,6 +284,25 @@ struct RuntimePk4InspectionOutput {
|
||||||
inspection: Pk4InspectionReport,
|
inspection: Pk4InspectionReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RuntimeCargoTypeInspectionOutput {
|
||||||
|
path: String,
|
||||||
|
inspection: CargoTypeInspectionReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RuntimeCargoSkinInspectionOutput {
|
||||||
|
path: String,
|
||||||
|
inspection: CargoSkinInspectionReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RuntimeCargoEconomyInspectionOutput {
|
||||||
|
cargo_types_dir: String,
|
||||||
|
cargo_skin_pk4_path: String,
|
||||||
|
inspection: CargoEconomySourceReport,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct RuntimeWinInspectionOutput {
|
struct RuntimeWinInspectionOutput {
|
||||||
path: String,
|
path: String,
|
||||||
|
|
@ -815,6 +846,20 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Command::RuntimeInspectPk4 { pk4_path } => {
|
Command::RuntimeInspectPk4 { pk4_path } => {
|
||||||
run_runtime_inspect_pk4(&pk4_path)?;
|
run_runtime_inspect_pk4(&pk4_path)?;
|
||||||
}
|
}
|
||||||
|
Command::RuntimeInspectCargoTypes { cargo_types_dir } => {
|
||||||
|
run_runtime_inspect_cargo_types(&cargo_types_dir)?;
|
||||||
|
}
|
||||||
|
Command::RuntimeInspectCargoSkins {
|
||||||
|
cargo_skin_pk4_path,
|
||||||
|
} => {
|
||||||
|
run_runtime_inspect_cargo_skins(&cargo_skin_pk4_path)?;
|
||||||
|
}
|
||||||
|
Command::RuntimeInspectCargoEconomySources {
|
||||||
|
cargo_types_dir,
|
||||||
|
cargo_skin_pk4_path,
|
||||||
|
} => {
|
||||||
|
run_runtime_inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path)?;
|
||||||
|
}
|
||||||
Command::RuntimeInspectWin { win_path } => {
|
Command::RuntimeInspectWin { win_path } => {
|
||||||
run_runtime_inspect_win(&win_path)?;
|
run_runtime_inspect_win(&win_path)?;
|
||||||
}
|
}
|
||||||
|
|
@ -993,6 +1038,28 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
||||||
pk4_path: PathBuf::from(path),
|
pk4_path: PathBuf::from(path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
[command, subcommand, path]
|
||||||
|
if command == "runtime" && subcommand == "inspect-cargo-types" =>
|
||||||
|
{
|
||||||
|
Ok(Command::RuntimeInspectCargoTypes {
|
||||||
|
cargo_types_dir: PathBuf::from(path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
[command, subcommand, path]
|
||||||
|
if command == "runtime" && subcommand == "inspect-cargo-skins" =>
|
||||||
|
{
|
||||||
|
Ok(Command::RuntimeInspectCargoSkins {
|
||||||
|
cargo_skin_pk4_path: PathBuf::from(path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
[command, subcommand, cargo_types_dir, cargo_skin_pk4_path]
|
||||||
|
if command == "runtime" && subcommand == "inspect-cargo-economy-sources" =>
|
||||||
|
{
|
||||||
|
Ok(Command::RuntimeInspectCargoEconomySources {
|
||||||
|
cargo_types_dir: PathBuf::from(cargo_types_dir),
|
||||||
|
cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
[command, subcommand, path] if command == "runtime" && subcommand == "inspect-win" => {
|
[command, subcommand, path] if command == "runtime" && subcommand == "inspect-win" => {
|
||||||
Ok(Command::RuntimeInspectWin {
|
Ok(Command::RuntimeInspectWin {
|
||||||
win_path: PathBuf::from(path),
|
win_path: PathBuf::from(path),
|
||||||
|
|
@ -1128,7 +1195,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => Err(
|
_ => Err(
|
||||||
"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 import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-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-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-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>]"
|
"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 import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-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-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <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-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>]"
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -1490,6 +1557,41 @@ fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Er
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_runtime_inspect_cargo_types(
|
||||||
|
cargo_types_dir: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let report = RuntimeCargoTypeInspectionOutput {
|
||||||
|
path: cargo_types_dir.display().to_string(),
|
||||||
|
inspection: inspect_cargo_types_dir(cargo_types_dir)?,
|
||||||
|
};
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_runtime_inspect_cargo_skins(
|
||||||
|
cargo_skin_pk4_path: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let report = RuntimeCargoSkinInspectionOutput {
|
||||||
|
path: cargo_skin_pk4_path.display().to_string(),
|
||||||
|
inspection: inspect_cargo_skin_pk4(cargo_skin_pk4_path)?,
|
||||||
|
};
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_runtime_inspect_cargo_economy_sources(
|
||||||
|
cargo_types_dir: &Path,
|
||||||
|
cargo_skin_pk4_path: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let report = RuntimeCargoEconomyInspectionOutput {
|
||||||
|
cargo_types_dir: cargo_types_dir.display().to_string(),
|
||||||
|
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
|
||||||
|
inspection: inspect_cargo_economy_sources(cargo_types_dir, cargo_skin_pk4_path)?,
|
||||||
|
};
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_runtime_inspect_win(win_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_runtime_inspect_win(win_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let report = RuntimeWinInspectionOutput {
|
let report = RuntimeWinInspectionOutput {
|
||||||
path: win_path.display().to_string(),
|
path: win_path.display().to_string(),
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ pub const REQUIRED_EXPORTS: &[&str] = &[
|
||||||
"artifacts/exports/rt3-1.06/event-effects-table.json",
|
"artifacts/exports/rt3-1.06/event-effects-table.json",
|
||||||
"artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json",
|
"artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json",
|
||||||
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
|
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
|
||||||
|
"artifacts/exports/rt3-1.06/economy-cargo-sources.json",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[
|
pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[
|
||||||
|
|
|
||||||
417
crates/rrt-runtime/src/economy.rs
Normal file
417
crates/rrt-runtime/src/economy.rs
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::pk4::inspect_pk4_bytes;
|
||||||
|
|
||||||
|
pub const NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT: usize = 71;
|
||||||
|
pub const NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT: usize = 50;
|
||||||
|
pub const CARGO_TYPE_MAGIC: u32 = 0x0000_03ea;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoNameToken {
|
||||||
|
pub raw_name: String,
|
||||||
|
pub visible_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub localized_string_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoTypeEntry {
|
||||||
|
pub file_name: String,
|
||||||
|
pub file_size: usize,
|
||||||
|
pub file_size_hex: String,
|
||||||
|
pub header_magic: u32,
|
||||||
|
pub header_magic_hex: String,
|
||||||
|
pub name: CargoNameToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoTypeInspectionReport {
|
||||||
|
pub directory_path: String,
|
||||||
|
pub entry_count: usize,
|
||||||
|
pub unique_visible_name_count: usize,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
pub entries: Vec<CargoTypeEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoSkinDescriptorEntry {
|
||||||
|
pub pk4_entry_name: String,
|
||||||
|
pub payload_len: usize,
|
||||||
|
pub payload_len_hex: String,
|
||||||
|
pub descriptor_kind: String,
|
||||||
|
pub name: CargoNameToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoSkinInspectionReport {
|
||||||
|
pub pk4_path: String,
|
||||||
|
pub entry_count: usize,
|
||||||
|
pub unique_visible_name_count: usize,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
pub entries: Vec<CargoSkinDescriptorEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CargoEconomySourceReport {
|
||||||
|
pub cargo_types_dir: String,
|
||||||
|
pub cargo_skin_pk4_path: String,
|
||||||
|
pub named_cargo_price_row_count: usize,
|
||||||
|
pub named_cargo_production_row_count: usize,
|
||||||
|
pub cargo_type_count: usize,
|
||||||
|
pub cargo_skin_count: usize,
|
||||||
|
pub shared_visible_name_count: usize,
|
||||||
|
pub visible_name_union_count: usize,
|
||||||
|
pub cargo_type_only_visible_names: Vec<String>,
|
||||||
|
pub cargo_skin_only_visible_names: Vec<String>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
pub cargo_type_entries: Vec<CargoTypeEntry>,
|
||||||
|
pub cargo_skin_entries: Vec<CargoSkinDescriptorEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inspect_cargo_types_dir(
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<CargoTypeInspectionReport, Box<dyn std::error::Error>> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type()?.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let file_name = file_name.to_string_lossy().into_owned();
|
||||||
|
if Path::new(&file_name)
|
||||||
|
.extension()
|
||||||
|
.and_then(|extension| extension.to_str())
|
||||||
|
.map(|extension| extension.eq_ignore_ascii_case("cty"))
|
||||||
|
!= Some(true)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bytes = fs::read(entry.path())?;
|
||||||
|
entries.push(parse_cargo_type_entry(&file_name, &bytes)?);
|
||||||
|
}
|
||||||
|
entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name));
|
||||||
|
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
notes.push(
|
||||||
|
"CargoTypes entries carry a 0x03ea header and an inline NUL-terminated cargo token string."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
notes.push(
|
||||||
|
"A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let unique_visible_name_count = entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.visible_name.as_str())
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
.len();
|
||||||
|
|
||||||
|
Ok(CargoTypeInspectionReport {
|
||||||
|
directory_path: path.display().to_string(),
|
||||||
|
entry_count: entries.len(),
|
||||||
|
unique_visible_name_count,
|
||||||
|
notes,
|
||||||
|
entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inspect_cargo_skin_pk4(
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<CargoSkinInspectionReport, Box<dyn std::error::Error>> {
|
||||||
|
let bytes = fs::read(path)?;
|
||||||
|
let inspection = inspect_pk4_bytes(&bytes)?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for entry in &inspection.entries {
|
||||||
|
if entry.extension.as_deref() != Some("dsc") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let payload = bytes
|
||||||
|
.get(entry.payload_absolute_offset..entry.payload_end_offset)
|
||||||
|
.ok_or_else(|| format!("pk4 payload range out of bounds for {}", entry.name))?;
|
||||||
|
let parsed = parse_cargo_skin_descriptor_entry(&entry.name, payload)?;
|
||||||
|
entries.push(parsed);
|
||||||
|
}
|
||||||
|
entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name));
|
||||||
|
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
notes.push(
|
||||||
|
"cargoSkin descriptors are parsed from .dsc payloads whose first non-empty line names the descriptor kind and whose second non-empty line carries the cargo token."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
notes.push(
|
||||||
|
"A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let unique_visible_name_count = entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.visible_name.as_str())
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
.len();
|
||||||
|
|
||||||
|
Ok(CargoSkinInspectionReport {
|
||||||
|
pk4_path: path.display().to_string(),
|
||||||
|
entry_count: entries.len(),
|
||||||
|
unique_visible_name_count,
|
||||||
|
notes,
|
||||||
|
entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inspect_cargo_economy_sources(
|
||||||
|
cargo_types_dir: &Path,
|
||||||
|
cargo_skin_pk4_path: &Path,
|
||||||
|
) -> Result<CargoEconomySourceReport, Box<dyn std::error::Error>> {
|
||||||
|
let cargo_types = inspect_cargo_types_dir(cargo_types_dir)?;
|
||||||
|
let cargo_skins = inspect_cargo_skin_pk4(cargo_skin_pk4_path)?;
|
||||||
|
Ok(build_cargo_economy_source_report(cargo_types, cargo_skins))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_cargo_economy_source_report(
|
||||||
|
cargo_types: CargoTypeInspectionReport,
|
||||||
|
cargo_skins: CargoSkinInspectionReport,
|
||||||
|
) -> CargoEconomySourceReport {
|
||||||
|
let cargo_type_visible_names = cargo_types
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.visible_name.clone())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let cargo_skin_visible_names = cargo_skins
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.visible_name.clone())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let shared_visible_name_count = cargo_type_visible_names
|
||||||
|
.intersection(&cargo_skin_visible_names)
|
||||||
|
.count();
|
||||||
|
let visible_name_union_count = cargo_type_visible_names
|
||||||
|
.union(&cargo_skin_visible_names)
|
||||||
|
.count();
|
||||||
|
let cargo_type_only_visible_names = cargo_type_visible_names
|
||||||
|
.difference(&cargo_skin_visible_names)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let cargo_skin_only_visible_names = cargo_skin_visible_names
|
||||||
|
.difference(&cargo_type_visible_names)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
notes.push(format!(
|
||||||
|
"Named cargo-price descriptors 106..176 span {} rows, while named cargo-production descriptors 180..229 span {} rows.",
|
||||||
|
NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT
|
||||||
|
));
|
||||||
|
notes.push(format!(
|
||||||
|
"The inspected CargoTypes corpus exposes {} visible names, the inspected cargoSkin corpus exposes {} visible names, and their union exposes {} visible names.",
|
||||||
|
cargo_types.unique_visible_name_count, cargo_skins.unique_visible_name_count, visible_name_union_count
|
||||||
|
));
|
||||||
|
if visible_name_union_count != NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT {
|
||||||
|
notes.push(
|
||||||
|
"That visible-name union still does not match the 71-row named cargo-price strip, so this offline source reconstruction is groundwork rather than a complete price-selector binding."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notes.push(
|
||||||
|
"The visible-name union matches the 71-row named cargo-price strip; a later pass can decide whether ordering evidence is now strong enough for descriptor bindings."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if cargo_types.unique_visible_name_count == NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT {
|
||||||
|
notes.push(
|
||||||
|
"The CargoTypes corpus still matches the 50-row named cargo-production strip cardinality that grounds the current production bindings."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CargoEconomySourceReport {
|
||||||
|
cargo_types_dir: cargo_types.directory_path,
|
||||||
|
cargo_skin_pk4_path: cargo_skins.pk4_path,
|
||||||
|
named_cargo_price_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT,
|
||||||
|
named_cargo_production_row_count: NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT,
|
||||||
|
cargo_type_count: cargo_types.entry_count,
|
||||||
|
cargo_skin_count: cargo_skins.entry_count,
|
||||||
|
shared_visible_name_count,
|
||||||
|
visible_name_union_count,
|
||||||
|
cargo_type_only_visible_names,
|
||||||
|
cargo_skin_only_visible_names,
|
||||||
|
notes,
|
||||||
|
cargo_type_entries: cargo_types.entries,
|
||||||
|
cargo_skin_entries: cargo_skins.entries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cargo_type_entry(
|
||||||
|
file_name: &str,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<CargoTypeEntry, Box<dyn std::error::Error>> {
|
||||||
|
if bytes.len() < 5 {
|
||||||
|
return Err(format!("cargo type entry {file_name} is too short").into());
|
||||||
|
}
|
||||||
|
let header_magic = u32::from_le_bytes(bytes[0..4].try_into().expect("length checked"));
|
||||||
|
let raw_name = parse_nul_terminated_utf8(bytes, 4)
|
||||||
|
.ok_or_else(|| format!("cargo type entry {file_name} is missing a NUL-terminated name"))?;
|
||||||
|
|
||||||
|
Ok(CargoTypeEntry {
|
||||||
|
file_name: file_name.to_string(),
|
||||||
|
file_size: bytes.len(),
|
||||||
|
file_size_hex: format!("0x{:x}", bytes.len()),
|
||||||
|
header_magic,
|
||||||
|
header_magic_hex: format!("0x{header_magic:08x}"),
|
||||||
|
name: parse_cargo_name_token(&raw_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cargo_skin_descriptor_entry(
|
||||||
|
entry_name: &str,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Result<CargoSkinDescriptorEntry, Box<dyn std::error::Error>> {
|
||||||
|
let text = std::str::from_utf8(bytes)?;
|
||||||
|
let mut lines = text.lines().map(str::trim).filter(|line| !line.is_empty());
|
||||||
|
let descriptor_kind = lines
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the kind line"))?;
|
||||||
|
let raw_name = lines
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the name line"))?;
|
||||||
|
|
||||||
|
Ok(CargoSkinDescriptorEntry {
|
||||||
|
pk4_entry_name: entry_name.to_string(),
|
||||||
|
payload_len: bytes.len(),
|
||||||
|
payload_len_hex: format!("0x{:x}", bytes.len()),
|
||||||
|
descriptor_kind: descriptor_kind.to_string(),
|
||||||
|
name: parse_cargo_name_token(raw_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_nul_terminated_utf8(bytes: &[u8], offset: usize) -> Option<String> {
|
||||||
|
let tail = bytes.get(offset..)?;
|
||||||
|
let end = tail.iter().position(|byte| *byte == 0)?;
|
||||||
|
String::from_utf8(tail[..end].to_vec()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cargo_name_token(raw_name: &str) -> CargoNameToken {
|
||||||
|
let mut visible_name = raw_name.to_string();
|
||||||
|
let mut localized_string_id = None;
|
||||||
|
if let Some(rest) = raw_name.strip_prefix('~') {
|
||||||
|
let digits = rest
|
||||||
|
.chars()
|
||||||
|
.take_while(|character| character.is_ascii_digit())
|
||||||
|
.collect::<String>();
|
||||||
|
if !digits.is_empty() {
|
||||||
|
localized_string_id = digits.parse::<u32>().ok();
|
||||||
|
visible_name = rest[digits.len()..].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CargoNameToken {
|
||||||
|
raw_name: raw_name.to_string(),
|
||||||
|
visible_name,
|
||||||
|
localized_string_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_plain_cargo_type_entry() {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(b"Alcohol\0");
|
||||||
|
let entry = parse_cargo_type_entry("Alcohol.cty", &bytes).expect("entry should parse");
|
||||||
|
assert_eq!(entry.header_magic, CARGO_TYPE_MAGIC);
|
||||||
|
assert_eq!(entry.name.raw_name, "Alcohol");
|
||||||
|
assert_eq!(entry.name.visible_name, "Alcohol");
|
||||||
|
assert_eq!(entry.name.localized_string_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_localized_cargo_type_entry() {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(b"~4465Ceramics\0");
|
||||||
|
let entry =
|
||||||
|
parse_cargo_type_entry("~4465Ceramics.cty", &bytes).expect("entry should parse");
|
||||||
|
assert_eq!(entry.name.raw_name, "~4465Ceramics");
|
||||||
|
assert_eq!(entry.name.visible_name, "Ceramics");
|
||||||
|
assert_eq!(entry.name.localized_string_id, Some(4465));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_cargo_skin_descriptor_entry() {
|
||||||
|
let entry = parse_cargo_skin_descriptor_entry("Alcohol.dsc", b"cargoSkin\r\nAlcohol\r\n")
|
||||||
|
.expect("descriptor should parse");
|
||||||
|
assert_eq!(entry.descriptor_kind, "cargoSkin");
|
||||||
|
assert_eq!(entry.name.visible_name, "Alcohol");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_cargo_source_report_union_counts() {
|
||||||
|
let cargo_types = CargoTypeInspectionReport {
|
||||||
|
directory_path: "CargoTypes".to_string(),
|
||||||
|
entry_count: 2,
|
||||||
|
unique_visible_name_count: 2,
|
||||||
|
notes: Vec::new(),
|
||||||
|
entries: vec![
|
||||||
|
CargoTypeEntry {
|
||||||
|
file_name: "Alcohol.cty".to_string(),
|
||||||
|
file_size: 16,
|
||||||
|
file_size_hex: "0x10".to_string(),
|
||||||
|
header_magic: CARGO_TYPE_MAGIC,
|
||||||
|
header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"),
|
||||||
|
name: parse_cargo_name_token("Alcohol"),
|
||||||
|
},
|
||||||
|
CargoTypeEntry {
|
||||||
|
file_name: "Coal.cty".to_string(),
|
||||||
|
file_size: 16,
|
||||||
|
file_size_hex: "0x10".to_string(),
|
||||||
|
header_magic: CARGO_TYPE_MAGIC,
|
||||||
|
header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"),
|
||||||
|
name: parse_cargo_name_token("Coal"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let cargo_skins = CargoSkinInspectionReport {
|
||||||
|
pk4_path: "Cargo106.PK4".to_string(),
|
||||||
|
entry_count: 2,
|
||||||
|
unique_visible_name_count: 2,
|
||||||
|
notes: Vec::new(),
|
||||||
|
entries: vec![
|
||||||
|
CargoSkinDescriptorEntry {
|
||||||
|
pk4_entry_name: "Alcohol.dsc".to_string(),
|
||||||
|
payload_len: 20,
|
||||||
|
payload_len_hex: "0x14".to_string(),
|
||||||
|
descriptor_kind: "cargoSkin".to_string(),
|
||||||
|
name: parse_cargo_name_token("Alcohol"),
|
||||||
|
},
|
||||||
|
CargoSkinDescriptorEntry {
|
||||||
|
pk4_entry_name: "Beer.dsc".to_string(),
|
||||||
|
payload_len: 16,
|
||||||
|
payload_len_hex: "0x10".to_string(),
|
||||||
|
descriptor_kind: "cargoSkin".to_string(),
|
||||||
|
name: parse_cargo_name_token("Beer"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let report = build_cargo_economy_source_report(cargo_types, cargo_skins);
|
||||||
|
assert_eq!(report.shared_visible_name_count, 1);
|
||||||
|
assert_eq!(report.visible_name_union_count, 3);
|
||||||
|
assert_eq!(
|
||||||
|
report.cargo_type_only_visible_names,
|
||||||
|
vec!["Coal".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.cargo_skin_only_visible_names,
|
||||||
|
vec!["Beer".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod campaign_exe;
|
pub mod campaign_exe;
|
||||||
|
pub mod economy;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
pub mod pk4;
|
pub mod pk4;
|
||||||
|
|
@ -14,6 +15,12 @@ pub use campaign_exe::{
|
||||||
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CampaignPageBand, CampaignScenarioEntry,
|
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CampaignPageBand, CampaignScenarioEntry,
|
||||||
OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file,
|
OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file,
|
||||||
};
|
};
|
||||||
|
pub use economy::{
|
||||||
|
CARGO_TYPE_MAGIC, CargoEconomySourceReport, CargoNameToken, CargoSkinDescriptorEntry,
|
||||||
|
CargoSkinInspectionReport, CargoTypeEntry, CargoTypeInspectionReport,
|
||||||
|
NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT,
|
||||||
|
inspect_cargo_economy_sources, inspect_cargo_skin_pk4, inspect_cargo_types_dir,
|
||||||
|
};
|
||||||
pub use import::{
|
pub use import::{
|
||||||
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
||||||
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
|
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,12 @@ The highest-value next passes are now:
|
||||||
but the checked-in semantic catalog now gives that band stable `Named Cargo Price Slot N`
|
but the checked-in semantic catalog now gives that band stable `Named Cargo Price Slot N`
|
||||||
labels instead of anonymous `Unknown Cargo Price` residue; the checked-in static CargoTypes
|
labels instead of anonymous `Unknown Cargo Price` residue; the checked-in static CargoTypes
|
||||||
corpora also make the current limit explicit because the broader 1.06 corpus has `51` names and
|
corpora also make the current limit explicit because the broader 1.06 corpus has `51` names and
|
||||||
the 1.05 corpus has `41`, while the named price strip still spans `71` descriptors
|
the 1.05 corpus has `41`, while the named price strip still spans `71` descriptors; a new
|
||||||
|
checked-in offline cargo-source report at
|
||||||
|
`artifacts/exports/rt3-1.06/economy-cargo-sources.json` now parses both `CargoTypes` and the
|
||||||
|
`Cargo106.PK4` `cargoSkin` descriptors through rehosted code, normalizes localized
|
||||||
|
`~####Name` tokens into visible names, and shows that the current 1.06 visible-name union is
|
||||||
|
`80`, so source recovery alone still does not prove the live price-selector ordering
|
||||||
- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity,
|
- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity,
|
||||||
with tracked fixture coverage, instead of generic unresolved descriptor residue
|
with tracked fixture coverage, instead of generic unresolved descriptor residue
|
||||||
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
|
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,12 @@ Implemented today:
|
||||||
the checked-in semantic catalog now at least gives that band stable `Named Cargo Price Slot N`
|
the checked-in semantic catalog now at least gives that band stable `Named Cargo Price Slot N`
|
||||||
labels instead of anonymous `Unknown Cargo Price` residue, and the checked-in static CargoTypes
|
labels instead of anonymous `Unknown Cargo Price` residue, and the checked-in static CargoTypes
|
||||||
corpora now make the evidence gap explicit because they cover `51` names in the broader 1.06
|
corpora now make the evidence gap explicit because they cover `51` names in the broader 1.06
|
||||||
install and `41` in the 1.05 install while the named price strip still spans `71` descriptors
|
install and `41` in the 1.05 install while the named price strip still spans `71` descriptors;
|
||||||
|
a new checked-in offline cargo-source report at
|
||||||
|
`artifacts/exports/rt3-1.06/economy-cargo-sources.json` now parses both `CargoTypes` and the
|
||||||
|
`Cargo106.PK4` `cargoSkin` descriptors through rehosted code, normalizes localized
|
||||||
|
`~####Name` tokens into visible names, and shows that the current 1.06 visible-name union is
|
||||||
|
`80`, so source recovery alone still does not prove the live price-selector ordering
|
||||||
- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity
|
- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity
|
||||||
with tracked fixture coverage, not generic unresolved descriptor residue
|
with tracked fixture coverage, not generic unresolved descriptor residue
|
||||||
- a minimal event-owned train surface and an opaque economic-status lane now exist in runtime
|
- a minimal event-owned train surface and an opaque economic-status lane now exist in runtime
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue