Refactor runtime ownership and clean up warnings

This commit is contained in:
Jan Petykiewicz 2026-04-21 15:40:17 -07:00
commit 486b061558
628 changed files with 97954 additions and 90763 deletions

View file

@ -0,0 +1,20 @@
use super::{Command, FinanceCommand, usage_error};
pub(super) fn parse_finance_command(
args: &[String],
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[subcommand, snapshot_path] if subcommand == "eval" => {
Ok(Command::Finance(FinanceCommand::Eval {
snapshot_path: snapshot_path.into(),
}))
}
[subcommand, left_path, right_path] if subcommand == "diff" => {
Ok(Command::Finance(FinanceCommand::Diff {
left_path: left_path.into(),
right_path: right_path.into(),
}))
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,158 @@
mod finance;
mod model;
mod runtime;
mod validate;
use std::env;
use std::path::Path;
pub(crate) use model::{
Command, CompareCommand, FinanceCommand, FixtureStateCommand, InspectCommand, RuntimeCommand,
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>]";
pub(super) fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
let current_dir = env::current_dir()?;
parse_command_args(&args, &current_dir)
}
fn parse_command_args(
args: &[String],
current_dir: &Path,
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[] => Ok(Command::Validate {
repo_root: current_dir.to_path_buf(),
}),
[command, rest @ ..] if command == "validate" => {
validate::parse_validate_command(rest, current_dir)
}
[command, rest @ ..] if command == "finance" => finance::parse_finance_command(rest),
[command, rest @ ..] if command == "runtime" => runtime::parse_runtime_command(rest),
_ => usage_error(),
}
}
fn usage_error<T>() -> Result<T, Box<dyn std::error::Error>> {
Err(USAGE.into())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{
Command, CompareCommand, FinanceCommand, FixtureStateCommand, InspectCommand,
RuntimeCommand, ScanCommand, parse_command_args,
};
fn parse(args: &[&str]) -> Command {
parse_command_args(
&args
.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>(),
PathBuf::from("/tmp/workspace").as_path(),
)
.expect("command should parse")
}
#[test]
fn parses_validate_with_default_repo_root() {
assert_eq!(
parse(&[]),
Command::Validate {
repo_root: PathBuf::from("/tmp/workspace"),
}
);
}
#[test]
fn parses_finance_eval() {
assert_eq!(
parse(&["finance", "eval", "snapshot.json"]),
Command::Finance(FinanceCommand::Eval {
snapshot_path: PathBuf::from("snapshot.json"),
})
);
}
#[test]
fn parses_finance_diff() {
assert_eq!(
parse(&["finance", "diff", "left.json", "right.json"]),
Command::Finance(FinanceCommand::Diff {
left_path: PathBuf::from("left.json"),
right_path: PathBuf::from("right.json"),
})
);
}
#[test]
fn parses_runtime_snapshot_state_command() {
assert_eq!(
parse(&["runtime", "snapshot-state", "input.json", "snapshot.json"]),
Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SnapshotState {
input_path: PathBuf::from("input.json"),
output_path: PathBuf::from("snapshot.json"),
}
))
);
}
#[test]
fn parses_runtime_snapshot_save_state_command() {
assert_eq!(
parse(&[
"runtime",
"snapshot-save-state",
"save.gms",
"snapshot.json"
]),
Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SnapshotSaveState {
smp_path: PathBuf::from("save.gms"),
output_path: PathBuf::from("snapshot.json"),
}
))
);
}
#[test]
fn parses_runtime_inspect_command() {
assert_eq!(
parse(&["runtime", "inspect-campaign-exe", "RT3.exe"]),
Command::Runtime(RuntimeCommand::Inspect(
InspectCommand::InspectCampaignExe {
exe_path: PathBuf::from("RT3.exe"),
}
))
);
}
#[test]
fn parses_runtime_compare_command() {
assert_eq!(
parse(&["runtime", "compare-candidate-table", "a.gms", "b.gms"]),
Command::Runtime(RuntimeCommand::Compare(
CompareCommand::CompareCandidateTable {
smp_paths: vec![PathBuf::from("a.gms"), PathBuf::from("b.gms")],
}
))
);
}
#[test]
fn parses_runtime_scan_command() {
assert_eq!(
parse(&["runtime", "scan-special-conditions", "root"]),
Command::Runtime(RuntimeCommand::Scan(ScanCommand::ScanSpecialConditions {
root_path: PathBuf::from("root"),
}))
);
}
}

View file

@ -0,0 +1,194 @@
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Command {
Validate { repo_root: PathBuf },
Finance(FinanceCommand),
Runtime(RuntimeCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FinanceCommand {
Eval {
snapshot_path: PathBuf,
},
Diff {
left_path: PathBuf,
right_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RuntimeCommand {
FixtureState(FixtureStateCommand),
Inspect(InspectCommand),
Compare(CompareCommand),
Scan(ScanCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FixtureStateCommand {
ValidateFixture {
fixture_path: PathBuf,
},
SummarizeFixture {
fixture_path: PathBuf,
},
ExportFixtureState {
fixture_path: PathBuf,
output_path: PathBuf,
},
DiffState {
left_path: PathBuf,
right_path: PathBuf,
},
SummarizeState {
snapshot_path: PathBuf,
},
SnapshotState {
input_path: PathBuf,
output_path: PathBuf,
},
SummarizeSaveLoad {
smp_path: PathBuf,
},
LoadSaveSlice {
smp_path: PathBuf,
},
SnapshotSaveState {
smp_path: PathBuf,
output_path: PathBuf,
},
ExportSaveSlice {
smp_path: PathBuf,
output_path: PathBuf,
},
ExportOverlayImport {
snapshot_path: PathBuf,
save_slice_path: PathBuf,
output_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum InspectCommand {
InspectSmp {
smp_path: PathBuf,
},
InspectCandidateTable {
smp_path: PathBuf,
},
InspectCompactEventDispatchCluster {
root_path: PathBuf,
},
InspectCompactEventDispatchClusterCounts {
root_path: PathBuf,
},
InspectMapTitleHints {
root_path: PathBuf,
},
InspectSaveCompanyChairman {
smp_path: PathBuf,
},
InspectSavePlacedStructureTriplets {
smp_path: PathBuf,
},
InspectPeriodicCompanyServiceTrace {
smp_path: PathBuf,
},
InspectRegionServiceTrace {
smp_path: PathBuf,
},
InspectInfrastructureAssetTrace {
smp_path: PathBuf,
},
InspectSaveRegionQueuedNoticeRecords {
smp_path: PathBuf,
},
InspectPlacedStructureDynamicSideBuffer {
smp_path: PathBuf,
},
InspectUnclassifiedSaveCollections {
smp_path: PathBuf,
},
InspectPk4 {
pk4_path: PathBuf,
},
InspectCargoTypes {
cargo_types_dir: PathBuf,
},
InspectBuildingTypeSources {
building_types_dir: PathBuf,
bindings_path: Option<PathBuf>,
},
InspectCargoSkins {
cargo_skin_pk4_path: PathBuf,
},
InspectCargoEconomySources {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectCargoProductionSelector {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectCargoPriceSelector {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectWin {
win_path: PathBuf,
},
ExtractPk4Entry {
pk4_path: PathBuf,
entry_name: String,
output_path: PathBuf,
},
InspectCampaignExe {
exe_path: PathBuf,
},
ExportProfileBlock {
smp_path: PathBuf,
output_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CompareCommand {
CompareRegionFixedRowRuns {
left_path: PathBuf,
right_path: PathBuf,
},
CompareClassicProfile {
smp_paths: Vec<PathBuf>,
},
CompareRt3105Profile {
smp_paths: Vec<PathBuf>,
},
CompareCandidateTable {
smp_paths: Vec<PathBuf>,
},
CompareRecipeBookLines {
smp_paths: Vec<PathBuf>,
},
CompareSetupPayloadCore {
smp_paths: Vec<PathBuf>,
},
CompareSetupLaunchPayload {
smp_paths: Vec<PathBuf>,
},
ComparePostSpecialConditionsScalars {
smp_paths: Vec<PathBuf>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScanCommand {
ScanCandidateTableHeaders { root_path: PathBuf },
ScanCandidateTableNamedRuns { root_path: PathBuf },
ScanSpecialConditions { root_path: PathBuf },
ScanAlignedRuntimeRuleBand { root_path: PathBuf },
ScanPostSpecialConditionsScalars { root_path: PathBuf },
ScanPostSpecialConditionsTail { root_path: PathBuf },
ScanRecipeBookLines { root_path: PathBuf },
}

View file

@ -0,0 +1,66 @@
use std::path::PathBuf;
use super::super::{CompareCommand, usage_error};
pub(super) fn parse_compare_command(
args: &[String],
) -> Result<CompareCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, left_path, right_path] if subcommand == "compare-region-fixed-row-runs" => {
Ok(CompareCommand::CompareRegionFixedRowRuns {
left_path: left_path.into(),
right_path: right_path.into(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-classic-profile" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareClassicProfile {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-105-profile" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareRt3105Profile {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-candidate-table" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareCandidateTable {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-recipe-book-lines" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareRecipeBookLines {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-setup-payload-core" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareSetupPayloadCore {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-setup-launch-payload" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareSetupLaunchPayload {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-post-special-conditions-scalars" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::ComparePostSpecialConditionsScalars {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,73 @@
use super::super::{FixtureStateCommand, usage_error};
pub(super) fn parse_fixture_state_command(
args: &[String],
) -> Result<FixtureStateCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, fixture_path] if subcommand == "validate-fixture" => {
Ok(FixtureStateCommand::ValidateFixture {
fixture_path: fixture_path.into(),
})
}
[subcommand, fixture_path] if subcommand == "summarize-fixture" => {
Ok(FixtureStateCommand::SummarizeFixture {
fixture_path: fixture_path.into(),
})
}
[subcommand, fixture_path, output_path] if subcommand == "export-fixture-state" => {
Ok(FixtureStateCommand::ExportFixtureState {
fixture_path: fixture_path.into(),
output_path: output_path.into(),
})
}
[subcommand, left_path, right_path] if subcommand == "diff-state" => {
Ok(FixtureStateCommand::DiffState {
left_path: left_path.into(),
right_path: right_path.into(),
})
}
[subcommand, snapshot_path] if subcommand == "summarize-state" => {
Ok(FixtureStateCommand::SummarizeState {
snapshot_path: snapshot_path.into(),
})
}
[subcommand, input_path, output_path] if subcommand == "snapshot-state" => {
Ok(FixtureStateCommand::SnapshotState {
input_path: input_path.into(),
output_path: output_path.into(),
})
}
[subcommand, smp_path] if subcommand == "summarize-save-load" => {
Ok(FixtureStateCommand::SummarizeSaveLoad {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "load-save-slice" => {
Ok(FixtureStateCommand::LoadSaveSlice {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "snapshot-save-state" => {
Ok(FixtureStateCommand::SnapshotSaveState {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "export-save-slice" => {
Ok(FixtureStateCommand::ExportSaveSlice {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
[subcommand, snapshot_path, save_slice_path, output_path]
if subcommand == "export-overlay-import" =>
{
Ok(FixtureStateCommand::ExportOverlayImport {
snapshot_path: snapshot_path.into(),
save_slice_path: save_slice_path.into(),
output_path: output_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,146 @@
use super::super::{InspectCommand, usage_error};
pub(super) fn parse_inspect_command(
args: &[String],
) -> Result<InspectCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, smp_path] if subcommand == "inspect-smp" => Ok(InspectCommand::InspectSmp {
smp_path: smp_path.into(),
}),
[subcommand, smp_path] if subcommand == "inspect-candidate-table" => {
Ok(InspectCommand::InspectCandidateTable {
smp_path: smp_path.into(),
})
}
[subcommand, root_path] if subcommand == "inspect-compact-event-dispatch-cluster" => {
Ok(InspectCommand::InspectCompactEventDispatchCluster {
root_path: root_path.into(),
})
}
[subcommand, root_path]
if subcommand == "inspect-compact-event-dispatch-cluster-counts" =>
{
Ok(InspectCommand::InspectCompactEventDispatchClusterCounts {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "inspect-map-title-hints" => {
Ok(InspectCommand::InspectMapTitleHints {
root_path: root_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-company-chairman" => {
Ok(InspectCommand::InspectSaveCompanyChairman {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-placed-structure-triplets" => {
Ok(InspectCommand::InspectSavePlacedStructureTriplets {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-periodic-company-service-trace" => {
Ok(InspectCommand::InspectPeriodicCompanyServiceTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-region-service-trace" => {
Ok(InspectCommand::InspectRegionServiceTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-infrastructure-asset-trace" => {
Ok(InspectCommand::InspectInfrastructureAssetTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-region-queued-notice-records" => {
Ok(InspectCommand::InspectSaveRegionQueuedNoticeRecords {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-placed-structure-dynamic-side-buffer" => {
Ok(InspectCommand::InspectPlacedStructureDynamicSideBuffer {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-unclassified-save-collections" => {
Ok(InspectCommand::InspectUnclassifiedSaveCollections {
smp_path: smp_path.into(),
})
}
[subcommand, pk4_path] if subcommand == "inspect-pk4" => Ok(InspectCommand::InspectPk4 {
pk4_path: pk4_path.into(),
}),
[subcommand, cargo_types_dir] if subcommand == "inspect-cargo-types" => {
Ok(InspectCommand::InspectCargoTypes {
cargo_types_dir: cargo_types_dir.into(),
})
}
[subcommand, building_types_dir] if subcommand == "inspect-building-type-sources" => {
Ok(InspectCommand::InspectBuildingTypeSources {
building_types_dir: building_types_dir.into(),
bindings_path: None,
})
}
[subcommand, building_types_dir, bindings_path]
if subcommand == "inspect-building-type-sources" =>
{
Ok(InspectCommand::InspectBuildingTypeSources {
building_types_dir: building_types_dir.into(),
bindings_path: Some(bindings_path.into()),
})
}
[subcommand, cargo_skin_pk4_path] if subcommand == "inspect-cargo-skins" => {
Ok(InspectCommand::InspectCargoSkins {
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-economy-sources" =>
{
Ok(InspectCommand::InspectCargoEconomySources {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-production-selector" =>
{
Ok(InspectCommand::InspectCargoProductionSelector {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-price-selector" =>
{
Ok(InspectCommand::InspectCargoPriceSelector {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, win_path] if subcommand == "inspect-win" => Ok(InspectCommand::InspectWin {
win_path: win_path.into(),
}),
[subcommand, pk4_path, entry_name, output_path] if subcommand == "extract-pk4-entry" => {
Ok(InspectCommand::ExtractPk4Entry {
pk4_path: pk4_path.into(),
entry_name: entry_name.clone(),
output_path: output_path.into(),
})
}
[subcommand, exe_path] if subcommand == "inspect-campaign-exe" => {
Ok(InspectCommand::InspectCampaignExe {
exe_path: exe_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "export-profile-block" => {
Ok(InspectCommand::ExportProfileBlock {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,74 @@
mod compare;
mod fixture_state;
mod inspect;
mod scan;
use super::{Command, RuntimeCommand, usage_error};
pub(super) fn parse_runtime_command(
args: &[String],
) -> Result<Command, Box<dyn std::error::Error>> {
let [subcommand, ..] = args else {
return usage_error();
};
let runtime_command = match subcommand.as_str() {
"validate-fixture"
| "summarize-fixture"
| "export-fixture-state"
| "diff-state"
| "summarize-state"
| "snapshot-state"
| "summarize-save-load"
| "load-save-slice"
| "snapshot-save-state"
| "export-save-slice"
| "export-overlay-import" => {
RuntimeCommand::FixtureState(fixture_state::parse_fixture_state_command(args)?)
}
"inspect-smp"
| "inspect-candidate-table"
| "inspect-compact-event-dispatch-cluster"
| "inspect-compact-event-dispatch-cluster-counts"
| "inspect-map-title-hints"
| "inspect-save-company-chairman"
| "inspect-save-placed-structure-triplets"
| "inspect-periodic-company-service-trace"
| "inspect-region-service-trace"
| "inspect-infrastructure-asset-trace"
| "inspect-save-region-queued-notice-records"
| "inspect-placed-structure-dynamic-side-buffer"
| "inspect-unclassified-save-collections"
| "inspect-pk4"
| "inspect-cargo-types"
| "inspect-building-type-sources"
| "inspect-cargo-skins"
| "inspect-cargo-economy-sources"
| "inspect-cargo-production-selector"
| "inspect-cargo-price-selector"
| "inspect-win"
| "extract-pk4-entry"
| "inspect-campaign-exe"
| "export-profile-block" => RuntimeCommand::Inspect(inspect::parse_inspect_command(args)?),
"compare-region-fixed-row-runs"
| "compare-classic-profile"
| "compare-105-profile"
| "compare-candidate-table"
| "compare-recipe-book-lines"
| "compare-setup-payload-core"
| "compare-setup-launch-payload"
| "compare-post-special-conditions-scalars" => {
RuntimeCommand::Compare(compare::parse_compare_command(args)?)
}
"scan-candidate-table-headers"
| "scan-candidate-table-named-runs"
| "scan-special-conditions"
| "scan-aligned-runtime-rule-band"
| "scan-post-special-conditions-scalars"
| "scan-post-special-conditions-tail"
| "scan-recipe-book-lines" => RuntimeCommand::Scan(scan::parse_scan_command(args)?),
_ => return usage_error(),
};
Ok(Command::Runtime(runtime_command))
}

View file

@ -0,0 +1,44 @@
use super::super::{ScanCommand, usage_error};
pub(super) fn parse_scan_command(
args: &[String],
) -> Result<ScanCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, root_path] if subcommand == "scan-candidate-table-headers" => {
Ok(ScanCommand::ScanCandidateTableHeaders {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-candidate-table-named-runs" => {
Ok(ScanCommand::ScanCandidateTableNamedRuns {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-special-conditions" => {
Ok(ScanCommand::ScanSpecialConditions {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-aligned-runtime-rule-band" => {
Ok(ScanCommand::ScanAlignedRuntimeRuleBand {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-post-special-conditions-scalars" => {
Ok(ScanCommand::ScanPostSpecialConditionsScalars {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-post-special-conditions-tail" => {
Ok(ScanCommand::ScanPostSpecialConditionsTail {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-recipe-book-lines" => {
Ok(ScanCommand::ScanRecipeBookLines {
root_path: root_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,18 @@
use std::path::Path;
use super::{Command, usage_error};
pub(super) fn parse_validate_command(
args: &[String],
current_dir: &Path,
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[] => Ok(Command::Validate {
repo_root: current_dir.to_path_buf(),
}),
[repo_root] => Ok(Command::Validate {
repo_root: repo_root.into(),
}),
_ => usage_error(),
}
}

View file

@ -0,0 +1,12 @@
use crate::app::command::FinanceCommand;
use crate::app::finance::{run_finance_diff, run_finance_eval};
pub(super) fn dispatch_finance(command: FinanceCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
FinanceCommand::Eval { snapshot_path } => run_finance_eval(&snapshot_path),
FinanceCommand::Diff {
left_path,
right_path,
} => run_finance_diff(&left_path, &right_path),
}
}

View file

@ -0,0 +1,81 @@
mod finance;
mod runtime;
mod validate;
use crate::app::command::{Command, parse_command};
pub(super) fn run() -> Result<(), Box<dyn std::error::Error>> {
dispatch_command(parse_command()?)
}
fn dispatch_command(command: Command) -> Result<(), Box<dyn std::error::Error>> {
match command {
Command::Validate { repo_root } => validate::dispatch_validate(repo_root),
Command::Finance(command) => finance::dispatch_finance(command),
Command::Runtime(command) => runtime::dispatch_runtime(command),
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rrt_model::finance::FinanceSnapshot;
use super::dispatch_command;
use crate::app::command::{Command, FinanceCommand, FixtureStateCommand, RuntimeCommand};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn unique_temp_path(stem: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("rrt-cli-{stem}-{unique}.json"))
}
#[test]
fn dispatches_validate_command() {
dispatch_command(Command::Validate {
repo_root: workspace_root(),
})
.expect("validate dispatch should succeed");
}
#[test]
fn dispatches_finance_eval_command() {
let snapshot_path = unique_temp_path("finance-eval");
let snapshot = FinanceSnapshot {
policy: Default::default(),
company: Default::default(),
};
fs::write(
&snapshot_path,
serde_json::to_vec_pretty(&snapshot).expect("snapshot should serialize"),
)
.expect("snapshot should be written");
let result = dispatch_command(Command::Finance(FinanceCommand::Eval {
snapshot_path: snapshot_path.clone(),
}));
let _ = fs::remove_file(&snapshot_path);
result.expect("finance dispatch should succeed");
}
#[test]
fn dispatches_runtime_fixture_state_command() {
dispatch_command(Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SummarizeState {
snapshot_path: workspace_root()
.join("fixtures/runtime/minimal-world-state-input.json"),
},
)))
.expect("runtime fixture/state dispatch should succeed");
}
}

View file

@ -0,0 +1,30 @@
use crate::app::command::CompareCommand;
use crate::app::runtime_compare::{
compare_candidate_table, compare_classic_profile, compare_post_special_conditions_scalars,
compare_recipe_book_lines, compare_region_fixed_row_runs, compare_rt3_105_profile,
compare_setup_launch_payload, compare_setup_payload_core,
};
pub(super) fn dispatch_compare(command: CompareCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
CompareCommand::CompareRegionFixedRowRuns {
left_path,
right_path,
} => compare_region_fixed_row_runs(&left_path, &right_path),
CompareCommand::CompareClassicProfile { smp_paths } => compare_classic_profile(&smp_paths),
CompareCommand::CompareRt3105Profile { smp_paths } => compare_rt3_105_profile(&smp_paths),
CompareCommand::CompareCandidateTable { smp_paths } => compare_candidate_table(&smp_paths),
CompareCommand::CompareRecipeBookLines { smp_paths } => {
compare_recipe_book_lines(&smp_paths)
}
CompareCommand::CompareSetupPayloadCore { smp_paths } => {
compare_setup_payload_core(&smp_paths)
}
CompareCommand::CompareSetupLaunchPayload { smp_paths } => {
compare_setup_launch_payload(&smp_paths)
}
CompareCommand::ComparePostSpecialConditionsScalars { smp_paths } => {
compare_post_special_conditions_scalars(&smp_paths)
}
}
}

View file

@ -0,0 +1,43 @@
use crate::app::command::FixtureStateCommand;
use crate::app::runtime_fixture_state::{
diff_state, export_fixture_state, export_overlay_import, export_save_slice, load_save_slice,
snapshot_save_state, snapshot_state, summarize_fixture, summarize_save_load, summarize_state,
validate_fixture,
};
pub(super) fn dispatch_fixture_state(
command: FixtureStateCommand,
) -> Result<(), Box<dyn std::error::Error>> {
match command {
FixtureStateCommand::ValidateFixture { fixture_path } => validate_fixture(&fixture_path),
FixtureStateCommand::SummarizeFixture { fixture_path } => summarize_fixture(&fixture_path),
FixtureStateCommand::ExportFixtureState {
fixture_path,
output_path,
} => export_fixture_state(&fixture_path, &output_path),
FixtureStateCommand::DiffState {
left_path,
right_path,
} => diff_state(&left_path, &right_path),
FixtureStateCommand::SummarizeState { snapshot_path } => summarize_state(&snapshot_path),
FixtureStateCommand::SnapshotState {
input_path,
output_path,
} => snapshot_state(&input_path, &output_path),
FixtureStateCommand::SummarizeSaveLoad { smp_path } => summarize_save_load(&smp_path),
FixtureStateCommand::LoadSaveSlice { smp_path } => load_save_slice(&smp_path),
FixtureStateCommand::SnapshotSaveState {
smp_path,
output_path,
} => snapshot_save_state(&smp_path, &output_path),
FixtureStateCommand::ExportSaveSlice {
smp_path,
output_path,
} => export_save_slice(&smp_path, &output_path),
FixtureStateCommand::ExportOverlayImport {
snapshot_path,
save_slice_path,
output_path,
} => export_overlay_import(&snapshot_path, &save_slice_path, &output_path),
}
}

View file

@ -0,0 +1,85 @@
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_compact_event_dispatch_cluster_counts, inspect_infrastructure_asset_trace,
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,
inspect_win,
};
pub(super) fn dispatch_inspect(command: InspectCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
InspectCommand::InspectSmp { smp_path } => inspect_smp(&smp_path),
InspectCommand::InspectCandidateTable { smp_path } => inspect_candidate_table(&smp_path),
InspectCommand::InspectCompactEventDispatchCluster { root_path } => {
inspect_compact_event_dispatch_cluster(&root_path)
}
InspectCommand::InspectCompactEventDispatchClusterCounts { root_path } => {
inspect_compact_event_dispatch_cluster_counts(&root_path)
}
InspectCommand::InspectMapTitleHints { root_path } => inspect_map_title_hints(&root_path),
InspectCommand::InspectSaveCompanyChairman { smp_path } => {
inspect_save_company_chairman(&smp_path)
}
InspectCommand::InspectSavePlacedStructureTriplets { smp_path } => {
inspect_save_placed_structure_triplets(&smp_path)
}
InspectCommand::InspectPeriodicCompanyServiceTrace { smp_path } => {
inspect_periodic_company_service_trace(&smp_path)
}
InspectCommand::InspectRegionServiceTrace { smp_path } => {
inspect_region_service_trace(&smp_path)
}
InspectCommand::InspectInfrastructureAssetTrace { smp_path } => {
inspect_infrastructure_asset_trace(&smp_path)
}
InspectCommand::InspectSaveRegionQueuedNoticeRecords { smp_path } => {
inspect_save_region_queued_notice_records(&smp_path)
}
InspectCommand::InspectPlacedStructureDynamicSideBuffer { smp_path } => {
inspect_placed_structure_dynamic_side_buffer(&smp_path)
}
InspectCommand::InspectUnclassifiedSaveCollections { smp_path } => {
inspect_unclassified_save_collections(&smp_path)
}
InspectCommand::InspectPk4 { pk4_path } => inspect_pk4(&pk4_path),
InspectCommand::InspectCargoTypes { cargo_types_dir } => {
inspect_cargo_types(&cargo_types_dir)
}
InspectCommand::InspectBuildingTypeSources {
building_types_dir,
bindings_path,
} => inspect_building_type_sources(&building_types_dir, bindings_path.as_deref()),
InspectCommand::InspectCargoSkins {
cargo_skin_pk4_path,
} => inspect_cargo_skins(&cargo_skin_pk4_path),
InspectCommand::InspectCargoEconomySources {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectCargoProductionSelector {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_production_selector(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectCargoPriceSelector {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_price_selector(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectWin { win_path } => inspect_win(&win_path),
InspectCommand::ExtractPk4Entry {
pk4_path,
entry_name,
output_path,
} => extract_pk4_entry(&pk4_path, &entry_name, &output_path),
InspectCommand::InspectCampaignExe { exe_path } => inspect_campaign_exe(&exe_path),
InspectCommand::ExportProfileBlock {
smp_path,
output_path,
} => export_profile_block(&smp_path, &output_path),
}
}

View file

@ -0,0 +1,15 @@
mod compare;
mod fixture_state;
mod inspect;
mod scan;
use crate::app::command::RuntimeCommand;
pub(super) fn dispatch_runtime(command: RuntimeCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
RuntimeCommand::FixtureState(command) => fixture_state::dispatch_fixture_state(command),
RuntimeCommand::Inspect(command) => inspect::dispatch_inspect(command),
RuntimeCommand::Compare(command) => compare::dispatch_compare(command),
RuntimeCommand::Scan(command) => scan::dispatch_scan(command),
}
}

View file

@ -0,0 +1,28 @@
use crate::app::command::ScanCommand;
use crate::app::runtime_scan::{
scan_aligned_runtime_rule_band, scan_candidate_table_headers, scan_candidate_table_named_runs,
scan_post_special_conditions_scalars, scan_post_special_conditions_tail,
scan_recipe_book_lines, scan_special_conditions,
};
pub(super) fn dispatch_scan(command: ScanCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
ScanCommand::ScanCandidateTableHeaders { root_path } => {
scan_candidate_table_headers(&root_path)
}
ScanCommand::ScanCandidateTableNamedRuns { root_path } => {
scan_candidate_table_named_runs(&root_path)
}
ScanCommand::ScanSpecialConditions { root_path } => scan_special_conditions(&root_path),
ScanCommand::ScanAlignedRuntimeRuleBand { root_path } => {
scan_aligned_runtime_rule_band(&root_path)
}
ScanCommand::ScanPostSpecialConditionsScalars { root_path } => {
scan_post_special_conditions_scalars(&root_path)
}
ScanCommand::ScanPostSpecialConditionsTail { root_path } => {
scan_post_special_conditions_tail(&root_path)
}
ScanCommand::ScanRecipeBookLines { root_path } => scan_recipe_book_lines(&root_path),
}
}

View file

@ -0,0 +1,15 @@
use std::path::PathBuf;
use crate::app::validate::{
validate_binary_summary, validate_control_loop_atlas, validate_function_map,
validate_required_files,
};
pub(super) fn dispatch_validate(repo_root: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
validate_required_files(&repo_root)?;
validate_binary_summary(&repo_root)?;
validate_function_map(&repo_root)?;
validate_control_loop_atlas(&repo_root)?;
println!("baseline validation passed");
Ok(())
}

View file

@ -0,0 +1,110 @@
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use rrt_model::finance::{FinanceOutcome, FinanceSnapshot};
use serde_json::Value;
use crate::app::reports::state::{FinanceDiffEntry, FinanceDiffReport};
pub(crate) fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let outcome = load_finance_outcome(snapshot_path)?;
println!("{}", serde_json::to_string_pretty(&outcome)?);
Ok(())
}
pub(crate) fn run_finance_diff(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = load_finance_outcome(left_path)?;
let right = load_finance_outcome(right_path)?;
let report = diff_finance_outcomes(&left, &right)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_finance_outcome(
path: &Path,
) -> Result<FinanceOutcome, Box<dyn std::error::Error>> {
let text = fs::read_to_string(path)?;
if let Ok(snapshot) = serde_json::from_str::<FinanceSnapshot>(&text) {
return Ok(snapshot.evaluate());
}
if let Ok(outcome) = serde_json::from_str::<FinanceOutcome>(&text) {
return Ok(outcome);
}
Err(format!(
"unable to parse {} as FinanceSnapshot or FinanceOutcome",
path.display()
)
.into())
}
pub(crate) fn diff_finance_outcomes(
left: &FinanceOutcome,
right: &FinanceOutcome,
) -> Result<FinanceDiffReport, Box<dyn std::error::Error>> {
let left_value = serde_json::to_value(left)?;
let right_value = serde_json::to_value(right)?;
let mut differences = Vec::new();
collect_json_differences("$", &left_value, &right_value, &mut differences);
Ok(FinanceDiffReport {
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
})
}
fn collect_json_differences(
path: &str,
left: &Value,
right: &Value,
differences: &mut Vec<FinanceDiffEntry>,
) {
match (left, right) {
(Value::Object(left_map), Value::Object(right_map)) => {
let mut keys = BTreeSet::new();
keys.extend(left_map.keys().cloned());
keys.extend(right_map.keys().cloned());
for key in keys {
let next_path = format!("{path}.{key}");
match (left_map.get(&key), right_map.get(&key)) {
(Some(left_value), Some(right_value)) => {
collect_json_differences(&next_path, left_value, right_value, differences);
}
(left_value, right_value) => differences.push(FinanceDiffEntry {
path: next_path,
left: left_value.cloned().unwrap_or(Value::Null),
right: right_value.cloned().unwrap_or(Value::Null),
}),
}
}
}
(Value::Array(left_items), Value::Array(right_items)) => {
let max_len = left_items.len().max(right_items.len());
for index in 0..max_len {
let next_path = format!("{path}[{index}]");
match (left_items.get(index), right_items.get(index)) {
(Some(left_value), Some(right_value)) => {
collect_json_differences(&next_path, left_value, right_value, differences);
}
(left_value, right_value) => differences.push(FinanceDiffEntry {
path: next_path,
left: left_value.cloned().unwrap_or(Value::Null),
right: right_value.cloned().unwrap_or(Value::Null),
}),
}
}
}
_ if left != right => differences.push(FinanceDiffEntry {
path: path.to_string(),
left: left.clone(),
right: right.clone(),
}),
_ => {}
}
}

View file

@ -0,0 +1,446 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use crate::app::reports::inspect::{
RuntimeCompactEventDispatchClusterConditionTuple, RuntimeCompactEventDispatchClusterOccurrence,
RuntimeCompactEventDispatchClusterReport, RuntimeCompactEventDispatchClusterRow,
RuntimeProfileBlockExportDocument,
};
use rrt_runtime::inspect::smp::bundle::{SmpInspectionReport, inspect_smp_file};
pub(crate) fn build_runtime_compact_event_dispatch_cluster_report(
root_path: &Path,
) -> Result<RuntimeCompactEventDispatchClusterReport, Box<dyn std::error::Error>> {
let mut input_paths = Vec::new();
collect_compact_event_dispatch_cluster_input_paths(root_path, &mut input_paths)?;
input_paths.sort();
let mut maps_with_event_runtime_collection = 0usize;
let mut maps_with_dispatch_strip_records = 0usize;
let mut dispatch_strip_record_count = 0usize;
let mut dispatch_strip_records_with_trigger_kind = 0usize;
let mut dispatch_strip_records_missing_trigger_kind = 0usize;
let mut dispatch_strip_payload_families = BTreeMap::<String, usize>::new();
let mut dispatch_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
let mut dispatch_descriptor_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_dispatch_record_count = 0usize;
let mut add_building_dispatch_records_with_trigger_kind = 0usize;
let mut add_building_dispatch_records_missing_trigger_kind = 0usize;
let mut add_building_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_descriptor_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_row_shape_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_row_shape_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_family_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_family_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_condition_tuple_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_condition_tuple_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_condition_cluster_occurrence_counts =
BTreeMap::<String, usize>::new();
let mut add_building_signature_condition_cluster_map_counts = BTreeMap::<String, usize>::new();
let mut signature_condition_cluster_descriptor_keys =
BTreeMap::<String, BTreeSet<String>>::new();
let mut add_building_signature_condition_clusters = BTreeSet::<String>::new();
let mut dispatch_descriptor_occurrences =
BTreeMap::<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
let mut unknown_descriptor_occurrences =
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
for path in &input_paths {
let inspection = inspect_smp_file(path)?;
let Some(summary) = inspection.event_runtime_collection_summary else {
continue;
};
maps_with_event_runtime_collection += 1;
let mut map_dispatch_strip_record_count = 0usize;
let mut map_descriptor_keys = BTreeSet::<String>::new();
let mut map_add_building_descriptor_keys = BTreeSet::<String>::new();
let mut map_add_building_row_shapes = BTreeSet::<String>::new();
let mut map_add_building_signature_families = BTreeSet::<String>::new();
let mut map_add_building_condition_tuples = BTreeSet::<String>::new();
let mut map_add_building_signature_condition_clusters = BTreeSet::<String>::new();
for record in &summary.records {
let matching_rows = record
.grouped_effect_rows
.iter()
.filter(|row| compact_event_dispatch_strip_opcode(row.opcode))
.fold(
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterRow>>::new(),
|mut grouped, row| {
grouped.entry(row.descriptor_id).or_default().push(
RuntimeCompactEventDispatchClusterRow {
group_index: row.group_index,
descriptor_id: row.descriptor_id,
descriptor_label: row.descriptor_label.clone(),
opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value,
},
);
grouped
},
);
if matching_rows.is_empty() {
continue;
}
map_dispatch_strip_record_count += 1;
if record.trigger_kind.is_some() {
dispatch_strip_records_with_trigger_kind += 1;
} else {
dispatch_strip_records_missing_trigger_kind += 1;
}
*dispatch_strip_payload_families
.entry(record.payload_family.clone())
.or_insert(0) += 1;
let mut record_has_add_building = false;
let condition_tuples = record
.standalone_condition_rows
.iter()
.map(|row| RuntimeCompactEventDispatchClusterConditionTuple {
raw_condition_id: row.raw_condition_id,
subtype: row.subtype,
metric: row.metric.clone(),
})
.collect::<Vec<_>>();
let signature_family = compact_event_signature_family_from_notes(&record.notes);
let condition_tuple_family =
compact_event_dispatch_condition_tuple_family(&condition_tuples);
let row_shape_family = compact_event_dispatch_row_shape_family(&matching_rows);
let signature_family_key = signature_family
.clone()
.unwrap_or_else(|| "unknown-signature-family".to_string());
let signature_condition_cluster_key =
compact_event_dispatch_signature_condition_cluster_key(
signature_family.as_deref(),
&condition_tuples,
);
for (descriptor_id, rows) in matching_rows {
let occurrence = RuntimeCompactEventDispatchClusterOccurrence {
path: path.display().to_string(),
record_index: record.record_index,
live_entry_id: record.live_entry_id,
payload_family: record.payload_family.clone(),
trigger_kind: record.trigger_kind,
signature_family: signature_family.clone(),
condition_tuples: condition_tuples.clone(),
rows: rows.clone(),
};
let descriptor_key = compact_event_dispatch_descriptor_key(descriptor_id, &rows);
signature_condition_cluster_descriptor_keys
.entry(signature_condition_cluster_key.clone())
.or_default()
.insert(descriptor_key.clone());
*dispatch_descriptor_occurrence_counts
.entry(descriptor_key.clone())
.or_insert(0) += 1;
map_descriptor_keys.insert(descriptor_key.clone());
if compact_event_dispatch_add_building_descriptor_id(descriptor_id) {
record_has_add_building = true;
add_building_signature_condition_clusters
.insert(signature_condition_cluster_key.clone());
*add_building_descriptor_occurrence_counts
.entry(descriptor_key.clone())
.or_insert(0) += 1;
map_add_building_descriptor_keys.insert(descriptor_key.clone());
*add_building_row_shape_occurrence_counts
.entry(row_shape_family.clone())
.or_insert(0) += 1;
map_add_building_row_shapes.insert(row_shape_family.clone());
*add_building_signature_family_occurrence_counts
.entry(signature_family_key.clone())
.or_insert(0) += 1;
*add_building_condition_tuple_occurrence_counts
.entry(condition_tuple_family.clone())
.or_insert(0) += 1;
*add_building_signature_condition_cluster_occurrence_counts
.entry(signature_condition_cluster_key.clone())
.or_insert(0) += 1;
map_add_building_signature_families.insert(signature_family_key.clone());
map_add_building_condition_tuples.insert(condition_tuple_family.clone());
map_add_building_signature_condition_clusters
.insert(signature_condition_cluster_key.clone());
}
dispatch_descriptor_occurrences
.entry(descriptor_key)
.or_default()
.push(occurrence.clone());
if rows.iter().all(|row| row.descriptor_label.is_none()) {
unknown_descriptor_occurrences
.entry(descriptor_id)
.or_default()
.push(occurrence);
}
}
if record_has_add_building {
add_building_dispatch_record_count += 1;
if record.trigger_kind.is_some() {
add_building_dispatch_records_with_trigger_kind += 1;
} else {
add_building_dispatch_records_missing_trigger_kind += 1;
}
}
}
if map_dispatch_strip_record_count > 0 {
maps_with_dispatch_strip_records += 1;
dispatch_strip_record_count += map_dispatch_strip_record_count;
}
for descriptor_key in map_descriptor_keys {
*dispatch_descriptor_map_counts
.entry(descriptor_key)
.or_insert(0) += 1;
}
for descriptor_key in map_add_building_descriptor_keys {
*add_building_descriptor_map_counts
.entry(descriptor_key)
.or_insert(0) += 1;
}
for row_shape in map_add_building_row_shapes {
*add_building_row_shape_map_counts
.entry(row_shape)
.or_insert(0) += 1;
}
for signature_family in map_add_building_signature_families {
*add_building_signature_family_map_counts
.entry(signature_family)
.or_insert(0) += 1;
}
for condition_tuple_family in map_add_building_condition_tuples {
*add_building_condition_tuple_map_counts
.entry(condition_tuple_family)
.or_insert(0) += 1;
}
for signature_condition_cluster in map_add_building_signature_condition_clusters {
*add_building_signature_condition_cluster_map_counts
.entry(signature_condition_cluster)
.or_insert(0) += 1;
}
}
let unknown_descriptor_ids = unknown_descriptor_occurrences
.keys()
.copied()
.collect::<Vec<_>>();
let unknown_descriptor_special_condition_label_matches = unknown_descriptor_ids
.iter()
.filter_map(|descriptor_id| {
special_condition_label_for_compact_dispatch_descriptor(*descriptor_id)
.map(|label| format!("{descriptor_id} -> {label}"))
})
.collect::<Vec<_>>();
let add_building_signature_condition_cluster_descriptor_keys =
add_building_signature_condition_clusters
.iter()
.map(|cluster| {
let keys = signature_condition_cluster_descriptor_keys
.get(cluster)
.map(|keys| keys.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
(cluster.clone(), keys)
})
.collect::<BTreeMap<_, _>>();
let add_building_signature_condition_cluster_non_add_building_descriptor_keys =
add_building_signature_condition_cluster_descriptor_keys
.iter()
.map(|(cluster, keys)| {
let filtered = keys
.iter()
.filter(|key| !key.contains("Add Building"))
.cloned()
.collect::<Vec<_>>();
(cluster.clone(), filtered)
})
.collect::<BTreeMap<_, _>>();
Ok(RuntimeCompactEventDispatchClusterReport {
maps_scanned: input_paths.len(),
maps_with_event_runtime_collection,
maps_with_dispatch_strip_records,
dispatch_strip_record_count,
dispatch_strip_records_with_trigger_kind,
dispatch_strip_records_missing_trigger_kind,
dispatch_strip_payload_families,
dispatch_descriptor_occurrence_counts,
dispatch_descriptor_map_counts,
dispatch_descriptor_occurrences,
unknown_descriptor_ids,
unknown_descriptor_special_condition_label_matches,
unknown_descriptor_occurrences,
add_building_dispatch_record_count,
add_building_dispatch_records_with_trigger_kind,
add_building_dispatch_records_missing_trigger_kind,
add_building_descriptor_occurrence_counts,
add_building_descriptor_map_counts,
add_building_row_shape_occurrence_counts,
add_building_row_shape_map_counts,
add_building_signature_family_occurrence_counts,
add_building_signature_family_map_counts,
add_building_condition_tuple_occurrence_counts,
add_building_condition_tuple_map_counts,
add_building_signature_condition_cluster_occurrence_counts,
add_building_signature_condition_cluster_map_counts,
add_building_signature_condition_cluster_descriptor_keys,
add_building_signature_condition_cluster_non_add_building_descriptor_keys,
})
}
pub(crate) fn build_profile_block_export_document(
smp_path: &Path,
inspection: &SmpInspectionReport,
) -> Result<RuntimeProfileBlockExportDocument, Box<dyn std::error::Error>> {
if let Some(probe) = &inspection.classic_rehydrate_profile_probe {
return Ok(RuntimeProfileBlockExportDocument {
source_path: smp_path.display().to_string(),
profile_kind: "classic-rehydrate-profile".to_string(),
profile_family: probe.profile_family.clone(),
payload: serde_json::to_value(probe)?,
});
}
if let Some(probe) = &inspection.rt3_105_packed_profile_probe {
return Ok(RuntimeProfileBlockExportDocument {
source_path: smp_path.display().to_string(),
profile_kind: "rt3-105-packed-profile".to_string(),
profile_family: probe.profile_family.clone(),
payload: serde_json::to_value(probe)?,
});
}
Err(format!(
"{} did not expose an exportable packed-profile block",
smp_path.display()
)
.into())
}
pub(crate) fn compact_event_dispatch_add_building_descriptor_id(descriptor_id: u32) -> bool {
(503..=613).contains(&descriptor_id)
}
pub(crate) fn compact_event_dispatch_strip_opcode(opcode: u8) -> bool {
matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16)
}
pub(crate) fn compact_event_signature_family_from_notes(notes: &[String]) -> Option<String> {
notes.iter().find_map(|note| {
note.strip_prefix("compact signature family = ")
.map(ToString::to_string)
})
}
pub(crate) fn special_condition_label_for_compact_dispatch_descriptor(
descriptor_id: u32,
) -> Option<&'static str> {
let band_index = descriptor_id.checked_sub(535)? as usize;
crate::app::runtime_scan::common::SPECIAL_CONDITION_LABELS
.get(band_index)
.copied()
}
pub(crate) fn collect_compact_event_dispatch_cluster_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_compact_event_dispatch_cluster_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn compact_event_dispatch_descriptor_key(
descriptor_id: u32,
rows: &[RuntimeCompactEventDispatchClusterRow],
) -> String {
rows.first()
.and_then(|row| row.descriptor_label.as_deref())
.map(|label| format!("{descriptor_id} {label}"))
.unwrap_or_else(|| descriptor_id.to_string())
}
pub(crate) fn compact_event_dispatch_row_shape_family(
grouped_rows: &BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterRow>>,
) -> String {
let mut parts = grouped_rows
.values()
.flat_map(|rows| rows.iter())
.map(|row| {
format!(
"{}:{}:{}",
row.group_index, row.opcode, row.raw_scalar_value
)
})
.collect::<Vec<_>>();
if parts.is_empty() {
return "[]".to_string();
}
parts.sort();
format!("[{}]", parts.join(","))
}
pub(crate) fn compact_event_dispatch_condition_tuple_family(
tuples: &[RuntimeCompactEventDispatchClusterConditionTuple],
) -> String {
if tuples.is_empty() {
return "[]".to_string();
}
let parts = tuples
.iter()
.map(|tuple| match &tuple.metric {
Some(metric) => format!("{}:{}:{}", tuple.raw_condition_id, tuple.subtype, metric),
None => format!("{}:{}", tuple.raw_condition_id, tuple.subtype),
})
.collect::<Vec<_>>();
format!("[{}]", parts.join(","))
}
pub(crate) fn compact_event_dispatch_signature_condition_cluster_key(
signature_family: Option<&str>,
tuples: &[RuntimeCompactEventDispatchClusterConditionTuple],
) -> String {
format!(
"{} :: {}",
signature_family.unwrap_or("unknown-signature-family"),
compact_event_dispatch_condition_tuple_family(tuples)
)
}

View file

@ -0,0 +1,2 @@
pub(super) mod inspect;
pub(super) mod state_io;

View file

@ -0,0 +1,102 @@
use std::path::Path;
use crate::app::reports::state::{RuntimeOverlayImportExportOutput, RuntimeSaveSliceExportOutput};
use rrt_fixtures::{FixtureValidationReport, normalize_runtime_state};
use rrt_runtime::documents::{
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, load_runtime_state_input,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
};
use rrt_runtime::inspect::smp::save_load::SmpLoadedSaveSlice;
use rrt_runtime::persistence::{
load_runtime_snapshot_document, validate_runtime_snapshot_document,
};
use serde_json::Value;
pub(crate) fn load_normalized_runtime_state(
path: &Path,
) -> Result<Value, Box<dyn std::error::Error>> {
if let Ok(snapshot) = load_runtime_snapshot_document(path) {
validate_runtime_snapshot_document(&snapshot)
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
return normalize_runtime_state(&snapshot.state);
}
let input = load_runtime_state_input(path)?;
normalize_runtime_state(&input.state)
}
pub(crate) fn export_runtime_save_slice_document(
smp_path: &Path,
output_path: &Path,
save_slice: SmpLoadedSaveSlice,
) -> Result<RuntimeSaveSliceExportOutput, Box<dyn std::error::Error>> {
let document = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: smp_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("save-slice")
.to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some(format!(
"Exported loaded save slice from {}",
smp_path.display()
)),
original_save_filename: smp_path
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string),
original_save_sha256: None,
notes: vec![],
},
save_slice,
};
save_runtime_save_slice_document(output_path, &document)?;
Ok(RuntimeSaveSliceExportOutput {
path: smp_path.display().to_string(),
output_path: output_path.display().to_string(),
save_slice_id: document.save_slice_id,
})
}
pub(crate) fn export_runtime_overlay_import_document(
snapshot_path: &Path,
save_slice_path: &Path,
output_path: &Path,
) -> Result<RuntimeOverlayImportExportOutput, Box<dyn std::error::Error>> {
let import_id = output_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("overlay-import")
.to_string();
let document = RuntimeOverlayImportDocument {
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
import_id: import_id.clone(),
source: RuntimeOverlayImportDocumentSource {
description: Some(format!(
"Overlay import referencing {} and {}",
snapshot_path.display(),
save_slice_path.display()
)),
notes: vec![],
},
base_snapshot_path: snapshot_path.display().to_string(),
save_slice_path: save_slice_path.display().to_string(),
};
save_runtime_overlay_import_document(output_path, &document)?;
Ok(RuntimeOverlayImportExportOutput {
output_path: output_path.display().to_string(),
import_id,
base_snapshot_path: document.base_snapshot_path,
save_slice_path: document.save_slice_path,
})
}
pub(crate) fn print_runtime_validation_report(
report: &FixtureValidationReport,
) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", serde_json::to_string_pretty(report)?);
Ok(())
}

View file

@ -0,0 +1,16 @@
mod command;
mod dispatch;
mod finance;
mod helpers;
mod reports;
mod runtime_compare;
mod runtime_fixture_state;
mod runtime_inspect;
mod runtime_scan;
#[cfg(test)]
mod tests;
mod validate;
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
dispatch::run()
}

View file

@ -0,0 +1,258 @@
use std::collections::BTreeMap;
use rrt_runtime::inspect::{
building::BuildingTypeSourceReport,
campaign::CampaignExeInspectionReport,
cargo::{
CargoEconomySourceReport, CargoSelectorReport, CargoSkinInspectionReport,
CargoTypeInspectionReport,
},
pk4::{Pk4ExtractionReport, Pk4InspectionReport},
smp::{
bundle::SmpInspectionReport,
map_title::SmpMapTitleHintProbe,
services::{
SmpInfrastructureAssetTraceReport, SmpPeriodicCompanyServiceTraceReport,
SmpRegionServiceTraceReport,
},
world::SmpSaveCompanyChairmanAnalysisReport,
},
win::WinInspectionReport,
};
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSmpInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: SmpInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeCompactEventDispatchClusterReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterCountsOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeCompactEventDispatchClusterCountsReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintDirectoryOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeMapTitleHintDirectoryReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintDirectoryReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_probe: usize,
pub(crate) maps_with_grounded_title_hits: usize,
pub(crate) maps_with_adjacent_title_pairs: usize,
pub(crate) maps_with_same_stem_adjacent_pairs: usize,
pub(crate) maps: Vec<RuntimeMapTitleHintMapEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintMapEntry {
pub(crate) path: String,
pub(crate) probe: SmpMapTitleHintProbe,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_event_runtime_collection: usize,
pub(crate) maps_with_dispatch_strip_records: usize,
pub(crate) dispatch_strip_record_count: usize,
pub(crate) dispatch_strip_records_with_trigger_kind: usize,
pub(crate) dispatch_strip_records_missing_trigger_kind: usize,
pub(crate) dispatch_strip_payload_families: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrences:
BTreeMap<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
pub(crate) unknown_descriptor_ids: Vec<u32>,
pub(crate) unknown_descriptor_special_condition_label_matches: Vec<String>,
pub(crate) unknown_descriptor_occurrences:
BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
pub(crate) add_building_dispatch_record_count: usize,
pub(crate) add_building_dispatch_records_with_trigger_kind: usize,
pub(crate) add_building_dispatch_records_missing_trigger_kind: usize,
pub(crate) add_building_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_descriptor_keys:
BTreeMap<String, Vec<String>>,
pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys:
BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterCountsReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_event_runtime_collection: usize,
pub(crate) maps_with_dispatch_strip_records: usize,
pub(crate) dispatch_strip_record_count: usize,
pub(crate) dispatch_strip_records_with_trigger_kind: usize,
pub(crate) dispatch_strip_records_missing_trigger_kind: usize,
pub(crate) dispatch_strip_payload_families: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) unknown_descriptor_ids: Vec<u32>,
pub(crate) unknown_descriptor_special_condition_label_matches: Vec<String>,
pub(crate) add_building_dispatch_record_count: usize,
pub(crate) add_building_dispatch_records_with_trigger_kind: usize,
pub(crate) add_building_dispatch_records_missing_trigger_kind: usize,
pub(crate) add_building_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_descriptor_keys:
BTreeMap<String, Vec<String>>,
pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys:
BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterOccurrence {
pub(crate) path: String,
pub(crate) record_index: usize,
pub(crate) live_entry_id: u32,
pub(crate) payload_family: String,
pub(crate) trigger_kind: Option<u8>,
pub(crate) signature_family: Option<String>,
pub(crate) condition_tuples: Vec<RuntimeCompactEventDispatchClusterConditionTuple>,
pub(crate) rows: Vec<RuntimeCompactEventDispatchClusterRow>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterConditionTuple {
pub(crate) raw_condition_id: i32,
pub(crate) subtype: u8,
pub(crate) metric: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterRow {
pub(crate) group_index: usize,
pub(crate) descriptor_id: u32,
pub(crate) descriptor_label: Option<String>,
pub(crate) opcode: u8,
pub(crate) raw_scalar_value: i32,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveCompanyChairmanAnalysisOutput {
pub(crate) path: String,
pub(crate) analysis: SmpSaveCompanyChairmanAnalysisReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePeriodicCompanyServiceTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpPeriodicCompanyServiceTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRegionServiceTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpRegionServiceTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeInfrastructureAssetTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpInfrastructureAssetTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePk4InspectionOutput {
pub(crate) path: String,
pub(crate) inspection: Pk4InspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoTypeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CargoTypeInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeBuildingTypeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: BuildingTypeSourceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoSkinInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CargoSkinInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoEconomyInspectionOutput {
pub(crate) cargo_types_dir: String,
pub(crate) cargo_skin_pk4_path: String,
pub(crate) inspection: CargoEconomySourceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoSelectorInspectionOutput {
pub(crate) cargo_types_dir: String,
pub(crate) cargo_skin_pk4_path: String,
pub(crate) selector: CargoSelectorReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeWinInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: WinInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePk4ExtractionOutput {
pub(crate) path: String,
pub(crate) output_path: String,
pub(crate) extraction: Pk4ExtractionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCampaignExeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CampaignExeInspectionReport,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeProfileBlockExportDocument {
pub(crate) source_path: String,
pub(crate) profile_kind: String,
pub(crate) profile_family: String,
pub(crate) payload: Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeProfileBlockExportReport {
pub(crate) output_path: String,
pub(crate) profile_kind: String,
pub(crate) profile_family: String,
}

View file

@ -0,0 +1,2 @@
pub(super) mod inspect;
pub(super) mod state;

View file

@ -0,0 +1,72 @@
use rrt_fixtures::JsonDiffEntry;
use rrt_runtime::inspect::smp::save_load::{SmpLoadedSaveSlice, SmpSaveLoadSummary};
use rrt_runtime::summary::RuntimeSummary;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Serialize)]
pub(crate) struct FinanceDiffEntry {
pub(crate) path: String,
pub(crate) left: Value,
pub(crate) right: Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct FinanceDiffReport {
pub(crate) matches: bool,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<FinanceDiffEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeFixtureSummaryReport {
pub(crate) fixture_id: String,
pub(crate) command_count: usize,
pub(crate) final_summary: RuntimeSummary,
pub(crate) expected_summary_matches: bool,
pub(crate) expected_summary_mismatches: Vec<String>,
pub(crate) expected_state_fragment_matches: bool,
pub(crate) expected_state_fragment_mismatches: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeStateSummaryReport {
pub(crate) snapshot_id: String,
pub(crate) summary: RuntimeSummary,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeStateDiffReport {
pub(crate) matches: bool,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<JsonDiffEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveLoadSummaryOutput {
pub(crate) path: String,
pub(crate) summary: SmpSaveLoadSummary,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeLoadedSaveSliceOutput {
pub(crate) path: String,
pub(crate) save_slice: SmpLoadedSaveSlice,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveSliceExportOutput {
pub(crate) path: String,
pub(crate) output_path: String,
pub(crate) save_slice_id: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeOverlayImportExportOutput {
pub(crate) output_path: String,
pub(crate) import_id: String,
pub(crate) base_snapshot_path: String,
pub(crate) save_slice_path: String,
}

View file

@ -0,0 +1,472 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::bundle::inspect_smp_file;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) semantic_family: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) header_word_2_hex: String,
pub(crate) observed_entry_count: usize,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) nonzero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
pub(crate) footer_progress_word_0_hex: String,
pub(crate) footer_progress_word_1_hex: String,
pub(crate) availability_by_name: BTreeMap<String, u32>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableEntrySample {
pub(crate) index: usize,
pub(crate) offset: usize,
pub(crate) text: String,
pub(crate) availability_dword: u32,
pub(crate) availability_dword_hex: String,
pub(crate) trailer_word: u32,
pub(crate) trailer_word_hex: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableInspectionReport {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) semantic_family: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) header_word_2_hex: String,
pub(crate) observed_entry_capacity: usize,
pub(crate) observed_entry_count: usize,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) nonzero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
pub(crate) entries: Vec<RuntimeCandidateTableEntrySample>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRun {
pub(crate) prefix: String,
pub(crate) start_index: usize,
pub(crate) end_index: usize,
pub(crate) count: usize,
pub(crate) first_name: String,
pub(crate) last_name: String,
pub(crate) start_offset: usize,
pub(crate) end_offset: usize,
pub(crate) distinct_trailer_hex_words: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) common_semantic_family: Option<String>,
pub(crate) samples: Vec<RuntimeCandidateTableSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_candidate_table(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_candidate_table_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let common_semantic_family = samples
.first()
.map(|sample| sample.semantic_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.semantic_family == *family)
});
let differences = diff_candidate_table_samples(&samples)?;
let report = RuntimeCandidateTableComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
common_semantic_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_candidate_table(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = load_candidate_table_inspection_report(smp_path)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_candidate_table_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.rt3_105_save_name_table_probe.ok_or_else(|| {
format!(
"{} did not expose an RT3 1.05 candidate-availability table",
smp_path.display()
)
})?;
Ok(RuntimeCandidateTableSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
semantic_family: probe.semantic_family,
header_word_0_hex: probe.header_word_0_hex,
header_word_1_hex: probe.header_word_1_hex,
header_word_2_hex: probe.header_word_2_hex,
observed_entry_count: probe.observed_entry_count,
zero_trailer_entry_count: probe.zero_trailer_entry_count,
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
zero_trailer_entry_names: probe.zero_trailer_entry_names,
footer_progress_word_0_hex: probe.footer_progress_word_0_hex,
footer_progress_word_1_hex: probe.footer_progress_word_1_hex,
availability_by_name: probe
.entries
.into_iter()
.map(|entry| (entry.text, entry.availability_dword))
.collect(),
})
}
pub(crate) fn load_candidate_table_inspection_report(
smp_path: &Path,
) -> Result<RuntimeCandidateTableInspectionReport, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
if let Some(probe) = inspection.rt3_105_save_name_table_probe {
return Ok(RuntimeCandidateTableInspectionReport {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
semantic_family: probe.semantic_family,
header_word_0_hex: probe.header_word_0_hex,
header_word_1_hex: probe.header_word_1_hex,
header_word_2_hex: probe.header_word_2_hex,
observed_entry_capacity: probe.observed_entry_capacity,
observed_entry_count: probe.observed_entry_count,
zero_trailer_entry_count: probe.zero_trailer_entry_count,
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
zero_trailer_entry_names: probe.zero_trailer_entry_names,
entries: probe
.entries
.into_iter()
.map(|entry| RuntimeCandidateTableEntrySample {
index: entry.index,
offset: entry.offset,
text: entry.text,
availability_dword: entry.availability_dword,
availability_dword_hex: entry.availability_dword_hex,
trailer_word: entry.trailer_word,
trailer_word_hex: entry.trailer_word_hex,
})
.collect(),
});
}
let bytes = fs::read(smp_path)?;
let header_offset = 0x6a70usize;
let entries_offset = 0x6ad1usize;
let block_end_offset = 0x73c0usize;
let entry_stride = 0x22usize;
if bytes.len() < block_end_offset
|| !matches_candidate_table_header_bytes(&bytes, header_offset)
{
return Err(format!(
"{} did not expose an RT3 1.05 candidate-availability table",
smp_path.display()
)
.into());
}
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
as usize;
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
as usize;
if observed_entry_capacity < observed_entry_count {
return Err(format!(
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
smp_path.display()
)
.into());
}
let entries_end_offset = entries_offset
.checked_add(
observed_entry_count
.checked_mul(entry_stride)
.ok_or("candidate table length overflow")?,
)
.ok_or("candidate table end overflow")?;
if entries_end_offset > block_end_offset {
return Err(format!(
"{} candidate table overruns fixed block end",
smp_path.display()
)
.into());
}
let mut zero_trailer_entry_names = Vec::new();
let mut entries = Vec::new();
for index in 0..observed_entry_count {
let offset = entries_offset + index * entry_stride;
let chunk = &bytes[offset..offset + entry_stride];
let nul_index = chunk
.iter()
.position(|byte| *byte == 0)
.unwrap_or(entry_stride - 4);
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
format!(
"{} contains invalid UTF-8 in candidate table",
smp_path.display()
)
})?;
let availability_dword =
read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
format!(
"{} is missing candidate availability dword",
smp_path.display()
)
})?;
if availability_dword == 0 {
zero_trailer_entry_names.push(text.to_string());
}
entries.push(RuntimeCandidateTableEntrySample {
index,
offset,
text: text.to_string(),
availability_dword,
availability_dword_hex: format!("0x{availability_dword:08x}"),
trailer_word: availability_dword,
trailer_word_hex: format!("0x{availability_dword:08x}"),
});
}
Ok(RuntimeCandidateTableInspectionReport {
path: smp_path.display().to_string(),
profile_family: classify_candidate_table_header_profile(
smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase()),
&bytes,
),
source_kind: match smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("gmp") => "map-fixed-catalog-range",
Some("gms") => "save-fixed-catalog-range",
_ => "fixed-catalog-range",
}
.to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
),
header_word_1_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
),
header_word_2_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 8).ok_or("missing candidate header word 2")?
),
observed_entry_capacity,
observed_entry_count,
zero_trailer_entry_count: zero_trailer_entry_names.len(),
nonzero_trailer_entry_count: observed_entry_count - zero_trailer_entry_names.len(),
zero_trailer_entry_names,
entries,
})
}
pub(crate) fn diff_candidate_table_samples(
samples: &[RuntimeCandidateTableSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"semantic_family": sample.semantic_family,
"header_word_0_hex": sample.header_word_0_hex,
"header_word_1_hex": sample.header_word_1_hex,
"header_word_2_hex": sample.header_word_2_hex,
"observed_entry_count": sample.observed_entry_count,
"zero_trailer_entry_count": sample.zero_trailer_entry_count,
"nonzero_trailer_entry_count": sample.nonzero_trailer_entry_count,
"zero_trailer_entry_names": sample.zero_trailer_entry_names,
"footer_progress_word_0_hex": sample.footer_progress_word_0_hex,
"footer_progress_word_1_hex": sample.footer_progress_word_1_hex,
"availability_by_name": sample.availability_by_name,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn collect_numbered_candidate_name_runs(
entries: &[RuntimeCandidateTableEntrySample],
prefix: &str,
) -> Vec<RuntimeCandidateTableNamedRun> {
let mut numbered_entries = entries
.iter()
.filter_map(|entry| {
parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number))
})
.collect::<Vec<_>>();
numbered_entries.sort_by_key(|(entry, number)| (entry.index, *number));
let mut runs = Vec::new();
let mut cursor = 0usize;
while cursor < numbered_entries.len() {
let (first_entry, first_number) = numbered_entries[cursor];
let mut last_entry = first_entry;
let mut last_number = first_number;
let mut distinct_trailer_hex_words = BTreeSet::from([first_entry.trailer_word_hex.clone()]);
let mut next = cursor + 1;
while next < numbered_entries.len() {
let (entry, number) = numbered_entries[next];
if entry.index != last_entry.index + 1 || number != last_number + 1 {
break;
}
distinct_trailer_hex_words.insert(entry.trailer_word_hex.clone());
last_entry = entry;
last_number = number;
next += 1;
}
runs.push(RuntimeCandidateTableNamedRun {
prefix: prefix.to_string(),
start_index: first_entry.index,
end_index: last_entry.index,
count: next - cursor,
first_name: first_entry.text.clone(),
last_name: last_entry.text.clone(),
start_offset: first_entry.offset,
end_offset: last_entry.offset,
distinct_trailer_hex_words: distinct_trailer_hex_words.into_iter().collect(),
});
cursor = next;
}
runs
}
pub(crate) fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option<usize> {
let digits = text.strip_prefix(prefix)?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
digits.parse().ok()
}
pub(crate) fn matches_candidate_table_header_bytes(bytes: &[u8], header_offset: usize) -> bool {
matches!(
(
read_u32_le(bytes, header_offset + 0x08),
read_u32_le(bytes, header_offset + 0x0c),
read_u32_le(bytes, header_offset + 0x10),
read_u32_le(bytes, header_offset + 0x14),
read_u32_le(bytes, header_offset + 0x18),
read_u32_le(bytes, header_offset + 0x1c),
read_u32_le(bytes, header_offset + 0x20),
read_u32_le(bytes, header_offset + 0x24),
read_u32_le(bytes, header_offset + 0x28),
),
(
Some(0x0000332e),
Some(0x00000001),
Some(0x00000022),
Some(0x00000002),
Some(0x00000002),
Some(68),
Some(67),
Some(0x00000000),
Some(0x00000001),
)
)
}
pub(crate) fn classify_candidate_table_header_profile(
extension: Option<String>,
bytes: &[u8],
) -> String {
let word_2 = read_u32_le(bytes, 8);
let word_3 = read_u32_le(bytes, 12);
let word_5 = read_u32_le(bytes, 20);
match (extension.as_deref().unwrap_or(""), word_2, word_3, word_5) {
("gmp", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
"rt3-105-map-container-v1".to_string()
}
("gmp", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
"rt3-105-scenario-map-container-v1".to_string()
}
("gmp", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
"rt3-105-alt-map-container-v1".to_string()
}
("gms", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
"rt3-105-save-container-v1".to_string()
}
("gms", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
"rt3-105-scenario-save-container-v1".to_string()
}
("gms", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
"rt3-105-alt-save-container-v1".to_string()
}
("gmp", _, _, _) => "map-fixed-catalog-container-unknown".to_string(),
("gms", _, _, _) => "save-fixed-catalog-container-unknown".to_string(),
_ => "fixed-catalog-container-unknown".to_string(),
}
}
pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
let chunk = bytes.get(offset..offset + 4)?;
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
}
pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
let chunk = bytes.get(offset..offset + 2)?;
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
}
pub(crate) fn hex_encode(bytes: &[u8]) -> String {
let mut text = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut text, "{byte:02x}");
}
text
}

View file

@ -0,0 +1,104 @@
use std::collections::BTreeSet;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileDifferenceValue {
pub(crate) path: String,
pub(crate) value: Value,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileDifference {
pub(crate) field_path: String,
pub(crate) values: Vec<RuntimeClassicProfileDifferenceValue>,
}
pub(crate) fn collect_json_multi_differences(
path: &str,
labeled_values: &[(String, Value)],
differences: &mut Vec<RuntimeClassicProfileDifference>,
) {
if labeled_values.is_empty() {
return;
}
if labeled_values
.iter()
.all(|(_, value)| matches!(value, Value::Object(_)))
{
let mut keys = BTreeSet::new();
for (_, value) in labeled_values {
if let Value::Object(map) = value {
keys.extend(map.keys().cloned());
}
}
for key in keys {
let next_path = format!("{path}.{key}");
let nested = labeled_values
.iter()
.map(|(label, value)| {
let nested_value = match value {
Value::Object(map) => map.get(&key).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
(label.clone(), nested_value)
})
.collect::<Vec<_>>();
collect_json_multi_differences(&next_path, &nested, differences);
}
return;
}
if labeled_values
.iter()
.all(|(_, value)| matches!(value, Value::Array(_)))
{
let max_len = labeled_values
.iter()
.filter_map(|(_, value)| match value {
Value::Array(items) => Some(items.len()),
_ => None,
})
.max()
.unwrap_or(0);
for index in 0..max_len {
let next_path = format!("{path}[{index}]");
let nested = labeled_values
.iter()
.map(|(label, value)| {
let nested_value = match value {
Value::Array(items) => items.get(index).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
(label.clone(), nested_value)
})
.collect::<Vec<_>>();
collect_json_multi_differences(&next_path, &nested, differences);
}
return;
}
let first = &labeled_values[0].1;
if labeled_values
.iter()
.skip(1)
.all(|(_, value)| value == first)
{
return;
}
differences.push(RuntimeClassicProfileDifference {
field_path: path.to_string(),
values: labeled_values
.iter()
.map(|(label, value)| RuntimeClassicProfileDifferenceValue {
path: label.clone(),
value: value.clone(),
})
.collect(),
});
}

View file

@ -0,0 +1,43 @@
mod candidate_table;
mod common;
mod post_special;
mod profiles;
mod recipe_book;
mod region;
mod setup_payload;
pub(super) use candidate_table::{compare_candidate_table, inspect_candidate_table};
pub(super) use post_special::compare_post_special_conditions_scalars;
pub(super) use profiles::{compare_classic_profile, compare_rt3_105_profile};
pub(super) use recipe_book::compare_recipe_book_lines;
pub(super) use region::compare_region_fixed_row_runs;
pub(super) use setup_payload::{compare_setup_launch_payload, compare_setup_payload_core};
#[cfg(test)]
pub(crate) use candidate_table::{
RuntimeCandidateTableEntrySample, RuntimeCandidateTableSample, diff_candidate_table_samples,
};
pub(crate) use candidate_table::{
RuntimeCandidateTableNamedRun, classify_candidate_table_header_profile,
collect_numbered_candidate_name_runs, load_candidate_table_inspection_report,
matches_candidate_table_header_bytes, read_u32_le,
};
#[cfg(test)]
pub(crate) use profiles::{
RuntimeClassicProfileSample, RuntimeRt3105ProfileSample, diff_classic_profile_samples,
diff_rt3_105_profile_samples,
};
pub(crate) use recipe_book::{
RuntimeRecipeBookLineFieldSummary, build_recipe_line_field_summaries,
intersect_nonzero_recipe_line_paths, load_recipe_book_line_sample,
};
#[cfg(test)]
pub(crate) use recipe_book::{
RuntimeRecipeBookLineSample, diff_recipe_book_line_content_samples,
diff_recipe_book_line_samples,
};
#[cfg(test)]
pub(crate) use setup_payload::{
RuntimeSetupLaunchPayloadSample, RuntimeSetupPayloadCoreSample,
diff_setup_launch_payload_samples, diff_setup_payload_core_samples,
};

View file

@ -0,0 +1,94 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offset_hexes: Vec<String>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimePostSpecialConditionsScalarSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_post_special_conditions_scalars(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_post_special_conditions_scalar_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_post_special_conditions_scalar_samples(&samples)?;
let report = RuntimePostSpecialConditionsScalarComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_post_special_conditions_scalar_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsScalarSample, Box<dyn std::error::Error>> {
let sample =
crate::app::runtime_scan::post_special::load_post_special_conditions_scalar_scan_sample(
smp_path,
)?;
Ok(RuntimePostSpecialConditionsScalarSample {
path: sample.path,
profile_family: sample.profile_family,
source_kind: sample.source_kind,
nonzero_relative_offset_hexes: sample
.nonzero_relative_offsets
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
values_by_relative_offset_hex: sample.values_by_relative_offset_hex,
})
}
pub(crate) fn diff_post_special_conditions_scalar_samples(
samples: &[RuntimePostSpecialConditionsScalarSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"nonzero_relative_offset_hexes": sample.nonzero_relative_offset_hexes,
"values_by_relative_offset_hex": sample.values_by_relative_offset_hex,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,195 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::{
bundle::inspect_smp_file,
profiles::{SmpClassicPackedProfileBlock, SmpRt3105PackedProfileBlock},
};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) progress_32dc_offset: usize,
pub(crate) progress_3714_offset: usize,
pub(crate) progress_3715_offset: usize,
pub(crate) packed_profile_offset: usize,
pub(crate) packed_profile_len: usize,
pub(crate) packed_profile_block: SmpClassicPackedProfileBlock,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeClassicProfileComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeClassicProfileSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeRt3105ProfileSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) packed_profile_offset: usize,
pub(crate) packed_profile_len: usize,
pub(crate) packed_profile_block: SmpRt3105PackedProfileBlock,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRt3105ProfileComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeRt3105ProfileSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_classic_profile(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_classic_profile_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_classic_profile_samples(&samples)?;
let report = RuntimeClassicProfileComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn compare_rt3_105_profile(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_rt3_105_profile_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_rt3_105_profile_samples(&samples)?;
let report = RuntimeRt3105ProfileComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_classic_profile_sample(
smp_path: &Path,
) -> Result<RuntimeClassicProfileSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.classic_rehydrate_profile_probe.ok_or_else(|| {
format!(
"{} did not expose a classic rehydrate packed-profile block",
smp_path.display()
)
})?;
Ok(RuntimeClassicProfileSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
progress_32dc_offset: probe.progress_32dc_offset,
progress_3714_offset: probe.progress_3714_offset,
progress_3715_offset: probe.progress_3715_offset,
packed_profile_offset: probe.packed_profile_offset,
packed_profile_len: probe.packed_profile_len,
packed_profile_block: probe.packed_profile_block,
})
}
pub(crate) fn load_rt3_105_profile_sample(
smp_path: &Path,
) -> Result<RuntimeRt3105ProfileSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.rt3_105_packed_profile_probe.ok_or_else(|| {
format!(
"{} did not expose an RT3 1.05 packed-profile block",
smp_path.display()
)
})?;
Ok(RuntimeRt3105ProfileSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
packed_profile_offset: probe.packed_profile_offset,
packed_profile_len: probe.packed_profile_len,
packed_profile_block: probe.packed_profile_block,
})
}
pub(crate) fn diff_classic_profile_samples(
samples: &[RuntimeClassicProfileSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"progress_32dc_offset": sample.progress_32dc_offset,
"progress_3714_offset": sample.progress_3714_offset,
"progress_3715_offset": sample.progress_3715_offset,
"packed_profile_offset": sample.packed_profile_offset,
"packed_profile_len": sample.packed_profile_len,
"packed_profile_block": sample.packed_profile_block,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_rt3_105_profile_samples(
samples: &[RuntimeRt3105ProfileSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"packed_profile_offset": sample.packed_profile_offset,
"packed_profile_len": sample.packed_profile_len,
"packed_profile_block": sample.packed_profile_block,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,250 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::bundle::inspect_smp_file;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeRecipeBookLineSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) book_count: usize,
pub(crate) book_stride_hex: String,
pub(crate) line_count: usize,
pub(crate) line_stride_hex: String,
pub(crate) book_head_kind_by_index: BTreeMap<String, String>,
pub(crate) book_line_area_kind_by_index: BTreeMap<String, String>,
pub(crate) max_annual_production_word_hex_by_book: BTreeMap<String, String>,
pub(crate) line_kind_by_path: BTreeMap<String, String>,
pub(crate) mode_word_hex_by_path: BTreeMap<String, String>,
pub(crate) annual_amount_word_hex_by_path: BTreeMap<String, String>,
pub(crate) supplied_cargo_token_word_hex_by_path: BTreeMap<String, String>,
pub(crate) demanded_cargo_token_word_hex_by_path: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) content_matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeRecipeBookLineSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
pub(crate) content_difference_count: usize,
pub(crate) content_differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineFieldSummary {
pub(crate) line_path: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
pub(crate) fn compare_recipe_book_lines(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_recipe_book_line_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_recipe_book_line_samples(&samples)?;
let content_differences = diff_recipe_book_line_content_samples(&samples)?;
let report = RuntimeRecipeBookLineComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
content_matches: content_differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
content_difference_count: content_differences.len(),
content_differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_recipe_book_line_sample(
smp_path: &Path,
) -> Result<RuntimeRecipeBookLineSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.recipe_book_summary_probe.ok_or_else(|| {
format!(
"{} did not expose a grounded recipe-book summary block",
smp_path.display()
)
})?;
let mut book_head_kind_by_index = BTreeMap::new();
let mut book_line_area_kind_by_index = BTreeMap::new();
let mut max_annual_production_word_hex_by_book = BTreeMap::new();
let mut line_kind_by_path = BTreeMap::new();
let mut mode_word_hex_by_path = BTreeMap::new();
let mut annual_amount_word_hex_by_path = BTreeMap::new();
let mut supplied_cargo_token_word_hex_by_path = BTreeMap::new();
let mut demanded_cargo_token_word_hex_by_path = BTreeMap::new();
for book in &probe.books {
let book_key = format!("book{:02}", book.book_index);
book_head_kind_by_index.insert(book_key.clone(), book.head_kind.clone());
book_line_area_kind_by_index.insert(book_key.clone(), book.line_area_kind.clone());
max_annual_production_word_hex_by_book.insert(
book_key.clone(),
book.max_annual_production_word_hex.clone(),
);
for line in &book.lines {
let line_key = format!("{book_key}.line{:02}", line.line_index);
line_kind_by_path.insert(line_key.clone(), line.line_kind.clone());
mode_word_hex_by_path.insert(line_key.clone(), line.mode_word_hex.clone());
annual_amount_word_hex_by_path
.insert(line_key.clone(), line.annual_amount_word_hex.clone());
supplied_cargo_token_word_hex_by_path
.insert(line_key.clone(), line.supplied_cargo_token_word_hex.clone());
demanded_cargo_token_word_hex_by_path
.insert(line_key.clone(), line.demanded_cargo_token_word_hex.clone());
}
}
Ok(RuntimeRecipeBookLineSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
book_count: probe.book_count,
book_stride_hex: probe.book_stride_hex,
line_count: probe.line_count,
line_stride_hex: probe.line_stride_hex,
book_head_kind_by_index,
book_line_area_kind_by_index,
max_annual_production_word_hex_by_book,
line_kind_by_path,
mode_word_hex_by_path,
annual_amount_word_hex_by_path,
supplied_cargo_token_word_hex_by_path,
demanded_cargo_token_word_hex_by_path,
})
}
pub(crate) fn diff_recipe_book_line_samples(
samples: &[RuntimeRecipeBookLineSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"book_count": sample.book_count,
"book_stride_hex": sample.book_stride_hex,
"line_count": sample.line_count,
"line_stride_hex": sample.line_stride_hex,
"book_head_kind_by_index": sample.book_head_kind_by_index,
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
"line_kind_by_path": sample.line_kind_by_path,
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_recipe_book_line_content_samples(
samples: &[RuntimeRecipeBookLineSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"book_count": sample.book_count,
"book_stride_hex": sample.book_stride_hex,
"line_count": sample.line_count,
"line_stride_hex": sample.line_stride_hex,
"book_head_kind_by_index": sample.book_head_kind_by_index,
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
"line_kind_by_path": sample.line_kind_by_path,
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn intersect_nonzero_recipe_line_paths<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> Vec<String> {
let mut maps = maps.peekable();
if maps.peek().is_none() {
return Vec::new();
}
let mut stable = maps
.next()
.map(|map| map.keys().cloned().collect::<BTreeSet<_>>())
.unwrap_or_default();
for map in maps {
let current = map.keys().cloned().collect::<BTreeSet<_>>();
stable = stable.intersection(&current).cloned().collect();
}
stable.into_iter().collect()
}
pub(crate) fn build_recipe_line_field_summaries<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> Vec<RuntimeRecipeBookLineFieldSummary> {
let mut value_sets = BTreeMap::<String, BTreeSet<String>>::new();
let mut counts = BTreeMap::<String, usize>::new();
for map in maps {
for (line_path, value_hex) in map {
*counts.entry(line_path.clone()).or_default() += 1;
value_sets
.entry(line_path.clone())
.or_default()
.insert(value_hex.clone());
}
}
counts
.into_iter()
.map(
|(line_path, file_count_present)| RuntimeRecipeBookLineFieldSummary {
line_path: line_path.clone(),
file_count_present,
distinct_value_count: value_sets.get(&line_path).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: value_sets
.get(&line_path)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}

View file

@ -0,0 +1,33 @@
use std::path::Path;
use rrt_runtime::inspect::smp::{
regions::{
SmpSaveRegionFixedRowRunComparisonReport, compare_save_region_fixed_row_run_candidates,
},
world::inspect_save_company_and_chairman_analysis_file,
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRegionFixedRowRunComparisonOutput {
pub(crate) left_path: String,
pub(crate) right_path: String,
pub(crate) comparison: SmpSaveRegionFixedRowRunComparisonReport,
}
pub(crate) fn compare_region_fixed_row_runs(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = inspect_save_company_and_chairman_analysis_file(left_path)?;
let right = inspect_save_company_and_chairman_analysis_file(right_path)?;
let comparison = compare_save_region_fixed_row_run_candidates(&left, &right)
.ok_or("save inspection did not expose grounded region fixed-row candidate probes")?;
let report = RuntimeRegionFixedRowRunComparisonOutput {
left_path: left_path.display().to_string(),
right_path: right_path.display().to_string(),
comparison,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,345 @@
use super::candidate_table::{
classify_candidate_table_header_profile, hex_encode, read_u16_le, read_u32_le,
};
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::campaign::{CAMPAIGN_SCENARIO_COUNT, OBSERVED_CAMPAIGN_SCENARIO_NAMES};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreSample {
pub(crate) path: String,
pub(crate) file_extension: String,
pub(crate) inferred_profile_family: String,
pub(crate) payload_word_0x14: u16,
pub(crate) payload_word_0x14_hex: String,
pub(crate) payload_byte_0x20: u8,
pub(crate) payload_byte_0x20_hex: String,
pub(crate) marker_bytes_0x2c9_0x2d0_hex: String,
pub(crate) row_category_byte_0x31a: u8,
pub(crate) row_category_byte_0x31a_hex: String,
pub(crate) row_visibility_byte_0x31b: u8,
pub(crate) row_visibility_byte_0x31b_hex: String,
pub(crate) row_visibility_byte_0x31c: u8,
pub(crate) row_visibility_byte_0x31c_hex: String,
pub(crate) row_count_word_0x3ae: u16,
pub(crate) row_count_word_0x3ae_hex: String,
pub(crate) payload_word_0x3b2: u16,
pub(crate) payload_word_0x3b2_hex: String,
pub(crate) payload_word_0x3ba: u16,
pub(crate) payload_word_0x3ba_hex: String,
pub(crate) candidate_header_word_0_hex: Option<String>,
pub(crate) candidate_header_word_1_hex: Option<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) samples: Vec<RuntimeSetupPayloadCoreSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadSample {
pub(crate) path: String,
pub(crate) file_extension: String,
pub(crate) inferred_profile_family: String,
pub(crate) launch_flag_byte_0x22: u8,
pub(crate) launch_flag_byte_0x22_hex: String,
pub(crate) campaign_progress_in_known_range: bool,
pub(crate) campaign_progress_scenario_name: Option<String>,
pub(crate) campaign_progress_page_index: Option<usize>,
pub(crate) launch_selector_byte_0x33: u8,
pub(crate) launch_selector_byte_0x33_hex: String,
pub(crate) launch_token_block_0x23_0x32_hex: String,
pub(crate) campaign_selector_values: BTreeMap<String, u8>,
pub(crate) nonzero_campaign_selector_values: BTreeMap<String, u8>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) samples: Vec<RuntimeSetupLaunchPayloadSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_setup_payload_core(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_setup_payload_core_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let differences = diff_setup_payload_core_samples(&samples)?;
let report = RuntimeSetupPayloadCoreComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn compare_setup_launch_payload(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_setup_launch_payload_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let differences = diff_setup_launch_payload_samples(&samples)?;
let report = RuntimeSetupLaunchPayloadComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_setup_payload_core_sample(
smp_path: &Path,
) -> Result<RuntimeSetupPayloadCoreSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let inferred_profile_family =
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let candidate_header_word_0 = read_u32_le(&bytes, 0x6a70);
let candidate_header_word_1 = read_u32_le(&bytes, 0x6a74);
Ok(RuntimeSetupPayloadCoreSample {
path: smp_path.display().to_string(),
file_extension: extension,
inferred_profile_family,
payload_word_0x14: read_u16_le(&bytes, 0x14)
.ok_or_else(|| format!("{} missing setup payload word +0x14", smp_path.display()))?,
payload_word_0x14_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x14).ok_or_else(|| format!(
"{} missing setup payload word +0x14",
smp_path.display()
))?
),
payload_byte_0x20: bytes
.get(0x20)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x20", smp_path.display()))?,
payload_byte_0x20_hex: format!(
"0x{:02x}",
bytes.get(0x20).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x20",
smp_path.display()
))?
),
marker_bytes_0x2c9_0x2d0_hex: bytes
.get(0x2c9..0x2d1)
.map(hex_encode)
.ok_or_else(|| format!("{} missing setup payload marker bytes", smp_path.display()))?,
row_category_byte_0x31a: bytes
.get(0x31a)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31a", smp_path.display()))?,
row_category_byte_0x31a_hex: format!(
"0x{:02x}",
bytes.get(0x31a).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31a",
smp_path.display()
))?
),
row_visibility_byte_0x31b: bytes
.get(0x31b)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31b", smp_path.display()))?,
row_visibility_byte_0x31b_hex: format!(
"0x{:02x}",
bytes.get(0x31b).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31b",
smp_path.display()
))?
),
row_visibility_byte_0x31c: bytes
.get(0x31c)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31c", smp_path.display()))?,
row_visibility_byte_0x31c_hex: format!(
"0x{:02x}",
bytes.get(0x31c).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31c",
smp_path.display()
))?
),
row_count_word_0x3ae: read_u16_le(&bytes, 0x3ae)
.ok_or_else(|| format!("{} missing setup payload word +0x3ae", smp_path.display()))?,
row_count_word_0x3ae_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3ae).ok_or_else(|| format!(
"{} missing setup payload word +0x3ae",
smp_path.display()
))?
),
payload_word_0x3b2: read_u16_le(&bytes, 0x3b2)
.ok_or_else(|| format!("{} missing setup payload word +0x3b2", smp_path.display()))?,
payload_word_0x3b2_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3b2).ok_or_else(|| format!(
"{} missing setup payload word +0x3b2",
smp_path.display()
))?
),
payload_word_0x3ba: read_u16_le(&bytes, 0x3ba)
.ok_or_else(|| format!("{} missing setup payload word +0x3ba", smp_path.display()))?,
payload_word_0x3ba_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3ba).ok_or_else(|| format!(
"{} missing setup payload word +0x3ba",
smp_path.display()
))?
),
candidate_header_word_0_hex: candidate_header_word_0.map(|value| format!("0x{value:08x}")),
candidate_header_word_1_hex: candidate_header_word_1.map(|value| format!("0x{value:08x}")),
})
}
pub(crate) fn load_setup_launch_payload_sample(
smp_path: &Path,
) -> Result<RuntimeSetupLaunchPayloadSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let inferred_profile_family =
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let launch_flag_byte_0x22 = bytes
.get(0x22)
.copied()
.ok_or_else(|| format!("{} missing setup launch byte +0x22", smp_path.display()))?;
let launch_selector_byte_0x33 = bytes
.get(0x33)
.copied()
.ok_or_else(|| format!("{} missing setup launch byte +0x33", smp_path.display()))?;
let token_block = bytes
.get(0x23..0x33)
.ok_or_else(|| format!("{} missing setup launch token block", smp_path.display()))?;
let campaign_progress_in_known_range =
(launch_flag_byte_0x22 as usize) < CAMPAIGN_SCENARIO_COUNT;
let campaign_progress_scenario_name = campaign_progress_in_known_range
.then(|| OBSERVED_CAMPAIGN_SCENARIO_NAMES[launch_flag_byte_0x22 as usize].to_string());
let campaign_progress_page_index = match launch_flag_byte_0x22 {
0..=4 => Some(1),
5..=9 => Some(2),
10..=12 => Some(3),
13..=15 => Some(4),
_ => None,
};
let campaign_selector_values = OBSERVED_CAMPAIGN_SCENARIO_NAMES
.iter()
.enumerate()
.map(|(index, name)| (name.to_string(), token_block[index]))
.collect::<BTreeMap<_, _>>();
let nonzero_campaign_selector_values = campaign_selector_values
.iter()
.filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value)))
.collect::<BTreeMap<_, _>>();
Ok(RuntimeSetupLaunchPayloadSample {
path: smp_path.display().to_string(),
file_extension: extension,
inferred_profile_family,
launch_flag_byte_0x22,
launch_flag_byte_0x22_hex: format!("0x{launch_flag_byte_0x22:02x}"),
campaign_progress_in_known_range,
campaign_progress_scenario_name,
campaign_progress_page_index,
launch_selector_byte_0x33,
launch_selector_byte_0x33_hex: format!("0x{launch_selector_byte_0x33:02x}"),
launch_token_block_0x23_0x32_hex: hex_encode(token_block),
campaign_selector_values,
nonzero_campaign_selector_values,
})
}
pub(crate) fn diff_setup_payload_core_samples(
samples: &[RuntimeSetupPayloadCoreSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"file_extension": sample.file_extension,
"inferred_profile_family": sample.inferred_profile_family,
"payload_word_0x14": sample.payload_word_0x14,
"payload_word_0x14_hex": sample.payload_word_0x14_hex,
"payload_byte_0x20": sample.payload_byte_0x20,
"payload_byte_0x20_hex": sample.payload_byte_0x20_hex,
"marker_bytes_0x2c9_0x2d0_hex": sample.marker_bytes_0x2c9_0x2d0_hex,
"row_category_byte_0x31a": sample.row_category_byte_0x31a,
"row_category_byte_0x31a_hex": sample.row_category_byte_0x31a_hex,
"row_visibility_byte_0x31b": sample.row_visibility_byte_0x31b,
"row_visibility_byte_0x31b_hex": sample.row_visibility_byte_0x31b_hex,
"row_visibility_byte_0x31c": sample.row_visibility_byte_0x31c,
"row_visibility_byte_0x31c_hex": sample.row_visibility_byte_0x31c_hex,
"row_count_word_0x3ae": sample.row_count_word_0x3ae,
"row_count_word_0x3ae_hex": sample.row_count_word_0x3ae_hex,
"payload_word_0x3b2": sample.payload_word_0x3b2,
"payload_word_0x3b2_hex": sample.payload_word_0x3b2_hex,
"payload_word_0x3ba": sample.payload_word_0x3ba,
"payload_word_0x3ba_hex": sample.payload_word_0x3ba_hex,
"candidate_header_word_0_hex": sample.candidate_header_word_0_hex,
"candidate_header_word_1_hex": sample.candidate_header_word_1_hex,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_setup_launch_payload_samples(
samples: &[RuntimeSetupLaunchPayloadSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"file_extension": sample.file_extension,
"inferred_profile_family": sample.inferred_profile_family,
"launch_flag_byte_0x22": sample.launch_flag_byte_0x22,
"launch_flag_byte_0x22_hex": sample.launch_flag_byte_0x22_hex,
"campaign_progress_in_known_range": sample.campaign_progress_in_known_range,
"campaign_progress_scenario_name": sample.campaign_progress_scenario_name,
"campaign_progress_page_index": sample.campaign_progress_page_index,
"launch_selector_byte_0x33": sample.launch_selector_byte_0x33,
"launch_selector_byte_0x33_hex": sample.launch_selector_byte_0x33_hex,
"launch_token_block_0x23_0x32_hex": sample.launch_token_block_0x23_0x32_hex,
"campaign_selector_values": sample.campaign_selector_values,
"nonzero_campaign_selector_values": sample.nonzero_campaign_selector_values,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,115 @@
use std::path::Path;
use crate::app::helpers::state_io::print_runtime_validation_report;
use crate::app::reports::state::{RuntimeFixtureSummaryReport, RuntimeStateSummaryReport};
use rrt_fixtures::{
normalize_runtime_state,
summary::compare_expected_state_fragment,
validation::{load_fixture_document, validate_fixture_document},
};
use rrt_runtime::{
engine::execute_step_command,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
},
summary::RuntimeSummary,
};
pub(crate) fn validate_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let report = validate_fixture_document(&fixture);
print_runtime_validation_report(&report)?;
if !report.valid {
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
Ok(())
}
pub(crate) fn summarize_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let validation_report = validate_fixture_document(&fixture);
if !validation_report.valid {
print_runtime_validation_report(&validation_report)?;
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
let mut state = fixture.state.clone();
for command in &fixture.commands {
execute_step_command(&mut state, command)?;
}
let final_summary = RuntimeSummary::from_state(&state);
let expected_summary_mismatches = fixture.expected_summary.compare(&final_summary);
let expected_state_fragment_mismatches = match &fixture.expected_state_fragment {
Some(expected_fragment) => {
let normalized_state = normalize_runtime_state(&state)?;
compare_expected_state_fragment(expected_fragment, &normalized_state)
}
None => Vec::new(),
};
let report = RuntimeFixtureSummaryReport {
fixture_id: fixture.fixture_id,
command_count: fixture.commands.len(),
expected_summary_matches: expected_summary_mismatches.is_empty(),
expected_summary_mismatches: expected_summary_mismatches.clone(),
expected_state_fragment_matches: expected_state_fragment_mismatches.is_empty(),
expected_state_fragment_mismatches: expected_state_fragment_mismatches.clone(),
final_summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
if !expected_summary_mismatches.is_empty() || !expected_state_fragment_mismatches.is_empty() {
let mut mismatch_messages = expected_summary_mismatches;
mismatch_messages.extend(expected_state_fragment_mismatches);
return Err(format!(
"fixture summary mismatched expected output: {}",
mismatch_messages.join("; ")
)
.into());
}
Ok(())
}
pub(crate) fn export_fixture_state(
fixture_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let validation_report = validate_fixture_document(&fixture);
if !validation_report.valid {
print_runtime_validation_report(&validation_report)?;
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
let mut state = fixture.state.clone();
for command in &fixture.commands {
execute_step_command(&mut state, command)?;
}
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-final-state", fixture.fixture_id),
source: RuntimeSnapshotSource {
source_fixture_id: Some(fixture.fixture_id.clone()),
description: Some(format!(
"Exported final runtime state for fixture {}",
fixture.fixture_id
)),
},
state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let summary = snapshot.summary();
println!(
"{}",
serde_json::to_string_pretty(&RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id,
summary,
})?
);
Ok(())
}

View file

@ -0,0 +1,9 @@
mod fixtures;
mod save_import;
mod save_load;
mod state;
pub(crate) use fixtures::{export_fixture_state, summarize_fixture, validate_fixture};
pub(crate) use save_import::{export_overlay_import, export_save_slice, snapshot_save_state};
pub(crate) use save_load::{load_save_slice, summarize_save_load};
pub(crate) use state::{diff_state, snapshot_state, summarize_state};

View file

@ -0,0 +1,70 @@
use std::path::Path;
use crate::app::helpers::state_io::{
export_runtime_overlay_import_document, export_runtime_save_slice_document,
};
use crate::app::reports::state::RuntimeStateSummaryReport;
use rrt_runtime::{
documents::build_runtime_state_input_from_save_slice,
inspect::smp::save_load::load_save_slice_file,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
},
};
pub(crate) fn snapshot_save_state(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let save_slice = load_save_slice_file(smp_path)?;
let input = build_runtime_state_input_from_save_slice(
&save_slice,
smp_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("save-state"),
Some(format!(
"Projected partial runtime state from save {}",
smp_path.display()
)),
)
.map_err(|err| format!("failed to project save slice: {err}"))?;
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-snapshot", input.input_id),
source: RuntimeSnapshotSource {
source_fixture_id: None,
description: input.description,
},
state: input.state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id.clone(),
summary: snapshot.summary(),
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_save_slice(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let save_slice = load_save_slice_file(smp_path)?;
let report = export_runtime_save_slice_document(smp_path, output_path, save_slice)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_overlay_import(
snapshot_path: &Path,
save_slice_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report =
export_runtime_overlay_import_document(snapshot_path, save_slice_path, output_path)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,29 @@
use std::path::Path;
use crate::app::reports::state::{RuntimeLoadedSaveSliceOutput, RuntimeSaveLoadSummaryOutput};
use rrt_runtime::inspect::smp::{bundle::inspect_smp_file, save_load::load_save_slice_file};
pub(crate) fn summarize_save_load(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let summary = inspection.save_load_summary.ok_or_else(|| {
format!(
"{} did not expose a recognizable save-load summary",
smp_path.display()
)
})?;
let report = RuntimeSaveLoadSummaryOutput {
path: smp_path.display().to_string(),
summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_save_slice(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeLoadedSaveSliceOutput {
path: smp_path.display().to_string(),
save_slice: load_save_slice_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,81 @@
use std::path::Path;
use crate::app::helpers::state_io::load_normalized_runtime_state;
use crate::app::reports::state::{RuntimeStateDiffReport, RuntimeStateSummaryReport};
use rrt_fixtures::diff_json_values;
use rrt_runtime::{
documents::load_runtime_state_input,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
load_runtime_snapshot_document, save_runtime_snapshot_document,
validate_runtime_snapshot_document,
},
summary::RuntimeSummary,
};
pub(crate) fn summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Ok(snapshot) = load_runtime_snapshot_document(snapshot_path) {
validate_runtime_snapshot_document(&snapshot)
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id.clone(),
summary: snapshot.summary(),
};
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(());
}
let input = load_runtime_state_input(snapshot_path)?;
let report = RuntimeStateSummaryReport {
snapshot_id: input.input_id,
summary: RuntimeSummary::from_state(&input.state),
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn diff_state(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = load_normalized_runtime_state(left_path)?;
let right = load_normalized_runtime_state(right_path)?;
let differences = diff_json_values(&left, &right);
let report = RuntimeStateDiffReport {
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn snapshot_state(
input_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let input = load_runtime_state_input(input_path)?;
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-snapshot", input.input_id),
source: RuntimeSnapshotSource {
source_fixture_id: None,
description: Some(match input.description {
Some(description) => format!(
"Runtime snapshot from {} ({description})",
input_path.display()
),
None => format!("Runtime snapshot from {}", input_path.display()),
}),
},
state: input.state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let summary = snapshot.summary();
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id,
summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,175 @@
use std::fs;
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,
};
use rrt_runtime::inspect::{
building::inspect_building_types_dir_with_bindings,
campaign::inspect_campaign_exe_file,
cargo::{
inspect_cargo_economy_sources_with_bindings, inspect_cargo_skin_pk4,
inspect_cargo_types_dir,
},
pk4::{extract_pk4_entry_file, inspect_pk4_file},
smp::bundle::inspect_smp_file,
win::inspect_win_file,
};
pub(crate) fn inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePk4InspectionOutput {
path: pk4_path.display().to_string(),
inspection: inspect_pk4_file(pk4_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn 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(())
}
pub(crate) fn inspect_building_type_sources(
building_types_dir: &Path,
bindings_path: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeBuildingTypeInspectionOutput {
path: building_types_dir.display().to_string(),
inspection: inspect_building_types_dir_with_bindings(building_types_dir, bindings_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn 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(())
}
pub(crate) fn inspect_cargo_economy_sources(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
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_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_production_selector(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
let inspection = inspect_cargo_economy_sources_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?;
let selector = inspection
.production_selector
.ok_or("named cargo production selector is not available in the checked-in bindings")?;
let report = RuntimeCargoSelectorInspectionOutput {
cargo_types_dir: cargo_types_dir.display().to_string(),
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
selector,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_price_selector(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
let inspection = inspect_cargo_economy_sources_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?;
let report = RuntimeCargoSelectorInspectionOutput {
cargo_types_dir: cargo_types_dir.display().to_string(),
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
selector: inspection.price_selector,
};
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(),
inspection: inspect_win_file(win_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn extract_pk4_entry(
pk4_path: &Path,
entry_name: &str,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePk4ExtractionOutput {
path: pk4_path.display().to_string(),
output_path: output_path.display().to_string(),
extraction: extract_pk4_entry_file(pk4_path, entry_name, output_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_campaign_exe(exe_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCampaignExeInspectionOutput {
path: exe_path.display().to_string(),
inspection: inspect_campaign_exe_file(exe_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_profile_block(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let document = build_profile_block_export_document(smp_path, &inspection)?;
let bytes = serde_json::to_vec_pretty(&document)?;
fs::write(output_path, bytes)?;
let report = RuntimeProfileBlockExportReport {
output_path: output_path.display().to_string(),
profile_kind: document.profile_kind,
profile_family: document.profile_family,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,128 @@
use std::fs;
use std::path::Path;
use crate::app::helpers::inspect::build_runtime_compact_event_dispatch_cluster_report;
use crate::app::reports::inspect::{
RuntimeCompactEventDispatchClusterCountsOutput, RuntimeCompactEventDispatchClusterCountsReport,
RuntimeCompactEventDispatchClusterOutput, RuntimeMapTitleHintDirectoryOutput,
RuntimeMapTitleHintDirectoryReport, RuntimeMapTitleHintMapEntry,
};
use rrt_runtime::inspect::smp::map_title::inspect_map_title_hint_file;
pub(crate) fn inspect_map_title_hints(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut maps = Vec::new();
let mut maps_scanned = 0usize;
let mut maps_with_probe = 0usize;
let mut maps_with_grounded_title_hits = 0usize;
let mut maps_with_adjacent_title_pairs = 0usize;
let mut maps_with_same_stem_adjacent_pairs = 0usize;
let mut paths = fs::read_dir(root_path)?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("gmp"))
})
.collect::<Vec<_>>();
paths.sort();
for path in paths {
maps_scanned += 1;
if let Some(probe) = inspect_map_title_hint_file(&path)? {
maps_with_probe += 1;
if !probe.grounded_title_hits.is_empty() {
maps_with_grounded_title_hits += 1;
}
if !probe.adjacent_reference_title_pairs.is_empty() {
maps_with_adjacent_title_pairs += 1;
}
if probe.strongest_same_stem_pair.is_some() {
maps_with_same_stem_adjacent_pairs += 1;
}
maps.push(RuntimeMapTitleHintMapEntry {
path: path.display().to_string(),
probe,
});
}
}
let output = RuntimeMapTitleHintDirectoryOutput {
root_path: root_path.display().to_string(),
report: RuntimeMapTitleHintDirectoryReport {
maps_scanned,
maps_with_probe,
maps_with_grounded_title_hits,
maps_with_adjacent_title_pairs,
maps_with_same_stem_adjacent_pairs,
maps,
},
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
pub(crate) fn inspect_compact_event_dispatch_cluster(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
let output = RuntimeCompactEventDispatchClusterOutput {
root_path: root_path.display().to_string(),
report,
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
pub(crate) fn inspect_compact_event_dispatch_cluster_counts(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
let output = RuntimeCompactEventDispatchClusterCountsOutput {
root_path: root_path.display().to_string(),
report: RuntimeCompactEventDispatchClusterCountsReport {
maps_scanned: report.maps_scanned,
maps_with_event_runtime_collection: report.maps_with_event_runtime_collection,
maps_with_dispatch_strip_records: report.maps_with_dispatch_strip_records,
dispatch_strip_record_count: report.dispatch_strip_record_count,
dispatch_strip_records_with_trigger_kind: report
.dispatch_strip_records_with_trigger_kind,
dispatch_strip_records_missing_trigger_kind: report
.dispatch_strip_records_missing_trigger_kind,
dispatch_strip_payload_families: report.dispatch_strip_payload_families,
dispatch_descriptor_occurrence_counts: report.dispatch_descriptor_occurrence_counts,
dispatch_descriptor_map_counts: report.dispatch_descriptor_map_counts,
unknown_descriptor_ids: report.unknown_descriptor_ids,
unknown_descriptor_special_condition_label_matches: report
.unknown_descriptor_special_condition_label_matches,
add_building_dispatch_record_count: report.add_building_dispatch_record_count,
add_building_dispatch_records_with_trigger_kind: report
.add_building_dispatch_records_with_trigger_kind,
add_building_dispatch_records_missing_trigger_kind: report
.add_building_dispatch_records_missing_trigger_kind,
add_building_descriptor_occurrence_counts: report
.add_building_descriptor_occurrence_counts,
add_building_descriptor_map_counts: report.add_building_descriptor_map_counts,
add_building_row_shape_occurrence_counts: report
.add_building_row_shape_occurrence_counts,
add_building_row_shape_map_counts: report.add_building_row_shape_map_counts,
add_building_signature_family_occurrence_counts: report
.add_building_signature_family_occurrence_counts,
add_building_signature_family_map_counts: report
.add_building_signature_family_map_counts,
add_building_condition_tuple_occurrence_counts: report
.add_building_condition_tuple_occurrence_counts,
add_building_condition_tuple_map_counts: report.add_building_condition_tuple_map_counts,
add_building_signature_condition_cluster_occurrence_counts: report
.add_building_signature_condition_cluster_occurrence_counts,
add_building_signature_condition_cluster_map_counts: report
.add_building_signature_condition_cluster_map_counts,
add_building_signature_condition_cluster_descriptor_keys: report
.add_building_signature_condition_cluster_descriptor_keys,
add_building_signature_condition_cluster_non_add_building_descriptor_keys: report
.add_building_signature_condition_cluster_non_add_building_descriptor_keys,
},
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}

View file

@ -0,0 +1,19 @@
mod assets;
mod maps;
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,
};
pub(crate) use maps::{
inspect_compact_event_dispatch_cluster, inspect_compact_event_dispatch_cluster_counts,
inspect_map_title_hints,
};
pub(crate) use smp::{
inspect_infrastructure_asset_trace, inspect_periodic_company_service_trace,
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,
};

View file

@ -0,0 +1,117 @@
use std::path::Path;
use crate::app::reports::inspect::{
RuntimeInfrastructureAssetTraceOutput, RuntimePeriodicCompanyServiceTraceOutput,
RuntimeRegionServiceTraceOutput, RuntimeSaveCompanyChairmanAnalysisOutput,
RuntimeSmpInspectionOutput,
};
use rrt_runtime::inspect::smp::{
bundle::{inspect_smp_file, inspect_unclassified_save_collection_headers_file},
services::{
inspect_save_infrastructure_asset_trace_file,
inspect_save_periodic_company_service_trace_file, inspect_save_region_service_trace_file,
},
structures::{
inspect_save_placed_structure_dynamic_side_buffer_file,
inspect_save_region_queued_notice_records_file,
},
world::inspect_save_company_and_chairman_analysis_file,
};
pub(crate) fn inspect_smp(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeSmpInspectionOutput {
path: smp_path.display().to_string(),
inspection: inspect_smp_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_company_chairman(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeSaveCompanyChairmanAnalysisOutput {
path: smp_path.display().to_string(),
analysis: inspect_save_company_and_chairman_analysis_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_placed_structure_triplets(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let analysis = inspect_save_company_and_chairman_analysis_file(smp_path)?;
println!(
"{}",
serde_json::to_string_pretty(&analysis.placed_structure_record_triplets)?
);
Ok(())
}
pub(crate) fn inspect_periodic_company_service_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePeriodicCompanyServiceTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_periodic_company_service_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_region_service_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeRegionServiceTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_region_service_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_infrastructure_asset_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeInfrastructureAssetTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_infrastructure_asset_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_region_queued_notice_records(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_save_region_queued_notice_records_file(smp_path)?)?
);
Ok(())
}
pub(crate) fn inspect_placed_structure_dynamic_side_buffer(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_save_placed_structure_dynamic_side_buffer_file(
smp_path
)?)?
);
Ok(())
}
pub(crate) fn inspect_unclassified_save_collections(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_unclassified_save_collection_headers_file(
smp_path
)?)?
);
Ok(())
}

View file

@ -0,0 +1,245 @@
use super::common::{
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT,
SPECIAL_CONDITION_COUNT, SPECIAL_CONDITIONS_OFFSET, aligned_runtime_rule_known_label,
aligned_runtime_rule_lane_kind, collect_special_conditions_input_paths,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_band_indices: Vec<usize>,
pub(crate) values_by_band_index: BTreeMap<usize, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandOffsetSummary {
pub(crate) band_index: usize,
pub(crate) relative_offset_hex: String,
pub(crate) lane_kind: String,
pub(crate) known_label: Option<String>,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_index_set_count: usize,
pub(crate) stable_nonzero_band_indices: Vec<usize>,
pub(crate) union_nonzero_band_indices: Vec<usize>,
pub(crate) offset_summaries: Vec<RuntimeAlignedRuntimeRuleBandOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimeAlignedRuntimeRuleBandFamilySummary>,
}
pub(crate) fn scan_aligned_runtime_rule_band(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_aligned_runtime_rule_band_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_band_indices.is_empty())
.count();
let mut grouped = BTreeMap::<String, Vec<RuntimeAlignedRuntimeRuleBandScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
let family_summaries = grouped
.into_iter()
.map(|(profile_family, samples)| {
let file_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_band_indices.is_empty())
.count();
let source_kinds = samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let distinct_nonzero_index_set_count = samples
.iter()
.map(|sample| sample.nonzero_band_indices.clone())
.collect::<BTreeSet<_>>()
.len();
let stable_band_indices = if samples.is_empty() {
BTreeSet::new()
} else {
let mut stable = samples[0]
.nonzero_band_indices
.iter()
.copied()
.collect::<BTreeSet<_>>();
for sample in samples.iter().skip(1) {
let current = sample
.nonzero_band_indices
.iter()
.copied()
.collect::<BTreeSet<_>>();
stable = stable.intersection(&current).copied().collect();
}
stable
};
let mut band_values = BTreeMap::<usize, BTreeSet<String>>::new();
let mut band_counts = BTreeMap::<usize, usize>::new();
for sample in &samples {
for band_index in &sample.nonzero_band_indices {
*band_counts.entry(*band_index).or_default() += 1;
}
for (band_index, value_hex) in &sample.values_by_band_index {
band_values
.entry(*band_index)
.or_default()
.insert(value_hex.clone());
}
}
let offset_summaries = band_counts
.into_iter()
.map(
|(band_index, count)| RuntimeAlignedRuntimeRuleBandOffsetSummary {
band_index,
relative_offset_hex: format!("0x{:x}", band_index * 4),
lane_kind: aligned_runtime_rule_lane_kind(band_index).to_string(),
known_label: aligned_runtime_rule_known_label(band_index)
.map(str::to_string),
file_count_present: count,
distinct_value_count: band_values
.get(&band_index)
.map(BTreeSet::len)
.unwrap_or(0),
sample_value_hexes: band_values
.get(&band_index)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect::<Vec<_>>();
RuntimeAlignedRuntimeRuleBandFamilySummary {
profile_family,
source_kinds,
file_count,
files_with_any_nonzero_count,
distinct_nonzero_index_set_count,
stable_nonzero_band_indices: stable_band_indices.into_iter().collect(),
union_nonzero_band_indices: band_values.keys().copied().collect(),
offset_summaries,
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
}
})
.collect::<Vec<_>>();
let report = RuntimeAlignedRuntimeRuleBandScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_aligned_runtime_rule_band_scan_sample(
smp_path: &Path,
) -> Result<RuntimeAlignedRuntimeRuleBandScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_len = SPECIAL_CONDITION_COUNT * 4;
let table_end = SPECIAL_CONDITIONS_OFFSET
.checked_add(table_len)
.ok_or("special-conditions table overflow")?;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed aligned-runtime-rule band",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-aligned-runtime-rule-band",
"gms" => "save-aligned-runtime-rule-band",
"gmx" => "sandbox-aligned-runtime-rule-band",
_ => "aligned-runtime-rule-band",
}
.to_string();
let mut nonzero_band_indices = Vec::new();
let mut values_by_band_index = BTreeMap::new();
for band_index in 0..SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT {
let offset = SPECIAL_CONDITIONS_OFFSET + band_index * 4;
let value = read_u32_le(&bytes, offset).ok_or_else(|| {
format!(
"{} is truncated inside the aligned-runtime-rule band",
smp_path.display()
)
})?;
if value == 0 {
continue;
}
nonzero_band_indices.push(band_index);
values_by_band_index.insert(band_index, format!("0x{value:08x}"));
}
Ok(RuntimeAlignedRuntimeRuleBandScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_band_indices,
values_by_band_index,
})
}

View file

@ -0,0 +1,388 @@
use crate::app::runtime_compare::{
RuntimeCandidateTableNamedRun, classify_candidate_table_header_profile,
collect_numbered_candidate_name_runs, load_candidate_table_inspection_report,
matches_candidate_table_header_bytes, read_u32_le,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderCluster {
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) file_count: usize,
pub(crate) profile_families: Vec<String>,
pub(crate) source_kinds: Vec<String>,
pub(crate) zero_trailer_count_min: usize,
pub(crate) zero_trailer_count_max: usize,
pub(crate) zero_trailer_count_values: Vec<usize>,
pub(crate) distinct_zero_name_set_count: usize,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) cluster_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) clusters: Vec<RuntimeCandidateTableHeaderCluster>,
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimeCandidateTableHeaderScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) observed_entry_count: usize,
pub(crate) port_runs: Vec<RuntimeCandidateTableNamedRun>,
pub(crate) warehouse_runs: Vec<RuntimeCandidateTableNamedRun>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_numbered_port_runs_count: usize,
pub(crate) files_with_any_numbered_warehouse_runs_count: usize,
pub(crate) files_with_both_numbered_run_families_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) samples: Vec<RuntimeCandidateTableNamedRunScanSample>,
}
pub(crate) fn scan_candidate_table_headers(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_candidate_table_header_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let mut grouped =
BTreeMap::<(String, String), Vec<RuntimeCandidateTableHeaderScanSample>>::new();
for sample in samples {
grouped
.entry((
sample.header_word_0_hex.clone(),
sample.header_word_1_hex.clone(),
))
.or_default()
.push(sample);
}
let file_count = grouped.values().map(Vec::len).sum();
let clusters = grouped
.into_iter()
.map(|((header_word_0_hex, header_word_1_hex), samples)| {
let mut profile_families = samples
.iter()
.map(|sample| sample.profile_family.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let mut source_kinds = samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let mut zero_trailer_count_values = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let distinct_zero_name_set_count = samples
.iter()
.map(|sample| sample.zero_trailer_entry_names.clone())
.collect::<BTreeSet<_>>()
.len();
let zero_trailer_count_min = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.min()
.unwrap_or(0);
let zero_trailer_count_max = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.max()
.unwrap_or(0);
let sample_paths = samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect::<Vec<_>>();
profile_families.sort();
source_kinds.sort();
zero_trailer_count_values.sort();
RuntimeCandidateTableHeaderCluster {
header_word_0_hex,
header_word_1_hex,
file_count: samples.len(),
profile_families,
source_kinds,
zero_trailer_count_min,
zero_trailer_count_max,
zero_trailer_count_values,
distinct_zero_name_set_count,
sample_paths,
}
})
.collect::<Vec<_>>();
let report = RuntimeCandidateTableHeaderScanReport {
root_path: root_path.display().to_string(),
file_count,
cluster_count: clusters.len(),
skipped_file_count,
clusters,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn scan_candidate_table_named_runs(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_candidate_table_named_run_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_numbered_port_runs_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty())
.count();
let files_with_any_numbered_warehouse_runs_count = samples
.iter()
.filter(|sample| !sample.warehouse_runs.is_empty())
.count();
let files_with_both_numbered_run_families_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty() && !sample.warehouse_runs.is_empty())
.count();
let report = RuntimeCandidateTableNamedRunScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_numbered_port_runs_count,
files_with_any_numbered_warehouse_runs_count,
files_with_both_numbered_run_families_count,
skipped_file_count,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn collect_candidate_table_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_candidate_table_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn load_candidate_table_header_scan_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableHeaderScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let header_offset = 0x6a70usize;
let entries_offset = 0x6ad1usize;
let block_end_offset = 0x73c0usize;
let entry_stride = 0x22usize;
if bytes.len() < block_end_offset {
return Err(format!(
"{} is too small for the fixed candidate table range",
smp_path.display()
)
.into());
}
if !matches_candidate_table_header_bytes(&bytes, header_offset) {
return Err(format!(
"{} does not contain the fixed candidate table header",
smp_path.display()
)
.into());
}
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
as usize;
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
as usize;
if observed_entry_capacity < observed_entry_count {
return Err(format!(
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
smp_path.display()
)
.into());
}
let entries_end_offset = entries_offset
.checked_add(
observed_entry_count
.checked_mul(entry_stride)
.ok_or("candidate table length overflow")?,
)
.ok_or("candidate table end overflow")?;
if entries_end_offset > block_end_offset {
return Err(format!(
"{} candidate table overruns fixed block end",
smp_path.display()
)
.into());
}
let mut zero_trailer_entry_names = Vec::new();
for index in 0..observed_entry_count {
let offset = entries_offset + index * entry_stride;
let chunk = &bytes[offset..offset + entry_stride];
let nul_index = chunk
.iter()
.position(|byte| *byte == 0)
.unwrap_or(entry_stride - 4);
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
format!(
"{} contains invalid UTF-8 in candidate table",
smp_path.display()
)
})?;
let availability = read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
format!(
"{} is missing candidate availability dword",
smp_path.display()
)
})?;
if availability == 0 {
zero_trailer_entry_names.push(text.to_string());
}
}
let profile_family = classify_candidate_table_header_profile(
smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase()),
&bytes,
);
let source_kind = match smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("gmp") => "map-fixed-catalog-range",
Some("gms") => "save-fixed-catalog-range",
_ => "fixed-catalog-range",
}
.to_string();
Ok(RuntimeCandidateTableHeaderScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
header_word_0_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
),
header_word_1_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
),
zero_trailer_entry_count: zero_trailer_entry_names.len(),
zero_trailer_entry_names,
})
}
pub(crate) fn load_candidate_table_named_run_scan_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableNamedRunScanSample, Box<dyn std::error::Error>> {
let report = load_candidate_table_inspection_report(smp_path)?;
let port_runs = collect_numbered_candidate_name_runs(&report.entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&report.entries, "Warehouse");
Ok(RuntimeCandidateTableNamedRunScanSample {
path: report.path,
profile_family: report.profile_family,
source_kind: report.source_kind,
observed_entry_count: report.observed_entry_count,
port_runs,
warehouse_runs,
})
}

View file

@ -0,0 +1,130 @@
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64;
pub(crate) const SPECIAL_CONDITION_COUNT: usize = 36;
pub(crate) const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize =
SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize =
SMP_ALIGNED_RUNTIME_RULE_END_OFFSET;
pub(crate) const SPECIAL_CONDITION_LABELS: [&str; SPECIAL_CONDITION_COUNT] = [
"Disable Stock Buying and Selling",
"Disable Margin Buying/Short Selling Stock",
"Disable Company Issue/Buy Back Stock",
"Disable Issuing/Repaying Bonds",
"Disable Declaring Bankruptcy",
"Disable Changing the Dividend Rate",
"Disable Replacing a Locomotive",
"Disable Retiring a Train",
"Disable Changing Cargo Consist On Train",
"Disable Buying a Train",
"Disable All Track Building",
"Disable Unconnected Track Building",
"Limited Track Building Amount",
"Disable Building Stations",
"Disable Building Hotel/Restaurant/Tavern/Post Office",
"Disable Building Customs House",
"Disable Building Industry Buildings",
"Disable Buying Existing Industry Buildings",
"Disable Being Fired As Chairman",
"Disable Resigning as Chairman",
"Disable Chairmanship Takeover",
"Disable Starting Any Companies",
"Disable Starting Multiple Companies",
"Disable Merging Companies",
"Disable Bulldozing",
"Show Visited Track",
"Show Visited Stations",
"Use Slow Date",
"Completely Disable Money-Related Things",
"Use Bio-Accelerator Cars",
"Disable Cargo Economy",
"Use Wartime Cargos",
"Disable Train Crashes",
"Disable Train Crashes AND Breakdowns",
"AI Ignore Territories At Startup",
"Hidden sentinel",
];
pub(crate) fn collect_special_conditions_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_special_conditions_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn parse_special_condition_slot_index(label: &str) -> Option<u8> {
let suffix = label.strip_prefix("slot ")?;
let (slot_index, _) = suffix.split_once(':')?;
slot_index.parse().ok()
}
pub(crate) fn parse_hex_offset(text: &str) -> Option<usize> {
text.strip_prefix("0x")
.and_then(|digits| usize::from_str_radix(digits, 16).ok())
}
pub(crate) fn aligned_runtime_rule_lane_kind(band_index: usize) -> &'static str {
if band_index < SPECIAL_CONDITION_COUNT {
"known-special-condition-dword"
} else if band_index < SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT {
"unlabeled-editor-rule-dword"
} else {
"trailing-runtime-scalar"
}
}
pub(crate) fn aligned_runtime_rule_known_label(band_index: usize) -> Option<&'static str> {
if band_index < SPECIAL_CONDITION_LABELS.len() {
Some(SPECIAL_CONDITION_LABELS[band_index])
} else {
None
}
}

View file

@ -0,0 +1,15 @@
pub(crate) mod common;
pub(crate) mod post_special;
mod aligned_band;
mod candidate_table;
mod recipe_book;
mod special_conditions;
pub(super) use aligned_band::scan_aligned_runtime_rule_band;
pub(super) use candidate_table::{scan_candidate_table_headers, scan_candidate_table_named_runs};
pub(super) use post_special::{
scan_post_special_conditions_scalars, scan_post_special_conditions_tail,
};
pub(super) use recipe_book::scan_recipe_book_lines;
pub(super) use special_conditions::scan_special_conditions;

View file

@ -0,0 +1,489 @@
use super::common::{
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT,
SPECIAL_CONDITIONS_OFFSET, collect_special_conditions_input_paths, parse_hex_offset,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offsets: Vec<usize>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarOffsetSummary {
pub(crate) relative_offset_hex: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_offset_set_count: usize,
pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsScalarOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsScalarFamilySummary>,
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsTailScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offsets: Vec<usize>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailOffsetSummary {
pub(crate) relative_offset_hex: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_offset_set_count: usize,
pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsTailOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsTailFamilySummary>,
}
pub(crate) fn scan_post_special_conditions_scalars(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_post_special_conditions_scalar_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count();
let family_summaries = build_scalar_family_summaries(samples);
let report = RuntimePostSpecialConditionsScalarScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn scan_post_special_conditions_tail(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_post_special_conditions_tail_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count();
let family_summaries = build_tail_family_summaries(samples);
let report = RuntimePostSpecialConditionsTailScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_post_special_conditions_scalar_scan_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsScalarScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_end = SPECIAL_CONDITIONS_OFFSET + 36 * 4;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed post-special-conditions scalar window",
smp_path.display()
)
.into());
}
let hidden_sentinel = read_u32_le(
&bytes,
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
)
.ok_or_else(|| {
format!(
"{} is missing the hidden special-condition sentinel",
smp_path.display()
)
})?;
if hidden_sentinel != 1 {
return Err(format!(
"{} does not match the fixed special-conditions table sentinel",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-post-special-conditions-window",
"gms" => "save-post-special-conditions-window",
"gmx" => "sandbox-post-special-conditions-window",
_ => "post-special-conditions-window",
}
.to_string();
let mut nonzero_relative_offsets = Vec::new();
let mut values_by_relative_offset_hex = BTreeMap::new();
for offset in (POST_SPECIAL_CONDITIONS_SCALAR_OFFSET..POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET)
.step_by(4)
{
let value = read_u32_le(&bytes, offset).ok_or_else(|| {
format!(
"{} is truncated inside the fixed post-special-conditions scalar window",
smp_path.display()
)
})?;
if value == 0 {
continue;
}
let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET;
nonzero_relative_offsets.push(relative_offset);
values_by_relative_offset_hex
.insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
}
Ok(RuntimePostSpecialConditionsScalarScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_relative_offsets,
values_by_relative_offset_hex,
})
}
pub(crate) fn load_post_special_conditions_tail_scan_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsTailScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET {
return Err(format!(
"{} is too small for the fixed post-special-conditions tail window",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-post-special-conditions-tail",
"gms" => "save-post-special-conditions-tail",
"gmx" => "sandbox-post-special-conditions-tail",
_ => "post-special-conditions-tail",
}
.to_string();
let mut nonzero_relative_offsets = Vec::new();
let mut values_by_relative_offset_hex = BTreeMap::new();
for offset in (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET..bytes.len()).step_by(4) {
let Some(value) = read_u32_le(&bytes, offset) else {
break;
};
if value == 0 {
continue;
}
let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET;
nonzero_relative_offsets.push(relative_offset);
values_by_relative_offset_hex
.insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
}
Ok(RuntimePostSpecialConditionsTailScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_relative_offsets,
values_by_relative_offset_hex,
})
}
fn build_scalar_family_summaries(
samples: Vec<RuntimePostSpecialConditionsScalarScanSample>,
) -> Vec<RuntimePostSpecialConditionsScalarFamilySummary> {
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsScalarScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimePostSpecialConditionsScalarFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_count: samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count(),
distinct_nonzero_offset_set_count: samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.clone())
.collect::<BTreeSet<_>>()
.len(),
stable_nonzero_relative_offset_hexes: stable_offsets(
samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.as_slice()),
)
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
union_nonzero_relative_offset_hexes: collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)
.0
.keys()
.copied()
.map(|offset| format!("0x{offset:x}"))
.collect(),
offset_summaries: build_offset_summaries(collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect()
}
fn build_tail_family_summaries(
samples: Vec<RuntimePostSpecialConditionsTailScanSample>,
) -> Vec<RuntimePostSpecialConditionsTailFamilySummary> {
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsTailScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimePostSpecialConditionsTailFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_count: samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count(),
distinct_nonzero_offset_set_count: samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.clone())
.collect::<BTreeSet<_>>()
.len(),
stable_nonzero_relative_offset_hexes: stable_offsets(
samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.as_slice()),
)
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
union_nonzero_relative_offset_hexes: collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)
.0
.keys()
.copied()
.map(|offset| format!("0x{offset:x}"))
.collect(),
offset_summaries: build_tail_offset_summaries(collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect()
}
fn stable_offsets<'a>(offset_sets: impl Iterator<Item = &'a [usize]>) -> BTreeSet<usize> {
let mut offset_sets = offset_sets.peekable();
if offset_sets.peek().is_none() {
return BTreeSet::new();
}
let mut stable = offset_sets
.next()
.map(|set| set.iter().copied().collect::<BTreeSet<_>>())
.unwrap_or_default();
for offsets in offset_sets {
let current = offsets.iter().copied().collect::<BTreeSet<_>>();
stable = stable.intersection(&current).copied().collect();
}
stable
}
fn collect_offset_values<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>) {
let mut offset_values = BTreeMap::<usize, BTreeSet<String>>::new();
let mut offset_counts = BTreeMap::<usize, usize>::new();
for map in maps {
for (offset_hex, value_hex) in map {
if let Some(offset) = parse_hex_offset(offset_hex) {
*offset_counts.entry(offset).or_default() += 1;
offset_values
.entry(offset)
.or_default()
.insert(value_hex.clone());
}
}
}
(offset_values, offset_counts)
}
fn build_offset_summaries(
(offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsScalarOffsetSummary> {
offset_counts
.into_iter()
.map(
|(offset, count)| RuntimePostSpecialConditionsScalarOffsetSummary {
relative_offset_hex: format!("0x{offset:x}"),
file_count_present: count,
distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: offset_values
.get(&offset)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}
fn build_tail_offset_summaries(
(offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsTailOffsetSummary> {
offset_counts
.into_iter()
.map(
|(offset, count)| RuntimePostSpecialConditionsTailOffsetSummary {
relative_offset_hex: format!("0x{offset:x}"),
file_count_present: count,
distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: offset_values
.get(&offset)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}

View file

@ -0,0 +1,183 @@
use super::common::collect_special_conditions_input_paths;
use crate::app::runtime_compare::{
RuntimeRecipeBookLineFieldSummary, build_recipe_line_field_summaries,
intersect_nonzero_recipe_line_paths, load_recipe_book_line_sample,
};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimeRecipeBookLineScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_mode_paths: BTreeMap<String, String>,
pub(crate) nonzero_supplied_token_paths: BTreeMap<String, String>,
pub(crate) nonzero_demanded_token_paths: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_modes_count: usize,
pub(crate) files_with_any_nonzero_supplied_tokens_count: usize,
pub(crate) files_with_any_nonzero_demanded_tokens_count: usize,
pub(crate) stable_nonzero_mode_paths: Vec<String>,
pub(crate) stable_nonzero_supplied_token_paths: Vec<String>,
pub(crate) stable_nonzero_demanded_token_paths: Vec<String>,
pub(crate) mode_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) supplied_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) demanded_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_modes_count: usize,
pub(crate) files_with_any_nonzero_supplied_tokens_count: usize,
pub(crate) files_with_any_nonzero_demanded_tokens_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimeRecipeBookLineFamilySummary>,
}
pub(crate) fn scan_recipe_book_lines(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_recipe_book_line_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_modes_count = samples
.iter()
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
.count();
let files_with_any_nonzero_supplied_tokens_count = samples
.iter()
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
.count();
let files_with_any_nonzero_demanded_tokens_count = samples
.iter()
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
.count();
let mut grouped = BTreeMap::<String, Vec<RuntimeRecipeBookLineScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
let family_summaries = grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimeRecipeBookLineFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_modes_count: samples
.iter()
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
.count(),
files_with_any_nonzero_supplied_tokens_count: samples
.iter()
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
.count(),
files_with_any_nonzero_demanded_tokens_count: samples
.iter()
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
.count(),
stable_nonzero_mode_paths: intersect_nonzero_recipe_line_paths(
samples.iter().map(|sample| &sample.nonzero_mode_paths),
),
stable_nonzero_supplied_token_paths: intersect_nonzero_recipe_line_paths(
samples
.iter()
.map(|sample| &sample.nonzero_supplied_token_paths),
),
stable_nonzero_demanded_token_paths: intersect_nonzero_recipe_line_paths(
samples
.iter()
.map(|sample| &sample.nonzero_demanded_token_paths),
),
mode_summaries: build_recipe_line_field_summaries(
samples.iter().map(|sample| &sample.nonzero_mode_paths),
),
supplied_token_summaries: build_recipe_line_field_summaries(
samples
.iter()
.map(|sample| &sample.nonzero_supplied_token_paths),
),
demanded_token_summaries: build_recipe_line_field_summaries(
samples
.iter()
.map(|sample| &sample.nonzero_demanded_token_paths),
),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect::<Vec<_>>();
let report = RuntimeRecipeBookLineScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_modes_count,
files_with_any_nonzero_supplied_tokens_count,
files_with_any_nonzero_demanded_tokens_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_recipe_book_line_scan_sample(
smp_path: &Path,
) -> Result<RuntimeRecipeBookLineScanSample, Box<dyn std::error::Error>> {
let sample = load_recipe_book_line_sample(smp_path)?;
Ok(RuntimeRecipeBookLineScanSample {
path: sample.path,
profile_family: sample.profile_family,
source_kind: sample.source_kind,
nonzero_mode_paths: sample
.mode_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
nonzero_supplied_token_paths: sample
.supplied_cargo_token_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
nonzero_demanded_token_paths: sample
.demanded_cargo_token_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
})
}

View file

@ -0,0 +1,167 @@
use super::common::{
SPECIAL_CONDITION_COUNT, SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT, SPECIAL_CONDITION_LABELS,
SPECIAL_CONDITIONS_OFFSET, collect_special_conditions_input_paths,
parse_special_condition_slot_index,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeSpecialConditionsScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) enabled_visible_count: usize,
pub(crate) enabled_visible_labels: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSpecialConditionsSlotSummary {
pub(crate) slot_index: u8,
pub(crate) label: String,
pub(crate) file_count_enabled: usize,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSpecialConditionsScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_enabled_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) enabled_slot_summaries: Vec<RuntimeSpecialConditionsSlotSummary>,
pub(crate) sample_files_with_any_enabled: Vec<RuntimeSpecialConditionsScanSample>,
}
pub(crate) fn scan_special_conditions(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_special_conditions_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let sample_files_with_any_enabled = samples
.iter()
.filter(|sample| sample.enabled_visible_count != 0)
.cloned()
.collect::<Vec<_>>();
let files_with_any_enabled_count = sample_files_with_any_enabled.len();
let mut grouped = BTreeMap::<(u8, String), Vec<String>>::new();
for sample in &samples {
for label in &sample.enabled_visible_labels {
if let Some(slot_index) = parse_special_condition_slot_index(label) {
grouped
.entry((slot_index, label.clone()))
.or_default()
.push(sample.path.clone());
}
}
}
let enabled_slot_summaries = grouped
.into_iter()
.map(
|((slot_index, label), paths)| RuntimeSpecialConditionsSlotSummary {
slot_index,
label,
file_count_enabled: paths.len(),
sample_paths: paths.into_iter().take(12).collect(),
},
)
.collect::<Vec<_>>();
let report = RuntimeSpecialConditionsScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_enabled_count,
skipped_file_count,
enabled_slot_summaries,
sample_files_with_any_enabled,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_special_conditions_scan_sample(
smp_path: &Path,
) -> Result<RuntimeSpecialConditionsScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_len = SPECIAL_CONDITION_COUNT * 4;
let table_end = SPECIAL_CONDITIONS_OFFSET
.checked_add(table_len)
.ok_or("special-conditions table overflow")?;
if bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed special-conditions table",
smp_path.display()
)
.into());
}
let hidden_sentinel = read_u32_le(
&bytes,
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
)
.ok_or_else(|| {
format!(
"{} is missing the hidden special-condition sentinel",
smp_path.display()
)
})?;
if hidden_sentinel != 1 {
return Err(format!(
"{} does not match the fixed special-conditions table sentinel",
smp_path.display()
)
.into());
}
let enabled_visible_labels = (0..SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT)
.filter_map(|slot_index| {
let value = read_u32_le(&bytes, SPECIAL_CONDITIONS_OFFSET + slot_index * 4)?;
(value != 0).then(|| {
format!(
"slot {}: {}",
slot_index, SPECIAL_CONDITION_LABELS[slot_index]
)
})
})
.collect::<Vec<_>>();
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-fixed-special-conditions-range",
"gms" => "save-fixed-special-conditions-range",
"gmx" => "sandbox-fixed-special-conditions-range",
_ => "fixed-special-conditions-range",
}
.to_string();
Ok(RuntimeSpecialConditionsScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
enabled_visible_count: enabled_visible_labels.len(),
enabled_visible_labels,
})
}

View file

@ -0,0 +1,581 @@
use super::*;
use crate::app::runtime_compare::{
RuntimeCandidateTableEntrySample, RuntimeCandidateTableSample, RuntimeClassicProfileSample,
RuntimeRecipeBookLineSample, RuntimeRt3105ProfileSample, RuntimeSetupLaunchPayloadSample,
RuntimeSetupPayloadCoreSample, collect_numbered_candidate_name_runs,
diff_candidate_table_samples, diff_classic_profile_samples,
diff_recipe_book_line_content_samples, diff_recipe_book_line_samples,
diff_rt3_105_profile_samples, diff_setup_launch_payload_samples,
diff_setup_payload_core_samples,
};
#[test]
fn diffs_classic_profile_samples_across_multiple_files() {
let sample_a = RuntimeClassicProfileSample {
path: "a.gms".to_string(),
profile_family: "rt3-classic-save-container-v1".to_string(),
progress_32dc_offset: 0x76e8,
progress_3714_offset: 0x76ec,
progress_3715_offset: 0x77f8,
packed_profile_offset: 0x76f0,
packed_profile_len: 0x108,
packed_profile_block: SmpClassicPackedProfileBlock {
relative_len: 0x108,
relative_len_hex: "0x108".to_string(),
leading_word_0: 0x03000000,
leading_word_0_hex: "0x03000000".to_string(),
trailing_zero_word_count_after_leading_word: 3,
map_path_offset: 0x13,
map_path: Some("British Isles.gmp".to_string()),
display_name_offset: 0x46,
display_name: Some("British Isles".to_string()),
profile_byte_0x77: 0,
profile_byte_0x77_hex: "0x00".to_string(),
profile_byte_0x82: 0,
profile_byte_0x82_hex: "0x00".to_string(),
profile_byte_0x97: 0,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0,
profile_byte_0xc5_hex: "0x00".to_string(),
stable_nonzero_words: vec![SmpPackedProfileWordLane {
relative_offset: 0,
relative_offset_hex: "0x00".to_string(),
value: 0x03000000,
value_hex: "0x03000000".to_string(),
}],
},
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.packed_profile_block.leading_word_0 = 0x05000000;
sample_b.packed_profile_block.leading_word_0_hex = "0x05000000".to_string();
sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x05000000;
sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x05000000".to_string();
let differences =
diff_classic_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0_hex")
);
assert!(differences.iter().any(
|entry| entry.field_path == "$.packed_profile_block.stable_nonzero_words[0].value"
));
}
#[test]
fn diffs_rt3_105_profile_samples_across_multiple_files() {
let sample_a = RuntimeRt3105ProfileSample {
path: "a.gms".to_string(),
profile_family: "rt3-105-save-container-v1".to_string(),
packed_profile_offset: 0x73c0,
packed_profile_len: 0x108,
packed_profile_block: SmpRt3105PackedProfileBlock {
relative_len: 0x108,
relative_len_hex: "0x108".to_string(),
leading_word_0: 3,
leading_word_0_hex: "0x00000003".to_string(),
trailing_zero_word_count_after_leading_word: 2,
header_flag_word_3: 0x01000000,
header_flag_word_3_hex: "0x01000000".to_string(),
map_path_offset: 0x10,
map_path: Some("Alternate USA.gmp".to_string()),
display_name_offset: 0x43,
display_name: Some("Alternate USA".to_string()),
profile_byte_0x77: 0x07,
profile_byte_0x77_hex: "0x07".to_string(),
profile_byte_0x82: 0x4d,
profile_byte_0x82_hex: "0x4d".to_string(),
profile_byte_0x97: 0x00,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0x00,
profile_byte_0xc5_hex: "0x00".to_string(),
stable_nonzero_words: vec![SmpPackedProfileWordLane {
relative_offset: 0x80,
relative_offset_hex: "0x80".to_string(),
value: 0x364d0000,
value_hex: "0x364d0000".to_string(),
}],
},
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.profile_family = "rt3-105-alt-save-container-v1".to_string();
sample_b.packed_profile_block.map_path = Some("Southern Pacific.gmp".to_string());
sample_b.packed_profile_block.display_name = Some("Southern Pacific".to_string());
sample_b.packed_profile_block.leading_word_0 = 5;
sample_b.packed_profile_block.leading_word_0_hex = "0x00000005".to_string();
sample_b.packed_profile_block.profile_byte_0x82 = 0x90;
sample_b.packed_profile_block.profile_byte_0x82_hex = "0x90".to_string();
sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x1b900000;
sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x1b900000".to_string();
let differences =
diff_rt3_105_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.map_path")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.profile_byte_0x82")
);
}
#[test]
fn diffs_candidate_table_samples_across_multiple_files() {
let mut availability_a = BTreeMap::new();
availability_a.insert("AutoPlant".to_string(), 1u32);
availability_a.insert("Nuclear Power Plant".to_string(), 0u32);
let sample_a = RuntimeCandidateTableSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "map-fixed-catalog-range".to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: "0x10000000".to_string(),
header_word_1_hex: "0x00009000".to_string(),
header_word_2_hex: "0x0000332e".to_string(),
observed_entry_count: 67,
zero_trailer_entry_count: 1,
nonzero_trailer_entry_count: 66,
zero_trailer_entry_names: vec!["Nuclear Power Plant".to_string()],
footer_progress_word_0_hex: "0x000032dc".to_string(),
footer_progress_word_1_hex: "0x00003714".to_string(),
availability_by_name: availability_a,
};
let mut availability_b = BTreeMap::new();
availability_b.insert("AutoPlant".to_string(), 0u32);
availability_b.insert("Nuclear Power Plant".to_string(), 0u32);
let sample_b = RuntimeCandidateTableSample {
path: "b.gmp".to_string(),
profile_family: "rt3-105-scenario-map-container-v1".to_string(),
source_kind: "map-fixed-catalog-range".to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: "0x00000000".to_string(),
header_word_1_hex: "0x00000000".to_string(),
header_word_2_hex: "0x0000332e".to_string(),
observed_entry_count: 67,
zero_trailer_entry_count: 2,
nonzero_trailer_entry_count: 65,
zero_trailer_entry_names: vec!["AutoPlant".to_string(), "Nuclear Power Plant".to_string()],
footer_progress_word_0_hex: "0x000032dc".to_string(),
footer_progress_word_1_hex: "0x00003714".to_string(),
availability_by_name: availability_b,
};
let differences =
diff_candidate_table_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.header_word_0_hex")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.availability_by_name.AutoPlant")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.zero_trailer_entry_names[0]")
);
}
#[test]
fn collects_numbered_candidate_name_runs_by_prefix() {
let entries = vec![
RuntimeCandidateTableEntrySample {
index: 35,
offset: 28535,
text: "Port00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 43,
offset: 28807,
text: "Warehouse00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 45,
offset: 28875,
text: "Port01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 46,
offset: 28909,
text: "Port02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 56,
offset: 29249,
text: "Warehouse01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 57,
offset: 29283,
text: "Warehouse02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
];
let port_runs = collect_numbered_candidate_name_runs(&entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&entries, "Warehouse");
assert_eq!(port_runs.len(), 2);
assert_eq!(port_runs[0].first_name, "Port00");
assert_eq!(port_runs[0].count, 1);
assert_eq!(port_runs[1].first_name, "Port01");
assert_eq!(port_runs[1].last_name, "Port02");
assert_eq!(port_runs[1].count, 2);
assert_eq!(warehouse_runs.len(), 2);
assert_eq!(warehouse_runs[0].first_name, "Warehouse00");
assert_eq!(warehouse_runs[0].count, 1);
assert_eq!(warehouse_runs[1].first_name, "Warehouse01");
assert_eq!(warehouse_runs[1].last_name, "Warehouse02");
assert_eq!(warehouse_runs[1].count, 2);
}
#[test]
fn diffs_recipe_book_line_samples_across_multiple_files() {
let sample_a = RuntimeRecipeBookLineSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x41200000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "mixed".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000003".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x41a00000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000017".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x0000002a".to_string(),
)]),
};
let sample_b = RuntimeRecipeBookLineSample {
path: "b.gms".to_string(),
profile_family: "rt3-105-alt-save-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x41200000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "zero".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
};
let differences = diff_recipe_book_line_samples(&[sample_a, sample_b])
.expect("recipe-book diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.line_kind_by_path.book00.line00")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.mode_word_hex_by_path.book00.line00")
);
assert!(
differences.iter().any(
|entry| entry.field_path == "$.supplied_cargo_token_word_hex_by_path.book00.line00"
)
);
}
#[test]
fn recipe_book_content_diff_ignores_wrapper_metadata() {
let sample_a = RuntimeRecipeBookLineSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x00000000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line02".to_string(), "mixed".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x00110000".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x00000000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x000040a0".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line01".to_string(),
"0x72470000".to_string(),
)]),
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.profile_family = "rt3-105-save-container-v1".to_string();
sample_b.source_kind = "recipe-book-summary".to_string();
let differences = diff_recipe_book_line_samples(&[sample_a.clone(), sample_b.clone()])
.expect("wrapper-aware diff should succeed");
let content_differences = diff_recipe_book_line_content_samples(&[sample_a, sample_b])
.expect("content diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(content_differences.is_empty());
}
#[test]
fn diffs_setup_payload_core_samples_across_multiple_files() {
let sample_a = RuntimeSetupPayloadCoreSample {
path: "a.gmp".to_string(),
file_extension: "gmp".to_string(),
inferred_profile_family: "rt3-105-map-container-v1".to_string(),
payload_word_0x14: 0x0001,
payload_word_0x14_hex: "0x0001".to_string(),
payload_byte_0x20: 0x05,
payload_byte_0x20_hex: "0x05".to_string(),
marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
row_category_byte_0x31a: 0x00,
row_category_byte_0x31a_hex: "0x00".to_string(),
row_visibility_byte_0x31b: 0x00,
row_visibility_byte_0x31b_hex: "0x00".to_string(),
row_visibility_byte_0x31c: 0x00,
row_visibility_byte_0x31c_hex: "0x00".to_string(),
row_count_word_0x3ae: 0x0186,
row_count_word_0x3ae_hex: "0x0186".to_string(),
payload_word_0x3b2: 0x0001,
payload_word_0x3b2_hex: "0x0001".to_string(),
payload_word_0x3ba: 0x0001,
payload_word_0x3ba_hex: "0x0001".to_string(),
candidate_header_word_0_hex: Some("0x10000000".to_string()),
candidate_header_word_1_hex: Some("0x00009000".to_string()),
};
let sample_b = RuntimeSetupPayloadCoreSample {
path: "b.gms".to_string(),
file_extension: "gms".to_string(),
inferred_profile_family: "rt3-105-scenario-save-container-v1".to_string(),
payload_word_0x14: 0x0001,
payload_word_0x14_hex: "0x0001".to_string(),
payload_byte_0x20: 0x05,
payload_byte_0x20_hex: "0x05".to_string(),
marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
row_category_byte_0x31a: 0x00,
row_category_byte_0x31a_hex: "0x00".to_string(),
row_visibility_byte_0x31b: 0x00,
row_visibility_byte_0x31b_hex: "0x00".to_string(),
row_visibility_byte_0x31c: 0x00,
row_visibility_byte_0x31c_hex: "0x00".to_string(),
row_count_word_0x3ae: 0x0186,
row_count_word_0x3ae_hex: "0x0186".to_string(),
payload_word_0x3b2: 0x0006,
payload_word_0x3b2_hex: "0x0006".to_string(),
payload_word_0x3ba: 0x0001,
payload_word_0x3ba_hex: "0x0001".to_string(),
candidate_header_word_0_hex: Some("0x00000000".to_string()),
candidate_header_word_1_hex: Some("0x00000000".to_string()),
};
let differences =
diff_setup_payload_core_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.file_extension")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.inferred_profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.payload_word_0x3b2")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.candidate_header_word_0_hex")
);
}
#[test]
fn diffs_setup_launch_payload_samples_across_multiple_files() {
let sample_a = RuntimeSetupLaunchPayloadSample {
path: "a.gmp".to_string(),
file_extension: "gmp".to_string(),
inferred_profile_family: "rt3-105-map-container-v1".to_string(),
launch_flag_byte_0x22: 0x53,
launch_flag_byte_0x22_hex: "0x53".to_string(),
campaign_progress_in_known_range: false,
campaign_progress_scenario_name: None,
campaign_progress_page_index: None,
launch_selector_byte_0x33: 0x00,
launch_selector_byte_0x33_hex: "0x00".to_string(),
launch_token_block_0x23_0x32_hex: "01311154010000000000000000000000".to_string(),
campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x31),
]),
nonzero_campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x31),
]),
};
let sample_b = RuntimeSetupLaunchPayloadSample {
path: "b.gms".to_string(),
file_extension: "gms".to_string(),
inferred_profile_family: "rt3-105-save-container-v1".to_string(),
launch_flag_byte_0x22: 0xae,
launch_flag_byte_0x22_hex: "0xae".to_string(),
campaign_progress_in_known_range: false,
campaign_progress_scenario_name: None,
campaign_progress_page_index: None,
launch_selector_byte_0x33: 0x00,
launch_selector_byte_0x33_hex: "0x00".to_string(),
launch_token_block_0x23_0x32_hex: "01439aae010000000000000000000000".to_string(),
campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x43),
]),
nonzero_campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x43),
]),
};
let differences =
diff_setup_launch_payload_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.file_extension")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.inferred_profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.launch_flag_byte_0x22")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.launch_token_block_0x23_0x32_hex")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.campaign_selector_values.Germantown")
);
}

View file

@ -0,0 +1,32 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use super::finance::{diff_finance_outcomes, load_finance_outcome};
use super::helpers::state_io::{
export_runtime_overlay_import_document, export_runtime_save_slice_document,
load_normalized_runtime_state,
};
use super::runtime_fixture_state::{
diff_state, export_fixture_state, snapshot_state, summarize_fixture, summarize_state,
};
use rrt_fixtures::diff_json_values;
use rrt_model::finance::{
AnnualFinanceDecision, AnnualFinanceEvaluation, CompanyFinanceState, DebtRestructureSummary,
FinanceOutcome, FinanceSnapshot,
};
use rrt_runtime::documents::{
load_runtime_overlay_import_document, load_runtime_save_slice_document,
};
use rrt_runtime::inspect::smp::{
profiles::{
SmpClassicPackedProfileBlock, SmpPackedProfileWordLane, SmpRt3105PackedProfileBlock,
},
save_load::SmpLoadedSaveSlice,
};
mod compare;
mod state;
mod support;
use support::*;

View file

@ -0,0 +1,410 @@
use super::*;
#[test]
fn diffs_outcomes_recursively() {
let left = FinanceOutcome {
evaluation: AnnualFinanceEvaluation::no_action(),
post_company: CompanyFinanceState::default(),
};
let mut right = left.clone();
right.post_company.current_cash = 123;
right.evaluation.debt_restructure = DebtRestructureSummary {
retired_principal: 10,
issued_principal: 20,
};
let report = diff_finance_outcomes(&left, &right).expect("diff should succeed");
assert!(!report.matches);
let diff_paths: Vec<_> = report
.differences
.iter()
.map(|entry| entry.path.clone())
.collect();
assert_diff_paths_include(&diff_paths, "$.post_company.current_cash");
assert_diff_paths_include(
&diff_paths,
"$.evaluation.debt_restructure.retired_principal",
);
}
#[test]
fn diffs_runtime_states_recursively() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flags": {
"sandbox": true
},
"companies": []
}
});
let left_path = write_temp_json("runtime-diff-left", &left);
let right_path = write_temp_json("runtime-diff-right", &right);
diff_state(&left_path, &right_path).expect("runtime diff should succeed");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_event_record_additions_and_removals() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
},
{
"record_id": 2,
"trigger_kind": 7,
"active": false
}
]
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
}
]
}
});
let left_path = write_temp_json("runtime-diff-events-left", &left);
let right_path = write_temp_json("runtime-diff-events-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
let diff_paths: Vec<_> = differences.iter().map(|entry| entry.path.clone()).collect();
assert_diff_paths_include(&diff_paths, "$.event_runtime_records[1]");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_packed_event_collection_changes() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-packed-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 5,
"live_record_count": 3,
"live_entry_ids": [1, 3, 5],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
},
{
"record_index": 1,
"live_entry_id": 3,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
},
{
"record_index": 2,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
}
]
},
"event_runtime_records": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-packed-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 5,
"live_record_count": 2,
"live_entry_ids": [1, 5],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["right fixture"]
},
{
"record_index": 1,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["right fixture"]
}
]
},
"event_runtime_records": []
}
});
let left_path = write_temp_json("runtime-diff-packed-events-left", &left);
let right_path = write_temp_json("runtime-diff-packed-events-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.live_record_count"
|| entry.path == "$.packed_event_collection.live_entry_ids[1]"
}));
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-packed-import",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 7,
"live_record_count": 1,
"live_entry_ids": [7],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 7,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left placeholder"]
}
]
},
"event_runtime_records": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-packed-import",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 7,
"live_record_count": 1,
"live_entry_ids": [7],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 7,
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [
{
"label": "primary_text_band",
"packed_len": 5,
"present": true,
"preview": "Alpha"
}
],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_world_flag",
"key": "from_packed_root",
"value": true
}
],
"executable_import_ready": true,
"notes": ["decoded test record"]
}
]
},
"event_runtime_records": [
{
"record_id": 7,
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"has_fired": false,
"effects": [
{
"kind": "set_world_flag",
"key": "from_packed_root",
"value": true
}
]
}
]
}
});
let left_path = write_temp_json("runtime-diff-packed-import-left", &left);
let right_path = write_temp_json("runtime-diff-packed-import-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
let diff_paths: Vec<_> = differences.iter().map(|entry| entry.path.clone()).collect();
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.records[0].decode_status"
|| entry.path == "$.packed_event_collection.records[0].decoded_actions[0]"
}));
assert_diff_paths_include(&diff_paths, "$.event_runtime_records[0]");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}

View file

@ -0,0 +1,176 @@
use super::*;
#[test]
fn exports_and_summarizes_runtime_snapshot() {
let fixture = serde_json::json!({
"format_version": 1,
"fixture_id": "runtime-export-test",
"source": { "kind": "synthetic" },
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"event_runtime_records": []
},
"commands": [
{
"kind": "step_count",
"steps": 2
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flag_count": 0,
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
}
});
let fixture_path = write_temp_json("runtime-export-fixture", &fixture);
let snapshot_path = std::env::temp_dir().join(format!(
"rrt-cli-runtime-export-{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos()
));
export_fixture_state(&fixture_path, &snapshot_path).expect("fixture export should succeed");
summarize_state(&snapshot_path).expect("snapshot summary should succeed");
let _ = fs::remove_file(fixture_path);
let _ = fs::remove_file(snapshot_path);
}
#[test]
fn snapshots_runtime_state_input_into_snapshot() {
let input = serde_json::json!({
"format_version": 1,
"input_id": "runtime-input-test",
"source": {
"description": "test runtime state input"
},
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 9
},
"world_flags": {},
"companies": [],
"event_runtime_records": [],
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
}
});
let input_path = write_temp_json("runtime-input", &input);
let output_path = std::env::temp_dir().join(format!(
"rrt-cli-runtime-input-{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos()
));
snapshot_state(&input_path, &output_path).expect("runtime snapshot should succeed");
summarize_state(&output_path).expect("snapshotted output should summarize");
let _ = fs::remove_file(input_path);
let _ = fs::remove_file(output_path);
}
#[test]
fn exports_runtime_save_slice_document_from_loaded_slice() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let output_path = std::env::temp_dir().join(format!("rrt-export-save-slice-test-{nonce}.json"));
let smp_path = PathBuf::from("captured-test.gms");
let report = export_runtime_save_slice_document(
&smp_path,
&output_path,
SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
world_issue_37_state: None,
world_economic_tuning_state: None,
world_finance_neighborhood_state: None,
world_locomotive_policy_state: None,
company_roster: None,
chairman_profile_table: None,
region_collection: None,
region_fixed_row_run_summary: None,
placed_structure_collection: None,
placed_structure_dynamic_side_buffer_summary: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec!["exported for test".to_string()],
},
)
.expect("save slice export should succeed");
assert_eq!(report.save_slice_id, "captured-test");
let document = load_runtime_save_slice_document(&output_path)
.expect("exported save slice document should load");
assert_eq!(document.save_slice_id, "captured-test");
assert_eq!(
document.source.original_save_filename.as_deref(),
Some("captured-test.gms")
);
let _ = fs::remove_file(output_path);
}
#[test]
fn exports_runtime_overlay_import_document() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let output_path =
std::env::temp_dir().join(format!("rrt-export-overlay-import-test-{nonce}.json"));
let snapshot_path = PathBuf::from("base-snapshot.json");
let save_slice_path = PathBuf::from("captured-save-slice.json");
let report =
export_runtime_overlay_import_document(&snapshot_path, &save_slice_path, &output_path)
.expect("overlay import export should succeed");
let expected_import_id = output_path
.file_stem()
.and_then(|stem| stem.to_str())
.expect("output path should have a stem")
.to_string();
assert_eq!(report.import_id, expected_import_id);
let document = load_runtime_overlay_import_document(&output_path)
.expect("exported overlay import document should load");
assert_eq!(document.import_id, expected_import_id);
assert_eq!(document.base_snapshot_path, "base-snapshot.json");
assert_eq!(document.save_slice_path, "captured-save-slice.json");
let _ = fs::remove_file(output_path);
}

View file

@ -0,0 +1,77 @@
use super::*;
#[test]
fn summarizes_runtime_fixture() {
let fixture = serde_json::json!({
"format_version": 1,
"fixture_id": "runtime-fixture-test",
"source": { "kind": "synthetic" },
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": []
},
"commands": [
{
"kind": "advance_to",
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 3
}
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 3
},
"world_flag_count": 1,
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
},
"expected_state_fragment": {
"calendar": {
"tick_slot": 3
},
"world_flags": {
"sandbox": false
}
}
});
let path = write_temp_json("runtime-fixture", &fixture);
summarize_fixture(&path).expect("fixture summary should succeed");
let _ = fs::remove_file(path);
}
#[test]
fn summarizes_snapshot_backed_fixture_with_packed_event_collection() {
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-collection-from-snapshot.json");
summarize_fixture(&fixture_path)
.expect("snapshot-backed packed-event fixture should summarize");
}
#[test]
fn summarizes_snapshot_backed_fixture_with_imported_packed_event_record() {
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-record-import-from-snapshot.json");
summarize_fixture(&fixture_path)
.expect("snapshot-backed imported packed-event fixture should summarize");
}

View file

@ -0,0 +1,7 @@
use super::*;
mod diff;
mod document_io;
mod fixture_summary;
mod save_slice_overlay;
mod snapshot_io;

View file

@ -0,0 +1,265 @@
use super::*;
#[test]
fn summarizes_save_slice_backed_fixtures() {
let parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json");
let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json");
let negative_company_scope_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json");
let deactivate_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json");
let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json");
let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json");
let named_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-named-locomotive-availability-save-slice-fixture.json",
);
let missing_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-missing-catalog-save-slice-fixture.json",
);
let save_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-save-slice-fixture.json",
);
let overlay_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json");
let save_locomotive_cost_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-cost-save-slice-fixture.json");
let overlay_locomotive_cost_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json");
let scalar_band_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json",
);
let world_scalar_executable_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-executable-save-slice-fixture.json",
);
let world_scalar_override_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-world-scalar-override-save-slice-fixture.json");
let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json");
let runtime_variable_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(
"../../fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json",
);
let cargo_economics_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-economics-save-slice-fixture.json");
let cargo_economics_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-economics-parity-save-slice-fixture.json");
let add_building_shell_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json");
let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json");
let world_scalar_condition_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json",
);
let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-catalog-save-slice-fixture.json");
let chairman_cash_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json");
let chairman_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-cash-save-slice-fixture.json");
let chairman_condition_true_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json",
);
let chairman_human_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json");
let deactivate_chairman_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json");
let deactivate_chairman_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-save-slice-fixture.json");
let deactivate_chairman_ai_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json");
let deactivate_company_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-save-slice-fixture.json");
let track_capacity_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-save-slice-fixture.json");
let negative_company_scope_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-negative-company-scope-save-slice-fixture.json");
let missing_chairman_context_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json",
);
let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json");
let chairman_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json");
let chairman_condition_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-condition-save-slice-fixture.json");
let company_governance_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(
"../../fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json",
);
let company_governance_condition_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-company-governance-condition-save-slice-fixture.json",
);
let selection_only_context_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json");
let credit_rating_descriptor_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-credit-rating-descriptor-save-slice-fixture.json",
);
let stock_prices_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-stock-prices-shell-save-slice-fixture.json");
let game_won_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json");
let merger_premium_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-merger-premium-shell-save-slice-fixture.json");
let set_human_control_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json",
);
let investor_confidence_condition_save_fixture = PathBuf::from(env!(
"CARGO_MANIFEST_DIR"
))
.join(
"../../fixtures/runtime/packed-event-investor-confidence-condition-save-slice-fixture.json",
);
let management_attitude_condition_save_fixture = PathBuf::from(env!(
"CARGO_MANIFEST_DIR"
))
.join(
"../../fixtures/runtime/packed-event-management-attitude-condition-save-slice-fixture.json",
);
summarize_fixture(&parity_fixture).expect("save-slice-backed parity fixture should summarize");
summarize_fixture(&selective_fixture)
.expect("save-slice-backed selective-import fixture should summarize");
summarize_fixture(&overlay_fixture)
.expect("overlay-backed selective-import fixture should summarize");
summarize_fixture(&symbolic_overlay_fixture)
.expect("overlay-backed symbolic-target fixture should summarize");
summarize_fixture(&negative_company_scope_overlay_fixture)
.expect("overlay-backed negative-sentinel company-scope fixture should summarize");
summarize_fixture(&deactivate_overlay_fixture)
.expect("overlay-backed deactivate-company fixture should summarize");
summarize_fixture(&track_capacity_overlay_fixture)
.expect("overlay-backed track-capacity fixture should summarize");
summarize_fixture(&mixed_overlay_fixture)
.expect("overlay-backed mixed real-row fixture should summarize");
summarize_fixture(&named_locomotive_fixture)
.expect("save-slice-backed named locomotive availability fixture should summarize");
summarize_fixture(&missing_catalog_fixture).expect(
"save-slice-backed locomotive availability missing-catalog fixture should summarize",
);
summarize_fixture(&save_locomotive_fixture)
.expect("save-slice-backed locomotive availability descriptor fixture should summarize");
summarize_fixture(&overlay_locomotive_fixture)
.expect("overlay-backed locomotive availability fixture should summarize");
summarize_fixture(&save_locomotive_cost_fixture)
.expect("save-slice-backed locomotive cost fixture should summarize");
summarize_fixture(&overlay_locomotive_cost_fixture)
.expect("overlay-backed locomotive cost fixture should summarize");
summarize_fixture(&scalar_band_parity_fixture)
.expect("save-slice-backed recovered scalar-band parity fixture should summarize");
summarize_fixture(&world_scalar_executable_fixture)
.expect("save-slice-backed executable world-scalar fixture should summarize");
summarize_fixture(&world_scalar_override_fixture)
.expect("save-slice-backed world-scalar override fixture should summarize");
summarize_fixture(&runtime_variable_overlay_fixture)
.expect("overlay-backed runtime-variable fixture should summarize");
summarize_fixture(&runtime_variable_condition_overlay_fixture)
.expect("overlay-backed runtime-variable condition fixture should summarize");
summarize_fixture(&cargo_economics_fixture)
.expect("save-slice-backed cargo-economics fixture should summarize");
summarize_fixture(&cargo_economics_parity_fixture)
.expect("save-slice-backed cargo-economics parity fixture should summarize");
summarize_fixture(&add_building_shell_fixture)
.expect("save-slice-backed add-building shell fixture should summarize");
summarize_fixture(&world_scalar_condition_fixture)
.expect("save-slice-backed executable world-scalar condition fixture should summarize");
summarize_fixture(&world_scalar_condition_parity_fixture)
.expect("save-slice-backed parity world-scalar condition fixture should summarize");
summarize_fixture(&cargo_catalog_fixture)
.expect("save-slice-backed cargo catalog fixture should summarize");
summarize_fixture(&chairman_cash_overlay_fixture)
.expect("overlay-backed chairman-cash fixture should summarize");
summarize_fixture(&chairman_cash_save_fixture)
.expect("save-slice-backed chairman-cash fixture should summarize");
summarize_fixture(&chairman_condition_true_save_fixture)
.expect("save-slice-backed condition-true chairman fixture should summarize");
summarize_fixture(&chairman_human_cash_save_fixture)
.expect("save-slice-backed human-chairman cash fixture should summarize");
summarize_fixture(&deactivate_chairman_overlay_fixture)
.expect("overlay-backed deactivate-chairman fixture should summarize");
summarize_fixture(&deactivate_chairman_save_fixture)
.expect("save-slice-backed deactivate-chairman fixture should summarize");
summarize_fixture(&deactivate_chairman_ai_save_fixture)
.expect("save-slice-backed AI-chairman deactivate fixture should summarize");
summarize_fixture(&deactivate_company_save_fixture)
.expect("save-slice-backed deactivate-company fixture should summarize");
summarize_fixture(&track_capacity_save_fixture)
.expect("save-slice-backed track-capacity fixture should summarize");
summarize_fixture(&negative_company_scope_save_fixture)
.expect("save-slice-backed negative-sentinel company-scope fixture should summarize");
summarize_fixture(&missing_chairman_context_fixture)
.expect("save-slice-backed chairman missing-context fixture should summarize");
summarize_fixture(&chairman_scope_parity_fixture)
.expect("save-slice-backed chairman scope parity fixture should summarize");
summarize_fixture(&chairman_condition_overlay_fixture)
.expect("overlay-backed chairman condition fixture should summarize");
summarize_fixture(&chairman_condition_save_fixture)
.expect("save-slice-backed chairman condition fixture should summarize");
summarize_fixture(&company_governance_condition_overlay_fixture)
.expect("overlay-backed company governance condition fixture should summarize");
summarize_fixture(&company_governance_condition_save_fixture)
.expect("save-slice-backed company governance condition fixture should summarize");
summarize_fixture(&selection_only_context_overlay_fixture)
.expect("overlay-backed selection-only save context fixture should summarize");
summarize_fixture(&credit_rating_descriptor_save_fixture)
.expect("save-slice-backed credit-rating descriptor fixture should summarize");
summarize_fixture(&stock_prices_shell_save_fixture)
.expect("save-slice-backed shell-owned stock-prices fixture should summarize");
summarize_fixture(&game_won_shell_save_fixture)
.expect("save-slice-backed shell-owned game-won fixture should summarize");
summarize_fixture(&merger_premium_shell_save_fixture)
.expect("save-slice-backed shell-owned merger-premium fixture should summarize");
summarize_fixture(&set_human_control_shell_save_fixture)
.expect("save-slice-backed shell-owned set-human-control fixture should summarize");
summarize_fixture(&investor_confidence_condition_save_fixture)
.expect("save-slice-backed investor-confidence condition fixture should summarize");
summarize_fixture(&management_attitude_condition_save_fixture)
.expect("save-slice-backed management-attitude condition fixture should summarize");
}
#[test]
fn diffs_runtime_states_between_save_slice_and_overlay_import() {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice.json");
let overlay = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-overlay.json");
let left_state =
load_normalized_runtime_state(&base).expect("save-slice-backed state should load");
let right_state =
load_normalized_runtime_state(&overlay).expect("overlay-backed state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.companies[0].company_id"
|| entry.path == "$.packed_event_collection.imported_runtime_record_count"
|| entry.path == "$.packed_event_collection.records[1].import_outcome"
|| entry.path == "$.event_runtime_records[1].record_id"
}));
}
#[test]
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice.json");
let right_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice.json");
let left_state = load_normalized_runtime_state(&left_path)
.expect("left save-slice-backed state should load");
let right_state = load_normalized_runtime_state(&right_path)
.expect("right save-slice-backed state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.imported_runtime_record_count"
|| entry.path == "$.packed_event_collection.records[0].decode_status"
}));
}

View file

@ -0,0 +1,18 @@
use super::*;
#[test]
fn loads_snapshot_as_outcome() {
let snapshot = FinanceSnapshot {
policy: rrt_model::finance::AnnualFinancePolicy {
dividends_allowed: false,
..rrt_model::finance::AnnualFinancePolicy::default()
},
company: CompanyFinanceState::default(),
};
let path = write_temp_json("snapshot", &snapshot);
let outcome = load_finance_outcome(&path).expect("snapshot should load");
assert_eq!(outcome.evaluation.decision, AnnualFinanceDecision::NoAction);
let _ = fs::remove_file(path);
}

View file

@ -0,0 +1,6 @@
pub(crate) fn assert_diff_paths_include(paths: &[String], needle: &str) {
assert!(
paths.iter().any(|path| path == needle),
"expected diff paths to include {needle}, got {paths:?}"
);
}

View file

@ -0,0 +1,5 @@
mod json;
mod temp_files;
pub(crate) use json::*;
pub(crate) use temp_files::*;

View file

@ -0,0 +1,15 @@
use std::fs;
use std::path::PathBuf;
use serde::Serialize;
pub(crate) fn write_temp_json<T: Serialize>(stem: &str, value: &T) -> PathBuf {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("rrt-cli-{stem}-{nonce}.json"));
let bytes = serde_json::to_vec_pretty(value).expect("json serialization should succeed");
fs::write(&path, bytes).expect("temp json should be written");
path
}

View file

@ -0,0 +1,119 @@
use std::collections::BTreeSet;
use std::fs;
use std::io::Read;
use std::path::Path;
use rrt_model::{
BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, load_binary_summary, load_function_map,
};
use sha2::{Digest, Sha256};
pub(crate) fn validate_required_files(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut missing = Vec::new();
for relative in REQUIRED_EXPORTS {
let path = repo_root.join(relative);
if !path.exists() {
missing.push(path.display().to_string());
}
}
if !missing.is_empty() {
return Err(format!("missing required exports: {}", missing.join(", ")).into());
}
Ok(())
}
pub(crate) fn validate_binary_summary(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let summary = load_binary_summary(&repo_root.join(BINARY_SUMMARY_PATH))?;
let actual_exe = repo_root.join(CANONICAL_EXE_PATH);
if !actual_exe.exists() {
return Err(format!("canonical exe missing: {}", actual_exe.display()).into());
}
let actual_hash = sha256_file(&actual_exe)?;
if actual_hash != summary.sha256 {
return Err(format!(
"hash mismatch for {}: summary has {}, actual file is {}",
actual_exe.display(),
summary.sha256,
actual_hash
)
.into());
}
let docs_readme = fs::read_to_string(repo_root.join("docs/README.md"))?;
if !docs_readme.contains(&summary.sha256) {
return Err("docs/README.md does not include the canonical SHA-256".into());
}
Ok(())
}
pub(crate) fn validate_function_map(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let records = load_function_map(&repo_root.join(FUNCTION_MAP_PATH))?;
let mut seen = BTreeSet::new();
for record in records {
if !(1..=5).contains(&record.confidence) {
return Err(format!(
"invalid confidence {} for {} {}",
record.confidence, record.address, record.name
)
.into());
}
if !seen.insert(record.address) {
return Err(format!("duplicate function address {}", record.address).into());
}
if record.name.trim().is_empty() {
return Err(format!("blank function name at {}", record.address).into());
}
}
Ok(())
}
pub(crate) fn validate_control_loop_atlas(
repo_root: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let atlas = fs::read_to_string(repo_root.join(CONTROL_LOOP_ATLAS_PATH))?;
for heading in REQUIRED_ATLAS_HEADINGS {
if !atlas.contains(heading) {
return Err(format!("missing atlas heading `{heading}`").into());
}
}
for marker in [
"- Roots:",
"- Trigger/Cadence:",
"- Key Dispatchers:",
"- State Anchors:",
"- Subsystem Handoffs:",
"- Evidence:",
"- Open Questions:",
] {
if !atlas.contains(marker) {
return Err(format!("atlas is missing field marker `{marker}`").into());
}
}
Ok(())
}
fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 8192];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(format!("{:x}", hasher.finalize()))
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,8 @@ pub mod diff;
pub mod load;
pub mod normalize;
pub mod schema;
pub mod summary;
pub mod validation;
pub use diff::{JsonDiffEntry, diff_json_values};
pub use load::{load_fixture_document, load_fixture_document_from_str};

View file

@ -1,9 +1,40 @@
use std::path::{Path, PathBuf};
use rrt_runtime::{
load_runtime_save_slice_document, load_runtime_snapshot_document, load_runtime_state_import,
project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
validate_runtime_snapshot_document,
use rrt_runtime::documents::{
build_runtime_state_input_from_save_slice, load_runtime_save_slice_document,
load_runtime_state_input,
};
use rrt_runtime::persistence::{
load_runtime_snapshot_document, validate_runtime_snapshot_document,
};
#[cfg(test)]
use rrt_runtime::persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
};
use rrt_runtime::validation::validate_runtime_save_slice_document;
#[cfg(test)]
use rrt_runtime::documents::{
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, save_runtime_overlay_import_document,
save_runtime_save_slice_document,
};
#[cfg(test)]
use rrt_runtime::event::effects::RuntimeEffect;
#[cfg(test)]
use rrt_runtime::event::targets::RuntimeCompanyTarget;
#[cfg(test)]
use rrt_runtime::inspect::smp::{
events::{SmpLoadedEventRuntimeCollectionSummary, SmpLoadedPackedEventRecordSummary},
save_load::SmpLoadedSaveSlice,
};
#[cfg(test)]
use rrt_runtime::state::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
};
use crate::{FixtureDocument, FixtureStateOrigin, RawFixtureDocument};
@ -35,10 +66,10 @@ fn resolve_raw_fixture_document(
let specified_state_inputs = usize::from(raw.state.is_some())
+ usize::from(raw.state_snapshot_path.is_some())
+ usize::from(raw.state_save_slice_path.is_some())
+ usize::from(raw.state_import_path.is_some());
+ usize::from(raw.state_input_path.is_some());
if specified_state_inputs != 1 {
return Err(
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_import_path"
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_input_path"
.into(),
);
}
@ -47,7 +78,7 @@ fn resolve_raw_fixture_document(
&raw.state,
&raw.state_snapshot_path,
&raw.state_save_slice_path,
&raw.state_import_path,
&raw.state_input_path,
) {
(Some(state), None, None, None) => state.clone(),
(None, Some(snapshot_path), None, None) => {
@ -70,7 +101,7 @@ fn resolve_raw_fixture_document(
save_slice_path.display()
)
})?;
project_save_slice_to_runtime_state_import(
build_runtime_state_input_from_save_slice(
&document.save_slice,
&document.save_slice_id,
document.source.description.clone(),
@ -83,13 +114,13 @@ fn resolve_raw_fixture_document(
})?
.state
}
(None, None, None, Some(import_path)) => {
let import_path = resolve_snapshot_path(base_dir, import_path);
load_runtime_state_import(&import_path)
(None, None, None, Some(input_path)) => {
let input_path = resolve_snapshot_path(base_dir, input_path);
load_runtime_state_input(&input_path)
.map_err(|err| {
format!(
"failed to load runtime import {}: {err}",
import_path.display()
"failed to load runtime input {}: {err}",
input_path.display()
)
})?
.state
@ -100,11 +131,11 @@ fn resolve_raw_fixture_document(
let state_origin = match (
raw.state_snapshot_path,
raw.state_save_slice_path,
raw.state_import_path,
raw.state_input_path,
) {
(Some(snapshot_path), None, None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
(None, Some(save_slice_path), None) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
(None, None, Some(import_path)) => FixtureStateOrigin::ImportPath(import_path),
(None, None, Some(input_path)) => FixtureStateOrigin::InputPath(input_path),
_ => FixtureStateOrigin::Inline,
};
@ -133,15 +164,6 @@ fn resolve_snapshot_path(base_dir: &Path, snapshot_path: &str) -> PathBuf {
mod tests {
use super::*;
use crate::FixtureStateOrigin;
use rrt_runtime::{
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
save_runtime_snapshot_document,
};
use std::collections::BTreeMap;
#[test]
@ -268,7 +290,7 @@ mod tests {
original_save_sha256: None,
notes: vec![],
},
save_slice: rrt_runtime::SmpLoadedSaveSlice {
save_slice: SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -335,7 +357,7 @@ mod tests {
}
#[test]
fn loads_fixture_from_relative_import_path() {
fn loads_fixture_from_relative_input_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
@ -362,9 +384,9 @@ mod tests {
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![rrt_runtime::RuntimeCompany {
companies: vec![RuntimeCompany {
company_id: 42,
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
credit_rating_score: None,
@ -417,7 +439,7 @@ mod tests {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "slice".to_string(),
source: RuntimeSaveSliceDocumentSource::default(),
save_slice: rrt_runtime::SmpLoadedSaveSlice {
save_slice: SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -440,64 +462,62 @@ mod tests {
placed_structure_collection: None,
placed_structure_dynamic_side_buffer_summary: None,
special_conditions_table: None,
event_runtime_collection: Some(
rrt_runtime::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7,
live_record_count: 1,
live_entry_ids: vec![7],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records_with_trigger_kind: 0,
records_missing_trigger_kind: 0,
nondirect_compact_record_count: 0,
nondirect_compact_records_missing_trigger_kind: 0,
trigger_kinds_present: vec![],
add_building_dispatch_strip_record_indexes: vec![],
add_building_dispatch_strip_descriptor_labels: vec![],
add_building_dispatch_strip_records_with_trigger_kind: 0,
add_building_dispatch_strip_records_missing_trigger_kind: 0,
add_building_dispatch_strip_row_shape_families: vec![],
add_building_dispatch_strip_signature_families: vec![],
add_building_dispatch_strip_condition_tuple_families: vec![],
add_building_dispatch_strip_signature_condition_clusters: vec![],
control_lane_notes: vec![],
records: vec![rrt_runtime::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
compact_control: None,
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,
}],
executable_import_ready: false,
notes: vec![],
event_runtime_collection: Some(SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7,
live_record_count: 1,
live_entry_ids: vec![7],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records_with_trigger_kind: 0,
records_missing_trigger_kind: 0,
nondirect_compact_record_count: 0,
nondirect_compact_records_missing_trigger_kind: 0,
trigger_kinds_present: vec![],
add_building_dispatch_strip_record_indexes: vec![],
add_building_dispatch_strip_descriptor_labels: vec![],
add_building_dispatch_strip_records_with_trigger_kind: 0,
add_building_dispatch_strip_records_missing_trigger_kind: 0,
add_building_dispatch_strip_row_shape_families: vec![],
add_building_dispatch_strip_signature_families: vec![],
add_building_dispatch_strip_condition_tuple_families: vec![],
add_building_dispatch_strip_signature_condition_clusters: vec![],
control_lane_notes: vec![],
records: vec![SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
compact_control: None,
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,
}],
},
),
executable_import_ready: false,
notes: vec![],
}],
}),
notes: vec![],
},
};
@ -521,7 +541,7 @@ mod tests {
"source": {
"kind": "captured-runtime"
},
"state_import_path": "overlay.json",
"state_input_path": "overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
@ -540,7 +560,7 @@ mod tests {
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::ImportPath("overlay.json".to_string())
FixtureStateOrigin::InputPath("overlay.json".to_string())
);
assert_eq!(fixture.state.event_runtime_records.len(), 1);

View file

@ -1,6 +1,6 @@
use serde_json::Value;
use rrt_runtime::RuntimeState;
use rrt_runtime::state::RuntimeState;
pub fn normalize_runtime_state(state: &RuntimeState) -> Result<Value, Box<dyn std::error::Error>> {
Ok(serde_json::to_value(state)?)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use rrt_runtime::engine::StepCommand;
use rrt_runtime::state::RuntimeState;
use super::ExpectedRuntimeSummary;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct FixtureSource {
pub kind: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
pub state: RuntimeState,
pub state_origin: FixtureStateOrigin,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
#[serde(default)]
pub expected_state_fragment: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FixtureStateOrigin {
Inline,
SnapshotPath(String),
SaveSlicePath(String),
InputPath(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawFixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
#[serde(default)]
pub state: Option<RuntimeState>,
#[serde(default)]
pub state_snapshot_path: Option<String>,
#[serde(default)]
pub state_save_slice_path: Option<String>,
#[serde(default)]
pub state_input_path: Option<String>,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
#[serde(default)]
pub expected_state_fragment: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureValidationReport {
pub fixture_id: String,
pub valid: bool,
pub issue_count: usize,
pub issues: Vec<String>,
}

View file

@ -0,0 +1,17 @@
pub mod document;
pub mod state_fragment;
pub mod summary;
mod summary_compare;
pub mod validate;
pub const FIXTURE_FORMAT_VERSION: u32 = 1;
pub use document::{
FixtureDocument, FixtureSource, FixtureStateOrigin, FixtureValidationReport, RawFixtureDocument,
};
pub use state_fragment::compare_expected_state_fragment;
pub use summary::ExpectedRuntimeSummary;
pub use validate::validate_fixture_document;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,49 @@
use serde_json::Value;
pub fn compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec<String> {
let mut mismatches = Vec::new();
compare_expected_state_fragment_at_path("$", expected, actual, &mut mismatches);
mismatches
}
fn compare_expected_state_fragment_at_path(
path: &str,
expected: &Value,
actual: &Value,
mismatches: &mut Vec<String>,
) {
match (expected, actual) {
(Value::Object(expected_map), Value::Object(actual_map)) => {
for (key, expected_value) in expected_map {
let next_path = format!("{path}.{key}");
match actual_map.get(key) {
Some(actual_value) => compare_expected_state_fragment_at_path(
&next_path,
expected_value,
actual_value,
mismatches,
),
None => mismatches.push(format!("{next_path} missing in actual state")),
}
}
}
(Value::Array(expected_items), Value::Array(actual_items)) => {
for (index, expected_item) in expected_items.iter().enumerate() {
let next_path = format!("{path}[{index}]");
match actual_items.get(index) {
Some(actual_item) => compare_expected_state_fragment_at_path(
&next_path,
expected_item,
actual_item,
mismatches,
),
None => mismatches.push(format!("{next_path} missing in actual state")),
}
}
}
_ if expected != actual => mismatches.push(format!(
"{path} mismatch: expected {expected:?}, got {actual:?}"
)),
_ => {}
}
}

View file

@ -0,0 +1,302 @@
use serde::{Deserialize, Serialize};
use rrt_runtime::state::CalendarPoint;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub calendar: Option<CalendarPoint>,
#[serde(default)]
pub calendar_projection_source: Option<String>,
#[serde(default)]
pub calendar_projection_is_placeholder: Option<bool>,
#[serde(default)]
pub world_flag_count: Option<usize>,
#[serde(default)]
pub world_restore_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub world_restore_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub world_restore_sandbox_enabled: Option<bool>,
#[serde(default)]
pub world_restore_seed_tuple_written_from_raw_lane: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_requires_shell_context: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub world_restore_packed_year_word_raw_u16: Option<u16>,
#[serde(default)]
pub world_restore_partial_year_progress_raw_u8: Option<u8>,
#[serde(default)]
pub world_restore_current_calendar_tuple_word_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_current_calendar_tuple_word_2_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_absolute_counter_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_absolute_counter_mirror_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_slot: Option<u8>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save:
Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_bio_accelerator_cars_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_wartime_cargos_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
#[serde(default)]
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)]
pub world_restore_limited_track_building_amount: Option<i32>,
#[serde(default)]
pub world_restore_economic_status_code: Option<i32>,
#[serde(default)]
pub world_restore_territory_access_cost: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_38_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_39_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_3a_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_multiplier_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_multiplier_value_f32_text: Option<String>,
#[serde(default)]
pub world_restore_finance_neighborhood_count: Option<usize>,
#[serde(default)]
pub world_restore_finance_neighborhood_labels: Option<Vec<String>>,
#[serde(default)]
pub world_restore_economic_tuning_mirror_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_economic_tuning_mirror_value_f32_text: Option<String>,
#[serde(default)]
pub world_restore_economic_tuning_lane_count: Option<usize>,
#[serde(default)]
pub world_restore_economic_tuning_lane_value_f32_text: Option<Vec<String>>,
#[serde(default)]
pub world_restore_absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub world_restore_absolute_counter_adjustment_context: Option<String>,
#[serde(default)]
pub metadata_count: Option<usize>,
#[serde(default)]
pub company_count: Option<usize>,
#[serde(default)]
pub active_company_count: Option<usize>,
#[serde(default)]
pub company_market_state_owner_count: Option<usize>,
#[serde(default)]
pub selected_company_outstanding_shares: Option<u32>,
#[serde(default)]
pub selected_company_bond_count: Option<u8>,
#[serde(default)]
pub selected_company_largest_live_bond_principal: Option<u32>,
#[serde(default)]
pub selected_company_highest_coupon_live_bond_principal: Option<u32>,
#[serde(default)]
pub selected_company_assigned_share_pool: Option<u32>,
#[serde(default)]
pub selected_company_unassigned_share_pool: Option<u32>,
#[serde(default)]
pub selected_company_cached_share_price: Option<i64>,
#[serde(default)]
pub selected_company_cached_share_price_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_mutable_support_scalar_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_stat_band_root_0cfb_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_0d7f_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_1c47_count: Option<usize>,
#[serde(default)]
pub selected_company_last_dividend_year: Option<u32>,
#[serde(default)]
pub selected_company_years_since_founding: Option<u32>,
#[serde(default)]
pub selected_company_years_since_last_bankruptcy: Option<u32>,
#[serde(default)]
pub selected_company_years_since_last_dividend: Option<u32>,
#[serde(default)]
pub selected_company_current_partial_year_weight_numerator: Option<i64>,
#[serde(default)]
pub selected_company_current_issue_absolute_counter: Option<u32>,
#[serde(default)]
pub selected_company_prior_issue_absolute_counter: Option<u32>,
#[serde(default)]
pub selected_company_current_issue_age_absolute_counter_delta: Option<i64>,
#[serde(default)]
pub selected_company_chairman_bonus_year: Option<u32>,
#[serde(default)]
pub selected_company_chairman_bonus_amount: Option<i32>,
#[serde(default)]
pub player_count: Option<usize>,
#[serde(default)]
pub chairman_profile_count: Option<usize>,
#[serde(default)]
pub active_chairman_profile_count: Option<usize>,
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub linked_chairman_company_count: Option<usize>,
#[serde(default)]
pub company_takeover_cooldown_count: Option<usize>,
#[serde(default)]
pub company_merger_cooldown_count: Option<usize>,
#[serde(default)]
pub train_count: Option<usize>,
#[serde(default)]
pub active_train_count: Option<usize>,
#[serde(default)]
pub retired_train_count: Option<usize>,
#[serde(default)]
pub locomotive_catalog_count: Option<usize>,
#[serde(default)]
pub cargo_catalog_count: Option<usize>,
#[serde(default)]
pub territory_count: Option<usize>,
#[serde(default)]
pub company_territory_track_count: Option<usize>,
#[serde(default)]
pub packed_event_collection_present: Option<bool>,
#[serde(default)]
pub packed_event_record_count: Option<usize>,
#[serde(default)]
pub packed_event_decoded_record_count: Option<usize>,
#[serde(default)]
pub packed_event_imported_runtime_record_count: Option<usize>,
#[serde(default)]
pub packed_event_parity_only_record_count: Option<usize>,
#[serde(default)]
pub packed_event_unsupported_record_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_company_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_chairman_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_chairman_target_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_player_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_named_territory_binding_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_ordinary_condition_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_world_condition_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_compact_control_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_shell_owned_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_evidence_blocked_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_world_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_access_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_access_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_train_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_train_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_locomotive_catalog_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_confiscation_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_structural_only_count: Option<usize>,
#[serde(default)]
pub event_runtime_record_count: Option<usize>,
#[serde(default)]
pub candidate_availability_count: Option<usize>,
#[serde(default)]
pub zero_candidate_availability_count: Option<usize>,
#[serde(default)]
pub named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub zero_named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub named_locomotive_cost_count: Option<usize>,
#[serde(default)]
pub cargo_production_override_count: Option<usize>,
#[serde(default)]
pub world_runtime_variable_count: Option<usize>,
#[serde(default)]
pub company_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub player_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub territory_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub world_scalar_override_count: Option<usize>,
#[serde(default)]
pub special_condition_count: Option<usize>,
#[serde(default)]
pub enabled_special_condition_count: Option<usize>,
#[serde(default)]
pub save_profile_kind: Option<String>,
#[serde(default)]
pub save_profile_family: Option<String>,
#[serde(default)]
pub save_profile_map_path: Option<String>,
#[serde(default)]
pub save_profile_display_name: Option<String>,
#[serde(default)]
pub save_profile_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub save_profile_sandbox_enabled: Option<bool>,
#[serde(default)]
pub save_profile_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub save_profile_staged_profile_copy_on_restore: Option<bool>,
#[serde(default)]
pub total_event_record_service_count: Option<u64>,
#[serde(default)]
pub periodic_boundary_call_count: Option<u64>,
#[serde(default)]
pub total_trigger_dispatch_count: Option<u64>,
#[serde(default)]
pub dirty_rerun_count: Option<u64>,
#[serde(default)]
pub total_company_cash: Option<i64>,
}

View file

@ -0,0 +1,345 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_collections_prefix(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(count) = expected.player_count {
if actual.player_count != count {
mismatches.push(format!(
"player_count mismatch: expected {count}, got {}",
actual.player_count
));
}
}
if let Some(count) = expected.chairman_profile_count {
if actual.chairman_profile_count != count {
mismatches.push(format!(
"chairman_profile_count mismatch: expected {count}, got {}",
actual.chairman_profile_count
));
}
}
if let Some(count) = expected.active_chairman_profile_count {
if actual.active_chairman_profile_count != count {
mismatches.push(format!(
"active_chairman_profile_count mismatch: expected {count}, got {}",
actual.active_chairman_profile_count
));
}
}
if let Some(selected_id) = expected.selected_chairman_profile_id {
if actual.selected_chairman_profile_id != Some(selected_id) {
mismatches.push(format!(
"selected_chairman_profile_id mismatch: expected {selected_id:?}, got {:?}",
actual.selected_chairman_profile_id
));
}
}
if let Some(count) = expected.linked_chairman_company_count {
if actual.linked_chairman_company_count != count {
mismatches.push(format!(
"linked_chairman_company_count mismatch: expected {count}, got {}",
actual.linked_chairman_company_count
));
}
}
if let Some(count) = expected.company_takeover_cooldown_count {
if actual.company_takeover_cooldown_count != count {
mismatches.push(format!(
"company_takeover_cooldown_count mismatch: expected {count}, got {}",
actual.company_takeover_cooldown_count
));
}
}
if let Some(count) = expected.company_merger_cooldown_count {
if actual.company_merger_cooldown_count != count {
mismatches.push(format!(
"company_merger_cooldown_count mismatch: expected {count}, got {}",
actual.company_merger_cooldown_count
));
}
}
if let Some(count) = expected.train_count {
if actual.train_count != count {
mismatches.push(format!(
"train_count mismatch: expected {count}, got {}",
actual.train_count
));
}
}
if let Some(count) = expected.active_train_count {
if actual.active_train_count != count {
mismatches.push(format!(
"active_train_count mismatch: expected {count}, got {}",
actual.active_train_count
));
}
}
if let Some(count) = expected.retired_train_count {
if actual.retired_train_count != count {
mismatches.push(format!(
"retired_train_count mismatch: expected {count}, got {}",
actual.retired_train_count
));
}
}
if let Some(count) = expected.locomotive_catalog_count {
if actual.locomotive_catalog_count != count {
mismatches.push(format!(
"locomotive_catalog_count mismatch: expected {count}, got {}",
actual.locomotive_catalog_count
));
}
}
if let Some(count) = expected.cargo_catalog_count {
if actual.cargo_catalog_count != count {
mismatches.push(format!(
"cargo_catalog_count mismatch: expected {count}, got {}",
actual.cargo_catalog_count
));
}
}
if let Some(count) = expected.territory_count {
if actual.territory_count != count {
mismatches.push(format!(
"territory_count mismatch: expected {count}, got {}",
actual.territory_count
));
}
}
if let Some(count) = expected.company_territory_track_count {
if actual.company_territory_track_count != count {
mismatches.push(format!(
"company_territory_track_count mismatch: expected {count}, got {}",
actual.company_territory_track_count
));
}
}
}
pub(super) fn compare_collections_suffix(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(count) = expected.event_runtime_record_count {
if actual.event_runtime_record_count != count {
mismatches.push(format!(
"event_runtime_record_count mismatch: expected {count}, got {}",
actual.event_runtime_record_count
));
}
}
if let Some(count) = expected.candidate_availability_count {
if actual.candidate_availability_count != count {
mismatches.push(format!(
"candidate_availability_count mismatch: expected {count}, got {}",
actual.candidate_availability_count
));
}
}
if let Some(count) = expected.zero_candidate_availability_count {
if actual.zero_candidate_availability_count != count {
mismatches.push(format!(
"zero_candidate_availability_count mismatch: expected {count}, got {}",
actual.zero_candidate_availability_count
));
}
}
if let Some(count) = expected.named_locomotive_availability_count {
if actual.named_locomotive_availability_count != count {
mismatches.push(format!(
"named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.named_locomotive_availability_count
));
}
}
if let Some(count) = expected.zero_named_locomotive_availability_count {
if actual.zero_named_locomotive_availability_count != count {
mismatches.push(format!(
"zero_named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.zero_named_locomotive_availability_count
));
}
}
if let Some(count) = expected.named_locomotive_cost_count {
if actual.named_locomotive_cost_count != count {
mismatches.push(format!(
"named_locomotive_cost_count mismatch: expected {count}, got {}",
actual.named_locomotive_cost_count
));
}
}
if let Some(count) = expected.cargo_production_override_count {
if actual.cargo_production_override_count != count {
mismatches.push(format!(
"cargo_production_override_count mismatch: expected {count}, got {}",
actual.cargo_production_override_count
));
}
}
if let Some(count) = expected.world_runtime_variable_count {
if actual.world_runtime_variable_count != count {
mismatches.push(format!(
"world_runtime_variable_count mismatch: expected {count}, got {}",
actual.world_runtime_variable_count
));
}
}
if let Some(count) = expected.company_runtime_variable_owner_count {
if actual.company_runtime_variable_owner_count != count {
mismatches.push(format!(
"company_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.company_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.player_runtime_variable_owner_count {
if actual.player_runtime_variable_owner_count != count {
mismatches.push(format!(
"player_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.player_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.territory_runtime_variable_owner_count {
if actual.territory_runtime_variable_owner_count != count {
mismatches.push(format!(
"territory_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.territory_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.world_scalar_override_count {
if actual.world_scalar_override_count != count {
mismatches.push(format!(
"world_scalar_override_count mismatch: expected {count}, got {}",
actual.world_scalar_override_count
));
}
}
if let Some(count) = expected.special_condition_count {
if actual.special_condition_count != count {
mismatches.push(format!(
"special_condition_count mismatch: expected {count}, got {}",
actual.special_condition_count
));
}
}
if let Some(count) = expected.enabled_special_condition_count {
if actual.enabled_special_condition_count != count {
mismatches.push(format!(
"enabled_special_condition_count mismatch: expected {count}, got {}",
actual.enabled_special_condition_count
));
}
}
if let Some(kind) = &expected.save_profile_kind {
if actual.save_profile_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"save_profile_kind mismatch: expected {kind:?}, got {:?}",
actual.save_profile_kind
));
}
}
if let Some(family) = &expected.save_profile_family {
if actual.save_profile_family.as_ref() != Some(family) {
mismatches.push(format!(
"save_profile_family mismatch: expected {family:?}, got {:?}",
actual.save_profile_family
));
}
}
if let Some(map_path) = &expected.save_profile_map_path {
if actual.save_profile_map_path.as_ref() != Some(map_path) {
mismatches.push(format!(
"save_profile_map_path mismatch: expected {map_path:?}, got {:?}",
actual.save_profile_map_path
));
}
}
if let Some(display_name) = &expected.save_profile_display_name {
if actual.save_profile_display_name.as_ref() != Some(display_name) {
mismatches.push(format!(
"save_profile_display_name mismatch: expected {display_name:?}, got {:?}",
actual.save_profile_display_name
));
}
}
if let Some(lane) = expected.save_profile_selected_year_profile_lane {
if actual.save_profile_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"save_profile_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.save_profile_selected_year_profile_lane
));
}
}
if let Some(enabled) = expected.save_profile_sandbox_enabled {
if actual.save_profile_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_sandbox_enabled
));
}
}
if let Some(enabled) = expected.save_profile_campaign_scenario_enabled {
if actual.save_profile_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_campaign_scenario_enabled
));
}
}
if let Some(enabled) = expected.save_profile_staged_profile_copy_on_restore {
if actual.save_profile_staged_profile_copy_on_restore != Some(enabled) {
mismatches.push(format!(
"save_profile_staged_profile_copy_on_restore mismatch: expected {enabled}, got {:?}",
actual.save_profile_staged_profile_copy_on_restore
));
}
}
if let Some(count) = expected.total_event_record_service_count {
if actual.total_event_record_service_count != count {
mismatches.push(format!(
"total_event_record_service_count mismatch: expected {count}, got {}",
actual.total_event_record_service_count
));
}
}
if let Some(count) = expected.periodic_boundary_call_count {
if actual.periodic_boundary_call_count != count {
mismatches.push(format!(
"periodic_boundary_call_count mismatch: expected {count}, got {}",
actual.periodic_boundary_call_count
));
}
}
if let Some(count) = expected.total_trigger_dispatch_count {
if actual.total_trigger_dispatch_count != count {
mismatches.push(format!(
"total_trigger_dispatch_count mismatch: expected {count}, got {}",
actual.total_trigger_dispatch_count
));
}
}
if let Some(count) = expected.dirty_rerun_count {
if actual.dirty_rerun_count != count {
mismatches.push(format!(
"dirty_rerun_count mismatch: expected {count}, got {}",
actual.dirty_rerun_count
));
}
}
if let Some(total) = expected.total_company_cash {
if actual.total_company_cash != total {
mismatches.push(format!(
"total_company_cash mismatch: expected {total}, got {}",
actual.total_company_cash
));
}
}
}

View file

@ -0,0 +1,22 @@
use rrt_runtime::summary::RuntimeSummary;
use super::ExpectedRuntimeSummary;
mod collections;
mod packed_events;
mod selected_company;
mod world;
impl ExpectedRuntimeSummary {
pub fn compare(&self, actual: &RuntimeSummary) -> Vec<String> {
let mut mismatches = Vec::new();
world::compare_world(self, actual, &mut mismatches);
selected_company::compare_selected_company(self, actual, &mut mismatches);
collections::compare_collections_prefix(self, actual, &mut mismatches);
packed_events::compare_packed_events(self, actual, &mut mismatches);
collections::compare_collections_suffix(self, actual, &mut mismatches);
mismatches
}
}

View file

@ -0,0 +1,314 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_packed_events(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(present) = expected.packed_event_collection_present {
if actual.packed_event_collection_present != present {
mismatches.push(format!(
"packed_event_collection_present mismatch: expected {present}, got {}",
actual.packed_event_collection_present
));
}
}
if let Some(count) = expected.packed_event_record_count {
if actual.packed_event_record_count != count {
mismatches.push(format!(
"packed_event_record_count mismatch: expected {count}, got {}",
actual.packed_event_record_count
));
}
}
if let Some(count) = expected.packed_event_decoded_record_count {
if actual.packed_event_decoded_record_count != count {
mismatches.push(format!(
"packed_event_decoded_record_count mismatch: expected {count}, got {}",
actual.packed_event_decoded_record_count
));
}
}
if let Some(count) = expected.packed_event_imported_runtime_record_count {
if actual.packed_event_imported_runtime_record_count != count {
mismatches.push(format!(
"packed_event_imported_runtime_record_count mismatch: expected {count}, got {}",
actual.packed_event_imported_runtime_record_count
));
}
}
if let Some(count) = expected.packed_event_parity_only_record_count {
if actual.packed_event_parity_only_record_count != count {
mismatches.push(format!(
"packed_event_parity_only_record_count mismatch: expected {count}, got {}",
actual.packed_event_parity_only_record_count
));
}
}
if let Some(count) = expected.packed_event_unsupported_record_count {
if actual.packed_event_unsupported_record_count != count {
mismatches.push(format!(
"packed_event_unsupported_record_count mismatch: expected {count}, got {}",
actual.packed_event_unsupported_record_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_company_context_count {
if actual.packed_event_blocked_missing_company_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_company_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_company_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_selection_context_count {
if actual.packed_event_blocked_missing_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_selection_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_company_role_context_count {
if actual.packed_event_blocked_missing_company_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_company_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_company_role_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_context_count {
if actual.packed_event_blocked_missing_player_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_selection_context_count {
if actual.packed_event_blocked_missing_player_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_selection_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_role_context_count {
if actual.packed_event_blocked_missing_player_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_role_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_chairman_context_count {
if actual.packed_event_blocked_missing_chairman_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_chairman_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_chairman_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_chairman_target_scope_count {
if actual.packed_event_blocked_chairman_target_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_chairman_target_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_chairman_target_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_condition_context_count {
if actual.packed_event_blocked_missing_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_condition_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_condition_context_count {
if actual.packed_event_blocked_missing_player_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_condition_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_company_condition_scope_disabled_count {
if actual.packed_event_blocked_company_condition_scope_disabled_count != count {
mismatches.push(format!(
"packed_event_blocked_company_condition_scope_disabled_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_company_condition_scope_disabled_count
));
}
}
if let Some(count) = expected.packed_event_blocked_player_condition_scope_count {
if actual.packed_event_blocked_player_condition_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_player_condition_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_player_condition_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_condition_scope_count {
if actual.packed_event_blocked_territory_condition_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_condition_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_condition_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_territory_context_count {
if actual.packed_event_blocked_missing_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_territory_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_named_territory_binding_count {
if actual.packed_event_blocked_named_territory_binding_count != count {
mismatches.push(format!(
"packed_event_blocked_named_territory_binding_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_named_territory_binding_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_ordinary_condition_count {
if actual.packed_event_blocked_unmapped_ordinary_condition_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_ordinary_condition_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_ordinary_condition_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_world_condition_count {
if actual.packed_event_blocked_unmapped_world_condition_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_world_condition_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_world_condition_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_compact_control_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_compact_control_count
));
}
}
if let Some(count) = expected.packed_event_blocked_shell_owned_descriptor_count {
if actual.packed_event_blocked_shell_owned_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_shell_owned_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_shell_owned_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_evidence_blocked_descriptor_count {
if actual.packed_event_blocked_evidence_blocked_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_evidence_blocked_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_evidence_blocked_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_variant_or_scope_blocked_descriptor_count {
if actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_variant_or_scope_blocked_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_real_descriptor_count {
if actual.packed_event_blocked_unmapped_real_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_real_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_real_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_world_descriptor_count {
if actual.packed_event_blocked_unmapped_world_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_world_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_world_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_access_variant_count {
if actual.packed_event_blocked_territory_access_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_access_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_access_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_access_scope_count {
if actual.packed_event_blocked_territory_access_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_access_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_access_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_train_context_count {
if actual.packed_event_blocked_missing_train_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_train_territory_context_count {
if actual.packed_event_blocked_missing_train_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_territory_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_locomotive_catalog_context_count {
if actual.packed_event_blocked_missing_locomotive_catalog_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_locomotive_catalog_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_locomotive_catalog_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_confiscation_variant_count {
if actual.packed_event_blocked_confiscation_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_confiscation_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_confiscation_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_retire_train_variant_count {
if actual.packed_event_blocked_retire_train_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_retire_train_scope_count {
if actual.packed_event_blocked_retire_train_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_structural_only_count {
if actual.packed_event_blocked_structural_only_count != count {
mismatches.push(format!(
"packed_event_blocked_structural_only_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_structural_only_count
));
}
}
}

View file

@ -0,0 +1,194 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_selected_company(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(value) = expected.selected_company_outstanding_shares {
if actual.selected_company_outstanding_shares != Some(value) {
mismatches.push(format!(
"selected_company_outstanding_shares mismatch: expected {value}, got {:?}",
actual.selected_company_outstanding_shares
));
}
}
if let Some(value) = expected.selected_company_bond_count {
if actual.selected_company_bond_count != Some(value) {
mismatches.push(format!(
"selected_company_bond_count mismatch: expected {value}, got {:?}",
actual.selected_company_bond_count
));
}
}
if let Some(value) = expected.selected_company_largest_live_bond_principal {
if actual.selected_company_largest_live_bond_principal != Some(value) {
mismatches.push(format!(
"selected_company_largest_live_bond_principal mismatch: expected {value}, got {:?}",
actual.selected_company_largest_live_bond_principal
));
}
}
if let Some(value) = expected.selected_company_highest_coupon_live_bond_principal {
if actual.selected_company_highest_coupon_live_bond_principal != Some(value) {
mismatches.push(format!(
"selected_company_highest_coupon_live_bond_principal mismatch: expected {value}, got {:?}",
actual.selected_company_highest_coupon_live_bond_principal
));
}
}
if let Some(value) = expected.selected_company_assigned_share_pool {
if actual.selected_company_assigned_share_pool != Some(value) {
mismatches.push(format!(
"selected_company_assigned_share_pool mismatch: expected {value}, got {:?}",
actual.selected_company_assigned_share_pool
));
}
}
if let Some(value) = expected.selected_company_unassigned_share_pool {
if actual.selected_company_unassigned_share_pool != Some(value) {
mismatches.push(format!(
"selected_company_unassigned_share_pool mismatch: expected {value}, got {:?}",
actual.selected_company_unassigned_share_pool
));
}
}
if let Some(value) = expected.selected_company_cached_share_price {
if actual.selected_company_cached_share_price != Some(value) {
mismatches.push(format!(
"selected_company_cached_share_price mismatch: expected {value}, got {:?}",
actual.selected_company_cached_share_price
));
}
}
if let Some(value) = &expected.selected_company_cached_share_price_value_f32_text {
if actual
.selected_company_cached_share_price_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_cached_share_price_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_cached_share_price_value_f32_text
));
}
}
if let Some(value) = &expected.selected_company_mutable_support_scalar_value_f32_text {
if actual
.selected_company_mutable_support_scalar_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_mutable_support_scalar_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_mutable_support_scalar_value_f32_text
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_0cfb_count {
if actual.selected_company_stat_band_root_0cfb_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0cfb_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0cfb_count
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_0d7f_count {
if actual.selected_company_stat_band_root_0d7f_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0d7f_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0d7f_count
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_1c47_count {
if actual.selected_company_stat_band_root_1c47_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_1c47_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_1c47_count
));
}
}
if let Some(value) = expected.selected_company_last_dividend_year {
if actual.selected_company_last_dividend_year != Some(value) {
mismatches.push(format!(
"selected_company_last_dividend_year mismatch: expected {value}, got {:?}",
actual.selected_company_last_dividend_year
));
}
}
if let Some(value) = expected.selected_company_years_since_founding {
if actual.selected_company_years_since_founding != Some(value) {
mismatches.push(format!(
"selected_company_years_since_founding mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_founding
));
}
}
if let Some(value) = expected.selected_company_years_since_last_bankruptcy {
if actual.selected_company_years_since_last_bankruptcy != Some(value) {
mismatches.push(format!(
"selected_company_years_since_last_bankruptcy mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_last_bankruptcy
));
}
}
if let Some(value) = expected.selected_company_years_since_last_dividend {
if actual.selected_company_years_since_last_dividend != Some(value) {
mismatches.push(format!(
"selected_company_years_since_last_dividend mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_last_dividend
));
}
}
if let Some(value) = expected.selected_company_current_partial_year_weight_numerator {
if actual.selected_company_current_partial_year_weight_numerator != Some(value) {
mismatches.push(format!(
"selected_company_current_partial_year_weight_numerator mismatch: expected {value}, got {:?}",
actual.selected_company_current_partial_year_weight_numerator
));
}
}
if let Some(value) = expected.selected_company_current_issue_absolute_counter {
if actual.selected_company_current_issue_absolute_counter != Some(value) {
mismatches.push(format!(
"selected_company_current_issue_absolute_counter mismatch: expected {value}, got {:?}",
actual.selected_company_current_issue_absolute_counter
));
}
}
if let Some(value) = expected.selected_company_prior_issue_absolute_counter {
if actual.selected_company_prior_issue_absolute_counter != Some(value) {
mismatches.push(format!(
"selected_company_prior_issue_absolute_counter mismatch: expected {value}, got {:?}",
actual.selected_company_prior_issue_absolute_counter
));
}
}
if let Some(value) = expected.selected_company_current_issue_age_absolute_counter_delta {
if actual.selected_company_current_issue_age_absolute_counter_delta != Some(value) {
mismatches.push(format!(
"selected_company_current_issue_age_absolute_counter_delta mismatch: expected {value}, got {:?}",
actual.selected_company_current_issue_age_absolute_counter_delta
));
}
}
if let Some(value) = expected.selected_company_chairman_bonus_year {
if actual.selected_company_chairman_bonus_year != Some(value) {
mismatches.push(format!(
"selected_company_chairman_bonus_year mismatch: expected {value}, got {:?}",
actual.selected_company_chairman_bonus_year
));
}
}
if let Some(value) = expected.selected_company_chairman_bonus_amount {
if actual.selected_company_chairman_bonus_amount != Some(value) {
mismatches.push(format!(
"selected_company_chairman_bonus_amount mismatch: expected {value}, got {:?}",
actual.selected_company_chairman_bonus_amount
));
}
}
}

View file

@ -0,0 +1,398 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_world(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(calendar) = expected.calendar {
if actual.calendar != calendar {
mismatches.push(format!(
"calendar mismatch: expected {:?}, got {:?}",
calendar, actual.calendar
));
}
}
if let Some(source) = &expected.calendar_projection_source {
if actual.calendar_projection_source.as_ref() != Some(source) {
mismatches.push(format!(
"calendar_projection_source mismatch: expected {source:?}, got {:?}",
actual.calendar_projection_source
));
}
}
if let Some(is_placeholder) = expected.calendar_projection_is_placeholder {
if actual.calendar_projection_is_placeholder != is_placeholder {
mismatches.push(format!(
"calendar_projection_is_placeholder mismatch: expected {is_placeholder}, got {}",
actual.calendar_projection_is_placeholder
));
}
}
if let Some(count) = expected.world_flag_count {
if actual.world_flag_count != count {
mismatches.push(format!(
"world_flag_count mismatch: expected {count}, got {}",
actual.world_flag_count
));
}
}
if let Some(lane) = expected.world_restore_selected_year_profile_lane {
if actual.world_restore_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"world_restore_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.world_restore_selected_year_profile_lane
));
}
}
if let Some(enabled) = expected.world_restore_campaign_scenario_enabled {
if actual.world_restore_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_campaign_scenario_enabled
));
}
}
if let Some(enabled) = expected.world_restore_sandbox_enabled {
if actual.world_restore_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_sandbox_enabled
));
}
}
if let Some(enabled) = expected.world_restore_seed_tuple_written_from_raw_lane {
if actual.world_restore_seed_tuple_written_from_raw_lane != Some(enabled) {
mismatches.push(format!(
"world_restore_seed_tuple_written_from_raw_lane mismatch: expected {enabled}, got {:?}",
actual.world_restore_seed_tuple_written_from_raw_lane
));
}
}
if let Some(enabled) = expected.world_restore_absolute_counter_requires_shell_context {
if actual.world_restore_absolute_counter_requires_shell_context != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_requires_shell_context mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_requires_shell_context
));
}
}
if let Some(enabled) = expected.world_restore_absolute_counter_reconstructible_from_save {
if actual.world_restore_absolute_counter_reconstructible_from_save != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_reconstructible_from_save
));
}
}
if let Some(value) = expected.world_restore_packed_year_word_raw_u16 {
if actual.world_restore_packed_year_word_raw_u16 != Some(value) {
mismatches.push(format!(
"world_restore_packed_year_word_raw_u16 mismatch: expected {value}, got {:?}",
actual.world_restore_packed_year_word_raw_u16
));
}
}
if let Some(value) = expected.world_restore_partial_year_progress_raw_u8 {
if actual.world_restore_partial_year_progress_raw_u8 != Some(value) {
mismatches.push(format!(
"world_restore_partial_year_progress_raw_u8 mismatch: expected {value}, got {:?}",
actual.world_restore_partial_year_progress_raw_u8
));
}
}
if let Some(value) = expected.world_restore_current_calendar_tuple_word_raw_u32 {
if actual.world_restore_current_calendar_tuple_word_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_current_calendar_tuple_word_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_current_calendar_tuple_word_raw_u32
));
}
}
if let Some(value) = expected.world_restore_current_calendar_tuple_word_2_raw_u32 {
if actual.world_restore_current_calendar_tuple_word_2_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_current_calendar_tuple_word_2_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_current_calendar_tuple_word_2_raw_u32
));
}
}
if let Some(value) = expected.world_restore_absolute_counter_raw_u32 {
if actual.world_restore_absolute_counter_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_absolute_counter_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_absolute_counter_raw_u32
));
}
}
if let Some(value) = expected.world_restore_absolute_counter_mirror_raw_u32 {
if actual.world_restore_absolute_counter_mirror_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_absolute_counter_mirror_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_absolute_counter_mirror_raw_u32
));
}
}
if let Some(slot) = expected.world_restore_disable_cargo_economy_special_condition_slot {
if actual.world_restore_disable_cargo_economy_special_condition_slot != Some(slot) {
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_slot mismatch: expected {slot}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_slot
));
}
}
if let Some(enabled) =
expected.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
{
if actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
));
}
}
if let Some(enabled) =
expected.world_restore_disable_cargo_economy_special_condition_write_side_grounded
{
if actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_write_side_grounded mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
));
}
}
if let Some(enabled) = expected.world_restore_disable_cargo_economy_special_condition_enabled {
if actual.world_restore_disable_cargo_economy_special_condition_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_enabled
));
}
}
if let Some(enabled) = expected.world_restore_use_bio_accelerator_cars_enabled {
if actual.world_restore_use_bio_accelerator_cars_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_bio_accelerator_cars_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_bio_accelerator_cars_enabled
));
}
}
if let Some(enabled) = expected.world_restore_use_wartime_cargos_enabled {
if actual.world_restore_use_wartime_cargos_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_wartime_cargos_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_wartime_cargos_enabled
));
}
}
if let Some(enabled) = expected.world_restore_disable_train_crashes_enabled {
if actual.world_restore_disable_train_crashes_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_enabled
));
}
}
if let Some(enabled) = expected.world_restore_disable_train_crashes_and_breakdowns_enabled {
if actual.world_restore_disable_train_crashes_and_breakdowns_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_and_breakdowns_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_and_breakdowns_enabled
));
}
}
if let Some(enabled) = expected.world_restore_ai_ignore_territories_at_startup_enabled {
if actual.world_restore_ai_ignore_territories_at_startup_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_ai_ignore_territories_at_startup_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_ai_ignore_territories_at_startup_enabled
));
}
}
if let Some(value) = expected.world_restore_limited_track_building_amount {
if actual.world_restore_limited_track_building_amount != Some(value) {
mismatches.push(format!(
"world_restore_limited_track_building_amount mismatch: expected {value}, got {:?}",
actual.world_restore_limited_track_building_amount
));
}
}
if let Some(code) = expected.world_restore_economic_status_code {
if actual.world_restore_economic_status_code != Some(code) {
mismatches.push(format!(
"world_restore_economic_status_code mismatch: expected {code}, got {:?}",
actual.world_restore_economic_status_code
));
}
}
if let Some(value) = expected.world_restore_territory_access_cost {
if actual.world_restore_territory_access_cost != Some(value) {
mismatches.push(format!(
"world_restore_territory_access_cost mismatch: expected {value}, got {:?}",
actual.world_restore_territory_access_cost
));
}
}
if let Some(value) = expected.world_restore_issue_37_value {
if actual.world_restore_issue_37_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_37_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_37_value
));
}
}
if let Some(value) = expected.world_restore_issue_38_value {
if actual.world_restore_issue_38_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_38_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_38_value
));
}
}
if let Some(value) = expected.world_restore_issue_39_value {
if actual.world_restore_issue_39_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_39_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_39_value
));
}
}
if let Some(value) = expected.world_restore_issue_3a_value {
if actual.world_restore_issue_3a_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_3a_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_3a_value
));
}
}
if let Some(value) = expected.world_restore_issue_37_multiplier_raw_u32 {
if actual.world_restore_issue_37_multiplier_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_issue_37_multiplier_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_issue_37_multiplier_raw_u32
));
}
}
if let Some(value) = &expected.world_restore_issue_37_multiplier_value_f32_text {
if actual
.world_restore_issue_37_multiplier_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"world_restore_issue_37_multiplier_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.world_restore_issue_37_multiplier_value_f32_text
));
}
}
if let Some(count) = expected.world_restore_finance_neighborhood_count {
if actual.world_restore_finance_neighborhood_count != count {
mismatches.push(format!(
"world_restore_finance_neighborhood_count mismatch: expected {count}, got {}",
actual.world_restore_finance_neighborhood_count
));
}
}
if let Some(labels) = &expected.world_restore_finance_neighborhood_labels {
if &actual.world_restore_finance_neighborhood_labels != labels {
mismatches.push(format!(
"world_restore_finance_neighborhood_labels mismatch: expected {labels:?}, got {:?}",
actual.world_restore_finance_neighborhood_labels
));
}
}
if let Some(value) = expected.world_restore_economic_tuning_mirror_raw_u32 {
if actual.world_restore_economic_tuning_mirror_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_economic_tuning_mirror_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_economic_tuning_mirror_raw_u32
));
}
}
if let Some(value) = &expected.world_restore_economic_tuning_mirror_value_f32_text {
if actual
.world_restore_economic_tuning_mirror_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"world_restore_economic_tuning_mirror_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.world_restore_economic_tuning_mirror_value_f32_text
));
}
}
if let Some(count) = expected.world_restore_economic_tuning_lane_count {
if actual.world_restore_economic_tuning_lane_count != count {
mismatches.push(format!(
"world_restore_economic_tuning_lane_count mismatch: expected {count}, got {}",
actual.world_restore_economic_tuning_lane_count
));
}
}
if let Some(values) = &expected.world_restore_economic_tuning_lane_value_f32_text {
if &actual.world_restore_economic_tuning_lane_value_f32_text != values {
mismatches.push(format!(
"world_restore_economic_tuning_lane_value_f32_text mismatch: expected {values:?}, got {:?}",
actual.world_restore_economic_tuning_lane_value_f32_text
));
}
}
if let Some(kind) = &expected.world_restore_absolute_counter_restore_kind {
if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"world_restore_absolute_counter_restore_kind mismatch: expected {kind:?}, got {:?}",
actual.world_restore_absolute_counter_restore_kind
));
}
}
if let Some(context) = &expected.world_restore_absolute_counter_adjustment_context {
if actual
.world_restore_absolute_counter_adjustment_context
.as_ref()
!= Some(context)
{
mismatches.push(format!(
"world_restore_absolute_counter_adjustment_context mismatch: expected {context:?}, got {:?}",
actual.world_restore_absolute_counter_adjustment_context
));
}
}
if let Some(count) = expected.metadata_count {
if actual.metadata_count != count {
mismatches.push(format!(
"metadata_count mismatch: expected {count}, got {}",
actual.metadata_count
));
}
}
if let Some(count) = expected.company_count {
if actual.company_count != count {
mismatches.push(format!(
"company_count mismatch: expected {count}, got {}",
actual.company_count
));
}
}
if let Some(count) = expected.active_company_count {
if actual.active_company_count != count {
mismatches.push(format!(
"active_company_count mismatch: expected {count}, got {}",
actual.active_company_count
));
}
}
if let Some(count) = expected.company_market_state_owner_count {
if actual.company_market_state_owner_count != count {
mismatches.push(format!(
"company_market_state_owner_count mismatch: expected {count}, got {}",
actual.company_market_state_owner_count
));
}
}
}

View file

@ -0,0 +1,112 @@
use super::*;
use crate::load_fixture_document_from_str;
use rrt_runtime::summary::RuntimeSummary;
const FIXTURE_JSON: &str = r#"
{
"format_version": 1,
"fixture_id": "minimal-world-step-smoke",
"source": {
"kind": "synthetic",
"description": "basic milestone parser smoke fixture"
},
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {
"sandbox": false
},
"companies": [
{
"company_id": 1,
"current_cash": 250000,
"debt": 0
}
],
"event_runtime_records": [],
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
},
"commands": [
{
"kind": "advance_to",
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
}
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flag_count": 1,
"company_count": 1,
"event_runtime_record_count": 0,
"world_restore_economic_tuning_lane_count": 0,
"total_company_cash": 250000
}
}
"#;
#[test]
fn parses_and_validates_fixture() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let report = validate_fixture_document(&fixture);
assert!(report.valid, "report should be valid: {:?}", report.issues);
assert_eq!(fixture.state_origin, FixtureStateOrigin::Inline);
}
#[test]
fn compares_expected_summary() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let summary = RuntimeSummary::from_state(&fixture.state);
let mismatches = fixture.expected_summary.compare(&summary);
assert_eq!(mismatches.len(), 1);
assert!(mismatches[0].contains("calendar mismatch"));
}
#[test]
fn compares_expected_state_fragment_recursively() {
let expected = serde_json::json!({
"world_flags": {
"sandbox": false
},
"companies": [
{
"company_id": 1
}
]
});
let actual = serde_json::json!({
"world_flags": {
"sandbox": false,
"runtime.effect_fired": true
},
"companies": [
{
"company_id": 1,
"current_cash": 250000
}
]
});
let mismatches = compare_expected_state_fragment(&expected, &actual);
assert!(
mismatches.is_empty(),
"unexpected mismatches: {mismatches:?}"
);
}

View file

@ -0,0 +1,37 @@
use super::{FIXTURE_FORMAT_VERSION, FixtureDocument, FixtureValidationReport};
pub fn validate_fixture_document(document: &FixtureDocument) -> FixtureValidationReport {
let mut issues = Vec::new();
if document.format_version != FIXTURE_FORMAT_VERSION {
issues.push(format!(
"unsupported format_version {} (expected {})",
document.format_version, FIXTURE_FORMAT_VERSION
));
}
if document.fixture_id.trim().is_empty() {
issues.push("fixture_id must not be empty".to_string());
}
if document.source.kind.trim().is_empty() {
issues.push("source.kind must not be empty".to_string());
}
if document.commands.is_empty() {
issues.push("fixture must contain at least one command".to_string());
}
if let Err(err) = document.state.validate() {
issues.push(format!("invalid runtime state: {err}"));
}
for (index, command) in document.commands.iter().enumerate() {
if let Err(err) = command.validate() {
issues.push(format!("invalid command at index {index}: {err}"));
}
}
FixtureValidationReport {
fixture_id: document.fixture_id.clone(),
valid: issues.is_empty(),
issue_count: issues.len(),
issues,
}
}

View file

@ -0,0 +1 @@
pub use crate::schema::{ExpectedRuntimeSummary, compare_expected_state_fragment};

View file

@ -0,0 +1,2 @@
pub use crate::load::{load_fixture_document, load_fixture_document_from_str};
pub use crate::schema::{FixtureValidationReport, validate_fixture_document};

View file

@ -0,0 +1,156 @@
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use rrt_model::finance::{
AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceOutcome, FinanceSnapshot,
};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FinanceLogPaths {
pub snapshot_path: PathBuf,
pub outcome_path: PathBuf,
}
pub fn sample_finance_snapshot() -> FinanceSnapshot {
FinanceSnapshot {
policy: AnnualFinancePolicy {
dividends_allowed: false,
..AnnualFinancePolicy::default()
},
company: CompanyFinanceState {
current_cash: 100_000,
support_adjusted_share_price: 27.5,
book_value_per_share: 20.0,
outstanding_share_count: 60_000,
recent_net_profits: [40_000, 30_000, 20_000],
recent_revenue_totals: [250_000, 240_000, 230_000],
bonds: vec![
BondPosition {
principal: 150_000,
coupon_rate: 0.12,
years_remaining: 12,
},
BondPosition {
principal: 10_000,
coupon_rate: 0.10,
years_remaining: 10,
},
],
..CompanyFinanceState::default()
},
}
}
pub fn write_finance_snapshot_bundle(
base_dir: &Path,
stem: &str,
snapshot: &FinanceSnapshot,
) -> io::Result<FinanceLogPaths> {
fs::create_dir_all(base_dir)?;
let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json"));
let outcome_path = base_dir.join(format!("rrt_finance_{stem}_outcome.json"));
let outcome: FinanceOutcome = snapshot.evaluate();
let snapshot_json = serde_json::to_vec_pretty(snapshot)
.map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?;
let outcome_json = serde_json::to_vec_pretty(&outcome)
.map_err(|err| io::Error::other(format!("serialize outcome: {err}")))?;
fs::write(&snapshot_path, snapshot_json)?;
fs::write(&outcome_path, outcome_json)?;
Ok(FinanceLogPaths {
snapshot_path,
outcome_path,
})
}
pub fn write_finance_snapshot_only(
base_dir: &Path,
stem: &str,
snapshot: &FinanceSnapshot,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json"));
let snapshot_json = serde_json::to_vec_pretty(snapshot)
.map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?;
fs::write(&snapshot_path, snapshot_json)?;
Ok(snapshot_path)
}
#[derive(Debug, Clone, Serialize)]
pub struct IndexedCollectionProbeRow {
pub entry_id: usize,
pub live: bool,
pub resolved_ptr: usize,
pub active_flag: Option<u8>,
}
#[derive(Debug, Clone, Serialize)]
pub struct IndexedCollectionProbe {
pub collection_addr: usize,
pub flat_payload: bool,
pub stride: u32,
pub id_bound: i32,
pub payload_ptr: usize,
pub tombstone_ptr: usize,
pub first_rows: Vec<IndexedCollectionProbeRow>,
}
pub fn write_indexed_collection_probe(
base_dir: &Path,
stem: &str,
probe: &IndexedCollectionProbe,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let path = base_dir.join(format!("rrt_finance_{stem}_collection_probe.json"));
let json = serde_json::to_vec_pretty(probe)
.map_err(|err| io::Error::other(format!("serialize collection probe: {err}")))?;
fs::write(&path, json)?;
Ok(path)
}
#[derive(Debug, Clone, Serialize)]
pub struct CargoCollectionProbeRow {
pub entry_id: usize,
pub live: bool,
pub resolved_ptr: usize,
pub stem: Option<String>,
pub route_style_byte: Option<u8>,
pub subtype_byte: Option<u8>,
pub class_marker: Option<u32>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CargoCollectionProbe {
pub collection_addr: usize,
pub flat_payload: bool,
pub stride: u32,
pub id_bound: i32,
pub payload_ptr: usize,
pub tombstone_ptr: usize,
pub live_entry_count: usize,
pub rows: Vec<CargoCollectionProbeRow>,
}
pub fn write_cargo_collection_probe(
base_dir: &Path,
stem: &str,
probe: &CargoCollectionProbe,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let path = base_dir.join(format!("rrt_cargo_{stem}_collection_probe.json"));
let json = serde_json::to_vec_pretty(probe)
.map_err(|err| io::Error::other(format!("serialize cargo collection probe: {err}")))?;
fs::write(&path, json)?;
Ok(path)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,377 @@
use super::*;
pub(super) fn maybe_install_auto_load_hook() {
let save_stem = match env::var("RRT_AUTO_LOAD_SAVE") {
Ok(value) if !value.trim().is_empty() => value,
_ => return,
};
let _ = AUTO_LOAD_SAVE_STEM.set(save_stem);
if AUTO_LOAD_HOOK_INSTALLED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(AUTO_LOAD_STARTED_MESSAGE);
AUTO_LOAD_THREAD_STARTED.store(true, Ordering::Release);
if unsafe { install_shell_state_service_hook() } {
append_log_message(AUTO_LOAD_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_profile_startup_dispatch_hook() } {
append_log_message(AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_runtime_reset_hook() } {
append_log_message(AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_allocator_hook() } {
append_log_message(AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_load_screen_scalar_hook() } {
append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_load_screen_construct_hook() } {
append_log_message(AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_load_screen_message_hook() } {
append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_runtime_prime_hook() } {
append_log_message(AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_frame_cycle_hook() } {
append_log_message(AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_object_service_hook() } {
append_log_message(AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_child_service_hook() } {
append_log_message(AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_publish_hook() } {
append_log_message(AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_unpublish_hook() } {
append_log_message(AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_object_teardown_hook() } {
append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_object_range_remove_hook() } {
append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_remove_node_hook() } {
append_log_message(AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_shell_node_vcall_hook() } {
append_log_message(AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
if unsafe { install_mode2_teardown_hook() } {
append_log_message(AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
}
pub(super) fn run_auto_load_worker() {
append_log_message(AUTO_LOAD_CALLING_MESSAGE);
let staged = unsafe { invoke_manual_load_branch() };
if staged {
append_log_message(AUTO_LOAD_TRIGGERED_MESSAGE);
append_log_message(AUTO_LOAD_SUCCESS_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
AUTO_LOAD_IN_PROGRESS.store(false, Ordering::Release);
}
pub(super) unsafe fn invoke_manual_load_branch() -> bool {
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if shell_state.is_null() {
return false;
}
let shell_transition_mode: ShellTransitionModeFn =
unsafe { mem::transmute(SHELL_TRANSITION_MODE_ADDR) };
AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.store(0, Ordering::Release);
AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_OWNER_ENTRY_MESSAGE);
let _ = unsafe { shell_transition_mode(shell_state, SHELL_MODE_STARTUP_LOAD_DISPATCH, 0) };
AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE.store(false, Ordering::Release);
append_log_message(AUTO_LOAD_OWNER_RETURNED_MESSAGE);
log_post_transition_state();
true
}
pub(super) unsafe fn stage_manual_load_request(save_stem: &str) -> bool {
if save_stem.is_empty() || save_stem.as_bytes().contains(&0) {
return false;
}
let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) };
if runtime_profile.is_null() {
return false;
}
let path_seed = unsafe { runtime_profile.add(RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET) };
if unsafe { write_c_string(path_seed, 260, save_stem.as_bytes()) }.is_none() {
return false;
}
unsafe {
ptr::write_unaligned(
runtime_profile
.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)
.cast::<u8>(),
STARTUP_SELECTOR_SCENARIO_LOAD,
)
};
unsafe {
ptr::write_unaligned(
runtime_profile
.add(RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET)
.cast::<u8>(),
0,
)
};
log_auto_load_launch_state(runtime_profile);
true
}
pub(super) unsafe fn write_c_string(
destination: *mut u8,
capacity: usize,
bytes: &[u8],
) -> Option<()> {
if bytes.len() + 1 > capacity {
return None;
}
unsafe { ptr::write_bytes(destination, 0, capacity) };
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), destination, bytes.len()) };
Some(())
}
pub(super) unsafe fn runtime_saved_world_restore_gate_mask() -> u32 {
let mut mask = 0_u32;
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if !shell_state.is_null() {
mask |= 0x1;
}
let shell_controller = unsafe { read_ptr(SHELL_CONTROLLER_PTR_ADDR as *const u8) };
if !shell_controller.is_null() {
mask |= 0x2;
}
let active_mode = unsafe { resolve_active_mode_ptr() };
if !active_mode.is_null() {
mask |= 0x4;
}
mask
}
pub(super) unsafe fn current_mode_id() -> u32 {
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if shell_state.is_null() {
return 0;
}
unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) }
}
pub(super) fn auto_load_ready_polls() -> u32 {
env::var("RRT_AUTO_LOAD_READY_POLLS")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.filter(|value| *value > 0)
.unwrap_or(AUTO_LOAD_READY_POLLS)
}
pub(super) fn auto_load_defer_polls() -> u32 {
env::var("RRT_AUTO_LOAD_DEFER_POLLS")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(AUTO_LOAD_DEFER_POLLS)
}
pub(super) fn maybe_service_auto_load_on_main_thread() {
if !AUTO_LOAD_HOOK_INSTALLED.load(Ordering::Acquire)
|| AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire)
|| AUTO_LOAD_IN_PROGRESS.load(Ordering::Acquire)
{
return;
}
let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() };
let last_gate_mask = AUTO_LOAD_LAST_GATE_MASK.swap(gate_mask, Ordering::AcqRel);
if gate_mask != last_gate_mask {
log_auto_load_gate_mask(gate_mask);
}
let mode_id = unsafe { current_mode_id() };
let ready = gate_mask == 0x7 && mode_id == 2;
let ready_count = if ready {
AUTO_LOAD_READY_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
AUTO_LOAD_READY_COUNT.store(0, Ordering::Release);
AUTO_LOAD_DEFERRED.store(false, Ordering::Release);
0
};
if ready {
append_log_message(AUTO_LOAD_READY_COUNT_MESSAGE);
log_auto_load_ready_count(ready_count, gate_mask, mode_id);
}
let ready_polls = auto_load_ready_polls();
if ready_count < ready_polls {
return;
}
if !AUTO_LOAD_DEFERRED.load(Ordering::Acquire) {
AUTO_LOAD_DEFERRED.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_READY_MESSAGE);
append_log_message(AUTO_LOAD_DEFERRED_MESSAGE);
return;
}
if ready_count < ready_polls.saturating_add(auto_load_defer_polls()) {
return;
}
if AUTO_LOAD_ATTEMPTED.swap(true, Ordering::AcqRel) {
return;
}
if !AUTO_LOAD_TRANSITION_ARMED.load(Ordering::Acquire) {
let Some(save_stem) = AUTO_LOAD_SAVE_STEM.get() else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release);
return;
};
if unsafe { stage_manual_load_request(save_stem) } {
AUTO_LOAD_TRANSITION_ARMED.store(true, Ordering::Release);
AUTO_LOAD_ARMED_TICK_COUNT.store(0, Ordering::Release);
append_log_message(AUTO_LOAD_STAGED_MESSAGE);
log_auto_load_armed_tick();
AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release);
AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_ARMED_TICK_MESSAGE);
append_log_message(AUTO_LOAD_READY_MESSAGE);
run_auto_load_worker();
return;
}
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
AUTO_LOAD_ATTEMPTED.store(true, Ordering::Release);
return;
}
AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_READY_MESSAGE);
run_auto_load_worker();
}
pub(super) fn log_auto_load_gate_mask(mask: u32) {
let mut line = String::from("rrt-hook: auto load gate mask ");
let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize;
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
let mode_id = if shell_state.is_null() {
0
} else {
unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize }
};
let field_active_mode_object = if shell_state.is_null() {
0
} else {
unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize }
};
let startup_selector = unsafe {
let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8);
if runtime_profile.is_null() {
0
} else {
read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize
}
};
let _ = write!(
&mut line,
"0x{mask:01x} shell_state={} shell_controller={} active_mode={} global_active_mode=0x{global_active_mode:08x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} field_active_mode_object=0x{field_active_mode_object:08x}\n",
(mask & 0x1) != 0,
(mask & 0x2) != 0,
(mask & 0x4) != 0,
);
append_log_line(&line);
}
pub(super) fn log_auto_load_armed_tick() {
let tick_count = AUTO_LOAD_ARMED_TICK_COUNT.fetch_add(1, Ordering::AcqRel) + 1;
let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() };
let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize;
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
let mode_id = if shell_state.is_null() {
0
} else {
unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize }
};
let startup_selector = unsafe {
let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8);
if runtime_profile.is_null() {
0
} else {
read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize
}
};
let mut line = String::from("rrt-hook: auto load armed tick ");
let _ = write!(
&mut line,
"count=0x{tick_count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x} global_active_mode=0x{global_active_mode:08x}\n",
);
append_log_line(&line);
}
pub(super) fn log_auto_load_ready_count(count: u32, gate_mask: u32, mode_id: u32) {
let startup_selector = unsafe {
let runtime_profile = read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8);
if runtime_profile.is_null() {
0
} else {
read_u8(runtime_profile.add(RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET)) as usize
}
};
let mut line = String::from("rrt-hook: auto load ready count ");
let _ = write!(
&mut line,
"count=0x{count:08x} gate_mask=0x{gate_mask:01x} mode_id=0x{mode_id:08x} startup_selector=0x{startup_selector:08x}\n",
);
append_log_line(&line);
}

View file

@ -0,0 +1,333 @@
use super::*;
pub(super) fn maybe_emit_finance_template_bundle() {
if env::var_os("RRT_WRITE_FINANCE_TEMPLATE").is_none() {
return;
}
if FINANCE_TEMPLATE_EMITTED.swap(true, Ordering::AcqRel) {
return;
}
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = write_finance_snapshot_bundle(&base_dir, "attach_template", &sample_finance_snapshot());
}
pub(super) fn maybe_start_finance_capture_thread() {
if env::var_os("RRT_WRITE_FINANCE_CAPTURE").is_none() {
return;
}
if FINANCE_CAPTURE_STARTED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(FINANCE_CAPTURE_STARTED_MESSAGE);
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = thread::Builder::new()
.name("rrt-finance-capture".to_string())
.spawn(move || {
for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS {
if !FINANCE_COLLECTION_PROBE_WRITTEN.load(Ordering::Acquire) {
if let Some(probe) = unsafe { capture_company_collection_probe() } {
if write_indexed_collection_probe(&base_dir, "attach_probe", &probe).is_ok()
{
FINANCE_COLLECTION_PROBE_WRITTEN.store(true, Ordering::Release);
append_log_message(FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE);
}
}
}
if let Some(snapshot) = unsafe { try_capture_probe_snapshot() } {
append_log_message(FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE);
if write_finance_snapshot_only(&base_dir, "attach_probe", &snapshot).is_ok() {
append_log_message(FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE);
return;
}
}
thread::sleep(CAPTURE_POLL_INTERVAL);
}
append_log_message(FINANCE_CAPTURE_TIMEOUT_MESSAGE);
});
}
pub(super) fn maybe_start_cargo_capture_thread() {
if env::var_os("RRT_WRITE_CARGO_CAPTURE").is_none() {
return;
}
if CARGO_CAPTURE_STARTED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(CARGO_CAPTURE_STARTED_MESSAGE);
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = thread::Builder::new()
.name("rrt-cargo-capture".to_string())
.spawn(move || {
let mut last_probe: Option<CargoCollectionProbe> = None;
for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS {
if let Some(probe) = unsafe { capture_cargo_collection_probe() } {
last_probe = Some(probe.clone());
if probe.live_entry_count > 0
&& write_cargo_collection_probe(&base_dir, "attach_probe", &probe).is_ok()
{
append_log_message(CARGO_CAPTURE_WRITTEN_MESSAGE);
return;
}
}
thread::sleep(CAPTURE_POLL_INTERVAL);
}
if let Some(probe) = last_probe {
let _ = write_cargo_collection_probe(&base_dir, "attach_probe_timeout", &probe);
}
append_log_message(CARGO_CAPTURE_TIMEOUT_MESSAGE);
});
}
pub(super) unsafe fn try_capture_probe_snapshot() -> Option<FinanceSnapshot> {
append_log_message(FINANCE_CAPTURE_SCAN_MESSAGE);
let company = unsafe { resolve_first_active_company()? };
Some(unsafe { capture_probe_snapshot_from_company(company) })
}
pub(super) unsafe fn resolve_first_active_company() -> Option<*mut u8> {
let collection = COMPANY_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return None;
}
for entry_id in 1..=id_bound as usize {
if unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } {
let company =
unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) };
if !company.is_null() && unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 } {
return Some(company);
}
}
}
None
}
pub(super) unsafe fn capture_company_collection_probe() -> Option<IndexedCollectionProbe> {
let collection = COMPANY_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return Some(IndexedCollectionProbe {
collection_addr: COMPANY_COLLECTION_ADDR,
flat_payload: unsafe {
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
},
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
},
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
first_rows: Vec::new(),
});
}
let mut first_rows = Vec::new();
let sample_bound = (id_bound as usize).min(8);
for entry_id in 1..=sample_bound {
let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) };
let resolved_ptr =
unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize };
let active_flag = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(COMPANY_ACTIVE_OFFSET)) })
};
first_rows.push(IndexedCollectionProbeRow {
entry_id,
live,
resolved_ptr,
active_flag,
});
}
Some(IndexedCollectionProbe {
collection_addr: COMPANY_COLLECTION_ADDR,
flat_payload: unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 },
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
},
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
first_rows,
})
}
pub(super) unsafe fn capture_cargo_collection_probe() -> Option<CargoCollectionProbe> {
let collection = CARGO_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return Some(CargoCollectionProbe {
collection_addr: CARGO_COLLECTION_ADDR,
flat_payload: unsafe {
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
},
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
},
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
live_entry_count: 0,
rows: Vec::new(),
});
}
let mut live_entry_count = 0_usize;
let mut rows = Vec::with_capacity(id_bound as usize);
for entry_id in 1..=id_bound as usize {
let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) };
let resolved_ptr =
unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize };
if live && resolved_ptr != 0 {
live_entry_count += 1;
}
let stem = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_c_string((resolved_ptr as *const u8).add(CARGO_STEM_OFFSET), 0x1e) })
};
let route_style_byte = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_ROUTE_STYLE_OFFSET)) })
};
let subtype_byte = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_SUBTYPE_OFFSET)) })
};
let class_marker = if live {
Some(unsafe {
read_u32(collection.add(CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET + entry_id * 4))
})
} else {
None
};
rows.push(CargoCollectionProbeRow {
entry_id,
live,
resolved_ptr,
stem,
route_style_byte,
subtype_byte,
class_marker,
});
}
Some(CargoCollectionProbe {
collection_addr: CARGO_COLLECTION_ADDR,
flat_payload: unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 },
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
},
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
live_entry_count,
rows,
})
}
pub(super) unsafe fn capture_probe_snapshot_from_company(company: *mut u8) -> FinanceSnapshot {
let scenario = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as *const u8;
let current_year = unsafe { read_u16(scenario.add(SCENARIO_CURRENT_YEAR_OFFSET)) };
let founding_year = unsafe { read_u16(company.add(COMPANY_FOUNDING_YEAR_OFFSET)) };
let last_bankruptcy_year =
unsafe { read_u16(company.add(COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET)) };
let outstanding_share_count =
unsafe { read_u32(company.add(COMPANY_OUTSTANDING_SHARES_OFFSET)) };
let bonds = unsafe { capture_bonds(company, current_year) };
let company_value = unsafe { read_u32(company.add(COMPANY_COMPANY_VALUE_OFFSET)) as i64 };
let growth_setting = unsafe {
growth_setting_from_raw(read_u8(
scenario.add(SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET),
))
};
FinanceSnapshot {
policy: AnnualFinancePolicy {
annual_mode: 0x0c,
bankruptcy_allowed: unsafe {
read_u8(scenario.add(SCENARIO_BANKRUPTCY_TOGGLE_OFFSET)) == 0
},
bond_issuance_allowed: unsafe {
read_u8(scenario.add(SCENARIO_BOND_TOGGLE_OFFSET)) == 0
},
stock_actions_allowed: unsafe {
read_u8(scenario.add(SCENARIO_STOCK_TOGGLE_OFFSET)) == 0
},
dividends_allowed: unsafe {
read_u8(scenario.add(SCENARIO_DIVIDEND_TOGGLE_OFFSET)) == 0
},
growth_setting,
..AnnualFinancePolicy::default()
},
company: CompanyFinanceState {
active: unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 },
years_since_founding: year_delta(current_year, founding_year),
years_since_last_bankruptcy: year_delta(current_year, last_bankruptcy_year),
current_company_value: company_value,
outstanding_share_count,
city_connection_bonus_latch: unsafe {
read_u8(company.add(COMPANY_CITY_CONNECTION_LATCH_OFFSET)) != 0
},
linked_transit_service_latch: unsafe {
read_u8(company.add(COMPANY_LINKED_TRANSIT_LATCH_OFFSET)) != 0
},
chairman_buyback_factor: None,
bonds,
..CompanyFinanceState::default()
},
}
}
pub(super) unsafe fn capture_bonds(company: *mut u8, current_year: u16) -> Vec<BondPosition> {
let bond_count = unsafe { read_u8(company.add(COMPANY_BOND_COUNT_OFFSET)) as usize };
let table = unsafe { company.add(COMPANY_BOND_TABLE_OFFSET) };
let mut bonds = Vec::with_capacity(bond_count);
for index in 0..bond_count {
let slot = unsafe { table.add(index * 12) };
let principal = unsafe { read_i32(slot) } as i64;
let maturity_year = unsafe { read_u32(slot.add(4)) };
let coupon_rate = unsafe { read_f32(slot.add(8)) } as f64;
bonds.push(BondPosition {
principal,
coupon_rate,
years_remaining: maturity_year
.saturating_sub(current_year as u32)
.min(u8::MAX as u32) as u8,
});
}
bonds
}
pub(super) fn growth_setting_from_raw(raw: u8) -> GrowthSetting {
match raw {
1 => GrowthSetting::ExpansionBias,
2 => GrowthSetting::DividendSuppressed,
_ => GrowthSetting::Neutral,
}
}
pub(super) fn year_delta(current_year: u16, past_year: u16) -> u8 {
current_year.saturating_sub(past_year).min(u8::MAX as u16) as u8
}

View file

@ -0,0 +1,221 @@
use super::*;
pub(super) const DLL_PROCESS_ATTACH: u32 = 1;
pub(super) const E_FAIL: i32 = 0x8000_4005_u32 as i32;
pub(super) const FILE_APPEND_DATA: u32 = 0x0000_0004;
pub(super) const FILE_SHARE_READ: u32 = 0x0000_0001;
pub(super) const OPEN_ALWAYS: u32 = 4;
pub(super) const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
pub(super) const INVALID_HANDLE_VALUE: isize = -1;
pub(super) const FILE_END: u32 = 2;
pub(super) static LOG_PATH: &[u8] = b"rrt_hook_attach.log\0";
pub(super) static ATTACH_MESSAGE: &[u8] = b"rrt-hook: process attach\n";
pub(super) static FINANCE_CAPTURE_STARTED_MESSAGE: &[u8] =
b"rrt-hook: finance capture thread started\n";
pub(super) static FINANCE_CAPTURE_SCAN_MESSAGE: &[u8] =
b"rrt-hook: finance capture raw collection scan\n";
pub(super) static FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE: &[u8] =
b"rrt-hook: finance collection probe written\n";
pub(super) static FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE: &[u8] =
b"rrt-hook: finance capture company resolved\n";
pub(super) static FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE: &[u8] =
b"rrt-hook: finance probe snapshot written\n";
pub(super) static FINANCE_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: finance capture timed out\n";
pub(super) static CARGO_CAPTURE_STARTED_MESSAGE: &[u8] =
b"rrt-hook: cargo capture thread started\n";
pub(super) static CARGO_CAPTURE_WRITTEN_MESSAGE: &[u8] =
b"rrt-hook: cargo collection probe written\n";
pub(super) static CARGO_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: cargo capture timed out\n";
pub(super) static AUTO_LOAD_STARTED_MESSAGE: &[u8] = b"rrt-hook: auto load hook armed\n";
pub(super) static AUTO_LOAD_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell-state hook installed\n";
pub(super) static AUTO_LOAD_PROFILE_DISPATCH_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load startup-dispatch hook installed\n";
pub(super) static AUTO_LOAD_RUNTIME_RESET_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load runtime-reset hook installed\n";
pub(super) static AUTO_LOAD_ALLOCATOR_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load allocator hook installed\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen scalar hook installed\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen hook installed\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen message hook installed\n";
pub(super) static AUTO_LOAD_RUNTIME_PRIME_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell runtime-prime hook installed\n";
pub(super) static AUTO_LOAD_FRAME_CYCLE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell frame-cycle hook installed\n";
pub(super) static AUTO_LOAD_OBJECT_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object-service hook installed\n";
pub(super) static AUTO_LOAD_CHILD_SERVICE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell child-service hook installed\n";
pub(super) static AUTO_LOAD_PUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell publish hook installed\n";
pub(super) static AUTO_LOAD_UNPUBLISH_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell unpublish hook installed\n";
pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object teardown hook installed\n";
pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object range-remove hook installed\n";
pub(super) static AUTO_LOAD_REMOVE_NODE_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell remove-node hook installed\n";
pub(super) static AUTO_LOAD_NODE_VCALL_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell node-vcall hook installed\n";
pub(super) static AUTO_LOAD_MODE2_TEARDOWN_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load mode2 teardown hook installed\n";
pub(super) static AUTO_LOAD_READY_MESSAGE: &[u8] = b"rrt-hook: auto load ready gate passed\n";
pub(super) static AUTO_LOAD_DEFERRED_MESSAGE: &[u8] =
b"rrt-hook: auto load restore deferred to later service turn\n";
pub(super) static AUTO_LOAD_CALLING_MESSAGE: &[u8] = b"rrt-hook: auto load restore calling\n";
pub(super) static AUTO_LOAD_STAGED_MESSAGE: &[u8] =
b"rrt-hook: auto load restore staged for later transition\n";
pub(super) static AUTO_LOAD_READY_COUNT_MESSAGE: &[u8] = b"rrt-hook: auto load ready count\n";
pub(super) static AUTO_LOAD_ARMED_TICK_MESSAGE: &[u8] =
b"rrt-hook: auto load armed transition tick\n";
pub(super) static AUTO_LOAD_OWNER_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell transition entering\n";
pub(super) static AUTO_LOAD_OWNER_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell transition returned\n";
pub(super) static AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load startup dispatch entering\n";
pub(super) static AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load startup dispatch returned\n";
pub(super) static AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load runtime reset entering\n";
pub(super) static AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load runtime reset returned\n";
pub(super) static AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load allocator entering\n";
pub(super) static AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load allocator returned\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen scalar entering\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen scalar returned\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen construct entering\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen construct returned\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen message entering\n";
pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load load-screen message returned\n";
pub(super) static AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell runtime-prime entering\n";
pub(super) static AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell runtime-prime returned\n";
pub(super) static AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell frame-cycle entering\n";
pub(super) static AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell frame-cycle returned\n";
pub(super) static AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object-service entering\n";
pub(super) static AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object-service returned\n";
pub(super) static AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell child-service entering\n";
pub(super) static AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell child-service returned\n";
pub(super) static AUTO_LOAD_PUBLISH_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell publish entering\n";
pub(super) static AUTO_LOAD_PUBLISH_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell publish returned\n";
pub(super) static AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell unpublish entering\n";
pub(super) static AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell unpublish returned\n";
pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object teardown entering\n";
pub(super) static AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object teardown returned\n";
pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object range-remove entering\n";
pub(super) static AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell object range-remove returned\n";
pub(super) static AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell remove-node entering\n";
pub(super) static AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell remove-node returned\n";
pub(super) static AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load shell node-vcall entering\n";
pub(super) static AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell node-vcall returned\n";
pub(super) static AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load mode2 teardown entering\n";
pub(super) static AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load mode2 teardown returned\n";
pub(super) static AUTO_LOAD_TRIGGERED_MESSAGE: &[u8] = b"rrt-hook: auto load restore invoked\n";
pub(super) static AUTO_LOAD_SUCCESS_MESSAGE: &[u8] =
b"rrt-hook: auto load request reported success\n";
pub(super) static AUTO_LOAD_FAILURE_MESSAGE: &[u8] =
b"rrt-hook: auto load request reported failure\n";
pub(super) static DEBUG_MESSAGE: &[u8] = b"rrt-hook: DllMain process attach\0";
pub(super) static DIRECT_INPUT8_CREATE_NAME: &[u8] = b"DirectInput8Create\0";
pub(super) const COMPANY_COLLECTION_ADDR: usize = 0x0062be10;
pub(super) const CARGO_COLLECTION_ADDR: usize = 0x0062ba8c;
pub(super) const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024;
pub(super) const SHELL_STATE_PTR_ADDR: usize = 0x006cec74;
pub(super) const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78;
pub(super) const SHELL_STATE_SERVICE_ADDR: usize = 0x00482160;
pub(super) const SHELL_TRANSITION_MODE_ADDR: usize = 0x00482ec0;
pub(super) const PROFILE_STARTUP_DISPATCH_ADDR: usize = 0x00438890;
pub(super) const RUNTIME_RESET_ADDR: usize = 0x004336d0;
pub(super) const STARTUP_RUNTIME_ALLOC_THUNK_ADDR: usize = 0x0053b070;
pub(super) const LOAD_SCREEN_SET_SCALAR_ADDR: usize = 0x004ea710;
pub(super) const LOAD_SCREEN_CONSTRUCT_ADDR: usize = 0x004ea620;
pub(super) const LOAD_SCREEN_HANDLE_MESSAGE_ADDR: usize = 0x004e3a80;
pub(super) const SHELL_RUNTIME_PRIME_ADDR: usize = 0x00538b60;
pub(super) const SHELL_FRAME_CYCLE_ADDR: usize = 0x00520620;
pub(super) const SHELL_OBJECT_SERVICE_ADDR: usize = 0x0053fda0;
pub(super) const SHELL_CHILD_SERVICE_ADDR: usize = 0x005595d0;
pub(super) const SHELL_PUBLISH_WINDOW_ADDR: usize = 0x00538e50;
pub(super) const SHELL_UNPUBLISH_WINDOW_ADDR: usize = 0x005389c0;
pub(super) const SHELL_OBJECT_TEARDOWN_ADDR: usize = 0x005400c0;
pub(super) const SHELL_OBJECT_RANGE_REMOVE_ADDR: usize = 0x0053fe00;
pub(super) const SHELL_REMOVE_NODE_ADDR: usize = 0x0053f860;
pub(super) const SHELL_NODE_VCALL_ADDR: usize = 0x00540910;
pub(super) const MODE2_TEARDOWN_ADDR: usize = 0x00502720;
pub(super) const SHELL_STATE_ACTIVE_MODE_OFFSET: usize = 0x08;
pub(super) const SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET: usize = 0x0c;
pub(super) const RUNTIME_PROFILE_PTR_ADDR: usize = 0x006cec7c;
pub(super) const RUNTIME_PROFILE_STARTUP_SELECTOR_OFFSET: usize = 0x01;
pub(super) const RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET: usize = 0x11;
pub(super) const RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET: usize = 0x97;
pub(super) const SHELL_MODE_STARTUP_LOAD_DISPATCH: u32 = 1;
pub(super) const STARTUP_SELECTOR_SCENARIO_LOAD: u8 = 3;
pub(super) const STARTUP_RUNTIME_OBJECT_SIZE: u32 = 0x00046c40;
pub(super) const INDEXED_COLLECTION_FLAT_FLAG_OFFSET: usize = 0x04;
pub(super) const INDEXED_COLLECTION_STRIDE_OFFSET: usize = 0x08;
pub(super) const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14;
pub(super) const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30;
pub(super) const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34;
pub(super) const CARGO_STEM_OFFSET: usize = 0x04;
pub(super) const CARGO_SUBTYPE_OFFSET: usize = 0x32;
pub(super) const CARGO_ROUTE_STYLE_OFFSET: usize = 0x46;
pub(super) const CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET: usize = 0x9a;
pub(super) const COMPANY_ACTIVE_OFFSET: usize = 0x3f;
pub(super) const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47;
pub(super) const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57;
pub(super) const COMPANY_BOND_COUNT_OFFSET: usize = 0x5b;
pub(super) const COMPANY_BOND_TABLE_OFFSET: usize = 0x5f;
pub(super) const COMPANY_FOUNDING_YEAR_OFFSET: usize = 0x157;
pub(super) const COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163;
pub(super) const COMPANY_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18;
pub(super) const COMPANY_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
pub(super) const SCENARIO_CURRENT_YEAR_OFFSET: usize = 0x0d;
pub(super) const SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET: usize = 0x4c7c;
pub(super) const SCENARIO_BANKRUPTCY_TOGGLE_OFFSET: usize = 0x4a8f;
pub(super) const SCENARIO_BOND_TOGGLE_OFFSET: usize = 0x4a8b;
pub(super) const SCENARIO_STOCK_TOGGLE_OFFSET: usize = 0x4a87;
pub(super) const SCENARIO_DIVIDEND_TOGGLE_OFFSET: usize = 0x4a93;
pub(super) const MAX_CAPTURE_POLL_ATTEMPTS: usize = 120;
pub(super) const CAPTURE_POLL_INTERVAL: Duration = Duration::from_secs(1);
pub(super) const AUTO_LOAD_READY_POLLS: u32 = 1;
pub(super) const AUTO_LOAD_DEFER_POLLS: u32 = 0;
pub(super) const MEM_COMMIT: u32 = 0x1000;
pub(super) const MEM_RESERVE: u32 = 0x2000;
pub(super) const PAGE_EXECUTE_READWRITE: u32 = 0x40;

View file

@ -0,0 +1,292 @@
use super::*;
pub(super) unsafe extern "fastcall" fn shell_state_service_detour(
this: *mut u8,
_edx: usize,
) -> i32 {
log_shell_state_service_entry(this);
let trampoline: ShellStateServiceFn = unsafe { mem::transmute(SHELL_STATE_SERVICE_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
log_shell_state_service_return(this, result);
log_post_transition_service_state();
maybe_service_auto_load_on_main_thread();
result
}
pub(super) unsafe extern "fastcall" fn profile_startup_dispatch_detour(
this: *mut u8,
_edx: usize,
arg1: u32,
arg2: u32,
) -> i32 {
log_profile_startup_dispatch_entry(this, arg1, arg2);
append_log_message(AUTO_LOAD_PROFILE_DISPATCH_ENTRY_MESSAGE);
let trampoline: ProfileStartupDispatchFn =
unsafe { mem::transmute(PROFILE_STARTUP_DISPATCH_TRAMPOLINE) };
let result = unsafe { trampoline(this, arg1, arg2) };
append_log_message(AUTO_LOAD_PROFILE_DISPATCH_RETURNED_MESSAGE);
log_profile_startup_dispatch_return(this, arg1, arg2, result);
result
}
pub(super) unsafe extern "fastcall" fn runtime_reset_detour(this: *mut u8, _edx: usize) -> *mut u8 {
append_log_message(AUTO_LOAD_RUNTIME_RESET_ENTRY_MESSAGE);
log_runtime_reset_entry(this);
let trampoline: RuntimeResetFn = unsafe { mem::transmute(RUNTIME_RESET_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
append_log_message(AUTO_LOAD_RUNTIME_RESET_RETURNED_MESSAGE);
log_runtime_reset_return(this, result);
result
}
pub(super) unsafe extern "cdecl" fn allocator_detour(size: u32) -> *mut u8 {
let trace = should_trace_allocator(size);
let count = if trace {
AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_ALLOCATOR_ENTRY_MESSAGE);
log_allocator_entry(size, count);
}
let original: StartupRuntimeAllocThunkFn = unsafe { mem::transmute(0x005a125dusize) };
let result = unsafe { original(size) };
if trace {
append_log_message(AUTO_LOAD_ALLOCATOR_RETURNED_MESSAGE);
log_allocator_return(size, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn load_screen_scalar_detour(
this: *mut u8,
_edx: usize,
value_bits: u32,
) -> u32 {
append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_ENTRY_MESSAGE);
log_load_screen_scalar_entry(this, value_bits);
let trampoline: LoadScreenSetScalarFn =
unsafe { mem::transmute(LOAD_SCREEN_SCALAR_TRAMPOLINE) };
let result = unsafe { trampoline(this, value_bits) };
append_log_message(AUTO_LOAD_LOAD_SCREEN_SCALAR_RETURNED_MESSAGE);
log_load_screen_scalar_return(this, value_bits, result);
result
}
pub(super) unsafe extern "fastcall" fn load_screen_construct_detour(
this: *mut u8,
_edx: usize,
) -> *mut u8 {
append_log_message(AUTO_LOAD_LOAD_SCREEN_ENTRY_MESSAGE);
log_load_screen_construct_entry(this);
let trampoline: LoadScreenConstructFn =
unsafe { mem::transmute(LOAD_SCREEN_CONSTRUCT_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
append_log_message(AUTO_LOAD_LOAD_SCREEN_RETURNED_MESSAGE);
log_load_screen_construct_return(this, result);
result
}
pub(super) unsafe extern "fastcall" fn load_screen_message_detour(
this: *mut u8,
_edx: usize,
message: *mut u8,
) -> i32 {
let trace = should_trace_load_screen_message(this);
let count = if trace {
AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_ENTRY_MESSAGE);
log_load_screen_message_entry(this, message, count);
}
let trampoline: LoadScreenHandleMessageFn =
unsafe { mem::transmute(LOAD_SCREEN_MESSAGE_TRAMPOLINE) };
let result = unsafe { trampoline(this, message) };
if trace {
append_log_message(AUTO_LOAD_LOAD_SCREEN_MESSAGE_RETURNED_MESSAGE);
log_load_screen_message_return(this, message, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn runtime_prime_detour(this: *mut u8, _edx: usize) -> i32 {
let trace = should_trace_runtime_prime();
let count = if trace {
AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_RUNTIME_PRIME_ENTRY_MESSAGE);
log_runtime_prime_entry(this, count);
}
let trampoline: ShellRuntimePrimeFn = unsafe { mem::transmute(RUNTIME_PRIME_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
if trace {
append_log_message(AUTO_LOAD_RUNTIME_PRIME_RETURNED_MESSAGE);
log_runtime_prime_return(this, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn frame_cycle_detour(this: *mut u8, _edx: usize) -> i32 {
let trace = should_trace_frame_cycle();
let count = if trace {
AUTO_LOAD_FRAME_CYCLE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_FRAME_CYCLE_ENTRY_MESSAGE);
log_frame_cycle_entry(this, count);
}
let trampoline: ShellFrameCycleFn = unsafe { mem::transmute(FRAME_CYCLE_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
if trace {
append_log_message(AUTO_LOAD_FRAME_CYCLE_RETURNED_MESSAGE);
log_frame_cycle_return(this, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn object_service_detour(this: *mut u8, _edx: usize) -> i32 {
let trace = should_trace_object_service();
let count = if trace {
AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_OBJECT_SERVICE_ENTRY_MESSAGE);
log_object_service_entry(this, count);
}
let trampoline: ShellObjectServiceFn = unsafe { mem::transmute(OBJECT_SERVICE_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
if trace {
append_log_message(AUTO_LOAD_OBJECT_SERVICE_RETURNED_MESSAGE);
log_object_service_return(this, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn child_service_detour(this: *mut u8, _edx: usize) -> i32 {
let trace = should_trace_child_service();
let count = if trace {
AUTO_LOAD_CHILD_SERVICE_LOG_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
0
};
if trace {
append_log_message(AUTO_LOAD_CHILD_SERVICE_ENTRY_MESSAGE);
log_child_service_entry(this, count);
}
let trampoline: ShellChildServiceFn = unsafe { mem::transmute(CHILD_SERVICE_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
if trace {
append_log_message(AUTO_LOAD_CHILD_SERVICE_RETURNED_MESSAGE);
log_child_service_return(this, count, result);
}
result
}
pub(super) unsafe extern "fastcall" fn shell_publish_detour(
this: *mut u8,
_edx: usize,
object: *mut u8,
flag: u32,
) -> i32 {
append_log_message(AUTO_LOAD_PUBLISH_ENTRY_MESSAGE);
log_shell_publish_entry(this, object, flag);
let trampoline: ShellPublishWindowFn = unsafe { mem::transmute(SHELL_PUBLISH_TRAMPOLINE) };
let result = unsafe { trampoline(this, object, flag) };
append_log_message(AUTO_LOAD_PUBLISH_RETURNED_MESSAGE);
log_shell_publish_return(this, object, flag, result);
result
}
pub(super) unsafe extern "fastcall" fn shell_unpublish_detour(
this: *mut u8,
_edx: usize,
object: *mut u8,
) -> i32 {
append_log_message(AUTO_LOAD_UNPUBLISH_ENTRY_MESSAGE);
log_shell_unpublish_entry(this, object);
let trampoline: ShellUnpublishWindowFn = unsafe { mem::transmute(SHELL_UNPUBLISH_TRAMPOLINE) };
let result = unsafe { trampoline(this, object) };
append_log_message(AUTO_LOAD_UNPUBLISH_RETURNED_MESSAGE);
log_shell_unpublish_return(this, object, result);
result
}
pub(super) unsafe extern "fastcall" fn shell_object_range_remove_detour(
this: *mut u8,
_edx: usize,
arg1: u32,
arg2: u32,
arg3: u32,
) -> i32 {
append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_ENTRY_MESSAGE);
log_shell_object_range_remove_entry(this, arg1, arg2, arg3);
let trampoline: ShellObjectRangeRemoveFn =
unsafe { mem::transmute(SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE) };
let result = unsafe { trampoline(this, arg1, arg2, arg3) };
append_log_message(AUTO_LOAD_OBJECT_RANGE_REMOVE_RETURNED_MESSAGE);
log_shell_object_range_remove_return(this, arg1, arg2, arg3, result);
result
}
pub(super) unsafe extern "fastcall" fn shell_object_teardown_detour(
this: *mut u8,
_edx: usize,
) -> i32 {
append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_ENTRY_MESSAGE);
log_shell_object_teardown_entry(this);
let trampoline: ShellObjectTeardownFn =
unsafe { mem::transmute(SHELL_OBJECT_TEARDOWN_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
append_log_message(AUTO_LOAD_OBJECT_TEARDOWN_RETURNED_MESSAGE);
log_shell_object_teardown_return(this, result);
result
}
pub(super) unsafe extern "fastcall" fn shell_node_vcall_detour(
this: *mut u8,
_edx: usize,
record: *mut u8,
) -> i32 {
append_log_message(AUTO_LOAD_NODE_VCALL_ENTRY_MESSAGE);
log_shell_node_vcall_entry(this, record);
let trampoline: ShellNodeVcallFn = unsafe { mem::transmute(SHELL_NODE_VCALL_TRAMPOLINE) };
let result = unsafe { trampoline(this, record) };
append_log_message(AUTO_LOAD_NODE_VCALL_RETURNED_MESSAGE);
log_shell_node_vcall_return(this, record, result);
result
}
pub(super) unsafe extern "fastcall" fn shell_remove_node_detour(
this: *mut u8,
_edx: usize,
node: *mut u8,
) -> i32 {
append_log_message(AUTO_LOAD_REMOVE_NODE_ENTRY_MESSAGE);
log_shell_remove_node_entry(this, node);
let trampoline: ShellRemoveNodeFn = unsafe { mem::transmute(SHELL_REMOVE_NODE_TRAMPOLINE) };
let result = unsafe { trampoline(this, node) };
append_log_message(AUTO_LOAD_REMOVE_NODE_RETURNED_MESSAGE);
log_shell_remove_node_return(this, node, result);
result
}
pub(super) unsafe extern "fastcall" fn mode2_teardown_detour(this: *mut u8, _edx: usize) -> i32 {
append_log_message(AUTO_LOAD_MODE2_TEARDOWN_ENTRY_MESSAGE);
log_mode2_teardown_entry(this);
let trampoline: Mode2TeardownFn = unsafe { mem::transmute(MODE2_TEARDOWN_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
append_log_message(AUTO_LOAD_MODE2_TEARDOWN_RETURNED_MESSAGE);
log_mode2_teardown_return(this, result);
result
}

View file

@ -0,0 +1,86 @@
use super::*;
unsafe extern "system" {
pub(super) fn CreateFileA(
lp_file_name: *const c_char,
desired_access: u32,
share_mode: u32,
security_attributes: *mut c_void,
creation_disposition: u32,
flags_and_attributes: u32,
template_file: *mut c_void,
) -> isize;
pub(super) fn SetFilePointer(
file: isize,
distance: i32,
distance_high: *mut i32,
move_method: u32,
) -> u32;
pub(super) fn WriteFile(
file: isize,
buffer: *const c_void,
bytes_to_write: u32,
bytes_written: *mut u32,
overlapped: *mut c_void,
) -> i32;
pub(super) fn CloseHandle(handle: isize) -> i32;
pub(super) fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
pub(super) fn FlushInstructionCache(
process: *mut c_void,
base_address: *const c_void,
size: usize,
) -> i32;
pub(super) fn GetCurrentProcess() -> *mut c_void;
pub(super) fn GetSystemDirectoryA(buffer: *mut u8, size: u32) -> u32;
pub(super) fn GetProcAddress(module: isize, name: *const c_char) -> *mut c_void;
pub(super) fn LoadLibraryA(name: *const c_char) -> isize;
pub(super) fn OutputDebugStringA(output: *const c_char);
pub(super) fn VirtualAlloc(
address: *mut c_void,
size: usize,
allocation_type: u32,
protect: u32,
) -> *mut c_void;
pub(super) fn VirtualProtect(
address: *mut c_void,
size: usize,
new_protect: u32,
old_protect: *mut u32,
) -> i32;
}
#[repr(C)]
pub(crate) struct Guid {
data1: u32,
data2: u16,
data3: u16,
data4: [u8; 8],
}
pub(super) type DirectInput8CreateFn = unsafe extern "system" fn(
instance: *mut c_void,
version: u32,
riid: *const Guid,
out: *mut *mut c_void,
outer: *mut c_void,
) -> i32;
pub(super) type ShellStateServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellTransitionModeFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32;
pub(super) type ProfileStartupDispatchFn = unsafe extern "thiscall" fn(*mut u8, u32, u32) -> i32;
pub(super) type RuntimeResetFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8;
pub(super) type StartupRuntimeAllocThunkFn = unsafe extern "cdecl" fn(u32) -> *mut u8;
pub(super) type LoadScreenSetScalarFn = unsafe extern "thiscall" fn(*mut u8, u32) -> u32;
pub(super) type LoadScreenConstructFn = unsafe extern "thiscall" fn(*mut u8) -> *mut u8;
pub(super) type LoadScreenHandleMessageFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32;
pub(super) type ShellRuntimePrimeFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellFrameCycleFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellObjectServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellChildServiceFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellPublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) -> i32;
pub(super) type ShellUnpublishWindowFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32;
pub(super) type ShellObjectTeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
pub(super) type ShellObjectRangeRemoveFn =
unsafe extern "thiscall" fn(*mut u8, u32, u32, u32) -> i32;
pub(super) type ShellRemoveNodeFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32;
pub(super) type ShellNodeVcallFn = unsafe extern "thiscall" fn(*mut u8, *mut u8) -> i32;
pub(super) type Mode2TeardownFn = unsafe extern "thiscall" fn(*mut u8) -> i32;

View file

@ -0,0 +1,383 @@
use super::*;
pub(super) unsafe fn install_shell_state_service_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = SHELL_STATE_SERVICE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_state_service_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_STATE_SERVICE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_profile_startup_dispatch_hook() -> bool {
const STOLEN_LEN: usize = 16;
let target = PROFILE_STARTUP_DISPATCH_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
profile_startup_dispatch_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { PROFILE_STARTUP_DISPATCH_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_runtime_reset_hook() -> bool {
const STOLEN_LEN: usize = 16;
let target = RUNTIME_RESET_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
runtime_reset_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { RUNTIME_RESET_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_allocator_hook() -> bool {
const STOLEN_LEN: usize = 5;
let target = STARTUP_RUNTIME_ALLOC_THUNK_ADDR as *mut u8;
let trampoline =
unsafe { install_rel32_detour(target, STOLEN_LEN, allocator_detour as *const () as usize) };
if trampoline == 0 {
return false;
}
unsafe { ALLOCATOR_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_load_screen_scalar_hook() -> bool {
const STOLEN_LEN: usize = 10;
let target = LOAD_SCREEN_SET_SCALAR_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
load_screen_scalar_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { LOAD_SCREEN_SCALAR_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_load_screen_construct_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = LOAD_SCREEN_CONSTRUCT_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
load_screen_construct_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { LOAD_SCREEN_CONSTRUCT_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_load_screen_message_hook() -> bool {
const STOLEN_LEN: usize = 25;
let target = LOAD_SCREEN_HANDLE_MESSAGE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
load_screen_message_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { LOAD_SCREEN_MESSAGE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_runtime_prime_hook() -> bool {
const STOLEN_LEN: usize = 12;
let target = SHELL_RUNTIME_PRIME_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
runtime_prime_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { RUNTIME_PRIME_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_frame_cycle_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = SHELL_FRAME_CYCLE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(target, STOLEN_LEN, frame_cycle_detour as *const () as usize)
};
if trampoline == 0 {
return false;
}
unsafe { FRAME_CYCLE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_object_service_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = SHELL_OBJECT_SERVICE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
object_service_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { OBJECT_SERVICE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_child_service_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = SHELL_CHILD_SERVICE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
child_service_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { CHILD_SERVICE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_publish_hook() -> bool {
const STOLEN_LEN: usize = 6;
let target = SHELL_PUBLISH_WINDOW_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_publish_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_PUBLISH_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_unpublish_hook() -> bool {
const STOLEN_LEN: usize = 10;
let target = SHELL_UNPUBLISH_WINDOW_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_unpublish_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_UNPUBLISH_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_object_teardown_hook() -> bool {
const STOLEN_LEN: usize = 7;
let target = SHELL_OBJECT_TEARDOWN_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_object_teardown_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_OBJECT_TEARDOWN_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_object_range_remove_hook() -> bool {
const STOLEN_LEN: usize = 12;
let target = SHELL_OBJECT_RANGE_REMOVE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_object_range_remove_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_node_vcall_hook() -> bool {
const STOLEN_LEN: usize = 21;
let target = SHELL_NODE_VCALL_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_node_vcall_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_NODE_VCALL_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_shell_remove_node_hook() -> bool {
const STOLEN_LEN: usize = 10;
let target = SHELL_REMOVE_NODE_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
shell_remove_node_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { SHELL_REMOVE_NODE_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_mode2_teardown_hook() -> bool {
const STOLEN_LEN: usize = 14;
let target = MODE2_TEARDOWN_ADDR as *mut u8;
let trampoline = unsafe {
install_rel32_detour(
target,
STOLEN_LEN,
mode2_teardown_detour as *const () as usize,
)
};
if trampoline == 0 {
return false;
}
unsafe { MODE2_TEARDOWN_TRAMPOLINE = trampoline };
true
}
pub(super) unsafe fn install_rel32_detour(
target: *mut u8,
stolen_len: usize,
detour: usize,
) -> usize {
let trampoline_size = stolen_len + 5;
let trampoline = unsafe {
VirtualAlloc(
ptr::null_mut(),
trampoline_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE,
)
} as *mut u8;
if trampoline.is_null() {
return 0;
}
unsafe { ptr::copy_nonoverlapping(target, trampoline, stolen_len) };
unsafe { write_rel32_jump(trampoline.add(stolen_len), target.add(stolen_len) as usize) };
let mut old_protect = 0_u32;
if unsafe {
VirtualProtect(
target.cast(),
stolen_len,
PAGE_EXECUTE_READWRITE,
&mut old_protect,
)
} == 0
{
return 0;
}
unsafe { write_rel32_jump(target, detour) };
for offset in 5..stolen_len {
unsafe { ptr::write(target.add(offset), 0x90) };
}
let mut restore_protect = 0_u32;
let _ = unsafe { VirtualProtect(target.cast(), stolen_len, old_protect, &mut restore_protect) };
let _ = unsafe { FlushInstructionCache(GetCurrentProcess(), target.cast(), stolen_len) };
trampoline as usize
}
pub(super) unsafe fn write_rel32_jump(location: *mut u8, destination: usize) {
unsafe { ptr::write(location, 0xE9) };
let next_ip = unsafe { location.add(5) } as usize;
let relative = (destination as isize - next_ip as isize) as i32;
unsafe { ptr::write_unaligned(location.add(1).cast::<i32>(), relative) };
}
pub(super) unsafe fn load_direct_input8_create() -> Option<DirectInput8CreateFn> {
if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } {
return Some(callback);
}
let mut system_directory = [0_u8; 260];
let length = unsafe {
GetSystemDirectoryA(system_directory.as_mut_ptr(), system_directory.len() as u32)
};
if length == 0 || length as usize >= system_directory.len() {
return None;
}
let mut dll_path = system_directory[..length as usize].to_vec();
dll_path.extend_from_slice(br"\dinput8.dll");
dll_path.push(0);
let module = unsafe { LoadLibraryA(dll_path.as_ptr().cast()) };
if module == 0 {
return None;
}
let symbol = unsafe { GetProcAddress(module, DIRECT_INPUT8_CREATE_NAME.as_ptr().cast()) };
if symbol.is_null() {
return None;
}
let callback: DirectInput8CreateFn = unsafe { mem::transmute(symbol) };
unsafe {
REAL_DINPUT8_CREATE = Some(callback);
}
Some(callback)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,99 @@
use super::*;
pub(super) unsafe fn resolve_active_mode_ptr() -> *mut u8 {
let global_active_mode = unsafe { resolve_global_active_mode_ptr() };
if !global_active_mode.is_null() {
return global_active_mode;
}
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if shell_state.is_null() {
return ptr::null_mut();
}
unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) }
}
pub(super) unsafe fn resolve_global_active_mode_ptr() -> *mut u8 {
unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) }
}
pub(super) unsafe fn indexed_collection_entry_id_is_live(
collection: *const u8,
entry_id: usize,
) -> bool {
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if entry_id == 0 || entry_id > id_bound.max(0) as usize {
return false;
}
let tombstone_bits =
unsafe { read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) };
if tombstone_bits.is_null() {
return true;
}
let bit_index = entry_id as u32;
let word =
unsafe { ptr::read_unaligned(tombstone_bits.add((bit_index / 32) as usize).cast::<u32>()) };
(word & (1_u32 << (bit_index % 32))) == 0
}
pub(super) unsafe fn indexed_collection_resolve_live_entry_by_id(
collection: *const u8,
entry_id: usize,
) -> *mut u8 {
if !unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } {
return ptr::null_mut();
}
let payload = unsafe { read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) };
if payload.is_null() {
return ptr::null_mut();
}
let stride = unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) as usize };
let flat = unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 };
if flat {
unsafe { payload.add(stride * entry_id) }
} else {
unsafe { ptr::read_unaligned(payload.add(stride * entry_id).cast::<*mut u8>()) }
}
}
pub(super) unsafe fn read_u8(address: *const u8) -> u8 {
unsafe { ptr::read_unaligned(address) }
}
pub(super) unsafe fn read_u16(address: *const u8) -> u16 {
unsafe { ptr::read_unaligned(address.cast::<u16>()) }
}
pub(super) unsafe fn read_u32(address: *const u8) -> u32 {
unsafe { ptr::read_unaligned(address.cast::<u32>()) }
}
pub(super) unsafe fn read_i32(address: *const u8) -> i32 {
unsafe { ptr::read_unaligned(address.cast::<i32>()) }
}
pub(super) unsafe fn read_f32(address: *const u8) -> f32 {
unsafe { ptr::read_unaligned(address.cast::<f32>()) }
}
pub(super) unsafe fn read_ptr(address: *const u8) -> *mut u8 {
unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) }
}
pub(super) unsafe fn read_c_string(address: *const u8, max_len: usize) -> String {
let mut len = 0;
while len < max_len {
let byte = unsafe { read_u8(address.add(len)) };
if byte == 0 {
break;
}
len += 1;
}
String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(address, len) }).into_owned()
}

View file

@ -0,0 +1,71 @@
use crate::capture::{
CargoCollectionProbe, CargoCollectionProbeRow, IndexedCollectionProbe,
IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe,
write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe,
};
use core::ffi::{c_char, c_void};
use core::mem;
use core::ptr;
use std::env;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
use rrt_model::finance::{
AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceSnapshot, GrowthSetting,
};
mod auto_load;
mod capture;
mod constants;
mod detours;
mod ffi;
mod install;
mod logging;
mod memory;
mod state;
use auto_load::*;
use capture::*;
use constants::*;
use detours::*;
use ffi::*;
use install::*;
use logging::*;
use memory::*;
use state::*;
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
if reason == DLL_PROCESS_ATTACH {
unsafe {
let _ = DisableThreadLibraryCalls(module);
OutputDebugStringA(DEBUG_MESSAGE.as_ptr().cast());
append_attach_log();
}
}
1
}
#[unsafe(no_mangle)]
pub extern "system" fn DirectInput8Create(
instance: *mut c_void,
version: u32,
riid: *const Guid,
out: *mut *mut c_void,
outer: *mut c_void,
) -> i32 {
maybe_emit_finance_template_bundle();
maybe_start_finance_capture_thread();
maybe_start_cargo_capture_thread();
maybe_install_auto_load_hook();
let direct_input8_create = unsafe { load_direct_input8_create() };
match direct_input8_create {
Some(callback) => unsafe { callback(instance, version, riid, out, outer) },
None => E_FAIL,
}
}

View file

@ -0,0 +1,45 @@
use super::*;
pub(super) static mut REAL_DINPUT8_CREATE: Option<DirectInput8CreateFn> = None;
pub(super) static FINANCE_TEMPLATE_EMITTED: AtomicBool = AtomicBool::new(false);
pub(super) static FINANCE_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false);
pub(super) static FINANCE_COLLECTION_PROBE_WRITTEN: AtomicBool = AtomicBool::new(false);
pub(super) static CARGO_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_THREAD_STARTED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_ATTEMPTED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_DEFERRED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_TRANSITION_ARMED: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_LAST_GATE_MASK: AtomicU32 = AtomicU32::new(u32::MAX);
pub(super) static AUTO_LOAD_READY_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_ARMED_TICK_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_ALLOCATOR_WINDOW_ACTIVE: AtomicBool = AtomicBool::new(false);
pub(super) static AUTO_LOAD_ALLOCATOR_WINDOW_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_POST_TRANSITION_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_LOAD_SCREEN_MESSAGE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_SERVICE_ENTRY_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_SERVICE_RETURN_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_RUNTIME_PRIME_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_FRAME_CYCLE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_OBJECT_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_CHILD_SERVICE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
pub(super) static AUTO_LOAD_SAVE_STEM: OnceLock<String> = OnceLock::new();
pub(super) static mut SHELL_STATE_SERVICE_TRAMPOLINE: usize = 0;
pub(super) static mut PROFILE_STARTUP_DISPATCH_TRAMPOLINE: usize = 0;
pub(super) static mut RUNTIME_RESET_TRAMPOLINE: usize = 0;
pub(super) static mut ALLOCATOR_TRAMPOLINE: usize = 0;
pub(super) static mut LOAD_SCREEN_SCALAR_TRAMPOLINE: usize = 0;
pub(super) static mut LOAD_SCREEN_CONSTRUCT_TRAMPOLINE: usize = 0;
pub(super) static mut LOAD_SCREEN_MESSAGE_TRAMPOLINE: usize = 0;
pub(super) static mut RUNTIME_PRIME_TRAMPOLINE: usize = 0;
pub(super) static mut FRAME_CYCLE_TRAMPOLINE: usize = 0;
pub(super) static mut OBJECT_SERVICE_TRAMPOLINE: usize = 0;
pub(super) static mut CHILD_SERVICE_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_PUBLISH_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_UNPUBLISH_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_OBJECT_TEARDOWN_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_OBJECT_RANGE_REMOVE_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_REMOVE_NODE_TRAMPOLINE: usize = 0;
pub(super) static mut SHELL_NODE_VCALL_TRAMPOLINE: usize = 0;
pub(super) static mut MODE2_TEARDOWN_TRAMPOLINE: usize = 0;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,320 @@
use std::collections::BTreeMap;
use crate::derived::{
runtime_company_live_bond_coupon_burden_total, runtime_company_trailing_full_year_stat_series,
runtime_decode_packed_calendar_tuple, runtime_pack_packed_calendar_tuple_to_absolute_counter,
runtime_world_absolute_counter, runtime_world_partial_year_weight_numerator,
};
use crate::event::metrics::RuntimeCompanyMarketMetric;
use crate::state::{
RuntimeCompanyAnnualFinanceState, RuntimeCompanyPeriodicServiceState, RuntimeState,
RuntimeWorldRoutePreferenceOverrideState,
};
pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u32) -> Option<u32> {
let outstanding_shares = state
.service_state
.company_market_state
.get(&company_id)?
.outstanding_shares;
let assigned_shares = runtime_company_assigned_share_pool(state, company_id)?;
Some(outstanding_shares.saturating_sub(assigned_shares))
}
pub fn runtime_company_assigned_share_pool(state: &RuntimeState, company_id: u32) -> Option<u32> {
state.service_state.company_market_state.get(&company_id)?;
Some(
state
.chairman_profiles
.iter()
.filter_map(|profile| profile.company_holdings.get(&company_id).copied())
.sum::<u32>(),
)
}
pub fn runtime_company_annual_finance_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualFinanceState> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
let periodic_service_state = runtime_company_periodic_service_state(state, company_id);
let assigned_share_pool = runtime_company_assigned_share_pool(state, company_id)?;
let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id)?;
let years_since_founding =
derive_runtime_company_elapsed_years(state.calendar.year, market_state.founding_year);
let years_since_last_bankruptcy = derive_runtime_company_elapsed_years(
state.calendar.year,
market_state.last_bankruptcy_year,
);
let years_since_last_dividend =
derive_runtime_company_elapsed_years(state.calendar.year, market_state.last_dividend_year);
let current_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter(
runtime_decode_packed_calendar_tuple(
market_state.current_issue_calendar_word,
market_state.current_issue_calendar_word_2,
),
);
let prior_issue_absolute_counter = runtime_pack_packed_calendar_tuple_to_absolute_counter(
runtime_decode_packed_calendar_tuple(
market_state.prior_issue_calendar_word,
market_state.prior_issue_calendar_word_2,
),
);
let current_issue_age_absolute_counter_delta = match (
runtime_world_absolute_counter(state),
current_issue_absolute_counter,
) {
(Some(world_counter), Some(issue_counter)) if world_counter >= issue_counter => {
Some(i64::from(world_counter - issue_counter))
}
_ => None,
};
let (trailing_full_year_year_words, trailing_full_year_net_profits) =
runtime_company_trailing_full_year_stat_series(state, company_id, 0x2b, 4)
.unwrap_or_default();
let (_, trailing_full_year_revenues) =
runtime_company_trailing_full_year_stat_series(state, company_id, 0x2c, 4)
.unwrap_or_default();
let (_, trailing_full_year_fuel_costs) =
runtime_company_trailing_full_year_stat_series(state, company_id, 0x09, 4)
.unwrap_or_default();
Some(RuntimeCompanyAnnualFinanceState {
company_id,
outstanding_shares: market_state.outstanding_shares,
bond_count: market_state.bond_count,
largest_live_bond_principal: market_state.largest_live_bond_principal,
highest_coupon_live_bond_principal: market_state.highest_coupon_live_bond_principal,
live_bond_coupon_burden_total: runtime_company_live_bond_coupon_burden_total(
state, company_id,
),
assigned_share_pool,
unassigned_share_pool,
cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32),
chairman_salary_baseline: market_state.chairman_salary_baseline,
chairman_salary_current: market_state.chairman_salary_current,
chairman_bonus_year: market_state.chairman_bonus_year,
chairman_bonus_amount: market_state.chairman_bonus_amount,
founding_year: market_state.founding_year,
last_bankruptcy_year: market_state.last_bankruptcy_year,
last_dividend_year: market_state.last_dividend_year,
years_since_founding,
years_since_last_bankruptcy,
years_since_last_dividend,
current_partial_year_weight_numerator: runtime_world_partial_year_weight_numerator(state),
trailing_full_year_year_words,
trailing_full_year_net_profits,
trailing_full_year_revenues,
trailing_full_year_fuel_costs,
current_issue_absolute_counter,
prior_issue_absolute_counter,
current_issue_age_absolute_counter_delta,
current_issue_calendar_word: market_state.current_issue_calendar_word,
current_issue_calendar_word_2: market_state.current_issue_calendar_word_2,
prior_issue_calendar_word: market_state.prior_issue_calendar_word,
prior_issue_calendar_word_2: market_state.prior_issue_calendar_word_2,
preferred_locomotive_engine_type_raw_u8: periodic_service_state
.as_ref()
.and_then(|service_state| service_state.preferred_locomotive_engine_type_raw_u8),
city_connection_latch: periodic_service_state
.as_ref()
.map(|service_state| service_state.city_connection_latch)
.unwrap_or(market_state.city_connection_latch),
linked_transit_latch: periodic_service_state
.as_ref()
.map(|service_state| service_state.linked_transit_latch)
.unwrap_or(market_state.linked_transit_latch),
})
}
pub fn runtime_company_periodic_service_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyPeriodicServiceState> {
const DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 140;
const ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS: i64 = 180;
const ELECTRIC_ENGINE_TYPE_RAW_U8: u8 = 2;
let market_state = state.service_state.company_market_state.get(&company_id)?;
let periodic_side_latch_state = state
.service_state
.company_periodic_side_latch_state
.get(&company_id);
let preferred_locomotive_engine_type_raw_u8 = periodic_side_latch_state
.and_then(|latch_state| latch_state.preferred_locomotive_engine_type_raw_u8);
let base_route_preference_raw_u8 = state.world_restore.auto_show_grade_during_track_lay_raw_u8;
let electric_route_preference_override_active =
preferred_locomotive_engine_type_raw_u8 == Some(ELECTRIC_ENGINE_TYPE_RAW_U8);
let effective_route_preference_raw_u8 = if electric_route_preference_override_active {
Some(ELECTRIC_ENGINE_TYPE_RAW_U8)
} else {
base_route_preference_raw_u8
};
Some(RuntimeCompanyPeriodicServiceState {
company_id,
preferred_locomotive_engine_type_raw_u8,
city_connection_latch: periodic_side_latch_state
.map(|latch_state| latch_state.city_connection_latch)
.unwrap_or(market_state.city_connection_latch),
linked_transit_latch: periodic_side_latch_state
.map(|latch_state| latch_state.linked_transit_latch)
.unwrap_or(market_state.linked_transit_latch),
base_route_preference_raw_u8,
effective_route_preference_raw_u8,
electric_route_preference_override_active,
effective_route_quality_multiplier_basis_points:
if electric_route_preference_override_active {
ELECTRIC_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS
} else {
DEFAULT_ROUTE_QUALITY_MULTIPLIER_BASIS_POINTS
},
})
}
pub fn runtime_apply_company_periodic_route_preference_override(
state: &mut RuntimeState,
company_id: u32,
) -> Option<RuntimeWorldRoutePreferenceOverrideState> {
let periodic_service_state = runtime_company_periodic_service_state(state, company_id)?;
if !periodic_service_state.electric_route_preference_override_active {
return None;
}
let override_state = RuntimeWorldRoutePreferenceOverrideState {
company_id,
base_route_preference_raw_u8: periodic_service_state.base_route_preference_raw_u8,
effective_route_preference_raw_u8: periodic_service_state.effective_route_preference_raw_u8,
electric_route_preference_override_active: true,
};
state.world_restore.auto_show_grade_during_track_lay_raw_u8 =
override_state.effective_route_preference_raw_u8;
state
.service_state
.active_periodic_route_preference_override = Some(override_state.clone());
state.service_state.last_periodic_route_preference_override = Some(override_state.clone());
state
.service_state
.periodic_route_preference_override_apply_count += 1;
Some(override_state)
}
pub fn runtime_restore_company_periodic_route_preference_override(
state: &mut RuntimeState,
) -> Option<RuntimeWorldRoutePreferenceOverrideState> {
let override_state = state
.service_state
.active_periodic_route_preference_override
.take()?;
state.world_restore.auto_show_grade_during_track_lay_raw_u8 =
override_state.base_route_preference_raw_u8;
state
.service_state
.periodic_route_preference_override_restore_count += 1;
Some(override_state)
}
pub fn runtime_company_market_metric_value(
state: &RuntimeState,
company_id: u32,
metric: RuntimeCompanyMarketMetric,
) -> Option<i64> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
match metric {
RuntimeCompanyMarketMetric::OutstandingShares => {
Some(annual_finance_state.outstanding_shares as i64)
}
RuntimeCompanyMarketMetric::BondCount => Some(annual_finance_state.bond_count as i64),
RuntimeCompanyMarketMetric::LargestLiveBondPrincipal => annual_finance_state
.largest_live_bond_principal
.map(|value| value as i64),
RuntimeCompanyMarketMetric::HighestCouponLiveBondPrincipal => annual_finance_state
.highest_coupon_live_bond_principal
.map(|value| value as i64),
RuntimeCompanyMarketMetric::AssignedSharePool => {
Some(annual_finance_state.assigned_share_pool as i64)
}
RuntimeCompanyMarketMetric::UnassignedSharePool => {
Some(annual_finance_state.unassigned_share_pool as i64)
}
RuntimeCompanyMarketMetric::CachedSharePrice => annual_finance_state.cached_share_price,
RuntimeCompanyMarketMetric::ChairmanSalaryBaseline => {
Some(annual_finance_state.chairman_salary_baseline as i64)
}
RuntimeCompanyMarketMetric::ChairmanSalaryCurrent => {
Some(annual_finance_state.chairman_salary_current as i64)
}
RuntimeCompanyMarketMetric::ChairmanBonusAmount => {
Some(annual_finance_state.chairman_bonus_amount as i64)
}
RuntimeCompanyMarketMetric::CurrentIssueAbsoluteCounter => annual_finance_state
.current_issue_absolute_counter
.map(i64::from),
RuntimeCompanyMarketMetric::PriorIssueAbsoluteCounter => annual_finance_state
.prior_issue_absolute_counter
.map(i64::from),
RuntimeCompanyMarketMetric::CurrentIssueAgeAbsoluteCounterDelta => {
annual_finance_state.current_issue_age_absolute_counter_delta
}
RuntimeCompanyMarketMetric::CurrentIssueCalendarWord => {
Some(annual_finance_state.current_issue_calendar_word as i64)
}
RuntimeCompanyMarketMetric::CurrentIssueCalendarWord2 => {
Some(annual_finance_state.current_issue_calendar_word_2 as i64)
}
RuntimeCompanyMarketMetric::PriorIssueCalendarWord => {
Some(annual_finance_state.prior_issue_calendar_word as i64)
}
RuntimeCompanyMarketMetric::PriorIssueCalendarWord2 => {
Some(annual_finance_state.prior_issue_calendar_word_2 as i64)
}
}
}
pub(crate) fn rounded_cached_share_price_i64(raw_u32: u32) -> Option<i64> {
let value = f32::from_bits(raw_u32);
if !value.is_finite() {
return None;
}
if value < i64::MIN as f32 || value > i64::MAX as f32 {
return None;
}
Some(value.round() as i64)
}
pub(in crate::derived) fn runtime_decode_saved_f64_bits(bits: u64) -> Option<f64> {
let value = f64::from_bits(bits);
if !value.is_finite() {
return None;
}
Some(value)
}
pub(crate) fn runtime_round_f64_to_i64(value: f64) -> Option<i64> {
if !value.is_finite() {
return None;
}
if value < i64::MIN as f64 || value > i64::MAX as f64 {
return None;
}
Some(value.round() as i64)
}
pub(in crate::derived) fn derive_runtime_company_elapsed_years(
current_year: u32,
prior_year: u32,
) -> Option<u32> {
if prior_year == 0 || prior_year > current_year {
return None;
}
Some(current_year - prior_year)
}
pub(crate) fn derive_runtime_chairman_holdings_share_price_total(
holdings_by_company: &BTreeMap<u32, u32>,
company_share_prices: &BTreeMap<u32, i64>,
) -> Option<i64> {
let mut total = 0i64;
for (company_id, units) in holdings_by_company {
let share_price = *company_share_prices.get(company_id)?;
total = total.checked_add((*units as i64).checked_mul(share_price)?)?;
}
Some(total)
}

View file

@ -0,0 +1,138 @@
use crate::derived::{
runtime_annual_bond_principal_flow_relation_label, runtime_company_annual_bond_policy_state,
runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state,
runtime_company_annual_dividend_policy_state, runtime_company_annual_finance_state,
runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state,
};
use crate::state::{
RuntimeCompanyAnnualFinancePolicyAction, RuntimeCompanyAnnualFinancePolicyState, RuntimeState,
};
pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c)
}
pub fn runtime_world_bankruptcy_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.bankruptcy_policy_raw_u8? == 0)
}
pub fn runtime_world_bond_issue_and_repayment_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.bond_issue_and_repayment_policy_raw_u8? == 0)
}
pub fn runtime_world_stock_issue_and_buyback_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.stock_issue_and_buyback_policy_raw_u8? == 0)
}
pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.dividend_policy_raw_u8? == 0)
}
pub fn runtime_world_building_density_growth_setting(state: &RuntimeState) -> Option<u32> {
state.world_restore.building_density_growth_setting_raw_u32
}
pub fn runtime_company_annual_finance_policy_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualFinancePolicyState> {
runtime_company_annual_finance_state(state, company_id)?;
let creditor_pressure_bankruptcy_eligible =
runtime_company_annual_creditor_pressure_state(state, company_id)
.map(|state| state.eligible_for_bankruptcy_branch)
.unwrap_or(false);
let deep_distress_bankruptcy_fallback_eligible =
runtime_company_annual_deep_distress_state(state, company_id)
.map(|state| state.eligible_for_bankruptcy_fallback)
.unwrap_or(false);
let bond_issue_eligible = runtime_company_annual_bond_policy_state(state, company_id)
.map(|state| state.eligible_for_bond_issue_branch)
.unwrap_or(false);
let stock_repurchase_eligible =
runtime_company_annual_stock_repurchase_state(state, company_id)
.map(|state| state.eligible_for_single_batch_repurchase)
.unwrap_or(false);
let stock_issue_eligible = runtime_company_annual_stock_issue_state(state, company_id)
.map(|state| state.eligible_for_double_tranche_issue)
.unwrap_or(false);
let dividend_adjustment_eligible =
runtime_company_annual_dividend_policy_state(state, company_id)
.map(|state| state.eligible_for_dividend_adjustment_branch)
.unwrap_or(false);
let action = if creditor_pressure_bankruptcy_eligible {
RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy
} else if deep_distress_bankruptcy_fallback_eligible {
RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback
} else if bond_issue_eligible {
RuntimeCompanyAnnualFinancePolicyAction::BondIssue
} else if stock_repurchase_eligible {
RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase
} else if stock_issue_eligible {
RuntimeCompanyAnnualFinancePolicyAction::StockIssue
} else if dividend_adjustment_eligible {
RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment
} else {
RuntimeCompanyAnnualFinancePolicyAction::None
};
Some(RuntimeCompanyAnnualFinancePolicyState {
company_id,
action,
creditor_pressure_bankruptcy_eligible,
deep_distress_bankruptcy_fallback_eligible,
bond_issue_eligible,
stock_repurchase_eligible,
stock_issue_eligible,
dividend_adjustment_eligible,
})
}
pub fn runtime_company_annual_finance_policy_action_label(
action: RuntimeCompanyAnnualFinancePolicyAction,
) -> &'static str {
match action {
RuntimeCompanyAnnualFinancePolicyAction::None => "none",
RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy => {
"creditor_pressure_bankruptcy"
}
RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => {
"deep_distress_bankruptcy_fallback"
}
RuntimeCompanyAnnualFinancePolicyAction::BondIssue => "bond_issue",
RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => "stock_repurchase",
RuntimeCompanyAnnualFinancePolicyAction::StockIssue => "stock_issue",
RuntimeCompanyAnnualFinancePolicyAction::DividendAdjustment => "dividend_adjustment",
}
}
pub fn runtime_annual_finance_news_family_candidate_label(
action: RuntimeCompanyAnnualFinancePolicyAction,
retired_principal_total: u64,
issued_principal_total: u64,
repurchased_share_count: u64,
issued_share_count: u64,
) -> Option<&'static str> {
match action {
RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy
| RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => Some("2881"),
RuntimeCompanyAnnualFinancePolicyAction::BondIssue => {
match runtime_annual_bond_principal_flow_relation_label(
retired_principal_total,
issued_principal_total,
) {
Some("retired_equals_issued") => Some("2882"),
Some("issued_exceeds_retired") => Some("2883"),
Some("retired_exceeds_issued") => Some("2884"),
Some("retired_only") => Some("2885"),
Some("issued_only") => Some("2886"),
_ => None,
}
}
RuntimeCompanyAnnualFinancePolicyAction::StockRepurchase => {
(repurchased_share_count > 0).then_some("2887")
}
RuntimeCompanyAnnualFinancePolicyAction::StockIssue => {
(issued_share_count > 0).then_some("4053")
}
_ => None,
}
}

View file

@ -0,0 +1,168 @@
use crate::derived::{
runtime_company_annual_finance_state, runtime_company_control_transfer_stat_value_f64,
runtime_company_credit_rating, runtime_company_matured_live_bond_count,
runtime_company_matured_live_bond_principal_total,
runtime_company_next_live_bond_maturity_year, runtime_company_prime_rate,
runtime_company_total_live_bond_principal, runtime_round_f64_to_i64,
runtime_world_annual_finance_mode_active, runtime_world_bond_issue_and_repayment_allowed,
};
use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH;
use crate::state::{RuntimeCompanyAnnualBondPolicyState, RuntimeState};
pub fn runtime_company_average_live_bond_coupon(
state: &RuntimeState,
company_id: u32,
) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
if market_state.live_bond_slots.is_empty() {
return Some(0.0);
}
let mut weighted_coupon_sum = 0.0f64;
let mut total_principal = 0u64;
for slot in &market_state.live_bond_slots {
let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64;
if !coupon_rate.is_finite() {
continue;
}
weighted_coupon_sum += coupon_rate * (slot.principal as f64);
total_principal = total_principal.checked_add(slot.principal as u64)?;
}
if total_principal == 0 {
return Some(0.0);
}
Some(weighted_coupon_sum / total_principal as f64)
}
pub fn runtime_company_live_bond_coupon_burden_total(
state: &RuntimeState,
company_id: u32,
) -> Option<i64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
let mut total = 0i64;
for slot in &market_state.live_bond_slots {
let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64;
if !coupon_rate.is_finite() {
continue;
}
let coupon_burden =
runtime_round_f64_to_i64((slot.principal as f64) * coupon_rate).unwrap_or(0);
total = total.checked_add(coupon_burden)?;
}
Some(total)
}
pub fn runtime_company_bond_interest_rate_quote(
state: &RuntimeState,
company_id: u32,
_principal: u32,
_years_to_maturity: u32,
) -> Option<f64> {
let credit_rating = runtime_company_credit_rating(state, company_id)? as f64;
let prime_rate_percent = runtime_company_prime_rate(state, company_id)? as f64;
let quote = (prime_rate_percent + (10.0 - credit_rating)) / 100.0;
quote.is_finite().then_some(quote)
}
pub fn runtime_company_annual_bond_policy_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualBondPolicyState> {
const STANDARD_CASH_FLOOR: i64 = -250_000;
const LINKED_TRANSIT_CASH_FLOOR: i64 = -30_000;
const ISSUE_PRINCIPAL_STEP: u32 = 500_000;
const ISSUE_YEARS_TO_MATURITY: u32 = 30;
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_year_word = state
.world_restore
.packed_year_word_raw_u16
.map(u32::from)
.unwrap_or(state.calendar.year);
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64);
let live_bond_principal_total = runtime_company_total_live_bond_principal(state, company_id);
let matured_live_bond_count =
runtime_company_matured_live_bond_count(state, company_id, current_year_word);
let matured_live_bond_principal_total =
runtime_company_matured_live_bond_principal_total(state, company_id, current_year_word);
let next_live_bond_maturity_year =
runtime_company_next_live_bond_maturity_year(state, company_id);
let cash_after_full_repayment = current_cash
.zip(live_bond_principal_total)
.map(|(cash, principal)| cash - i64::from(principal));
let issue_cash_floor = Some(if annual_finance_state.linked_transit_latch {
LINKED_TRANSIT_CASH_FLOOR
} else {
STANDARD_CASH_FLOOR
});
let proposed_issue_bond_count = cash_after_full_repayment.zip(issue_cash_floor).map(
|(cash_after_repayment, cash_floor)| {
if cash_after_repayment >= cash_floor {
0
} else {
let deficit = (cash_floor - cash_after_repayment) as u64;
deficit.div_ceil(u64::from(ISSUE_PRINCIPAL_STEP)) as u32
}
},
);
let proposed_issue_total_principal =
proposed_issue_bond_count.and_then(|count| count.checked_mul(ISSUE_PRINCIPAL_STEP));
let eligible_for_bond_issue_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_bond_issue_and_repayment_allowed(state) == Some(true)
&& (matured_live_bond_principal_total.is_some_and(|principal| principal > 0)
|| proposed_issue_bond_count.is_some_and(|count| count > 0));
Some(RuntimeCompanyAnnualBondPolicyState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state),
linked_transit_latch: annual_finance_state.linked_transit_latch,
live_bond_count: Some(annual_finance_state.bond_count),
live_bond_principal_total,
matured_live_bond_count,
matured_live_bond_principal_total,
next_live_bond_maturity_year,
live_bond_coupon_burden_total: annual_finance_state.live_bond_coupon_burden_total,
current_cash,
cash_after_full_repayment,
issue_cash_floor,
issue_principal_step: Some(ISSUE_PRINCIPAL_STEP),
proposed_issue_bond_count,
proposed_issue_total_principal,
proposed_issue_years_to_maturity: Some(ISSUE_YEARS_TO_MATURITY),
eligible_for_bond_issue_branch,
})
}
pub fn runtime_annual_bond_principal_flow_relation_label(
retired_principal_total: u64,
issued_principal_total: u64,
) -> Option<&'static str> {
match retired_principal_total.cmp(&issued_principal_total) {
std::cmp::Ordering::Equal => {
if retired_principal_total == 0 {
None
} else {
Some("retired_equals_issued")
}
}
std::cmp::Ordering::Greater => {
if issued_principal_total == 0 {
Some("retired_only")
} else {
Some("retired_exceeds_issued")
}
}
std::cmp::Ordering::Less => {
if retired_principal_total == 0 {
Some("issued_only")
} else {
Some("issued_exceeds_retired")
}
}
}
}

View file

@ -0,0 +1,151 @@
use crate::derived::{
runtime_company_annual_finance_state, runtime_company_average_live_bond_coupon,
runtime_company_control_transfer_stat_value_f64, runtime_company_derived_stat_value,
runtime_round_f64_to_i64, runtime_world_issue_opinion_term_sum_raw,
runtime_world_prime_rate_baseline,
};
use crate::event::metrics::{
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_WORLD_ISSUE_CREDIT_MARKET,
RUNTIME_WORLD_ISSUE_PRIME_RATE,
};
use crate::state::RuntimeState;
pub fn runtime_company_credit_rating(state: &RuntimeState, company_id: u32) -> Option<i64> {
let company = state
.companies
.iter()
.find(|company| company.company_id == company_id)?;
if let Some(credit_rating_score) = company.credit_rating_score {
return Some(credit_rating_score);
}
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
if annual_finance_state.outstanding_shares == 0 {
return Some(-512);
}
let mut weighted_recent_profit_total = 0.0f64;
let mut weighted_recent_profit_weight = 0.0f64;
for (index, (net_profit, fuel_cost)) in annual_finance_state
.trailing_full_year_net_profits
.iter()
.zip(annual_finance_state.trailing_full_year_fuel_costs.iter())
.take(4)
.enumerate()
{
let weight = (4 - index) as f64;
weighted_recent_profit_total += (*net_profit - *fuel_cost) as f64 * weight;
weighted_recent_profit_weight += weight;
}
let weighted_recent_profit = if weighted_recent_profit_weight > 0.0 {
weighted_recent_profit_total / weighted_recent_profit_weight
} else {
0.0
};
let current_slot_12 = runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12)?;
let current_slot_30 = runtime_company_derived_stat_value(
state,
company_id,
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
0x30,
)?;
let current_slot_31 = runtime_company_derived_stat_value(
state,
company_id,
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
0x31,
)?;
let average_live_bond_coupon = runtime_company_average_live_bond_coupon(state, company_id)?;
let mut finance_pressure = average_live_bond_coupon * current_slot_12;
if company.current_cash > 0 {
let prime_baseline = runtime_world_prime_rate_baseline(state)?;
let raw_issue_39 = runtime_world_issue_opinion_term_sum_raw(
state,
RUNTIME_WORLD_ISSUE_PRIME_RATE,
company.linked_chairman_profile_id,
Some(company_id),
None,
)? as f64;
finance_pressure +=
company.current_cash as f64 * (prime_baseline + raw_issue_39 * 0.01 + 0.03);
}
let profitability_ratio = if finance_pressure < 0.0 {
weighted_recent_profit / (-finance_pressure)
} else {
10.0
};
let mut profitability_score = runtime_credit_rating_profitability_ladder(profitability_ratio);
if let Some(years_since_founding) = annual_finance_state.years_since_founding {
if years_since_founding < 5 {
let missing_years = (5 - years_since_founding) as f64;
profitability_score += (10.0 - profitability_score) * missing_years * 0.1;
}
}
if current_slot_31 > 1_000_000.0 {
profitability_score += current_slot_31 / 100_000.0 - 10.0;
}
let burden_ratio = if current_slot_30 > 0.0 {
(weighted_recent_profit - current_slot_12) / current_slot_30
} else {
1.0
};
let burden_score = runtime_credit_rating_burden_ladder(burden_ratio);
let mut rating =
(profitability_score * burden_score / 10.0 + profitability_score + burden_score) / 3.0;
rating *= runtime_world_credit_market_scale(state)?;
if let Some(years_since_last_bankruptcy) = annual_finance_state.years_since_last_bankruptcy {
if years_since_last_bankruptcy < 15 {
rating *= years_since_last_bankruptcy as f64 * 0.0666;
}
}
let raw_issue_38 = runtime_world_issue_opinion_term_sum_raw(
state,
RUNTIME_WORLD_ISSUE_CREDIT_MARKET,
company.linked_chairman_profile_id,
Some(company_id),
None,
)? as f64;
runtime_round_f64_to_i64((rating + raw_issue_38 + 0.5).clamp(0.0, 10.0))
}
pub(super) fn runtime_credit_rating_profitability_ladder(ratio: f64) -> f64 {
if !ratio.is_finite() || ratio <= 0.0 {
0.0
} else if ratio < 1.0 {
1.0 + ratio * 4.0
} else if ratio < 2.0 {
3.0 + ratio * 2.0
} else if ratio < 5.0 {
5.0 + ratio
} else {
10.0
}
}
pub(super) fn runtime_credit_rating_burden_ladder(ratio: f64) -> f64 {
if !ratio.is_finite() || ratio > 1.0 {
0.0
} else if ratio > 0.75 {
16.0 - ratio * 16.0
} else if ratio > 0.5 {
13.0 - ratio * 12.0
} else if ratio > 0.25 {
11.0 - ratio * 8.0
} else if ratio > 0.1 {
10.0 - ratio * 4.0
} else {
10.0
}
}
pub fn runtime_world_credit_market_scale(state: &RuntimeState) -> Option<f64> {
const ISSUE_38_SCALE_TABLE: [f64; 8] = [0.8, 0.9, 1.0, 1.1, 1.2, 0.9, 0.95, 1.0];
let index = state.world_restore.issue_38_value? as usize;
ISSUE_38_SCALE_TABLE.get(index).copied()
}

View file

@ -0,0 +1,168 @@
use crate::derived::{
runtime_company_annual_finance_state, runtime_company_control_transfer_stat_value_f64,
runtime_company_stat_value_f64, runtime_company_support_adjusted_share_price_scalar,
runtime_round_f64_to_i64, runtime_world_annual_finance_mode_active,
runtime_world_bankruptcy_allowed,
};
use crate::event::metrics::{
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
RuntimeCompanyStatSelector,
};
use crate::state::{
RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, RuntimeState,
};
pub fn runtime_company_annual_creditor_pressure_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualCreditorPressureState> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_cash_plus_slot_12_total =
runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12)
.and_then(runtime_round_f64_to_i64)
.and_then(|slot_12| {
runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64)
.map(|current_cash| current_cash + slot_12)
});
let support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar(state, company_id)
.and_then(runtime_round_f64_to_i64);
let current_fuel_cost = runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: 0x09,
},
)
.and_then(runtime_round_f64_to_i64);
let recent_bad_net_profit_year_count = annual_finance_state
.trailing_full_year_net_profits
.iter()
.take(3)
.filter(|value| **value < -10_000)
.count() as u32;
let recent_peak_revenue = annual_finance_state
.trailing_full_year_revenues
.iter()
.take(3)
.copied()
.max();
let recent_three_year_net_profit_total =
if annual_finance_state.trailing_full_year_net_profits.len() >= 3 {
Some(
annual_finance_state
.trailing_full_year_net_profits
.iter()
.take(3)
.sum::<i64>(),
)
} else {
None
};
let pressure_ladder_cash_floor = recent_peak_revenue.map(|revenue| {
if revenue < 120_000 {
-600_000
} else if revenue < 230_000 {
-1_100_000
} else if revenue < 340_000 {
-1_600_000
} else {
-2_000_000
}
});
let support_adjusted_share_price_floor = Some(if recent_bad_net_profit_year_count == 3 {
20
} else {
15
});
let current_fuel_cost_floor = pressure_ladder_cash_floor.map(|floor| floor * 8 / 100);
let eligible_for_bankruptcy_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_bankruptcy_allowed(state) == Some(true)
&& annual_finance_state
.years_since_last_bankruptcy
.is_some_and(|years| years >= 13)
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 4)
&& recent_bad_net_profit_year_count >= 2
&& current_cash_plus_slot_12_total
.zip(pressure_ladder_cash_floor)
.is_some_and(|(value, floor)| value <= floor)
&& support_adjusted_share_price_scalar
.zip(support_adjusted_share_price_floor)
.is_some_and(|(value, floor)| value >= floor)
&& current_fuel_cost
.zip(current_fuel_cost_floor)
.is_some_and(|(value, floor)| value <= floor)
&& recent_three_year_net_profit_total.is_some_and(|value| value <= -60_000);
Some(RuntimeCompanyAnnualCreditorPressureState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
bankruptcy_allowed: runtime_world_bankruptcy_allowed(state),
years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy,
years_since_founding: annual_finance_state.years_since_founding,
recent_bad_net_profit_year_count,
recent_peak_revenue,
recent_three_year_net_profit_total,
pressure_ladder_cash_floor,
current_cash_plus_slot_12_total,
support_adjusted_share_price_floor,
support_adjusted_share_price_scalar,
current_fuel_cost,
current_fuel_cost_floor,
eligible_for_bankruptcy_branch,
})
}
pub fn runtime_company_annual_deep_distress_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualDeepDistressState> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64);
let recent_first_three_net_profit_years = annual_finance_state
.trailing_full_year_net_profits
.iter()
.take(3)
.copied()
.collect::<Vec<_>>();
let deep_distress_cash_floor = Some(-300_000);
let deep_distress_net_profit_floor = Some(-20_000);
let eligible_for_bankruptcy_fallback = runtime_world_bankruptcy_allowed(state) == Some(true)
&& current_cash
.zip(deep_distress_cash_floor)
.is_some_and(|(value, floor)| value <= floor)
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 3)
&& recent_first_three_net_profit_years.len() == 3
&& recent_first_three_net_profit_years
.iter()
.all(|value| *value <= deep_distress_net_profit_floor.unwrap())
&& annual_finance_state
.years_since_last_bankruptcy
.is_some_and(|years| years >= 5);
Some(RuntimeCompanyAnnualDeepDistressState {
company_id,
bankruptcy_allowed: runtime_world_bankruptcy_allowed(state),
years_since_founding: annual_finance_state.years_since_founding,
years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy,
current_cash,
recent_first_three_net_profit_years,
deep_distress_cash_floor,
deep_distress_net_profit_floor,
eligible_for_bankruptcy_fallback,
})
}

View file

@ -0,0 +1,227 @@
use crate::derived::{
runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state,
runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state,
runtime_company_annual_stock_issue_state, runtime_company_annual_stock_repurchase_state,
runtime_company_control_transfer_stat_value_f64,
runtime_company_year_or_control_transfer_metric_value, runtime_decode_saved_f32_value,
runtime_round_f64_to_i64, runtime_world_annual_finance_mode_active,
runtime_world_building_density_growth_setting, runtime_world_dividend_adjustment_allowed,
};
use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH;
use crate::state::{RuntimeCompanyAnnualDividendPolicyState, RuntimeState};
pub(super) fn runtime_company_board_approved_dividend_rate_ceiling_f64(
state: &RuntimeState,
company_id: u32,
) -> Option<f64> {
const REVENUE_GUARD_DIVISOR: f64 = 2.0;
const EARLY_SUPPORT_MULTIPLIER: f64 = 0.05;
const HISTORICAL_GUARD_SCALE: f64 = 1.25;
const ANCHOR_SCALE: f64 = 0.35;
let market_state = state.service_state.company_market_state.get(&company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)?;
let shares_plus_one = market_state.outstanding_shares.checked_add(1)?;
let shares_plus_one_f64 = shares_plus_one as f64;
let current_cash_per_share_ceiling = current_cash / shares_plus_one_f64;
let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
let years_since_founding = current_year_word
.checked_sub(market_state.founding_year)
.unwrap_or(0)
.min(3);
let start_year_offset = if state.world_restore.partial_year_progress_raw_u8 == Some(0x0c) {
0
} else {
1
};
let mut strongest_net_profit_guard = 0.0f64;
let mut strongest_revenue_guard = 0.0f64;
if start_year_offset <= years_since_founding {
for year_offset in start_year_offset..=years_since_founding {
let year_word = current_year_word.checked_sub(year_offset)?;
let net_profit = runtime_company_year_or_control_transfer_metric_value(
state, company_id, year_word, 0x2b,
)?;
strongest_net_profit_guard = strongest_net_profit_guard.max(net_profit);
let revenue = runtime_company_year_or_control_transfer_metric_value(
state, company_id, year_word, 0x2c,
)?;
strongest_revenue_guard = strongest_revenue_guard.max(revenue);
}
}
let mut historical_guard_total =
strongest_net_profit_guard.min(strongest_revenue_guard / REVENUE_GUARD_DIVISOR);
if years_since_founding <= 1 {
let early_support_guard = market_state.outstanding_shares as f64
* runtime_decode_saved_f32_value(market_state.young_company_support_scalar_raw_u32)?
* EARLY_SUPPORT_MULTIPLIER;
historical_guard_total = historical_guard_total.max(early_support_guard);
}
let historical_guard_per_share_ceiling =
historical_guard_total / shares_plus_one_f64 * HISTORICAL_GUARD_SCALE;
let mut ceiling = current_cash_per_share_ceiling.min(historical_guard_per_share_ceiling);
let anchor_value = if years_since_founding == 0 {
runtime_decode_saved_f32_value(market_state.young_company_support_scalar_raw_u32)?
} else {
runtime_company_year_or_control_transfer_metric_value(
state,
company_id,
current_year_word.checked_sub(1)?,
0x1c,
)?
};
ceiling = ceiling.min(anchor_value * ANCHOR_SCALE);
Some(ceiling.max(0.0))
}
pub fn runtime_company_annual_dividend_policy_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualDividendPolicyState> {
const WEIGHTED_NET_PROFIT_DIVISOR: f64 = 6.0;
const CASH_SUPPLEMENT_DIVISOR: f64 = 3.0;
const STANDARD_TARGET_DIVISOR: f64 = 6.0;
const DIVIDEND_DELTA_COLLAPSE_THRESHOLD: f64 = 0.1;
const GROWTH_SETTING_ONE_DIVIDEND_SCALE: f64 = 0.66;
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64);
let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
let current_dividend_per_share =
runtime_company_control_transfer_stat_value_f64(state, company_id, 0x20)?;
let building_density_growth_setting = runtime_world_building_density_growth_setting(state);
let weighted_recent_net_profit_total = Some(
runtime_company_year_or_control_transfer_metric_value(
state,
company_id,
current_year_word,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?
.checked_mul(3)?
.checked_add(
runtime_company_year_or_control_transfer_metric_value(
state,
company_id,
current_year_word.checked_sub(1)?,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?
.checked_mul(2)?,
)?
.checked_add(
runtime_company_year_or_control_transfer_metric_value(
state,
company_id,
current_year_word.checked_sub(2)?,
0x2b,
)
.and_then(runtime_round_f64_to_i64)?,
)?,
);
let weighted_recent_net_profit_average = weighted_recent_net_profit_total
.and_then(|value| runtime_round_f64_to_i64(value as f64 / WEIGHTED_NET_PROFIT_DIVISOR));
let tiny_unassigned_share_cash_supplement_branch =
annual_finance_state.unassigned_share_pool <= 1_000;
let tentative_target_dividend_per_share =
weighted_recent_net_profit_average.and_then(|value| {
if annual_finance_state.outstanding_shares == 0 {
return None;
}
let shares = annual_finance_state.outstanding_shares as f64;
if tiny_unassigned_share_cash_supplement_branch {
let cash_component = current_cash.unwrap_or(0).max(0) as f64;
Some(
((value as f64 / CASH_SUPPLEMENT_DIVISOR)
+ cash_component / CASH_SUPPLEMENT_DIVISOR)
/ shares,
)
} else {
Some((value as f64 / STANDARD_TARGET_DIVISOR) / shares)
}
});
let growth_adjusted_current_dividend_per_share = Some(match building_density_growth_setting {
Some(1) => current_dividend_per_share * GROWTH_SETTING_ONE_DIVIDEND_SCALE,
Some(2) => 0.0,
_ => current_dividend_per_share,
});
let proposed_dividend_per_share = if tentative_target_dividend_per_share
.is_some_and(|value| value <= DIVIDEND_DELTA_COLLAPSE_THRESHOLD)
{
Some(0.0)
} else {
growth_adjusted_current_dividend_per_share
.zip(tentative_target_dividend_per_share)
.map(|(current_dividend, target)| {
((current_dividend + target + DIVIDEND_DELTA_COLLAPSE_THRESHOLD) / 2.0 * 10.0)
.round()
/ 10.0
})
};
let board_approved_dividend_rate_ceiling =
runtime_company_board_approved_dividend_rate_ceiling_f64(state, company_id);
let proposed_dividend_per_share = proposed_dividend_per_share
.zip(board_approved_dividend_rate_ceiling)
.map(|(proposed, ceiling)| proposed.min(ceiling));
let current_dividend_per_share_tenths =
runtime_round_f64_to_i64(current_dividend_per_share * 10.0);
let eligible_for_dividend_adjustment_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_dividend_adjustment_allowed(state) == Some(true)
&& annual_finance_state
.years_since_last_dividend
.is_some_and(|years| years >= 1)
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 2)
&& !runtime_company_annual_creditor_pressure_state(state, company_id)?
.eligible_for_bankruptcy_branch
&& !runtime_company_annual_deep_distress_state(state, company_id)?
.eligible_for_bankruptcy_fallback
&& !runtime_company_annual_bond_policy_state(state, company_id)?
.eligible_for_bond_issue_branch
&& !runtime_company_annual_stock_repurchase_state(state, company_id)?
.eligible_for_single_batch_repurchase
&& !runtime_company_annual_stock_issue_state(state, company_id)?
.eligible_for_double_tranche_issue
&& proposed_dividend_per_share.and_then(|value| runtime_round_f64_to_i64(value * 10.0))
!= current_dividend_per_share_tenths;
Some(RuntimeCompanyAnnualDividendPolicyState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
dividend_adjustment_allowed: runtime_world_dividend_adjustment_allowed(state),
years_since_last_dividend: annual_finance_state.years_since_last_dividend,
years_since_founding: annual_finance_state.years_since_founding,
outstanding_shares: Some(annual_finance_state.outstanding_shares),
unassigned_share_pool: Some(annual_finance_state.unassigned_share_pool),
weighted_recent_net_profit_total,
weighted_recent_net_profit_average,
current_cash,
tiny_unassigned_share_cash_supplement_branch,
tentative_target_dividend_per_share_tenths: tentative_target_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
current_dividend_per_share_tenths,
building_density_growth_setting,
growth_adjusted_current_dividend_per_share_tenths:
growth_adjusted_current_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
board_approved_dividend_rate_ceiling_tenths: board_approved_dividend_rate_ceiling
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
proposed_dividend_per_share_tenths: proposed_dividend_per_share
.and_then(|value| runtime_round_f64_to_i64(value * 10.0)),
eligible_for_dividend_adjustment_branch,
})
}

View file

@ -0,0 +1,13 @@
mod annual_policy;
mod bond_policy;
mod credit_rating;
mod distress;
mod dividend_policy;
mod stock_actions;
pub use annual_policy::*;
pub use bond_policy::*;
pub use credit_rating::*;
pub use distress::*;
pub use dividend_policy::*;
pub use stock_actions::*;

View file

@ -0,0 +1,271 @@
use crate::derived::{
runtime_company_annual_bond_policy_state, runtime_company_annual_creditor_pressure_state,
runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state,
runtime_company_book_value_per_share, runtime_company_control_transfer_stat_value_f64,
runtime_company_highest_live_bond_coupon_rate,
runtime_company_support_adjusted_share_price_scalar,
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64,
runtime_company_unassigned_share_pool, runtime_round_f64_to_i64,
runtime_world_annual_finance_mode_active, runtime_world_bond_issue_and_repayment_allowed,
runtime_world_building_density_growth_setting, runtime_world_stock_issue_and_buyback_allowed,
};
use crate::event::metrics::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH;
use crate::state::{
RuntimeCompanyAnnualStockIssueState, RuntimeCompanyAnnualStockRepurchaseState, RuntimeState,
};
pub(super) fn runtime_chairman_stock_repurchase_factor_f64(
state: &RuntimeState,
chairman_profile_id: Option<u32>,
) -> Option<f64> {
let personality_byte = chairman_profile_id
.and_then(|profile_id| {
state
.service_state
.chairman_personality_raw_u8
.get(&profile_id)
})
.copied();
let mut factor = personality_byte
.map(|byte| (f64::from(byte) * 39.0 + 300.0) / 400.0)
.unwrap_or(1.0);
if runtime_world_building_density_growth_setting(state) == Some(1) {
factor *= 1.6;
}
Some(factor)
}
pub fn runtime_company_annual_stock_repurchase_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualStockRepurchaseState> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let company = state
.companies
.iter()
.find(|company| company.company_id == company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64);
let support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar(state, company_id);
let repurchase_factor =
runtime_chairman_stock_repurchase_factor_f64(state, company.linked_chairman_profile_id)?;
let repurchase_factor_basis_points = runtime_round_f64_to_i64(repurchase_factor * 100.0);
let stock_value_gate_cash_floor = runtime_round_f64_to_i64(repurchase_factor * 800_000.0);
let affordability_cash_floor = support_adjusted_share_price_scalar
.and_then(|value| runtime_round_f64_to_i64(value * repurchase_factor * 1_000.0 * 1.2));
let support_adjusted_share_price_scalar =
support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64);
let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id);
let eligible_for_single_batch_repurchase = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_stock_issue_and_buyback_allowed(state) == Some(true)
&& annual_finance_state.city_connection_latch
&& current_cash
.zip(stock_value_gate_cash_floor)
.is_some_and(|(value, floor)| value >= floor)
&& current_cash
.zip(affordability_cash_floor)
.is_some_and(|(value, floor)| value >= floor)
&& unassigned_share_pool.is_some_and(|value| value >= 1_000);
Some(RuntimeCompanyAnnualStockRepurchaseState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state),
city_connection_latch: annual_finance_state.city_connection_latch,
building_density_growth_setting: runtime_world_building_density_growth_setting(state),
linked_chairman_profile_id: company.linked_chairman_profile_id,
linked_chairman_personality_raw_u8: company
.linked_chairman_profile_id
.and_then(|profile_id| {
state
.service_state
.chairman_personality_raw_u8
.get(&profile_id)
})
.copied(),
repurchase_batch_size: Some(1_000),
repurchase_factor_basis_points,
current_cash,
stock_value_gate_cash_floor,
support_adjusted_share_price_scalar,
affordability_cash_floor,
unassigned_share_pool,
eligible_for_single_batch_repurchase,
})
}
pub(super) fn runtime_company_stock_issue_price_to_book_ratio_f64(
pressured_support_adjusted_share_price_scalar: f64,
book_value_per_share: f64,
) -> Option<f64> {
let denominator = book_value_per_share.max(1.0);
if !pressured_support_adjusted_share_price_scalar.is_finite() || !denominator.is_finite() {
return None;
}
Some(pressured_support_adjusted_share_price_scalar / denominator)
}
pub(super) fn runtime_company_stock_issue_minimum_price_to_book_ratio_f64(
highest_coupon_rate: f64,
) -> Option<f64> {
if !highest_coupon_rate.is_finite() || highest_coupon_rate <= 0.0 {
return None;
}
Some(if highest_coupon_rate <= 0.07 {
1.30
} else if highest_coupon_rate <= 0.08 {
1.20
} else if highest_coupon_rate <= 0.09 {
1.10
} else if highest_coupon_rate <= 0.10 {
0.95
} else if highest_coupon_rate <= 0.11 {
0.80
} else if highest_coupon_rate <= 0.12 {
0.62
} else if highest_coupon_rate <= 0.13 {
0.50
} else if highest_coupon_rate <= 0.14 {
0.35
} else {
return None;
})
}
pub fn runtime_company_annual_stock_issue_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualStockIssueState> {
const ISSUE_PROCEEDS_CAP: i64 = 55_000;
const SHARE_PRICE_FLOOR: i64 = 22;
const ONE_YEAR_ABSOLUTE_COUNTER_SPAN: i64 = 12 * 28 * 24 * 60;
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_cash = runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64);
let highest_coupon_live_bond_principal =
annual_finance_state.highest_coupon_live_bond_principal;
let highest_coupon_live_bond_rate =
runtime_company_highest_live_bond_coupon_rate(state, company_id);
let highest_coupon_live_bond_rate_basis_points =
highest_coupon_live_bond_rate.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0));
let mut initial_issue_batch_size =
(annual_finance_state.outstanding_shares / 10 / 1_000) * 1_000;
if initial_issue_batch_size < 2_000 {
initial_issue_batch_size = 2_000;
}
let initial_issue_batch_size = Some(initial_issue_batch_size);
let mut trimmed_issue_batch_size = initial_issue_batch_size?;
let mut pressured_support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
state,
company_id,
-(trimmed_issue_batch_size as i64),
);
let mut pressured_proceeds = pressured_support_adjusted_share_price_scalar
.and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64));
while trimmed_issue_batch_size > 2_000
&& pressured_proceeds.is_some_and(|value| value > ISSUE_PROCEEDS_CAP)
{
trimmed_issue_batch_size = trimmed_issue_batch_size.saturating_sub(1_000);
pressured_support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar_with_pressure_f64(
state,
company_id,
-(trimmed_issue_batch_size as i64),
);
pressured_proceeds = pressured_support_adjusted_share_price_scalar
.and_then(|value| runtime_round_f64_to_i64(value * trimmed_issue_batch_size as f64));
}
let pressured_support_adjusted_share_price_scalar_i64 =
pressured_support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64);
let book_value_per_share_floor_applied =
runtime_company_book_value_per_share(state, company_id).map(|value| value.max(1));
let price_to_book_ratio = pressured_support_adjusted_share_price_scalar
.zip(book_value_per_share_floor_applied)
.and_then(|(share_price, book_value)| {
runtime_company_stock_issue_price_to_book_ratio_f64(share_price, book_value as f64)
});
let price_to_book_ratio_basis_points =
price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0));
let minimum_price_to_book_ratio = highest_coupon_live_bond_rate
.and_then(runtime_company_stock_issue_minimum_price_to_book_ratio_f64);
let minimum_price_to_book_ratio_basis_points =
minimum_price_to_book_ratio.and_then(|value| runtime_round_f64_to_i64(value * 10_000.0));
let passes_share_price_floor =
pressured_support_adjusted_share_price_scalar_i64.map(|value| value >= SHARE_PRICE_FLOOR);
let passes_proceeds_floor = pressured_proceeds.map(|value| value >= ISSUE_PROCEEDS_CAP);
let passes_cash_gate = current_cash
.zip(highest_coupon_live_bond_principal)
.map(|(cash, principal)| cash <= i64::from(principal) + 5_000);
let passes_issue_cooldown_gate = Some(
annual_finance_state
.current_issue_age_absolute_counter_delta
.is_none_or(|delta| delta >= ONE_YEAR_ABSOLUTE_COUNTER_SPAN),
);
let passes_coupon_price_to_book_gate = price_to_book_ratio_basis_points
.zip(minimum_price_to_book_ratio_basis_points)
.map(|(actual, minimum)| actual >= minimum);
let eligible_for_double_tranche_issue = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_stock_issue_and_buyback_allowed(state) == Some(true)
&& runtime_world_bond_issue_and_repayment_allowed(state) == Some(true)
&& annual_finance_state.bond_count >= 2
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 1)
&& !runtime_company_annual_creditor_pressure_state(state, company_id)?
.eligible_for_bankruptcy_branch
&& !runtime_company_annual_deep_distress_state(state, company_id)?
.eligible_for_bankruptcy_fallback
&& !runtime_company_annual_bond_policy_state(state, company_id)?
.eligible_for_bond_issue_branch
&& !runtime_company_annual_stock_repurchase_state(state, company_id)?
.eligible_for_single_batch_repurchase
&& passes_share_price_floor == Some(true)
&& passes_proceeds_floor == Some(true)
&& passes_cash_gate == Some(true)
&& passes_issue_cooldown_gate == Some(true)
&& passes_coupon_price_to_book_gate == Some(true);
Some(RuntimeCompanyAnnualStockIssueState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state),
bond_issue_and_repayment_allowed: runtime_world_bond_issue_and_repayment_allowed(state),
years_since_founding: annual_finance_state.years_since_founding,
live_bond_count: Some(annual_finance_state.bond_count),
initial_issue_batch_size,
trimmed_issue_batch_size: Some(trimmed_issue_batch_size),
share_pressure_basis_points: runtime_round_f64_to_i64(
-(trimmed_issue_batch_size as f64) / annual_finance_state.outstanding_shares as f64
* 10_000.0,
),
pressured_support_adjusted_share_price_scalar:
pressured_support_adjusted_share_price_scalar_i64,
pressured_proceeds,
book_value_per_share_floor_applied,
price_to_book_ratio_basis_points,
current_cash,
highest_coupon_live_bond_principal,
highest_coupon_live_bond_rate_basis_points,
current_issue_age_absolute_counter_delta: annual_finance_state
.current_issue_age_absolute_counter_delta,
current_issue_cooldown_floor: Some(ONE_YEAR_ABSOLUTE_COUNTER_SPAN),
minimum_price_to_book_ratio_basis_points,
passes_share_price_floor,
passes_proceeds_floor,
passes_cash_gate,
passes_issue_cooldown_gate,
passes_coupon_price_to_book_gate,
eligible_for_double_tranche_issue,
})
}

Some files were not shown because too many files have changed in this diff Show more