Rehost offline cargo economy sources

This commit is contained in:
Jan Petykiewicz 2026-04-17 11:49:20 -07:00
commit 517b01cd35
8 changed files with 1991 additions and 5 deletions

View file

@ -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`
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
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
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

File diff suppressed because it is too large Load diff

View file

@ -17,13 +17,15 @@ use rrt_model::{
load_binary_summary, load_function_map,
};
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,
RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
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,
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,
@ -139,6 +141,16 @@ enum Command {
RuntimeInspectPk4 {
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 {
win_path: PathBuf,
},
@ -272,6 +284,25 @@ struct RuntimePk4InspectionOutput {
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)]
struct RuntimeWinInspectionOutput {
path: String,
@ -815,6 +846,20 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
Command::RuntimeInspectPk4 { 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 } => {
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),
})
}
[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" => {
Ok(Command::RuntimeInspectWin {
win_path: PathBuf::from(path),
@ -1128,7 +1195,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
})
}
_ => 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(),
),
}
@ -1490,6 +1557,41 @@ fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Er
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>> {
let report = RuntimeWinInspectionOutput {
path: win_path.display().to_string(),

View file

@ -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-cargo-bindings.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] = &[

View 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()]
);
}
}

View file

@ -1,5 +1,6 @@
pub mod calendar;
pub mod campaign_exe;
pub mod economy;
pub mod import;
pub mod persistence;
pub mod pk4;
@ -14,6 +15,12 @@ pub use campaign_exe::{
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CampaignPageBand, CampaignScenarioEntry,
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::{
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,

View file

@ -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`
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
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,
with tracked fixture coverage, instead of generic unresolved descriptor residue
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask,

View file

@ -91,7 +91,12 @@ Implemented today:
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
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
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