Refactor runtime ownership and clean up warnings
This commit is contained in:
parent
f23a3b3add
commit
486b061558
628 changed files with 97954 additions and 90763 deletions
20
crates/rrt-cli/src/app/command/finance.rs
Normal file
20
crates/rrt-cli/src/app/command/finance.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
158
crates/rrt-cli/src/app/command/mod.rs
Normal file
158
crates/rrt-cli/src/app/command/mod.rs
Normal 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, ¤t_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"),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
194
crates/rrt-cli/src/app/command/model.rs
Normal file
194
crates/rrt-cli/src/app/command/model.rs
Normal 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 },
|
||||
}
|
||||
66
crates/rrt-cli/src/app/command/runtime/compare.rs
Normal file
66
crates/rrt-cli/src/app/command/runtime/compare.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
73
crates/rrt-cli/src/app/command/runtime/fixture_state.rs
Normal file
73
crates/rrt-cli/src/app/command/runtime/fixture_state.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
146
crates/rrt-cli/src/app/command/runtime/inspect.rs
Normal file
146
crates/rrt-cli/src/app/command/runtime/inspect.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
74
crates/rrt-cli/src/app/command/runtime/mod.rs
Normal file
74
crates/rrt-cli/src/app/command/runtime/mod.rs
Normal 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))
|
||||
}
|
||||
44
crates/rrt-cli/src/app/command/runtime/scan.rs
Normal file
44
crates/rrt-cli/src/app/command/runtime/scan.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
18
crates/rrt-cli/src/app/command/validate.rs
Normal file
18
crates/rrt-cli/src/app/command/validate.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
12
crates/rrt-cli/src/app/dispatch/finance.rs
Normal file
12
crates/rrt-cli/src/app/dispatch/finance.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
81
crates/rrt-cli/src/app/dispatch/mod.rs
Normal file
81
crates/rrt-cli/src/app/dispatch/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
30
crates/rrt-cli/src/app/dispatch/runtime/compare.rs
Normal file
30
crates/rrt-cli/src/app/dispatch/runtime/compare.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs
Normal file
43
crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
85
crates/rrt-cli/src/app/dispatch/runtime/inspect.rs
Normal file
85
crates/rrt-cli/src/app/dispatch/runtime/inspect.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/dispatch/runtime/mod.rs
Normal file
15
crates/rrt-cli/src/app/dispatch/runtime/mod.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
28
crates/rrt-cli/src/app/dispatch/runtime/scan.rs
Normal file
28
crates/rrt-cli/src/app/dispatch/runtime/scan.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/dispatch/validate.rs
Normal file
15
crates/rrt-cli/src/app/dispatch/validate.rs
Normal 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(())
|
||||
}
|
||||
110
crates/rrt-cli/src/app/finance.rs
Normal file
110
crates/rrt-cli/src/app/finance.rs
Normal 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(),
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
446
crates/rrt-cli/src/app/helpers/inspect.rs
Normal file
446
crates/rrt-cli/src/app/helpers/inspect.rs
Normal 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)
|
||||
)
|
||||
}
|
||||
2
crates/rrt-cli/src/app/helpers/mod.rs
Normal file
2
crates/rrt-cli/src/app/helpers/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub(super) mod inspect;
|
||||
pub(super) mod state_io;
|
||||
102
crates/rrt-cli/src/app/helpers/state_io.rs
Normal file
102
crates/rrt-cli/src/app/helpers/state_io.rs
Normal 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(())
|
||||
}
|
||||
16
crates/rrt-cli/src/app/mod.rs
Normal file
16
crates/rrt-cli/src/app/mod.rs
Normal 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()
|
||||
}
|
||||
258
crates/rrt-cli/src/app/reports/inspect.rs
Normal file
258
crates/rrt-cli/src/app/reports/inspect.rs
Normal 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,
|
||||
}
|
||||
2
crates/rrt-cli/src/app/reports/mod.rs
Normal file
2
crates/rrt-cli/src/app/reports/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub(super) mod inspect;
|
||||
pub(super) mod state;
|
||||
72
crates/rrt-cli/src/app/reports/state.rs
Normal file
72
crates/rrt-cli/src/app/reports/state.rs
Normal 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,
|
||||
}
|
||||
472
crates/rrt-cli/src/app/runtime_compare/candidate_table.rs
Normal file
472
crates/rrt-cli/src/app/runtime_compare/candidate_table.rs
Normal 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
|
||||
}
|
||||
104
crates/rrt-cli/src/app/runtime_compare/common.rs
Normal file
104
crates/rrt-cli/src/app/runtime_compare/common.rs
Normal 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(),
|
||||
});
|
||||
}
|
||||
43
crates/rrt-cli/src/app/runtime_compare/mod.rs
Normal file
43
crates/rrt-cli/src/app/runtime_compare/mod.rs
Normal 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,
|
||||
};
|
||||
94
crates/rrt-cli/src/app/runtime_compare/post_special.rs
Normal file
94
crates/rrt-cli/src/app/runtime_compare/post_special.rs
Normal 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)
|
||||
}
|
||||
195
crates/rrt-cli/src/app/runtime_compare/profiles.rs
Normal file
195
crates/rrt-cli/src/app/runtime_compare/profiles.rs
Normal 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)
|
||||
}
|
||||
250
crates/rrt-cli/src/app/runtime_compare/recipe_book.rs
Normal file
250
crates/rrt-cli/src/app/runtime_compare/recipe_book.rs
Normal 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(¤t).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()
|
||||
}
|
||||
33
crates/rrt-cli/src/app/runtime_compare/region.rs
Normal file
33
crates/rrt-cli/src/app/runtime_compare/region.rs
Normal 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(())
|
||||
}
|
||||
345
crates/rrt-cli/src/app/runtime_compare/setup_payload.rs
Normal file
345
crates/rrt-cli/src/app/runtime_compare/setup_payload.rs
Normal 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)
|
||||
}
|
||||
115
crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs
Normal file
115
crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs
Normal 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(())
|
||||
}
|
||||
9
crates/rrt-cli/src/app/runtime_fixture_state/mod.rs
Normal file
9
crates/rrt-cli/src/app/runtime_fixture_state/mod.rs
Normal 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};
|
||||
70
crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs
Normal file
70
crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs
Normal 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(())
|
||||
}
|
||||
29
crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs
Normal file
29
crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs
Normal 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(())
|
||||
}
|
||||
81
crates/rrt-cli/src/app/runtime_fixture_state/state.rs
Normal file
81
crates/rrt-cli/src/app/runtime_fixture_state/state.rs
Normal 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(())
|
||||
}
|
||||
175
crates/rrt-cli/src/app/runtime_inspect/assets.rs
Normal file
175
crates/rrt-cli/src/app/runtime_inspect/assets.rs
Normal 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(())
|
||||
}
|
||||
128
crates/rrt-cli/src/app/runtime_inspect/maps.rs
Normal file
128
crates/rrt-cli/src/app/runtime_inspect/maps.rs
Normal 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(())
|
||||
}
|
||||
19
crates/rrt-cli/src/app/runtime_inspect/mod.rs
Normal file
19
crates/rrt-cli/src/app/runtime_inspect/mod.rs
Normal 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,
|
||||
};
|
||||
117
crates/rrt-cli/src/app/runtime_inspect/smp.rs
Normal file
117
crates/rrt-cli/src/app/runtime_inspect/smp.rs
Normal 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(())
|
||||
}
|
||||
245
crates/rrt-cli/src/app/runtime_scan/aligned_band.rs
Normal file
245
crates/rrt-cli/src/app/runtime_scan/aligned_band.rs
Normal 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(¤t).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,
|
||||
})
|
||||
}
|
||||
388
crates/rrt-cli/src/app/runtime_scan/candidate_table.rs
Normal file
388
crates/rrt-cli/src/app/runtime_scan/candidate_table.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
130
crates/rrt-cli/src/app/runtime_scan/common.rs
Normal file
130
crates/rrt-cli/src/app/runtime_scan/common.rs
Normal 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
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/runtime_scan/mod.rs
Normal file
15
crates/rrt-cli/src/app/runtime_scan/mod.rs
Normal 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;
|
||||
489
crates/rrt-cli/src/app/runtime_scan/post_special.rs
Normal file
489
crates/rrt-cli/src/app/runtime_scan/post_special.rs
Normal 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(¤t).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()
|
||||
}
|
||||
183
crates/rrt-cli/src/app/runtime_scan/recipe_book.rs
Normal file
183
crates/rrt-cli/src/app/runtime_scan/recipe_book.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
167
crates/rrt-cli/src/app/runtime_scan/special_conditions.rs
Normal file
167
crates/rrt-cli/src/app/runtime_scan/special_conditions.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
581
crates/rrt-cli/src/app/tests/compare.rs
Normal file
581
crates/rrt-cli/src/app/tests/compare.rs
Normal 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")
|
||||
);
|
||||
}
|
||||
32
crates/rrt-cli/src/app/tests/mod.rs
Normal file
32
crates/rrt-cli/src/app/tests/mod.rs
Normal 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::*;
|
||||
410
crates/rrt-cli/src/app/tests/state/diff.rs
Normal file
410
crates/rrt-cli/src/app/tests/state/diff.rs
Normal 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);
|
||||
}
|
||||
176
crates/rrt-cli/src/app/tests/state/document_io.rs
Normal file
176
crates/rrt-cli/src/app/tests/state/document_io.rs
Normal 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);
|
||||
}
|
||||
77
crates/rrt-cli/src/app/tests/state/fixture_summary.rs
Normal file
77
crates/rrt-cli/src/app/tests/state/fixture_summary.rs
Normal 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");
|
||||
}
|
||||
7
crates/rrt-cli/src/app/tests/state/mod.rs
Normal file
7
crates/rrt-cli/src/app/tests/state/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
mod diff;
|
||||
mod document_io;
|
||||
mod fixture_summary;
|
||||
mod save_slice_overlay;
|
||||
mod snapshot_io;
|
||||
265
crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs
Normal file
265
crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs
Normal 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"
|
||||
}));
|
||||
}
|
||||
18
crates/rrt-cli/src/app/tests/state/snapshot_io.rs
Normal file
18
crates/rrt-cli/src/app/tests/state/snapshot_io.rs
Normal 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);
|
||||
}
|
||||
6
crates/rrt-cli/src/app/tests/support/json.rs
Normal file
6
crates/rrt-cli/src/app/tests/support/json.rs
Normal 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:?}"
|
||||
);
|
||||
}
|
||||
5
crates/rrt-cli/src/app/tests/support/mod.rs
Normal file
5
crates/rrt-cli/src/app/tests/support/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod json;
|
||||
mod temp_files;
|
||||
|
||||
pub(crate) use json::*;
|
||||
pub(crate) use temp_files::*;
|
||||
15
crates/rrt-cli/src/app/tests/support/temp_files.rs
Normal file
15
crates/rrt-cli/src/app/tests/support/temp_files.rs
Normal 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
|
||||
}
|
||||
119
crates/rrt-cli/src/app/validate.rs
Normal file
119
crates/rrt-cli/src/app/validate.rs
Normal 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
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
68
crates/rrt-fixtures/src/schema/document.rs
Normal file
68
crates/rrt-fixtures/src/schema/document.rs
Normal 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>,
|
||||
}
|
||||
17
crates/rrt-fixtures/src/schema/mod.rs
Normal file
17
crates/rrt-fixtures/src/schema/mod.rs
Normal 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;
|
||||
49
crates/rrt-fixtures/src/schema/state_fragment.rs
Normal file
49
crates/rrt-fixtures/src/schema/state_fragment.rs
Normal 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:?}"
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
302
crates/rrt-fixtures/src/schema/summary.rs
Normal file
302
crates/rrt-fixtures/src/schema/summary.rs
Normal 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>,
|
||||
}
|
||||
345
crates/rrt-fixtures/src/schema/summary_compare/collections.rs
Normal file
345
crates/rrt-fixtures/src/schema/summary_compare/collections.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
22
crates/rrt-fixtures/src/schema/summary_compare/mod.rs
Normal file
22
crates/rrt-fixtures/src/schema/summary_compare/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
314
crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs
Normal file
314
crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
398
crates/rrt-fixtures/src/schema/summary_compare/world.rs
Normal file
398
crates/rrt-fixtures/src/schema/summary_compare/world.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
112
crates/rrt-fixtures/src/schema/tests.rs
Normal file
112
crates/rrt-fixtures/src/schema/tests.rs
Normal 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:?}"
|
||||
);
|
||||
}
|
||||
37
crates/rrt-fixtures/src/schema/validate.rs
Normal file
37
crates/rrt-fixtures/src/schema/validate.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
1
crates/rrt-fixtures/src/summary.rs
Normal file
1
crates/rrt-fixtures/src/summary.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use crate::schema::{ExpectedRuntimeSummary, compare_expected_state_fragment};
|
||||
2
crates/rrt-fixtures/src/validation.rs
Normal file
2
crates/rrt-fixtures/src/validation.rs
Normal 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};
|
||||
156
crates/rrt-hook/src/capture.rs
Normal file
156
crates/rrt-hook/src/capture.rs
Normal 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
377
crates/rrt-hook/src/windows/auto_load.rs
Normal file
377
crates/rrt-hook/src/windows/auto_load.rs
Normal 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);
|
||||
}
|
||||
333
crates/rrt-hook/src/windows/capture.rs
Normal file
333
crates/rrt-hook/src/windows/capture.rs
Normal 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
|
||||
}
|
||||
221
crates/rrt-hook/src/windows/constants.rs
Normal file
221
crates/rrt-hook/src/windows/constants.rs
Normal 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;
|
||||
292
crates/rrt-hook/src/windows/detours.rs
Normal file
292
crates/rrt-hook/src/windows/detours.rs
Normal 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
|
||||
}
|
||||
86
crates/rrt-hook/src/windows/ffi.rs
Normal file
86
crates/rrt-hook/src/windows/ffi.rs
Normal 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;
|
||||
383
crates/rrt-hook/src/windows/install.rs
Normal file
383
crates/rrt-hook/src/windows/install.rs
Normal 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)
|
||||
}
|
||||
1096
crates/rrt-hook/src/windows/logging.rs
Normal file
1096
crates/rrt-hook/src/windows/logging.rs
Normal file
File diff suppressed because it is too large
Load diff
99
crates/rrt-hook/src/windows/memory.rs
Normal file
99
crates/rrt-hook/src/windows/memory.rs
Normal 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()
|
||||
}
|
||||
71
crates/rrt-hook/src/windows/mod.rs
Normal file
71
crates/rrt-hook/src/windows/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
45
crates/rrt-hook/src/windows/state.rs
Normal file
45
crates/rrt-hook/src/windows/state.rs
Normal 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
320
crates/rrt-runtime/src/derived/company.rs
Normal file
320
crates/rrt-runtime/src/derived/company.rs
Normal 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)
|
||||
}
|
||||
138
crates/rrt-runtime/src/derived/finance/annual_policy.rs
Normal file
138
crates/rrt-runtime/src/derived/finance/annual_policy.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
168
crates/rrt-runtime/src/derived/finance/bond_policy.rs
Normal file
168
crates/rrt-runtime/src/derived/finance/bond_policy.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
crates/rrt-runtime/src/derived/finance/credit_rating.rs
Normal file
151
crates/rrt-runtime/src/derived/finance/credit_rating.rs
Normal 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()
|
||||
}
|
||||
168
crates/rrt-runtime/src/derived/finance/distress.rs
Normal file
168
crates/rrt-runtime/src/derived/finance/distress.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
227
crates/rrt-runtime/src/derived/finance/dividend_policy.rs
Normal file
227
crates/rrt-runtime/src/derived/finance/dividend_policy.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
13
crates/rrt-runtime/src/derived/finance/mod.rs
Normal file
13
crates/rrt-runtime/src/derived/finance/mod.rs
Normal 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::*;
|
||||
271
crates/rrt-runtime/src/derived/finance/stock_actions.rs
Normal file
271
crates/rrt-runtime/src/derived/finance/stock_actions.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue