Add overlay runtime import for packed events
This commit is contained in:
parent
8ca65cbbfb
commit
fa63cefb70
13 changed files with 1248 additions and 153 deletions
|
|
@ -16,15 +16,16 @@ use rrt_model::{
|
||||||
};
|
};
|
||||||
use rrt_runtime::{
|
use rrt_runtime::{
|
||||||
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES,
|
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES,
|
||||||
Pk4ExtractionReport, Pk4InspectionReport, RuntimeSaveSliceDocument,
|
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, Pk4InspectionReport,
|
||||||
|
RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument,
|
||||||
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
|
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
|
||||||
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
|
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
|
||||||
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
|
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
|
||||||
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
|
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
|
||||||
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
|
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
|
||||||
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
|
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
|
||||||
save_runtime_save_slice_document, save_runtime_snapshot_document,
|
save_runtime_overlay_import_document, save_runtime_save_slice_document,
|
||||||
validate_runtime_snapshot_document,
|
save_runtime_snapshot_document, validate_runtime_snapshot_document,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
@ -128,6 +129,11 @@ enum Command {
|
||||||
smp_path: PathBuf,
|
smp_path: PathBuf,
|
||||||
output_path: PathBuf,
|
output_path: PathBuf,
|
||||||
},
|
},
|
||||||
|
RuntimeExportOverlayImport {
|
||||||
|
snapshot_path: PathBuf,
|
||||||
|
save_slice_path: PathBuf,
|
||||||
|
output_path: PathBuf,
|
||||||
|
},
|
||||||
RuntimeInspectPk4 {
|
RuntimeInspectPk4 {
|
||||||
pk4_path: PathBuf,
|
pk4_path: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
@ -250,6 +256,14 @@ struct RuntimeSaveSliceExportOutput {
|
||||||
save_slice_id: String,
|
save_slice_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RuntimeOverlayImportExportOutput {
|
||||||
|
output_path: String,
|
||||||
|
import_id: String,
|
||||||
|
base_snapshot_path: String,
|
||||||
|
save_slice_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct RuntimePk4InspectionOutput {
|
struct RuntimePk4InspectionOutput {
|
||||||
path: String,
|
path: String,
|
||||||
|
|
@ -789,6 +803,13 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
} => {
|
} => {
|
||||||
run_runtime_export_save_slice(&smp_path, &output_path)?;
|
run_runtime_export_save_slice(&smp_path, &output_path)?;
|
||||||
}
|
}
|
||||||
|
Command::RuntimeExportOverlayImport {
|
||||||
|
snapshot_path,
|
||||||
|
save_slice_path,
|
||||||
|
output_path,
|
||||||
|
} => {
|
||||||
|
run_runtime_export_overlay_import(&snapshot_path, &save_slice_path, &output_path)?;
|
||||||
|
}
|
||||||
Command::RuntimeInspectPk4 { pk4_path } => {
|
Command::RuntimeInspectPk4 { pk4_path } => {
|
||||||
run_runtime_inspect_pk4(&pk4_path)?;
|
run_runtime_inspect_pk4(&pk4_path)?;
|
||||||
}
|
}
|
||||||
|
|
@ -956,6 +977,15 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
||||||
output_path: PathBuf::from(output_path),
|
output_path: PathBuf::from(output_path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
[command, subcommand, snapshot_path, save_slice_path, output_path]
|
||||||
|
if command == "runtime" && subcommand == "export-overlay-import" =>
|
||||||
|
{
|
||||||
|
Ok(Command::RuntimeExportOverlayImport {
|
||||||
|
snapshot_path: PathBuf::from(snapshot_path),
|
||||||
|
save_slice_path: PathBuf::from(save_slice_path),
|
||||||
|
output_path: PathBuf::from(output_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
[command, subcommand, path] if command == "runtime" && subcommand == "inspect-pk4" => {
|
[command, subcommand, path] if command == "runtime" && subcommand == "inspect-pk4" => {
|
||||||
Ok(Command::RuntimeInspectPk4 {
|
Ok(Command::RuntimeInspectPk4 {
|
||||||
pk4_path: PathBuf::from(path),
|
pk4_path: PathBuf::from(path),
|
||||||
|
|
@ -1096,7 +1126,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => Err(
|
_ => Err(
|
||||||
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
|
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -1215,13 +1245,21 @@ fn run_runtime_export_fixture_state(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let snapshot = load_runtime_snapshot_document(snapshot_path)?;
|
if let Ok(snapshot) = load_runtime_snapshot_document(snapshot_path) {
|
||||||
validate_runtime_snapshot_document(&snapshot)
|
validate_runtime_snapshot_document(&snapshot)
|
||||||
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
||||||
let summary = snapshot.summary();
|
|
||||||
let report = RuntimeStateSummaryReport {
|
let report = RuntimeStateSummaryReport {
|
||||||
snapshot_id: snapshot.snapshot_id,
|
snapshot_id: snapshot.snapshot_id.clone(),
|
||||||
summary,
|
summary: snapshot.summary(),
|
||||||
|
};
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let import = load_runtime_state_import(snapshot_path)?;
|
||||||
|
let report = RuntimeStateSummaryReport {
|
||||||
|
snapshot_id: import.import_id,
|
||||||
|
summary: RuntimeSummary::from_state(&import.state),
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1363,6 +1401,17 @@ fn run_runtime_export_save_slice(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_runtime_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(())
|
||||||
|
}
|
||||||
|
|
||||||
fn export_runtime_save_slice_document(
|
fn export_runtime_save_slice_document(
|
||||||
smp_path: &Path,
|
smp_path: &Path,
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
|
|
@ -1397,6 +1446,39 @@ fn export_runtime_save_slice_document(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let report = RuntimePk4InspectionOutput {
|
let report = RuntimePk4InspectionOutput {
|
||||||
path: pk4_path.display().to_string(),
|
path: pk4_path.display().to_string(),
|
||||||
|
|
@ -4349,11 +4431,15 @@ mod tests {
|
||||||
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
|
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
|
||||||
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
|
.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");
|
||||||
|
|
||||||
run_runtime_summarize_fixture(&parity_fixture)
|
run_runtime_summarize_fixture(&parity_fixture)
|
||||||
.expect("save-slice-backed parity fixture should summarize");
|
.expect("save-slice-backed parity fixture should summarize");
|
||||||
run_runtime_summarize_fixture(&selective_fixture)
|
run_runtime_summarize_fixture(&selective_fixture)
|
||||||
.expect("save-slice-backed selective-import fixture should summarize");
|
.expect("save-slice-backed selective-import fixture should summarize");
|
||||||
|
run_runtime_summarize_fixture(&overlay_fixture)
|
||||||
|
.expect("overlay-backed selective-import fixture should summarize");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -4396,6 +4482,35 @@ mod tests {
|
||||||
let _ = fs::remove_file(output_path);
|
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 = rrt_runtime::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);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
|
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
|
||||||
let left = serde_json::json!({
|
let left = serde_json::json!({
|
||||||
|
|
@ -4536,6 +4651,27 @@ mod tests {
|
||||||
let _ = fs::remove_file(right_path);
|
let _ = fs::remove_file(right_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
|
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
|
||||||
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use rrt_runtime::{
|
use rrt_runtime::{
|
||||||
load_runtime_save_slice_document, load_runtime_snapshot_document,
|
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,
|
project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
|
||||||
validate_runtime_snapshot_document,
|
validate_runtime_snapshot_document,
|
||||||
};
|
};
|
||||||
|
|
@ -34,10 +34,11 @@ fn resolve_raw_fixture_document(
|
||||||
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
|
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
|
||||||
let specified_state_inputs = usize::from(raw.state.is_some())
|
let specified_state_inputs = usize::from(raw.state.is_some())
|
||||||
+ usize::from(raw.state_snapshot_path.is_some())
|
+ usize::from(raw.state_snapshot_path.is_some())
|
||||||
+ usize::from(raw.state_save_slice_path.is_some());
|
+ usize::from(raw.state_save_slice_path.is_some())
|
||||||
|
+ usize::from(raw.state_import_path.is_some());
|
||||||
if specified_state_inputs != 1 {
|
if specified_state_inputs != 1 {
|
||||||
return Err(
|
return Err(
|
||||||
"fixture must specify exactly one of inline state, state_snapshot_path, or state_save_slice_path"
|
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_import_path"
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +47,10 @@ fn resolve_raw_fixture_document(
|
||||||
&raw.state,
|
&raw.state,
|
||||||
&raw.state_snapshot_path,
|
&raw.state_snapshot_path,
|
||||||
&raw.state_save_slice_path,
|
&raw.state_save_slice_path,
|
||||||
|
&raw.state_import_path,
|
||||||
) {
|
) {
|
||||||
(Some(state), None, None) => state.clone(),
|
(Some(state), None, None, None) => state.clone(),
|
||||||
(None, Some(snapshot_path), None) => {
|
(None, Some(snapshot_path), None, None) => {
|
||||||
let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
|
let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
|
||||||
let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
|
let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
|
||||||
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
|
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
|
||||||
|
|
@ -59,7 +61,7 @@ fn resolve_raw_fixture_document(
|
||||||
})?;
|
})?;
|
||||||
snapshot.state
|
snapshot.state
|
||||||
}
|
}
|
||||||
(None, None, Some(save_slice_path)) => {
|
(None, None, Some(save_slice_path), None) => {
|
||||||
let save_slice_path = resolve_snapshot_path(base_dir, save_slice_path);
|
let save_slice_path = resolve_snapshot_path(base_dir, save_slice_path);
|
||||||
let document = load_runtime_save_slice_document(&save_slice_path)?;
|
let document = load_runtime_save_slice_document(&save_slice_path)?;
|
||||||
validate_runtime_save_slice_document(&document).map_err(|err| {
|
validate_runtime_save_slice_document(&document).map_err(|err| {
|
||||||
|
|
@ -81,12 +83,28 @@ fn resolve_raw_fixture_document(
|
||||||
})?
|
})?
|
||||||
.state
|
.state
|
||||||
}
|
}
|
||||||
|
(None, None, None, Some(import_path)) => {
|
||||||
|
let import_path = resolve_snapshot_path(base_dir, import_path);
|
||||||
|
load_runtime_state_import(&import_path)
|
||||||
|
.map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"failed to load runtime import {}: {err}",
|
||||||
|
import_path.display()
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.state
|
||||||
|
}
|
||||||
_ => unreachable!("state input exclusivity checked above"),
|
_ => unreachable!("state input exclusivity checked above"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let state_origin = match (raw.state_snapshot_path, raw.state_save_slice_path) {
|
let state_origin = match (
|
||||||
(Some(snapshot_path), None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
|
raw.state_snapshot_path,
|
||||||
(None, Some(save_slice_path)) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
|
raw.state_save_slice_path,
|
||||||
|
raw.state_import_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),
|
||||||
_ => FixtureStateOrigin::Inline,
|
_ => FixtureStateOrigin::Inline,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -116,11 +134,13 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::FixtureStateOrigin;
|
use crate::FixtureStateOrigin;
|
||||||
use rrt_runtime::{
|
use rrt_runtime::{
|
||||||
CalendarPoint, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
|
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
||||||
|
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
|
||||||
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
|
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
|
||||||
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState,
|
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState,
|
||||||
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
|
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
|
||||||
save_runtime_save_slice_document, save_runtime_snapshot_document,
|
save_runtime_overlay_import_document, save_runtime_save_slice_document,
|
||||||
|
save_runtime_snapshot_document,
|
||||||
};
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
|
@ -275,4 +295,150 @@ mod tests {
|
||||||
let _ = std::fs::remove_file(save_slice_path);
|
let _ = std::fs::remove_file(save_slice_path);
|
||||||
let _ = std::fs::remove_dir(fixture_dir);
|
let _ = std::fs::remove_dir(fixture_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_fixture_from_relative_import_path() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("system time should be after epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-import-{nonce}"));
|
||||||
|
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
|
||||||
|
|
||||||
|
let snapshot_path = fixture_dir.join("base.json");
|
||||||
|
let save_slice_path = fixture_dir.join("slice.json");
|
||||||
|
let import_path = fixture_dir.join("overlay.json");
|
||||||
|
|
||||||
|
let snapshot = RuntimeSnapshotDocument {
|
||||||
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||||
|
snapshot_id: "base".to_string(),
|
||||||
|
source: RuntimeSnapshotSource::default(),
|
||||||
|
state: RuntimeState {
|
||||||
|
calendar: CalendarPoint {
|
||||||
|
year: 1830,
|
||||||
|
month_slot: 0,
|
||||||
|
phase_slot: 0,
|
||||||
|
tick_slot: 5,
|
||||||
|
},
|
||||||
|
world_flags: BTreeMap::new(),
|
||||||
|
save_profile: RuntimeSaveProfileState::default(),
|
||||||
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
|
metadata: BTreeMap::new(),
|
||||||
|
companies: vec![rrt_runtime::RuntimeCompany {
|
||||||
|
company_id: 42,
|
||||||
|
current_cash: 100,
|
||||||
|
debt: 0,
|
||||||
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
|
event_runtime_records: Vec::new(),
|
||||||
|
candidate_availability: BTreeMap::new(),
|
||||||
|
special_conditions: BTreeMap::new(),
|
||||||
|
service_state: RuntimeServiceState::default(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save");
|
||||||
|
|
||||||
|
let save_slice = RuntimeSaveSliceDocument {
|
||||||
|
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
||||||
|
save_slice_id: "slice".to_string(),
|
||||||
|
source: RuntimeSaveSliceDocumentSource::default(),
|
||||||
|
save_slice: rrt_runtime::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,
|
||||||
|
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: 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(),
|
||||||
|
trigger_kind: Some(7),
|
||||||
|
active: Some(true),
|
||||||
|
marks_collection_dirty: Some(false),
|
||||||
|
one_shot: Some(false),
|
||||||
|
text_bands: vec![],
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
|
||||||
|
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
|
||||||
|
delta: 25,
|
||||||
|
}],
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
notes: vec![],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
save_runtime_save_slice_document(&save_slice_path, &save_slice)
|
||||||
|
.expect("save slice should save");
|
||||||
|
|
||||||
|
let overlay = RuntimeOverlayImportDocument {
|
||||||
|
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
|
||||||
|
import_id: "overlay-backed-fixture-state".to_string(),
|
||||||
|
source: RuntimeOverlayImportDocumentSource::default(),
|
||||||
|
base_snapshot_path: "base.json".to_string(),
|
||||||
|
save_slice_path: "slice.json".to_string(),
|
||||||
|
};
|
||||||
|
save_runtime_overlay_import_document(&import_path, &overlay)
|
||||||
|
.expect("overlay import should save");
|
||||||
|
|
||||||
|
let fixture_json = r#"
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"fixture_id": "overlay-backed-fixture",
|
||||||
|
"source": {
|
||||||
|
"kind": "captured-runtime"
|
||||||
|
},
|
||||||
|
"state_import_path": "overlay.json",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"kind": "service_trigger_kind",
|
||||||
|
"trigger_kind": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expected_summary": {
|
||||||
|
"company_count": 1,
|
||||||
|
"packed_event_imported_runtime_record_count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
|
||||||
|
.expect("overlay-backed fixture should load");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fixture.state_origin,
|
||||||
|
FixtureStateOrigin::ImportPath("overlay.json".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(fixture.state.event_runtime_records.len(), 1);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(snapshot_path);
|
||||||
|
let _ = std::fs::remove_file(save_slice_path);
|
||||||
|
let _ = std::fs::remove_file(import_path);
|
||||||
|
let _ = std::fs::remove_dir(fixture_dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ pub struct ExpectedRuntimeSummary {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub packed_event_unsupported_record_count: Option<usize>,
|
pub packed_event_unsupported_record_count: Option<usize>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub packed_event_blocked_missing_company_context_count: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
pub event_runtime_record_count: Option<usize>,
|
pub event_runtime_record_count: Option<usize>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub candidate_availability_count: Option<usize>,
|
pub candidate_availability_count: Option<usize>,
|
||||||
|
|
@ -361,6 +363,14 @@ impl ExpectedRuntimeSummary {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(count) = self.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) = self.event_runtime_record_count {
|
if let Some(count) = self.event_runtime_record_count {
|
||||||
if actual.event_runtime_record_count != count {
|
if actual.event_runtime_record_count != count {
|
||||||
mismatches.push(format!(
|
mismatches.push(format!(
|
||||||
|
|
@ -531,6 +541,7 @@ pub enum FixtureStateOrigin {
|
||||||
Inline,
|
Inline,
|
||||||
SnapshotPath(String),
|
SnapshotPath(String),
|
||||||
SaveSlicePath(String),
|
SaveSlicePath(String),
|
||||||
|
ImportPath(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -546,6 +557,8 @@ pub struct RawFixtureDocument {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub state_save_slice_path: Option<String>,
|
pub state_save_slice_path: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub state_import_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub commands: Vec<StepCommand>,
|
pub commands: Vec<StepCommand>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub expected_summary: ExpectedRuntimeSummary,
|
pub expected_summary: ExpectedRuntimeSummary,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
|
||||||
use crate::{
|
use crate::{
|
||||||
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||||
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||||
|
|
@ -13,6 +14,7 @@ use crate::{
|
||||||
|
|
||||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||||
pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1;
|
pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1;
|
||||||
|
pub const OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION: u32 = 1;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
pub struct RuntimeStateDumpSource {
|
pub struct RuntimeStateDumpSource {
|
||||||
|
|
@ -52,6 +54,24 @@ pub struct RuntimeSaveSliceDocument {
|
||||||
pub save_slice: SmpLoadedSaveSlice,
|
pub save_slice: SmpLoadedSaveSlice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
pub struct RuntimeOverlayImportDocumentSource {
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeOverlayImportDocument {
|
||||||
|
pub format_version: u32,
|
||||||
|
pub import_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub source: RuntimeOverlayImportDocumentSource,
|
||||||
|
pub base_snapshot_path: String,
|
||||||
|
pub save_slice_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RuntimeStateImport {
|
pub struct RuntimeStateImport {
|
||||||
pub import_id: String,
|
pub import_id: String,
|
||||||
|
|
@ -59,6 +79,24 @@ pub struct RuntimeStateImport {
|
||||||
pub state: RuntimeState,
|
pub state: RuntimeState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SaveSliceProjection {
|
||||||
|
world_flags: BTreeMap<String, bool>,
|
||||||
|
save_profile: RuntimeSaveProfileState,
|
||||||
|
world_restore: RuntimeWorldRestoreState,
|
||||||
|
metadata: BTreeMap<String, String>,
|
||||||
|
packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
|
||||||
|
event_runtime_records: Vec<RuntimeEventRecord>,
|
||||||
|
candidate_availability: BTreeMap<String, u32>,
|
||||||
|
special_conditions: BTreeMap<String, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum SaveSliceProjectionMode {
|
||||||
|
Standalone,
|
||||||
|
Overlay,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn project_save_slice_to_runtime_state_import(
|
pub fn project_save_slice_to_runtime_state_import(
|
||||||
save_slice: &SmpLoadedSaveSlice,
|
save_slice: &SmpLoadedSaveSlice,
|
||||||
import_id: &str,
|
import_id: &str,
|
||||||
|
|
@ -67,7 +105,96 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
if import_id.trim().is_empty() {
|
if import_id.trim().is_empty() {
|
||||||
return Err("import_id must not be empty".to_string());
|
return Err("import_id must not be empty".to_string());
|
||||||
}
|
}
|
||||||
|
let projection = project_save_slice_components(
|
||||||
|
save_slice,
|
||||||
|
&BTreeSet::new(),
|
||||||
|
SaveSliceProjectionMode::Standalone,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let state = RuntimeState {
|
||||||
|
calendar: CalendarPoint {
|
||||||
|
year: 1830,
|
||||||
|
month_slot: 0,
|
||||||
|
phase_slot: 0,
|
||||||
|
tick_slot: 0,
|
||||||
|
},
|
||||||
|
world_flags: projection.world_flags,
|
||||||
|
save_profile: projection.save_profile,
|
||||||
|
world_restore: projection.world_restore,
|
||||||
|
metadata: projection.metadata,
|
||||||
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: projection.packed_event_collection,
|
||||||
|
event_runtime_records: projection.event_runtime_records,
|
||||||
|
candidate_availability: projection.candidate_availability,
|
||||||
|
special_conditions: projection.special_conditions,
|
||||||
|
service_state: RuntimeServiceState::default(),
|
||||||
|
};
|
||||||
|
state.validate()?;
|
||||||
|
|
||||||
|
Ok(RuntimeStateImport {
|
||||||
|
import_id: import_id.to_string(),
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn project_save_slice_overlay_to_runtime_state_import(
|
||||||
|
base_state: &RuntimeState,
|
||||||
|
save_slice: &SmpLoadedSaveSlice,
|
||||||
|
import_id: &str,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<RuntimeStateImport, String> {
|
||||||
|
if import_id.trim().is_empty() {
|
||||||
|
return Err("import_id must not be empty".to_string());
|
||||||
|
}
|
||||||
|
base_state.validate()?;
|
||||||
|
|
||||||
|
let known_company_ids = base_state
|
||||||
|
.companies
|
||||||
|
.iter()
|
||||||
|
.map(|company| company.company_id)
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let projection = project_save_slice_components(
|
||||||
|
save_slice,
|
||||||
|
&known_company_ids,
|
||||||
|
SaveSliceProjectionMode::Overlay,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut world_flags = base_state.world_flags.clone();
|
||||||
|
world_flags.retain(|key, _| !key.starts_with("save_slice."));
|
||||||
|
world_flags.extend(projection.world_flags);
|
||||||
|
|
||||||
|
let mut metadata = base_state.metadata.clone();
|
||||||
|
metadata.retain(|key, _| !key.starts_with("save_slice."));
|
||||||
|
metadata.extend(projection.metadata);
|
||||||
|
|
||||||
|
let state = RuntimeState {
|
||||||
|
calendar: base_state.calendar,
|
||||||
|
world_flags,
|
||||||
|
save_profile: projection.save_profile,
|
||||||
|
world_restore: projection.world_restore,
|
||||||
|
metadata,
|
||||||
|
companies: base_state.companies.clone(),
|
||||||
|
packed_event_collection: projection.packed_event_collection,
|
||||||
|
event_runtime_records: projection.event_runtime_records,
|
||||||
|
candidate_availability: projection.candidate_availability,
|
||||||
|
special_conditions: projection.special_conditions,
|
||||||
|
service_state: base_state.service_state.clone(),
|
||||||
|
};
|
||||||
|
state.validate()?;
|
||||||
|
|
||||||
|
Ok(RuntimeStateImport {
|
||||||
|
import_id: import_id.to_string(),
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_save_slice_components(
|
||||||
|
save_slice: &SmpLoadedSaveSlice,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
mode: SaveSliceProjectionMode,
|
||||||
|
) -> Result<SaveSliceProjection, String> {
|
||||||
let mut world_flags = BTreeMap::new();
|
let mut world_flags = BTreeMap::new();
|
||||||
world_flags.insert(
|
world_flags.insert(
|
||||||
"save_slice.profile_present".to_string(),
|
"save_slice.profile_present".to_string(),
|
||||||
|
|
@ -107,11 +234,19 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
let mut metadata = BTreeMap::new();
|
let mut metadata = BTreeMap::new();
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.import_projection".to_string(),
|
"save_slice.import_projection".to_string(),
|
||||||
"partial-runtime-restore-v1".to_string(),
|
match mode {
|
||||||
|
SaveSliceProjectionMode::Standalone => "partial-runtime-restore-v1",
|
||||||
|
SaveSliceProjectionMode::Overlay => "overlay-runtime-restore-v1",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.calendar_source".to_string(),
|
"save_slice.calendar_source".to_string(),
|
||||||
"default-1830-placeholder".to_string(),
|
match mode {
|
||||||
|
SaveSliceProjectionMode::Standalone => "default-1830-placeholder",
|
||||||
|
SaveSliceProjectionMode::Overlay => "base-snapshot-preserved",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.selected_year_seed_tuple_source".to_string(),
|
"save_slice.selected_year_seed_tuple_source".to_string(),
|
||||||
|
|
@ -162,45 +297,9 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
if let Some(family) = &save_slice.bridge_family {
|
if let Some(family) = &save_slice.bridge_family {
|
||||||
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
||||||
}
|
}
|
||||||
let known_company_ids = BTreeSet::new();
|
|
||||||
let imported_event_runtime_records = save_slice
|
let (packed_event_collection, event_runtime_records) =
|
||||||
.event_runtime_collection
|
project_packed_event_collection(save_slice, known_company_ids)?;
|
||||||
.as_ref()
|
|
||||||
.map(|summary| {
|
|
||||||
summary
|
|
||||||
.records
|
|
||||||
.iter()
|
|
||||||
.filter_map(|record| {
|
|
||||||
smp_packed_record_to_runtime_event_record(record, &known_company_ids)
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default();
|
|
||||||
let packed_event_collection = save_slice.event_runtime_collection.as_ref().map(|summary| {
|
|
||||||
let records = summary
|
|
||||||
.records
|
|
||||||
.iter()
|
|
||||||
.map(runtime_packed_event_record_summary_from_smp)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
RuntimePackedEventCollectionSummary {
|
|
||||||
source_kind: summary.source_kind.clone(),
|
|
||||||
mechanism_family: summary.mechanism_family.clone(),
|
|
||||||
mechanism_confidence: summary.mechanism_confidence.clone(),
|
|
||||||
container_profile_family: summary.container_profile_family.clone(),
|
|
||||||
packed_state_version: summary.packed_state_version,
|
|
||||||
packed_state_version_hex: summary.packed_state_version_hex.clone(),
|
|
||||||
live_id_bound: summary.live_id_bound,
|
|
||||||
live_record_count: summary.live_record_count,
|
|
||||||
live_entry_ids: summary.live_entry_ids.clone(),
|
|
||||||
decoded_record_count: records
|
|
||||||
.iter()
|
|
||||||
.filter(|record| record.decode_status != "unsupported_framing")
|
|
||||||
.count(),
|
|
||||||
imported_runtime_record_count: imported_event_runtime_records.len(),
|
|
||||||
records,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(summary) = &save_slice.event_runtime_collection {
|
if let Some(summary) = &save_slice.event_runtime_collection {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.event_runtime_collection_source_kind".to_string(),
|
"save_slice.event_runtime_collection_source_kind".to_string(),
|
||||||
|
|
@ -220,9 +319,10 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
);
|
);
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.event_runtime_collection_imported_runtime_record_count".to_string(),
|
"save_slice.event_runtime_collection_imported_runtime_record_count".to_string(),
|
||||||
imported_event_runtime_records.len().to_string(),
|
event_runtime_records.len().to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let save_profile = if let Some(profile) = &save_slice.profile {
|
let save_profile = if let Some(profile) = &save_slice.profile {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"save_slice.profile_kind".to_string(),
|
"save_slice.profile_kind".to_string(),
|
||||||
|
|
@ -349,6 +449,7 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
candidate_availability.insert(entry.text.clone(), entry.availability_dword);
|
candidate_availability.insert(entry.text.clone(), entry.availability_dword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut special_conditions = BTreeMap::new();
|
let mut special_conditions = BTreeMap::new();
|
||||||
if let Some(table) = &save_slice.special_conditions_table {
|
if let Some(table) = &save_slice.special_conditions_table {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
|
|
@ -374,35 +475,82 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
metadata.insert(format!("save_slice.note.{index}"), note.clone());
|
metadata.insert(format!("save_slice.note.{index}"), note.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = RuntimeState {
|
Ok(SaveSliceProjection {
|
||||||
calendar: CalendarPoint {
|
|
||||||
year: 1830,
|
|
||||||
month_slot: 0,
|
|
||||||
phase_slot: 0,
|
|
||||||
tick_slot: 0,
|
|
||||||
},
|
|
||||||
world_flags,
|
world_flags,
|
||||||
save_profile,
|
save_profile,
|
||||||
world_restore,
|
world_restore,
|
||||||
metadata,
|
metadata,
|
||||||
companies: Vec::new(),
|
|
||||||
packed_event_collection,
|
packed_event_collection,
|
||||||
event_runtime_records: imported_event_runtime_records,
|
event_runtime_records,
|
||||||
candidate_availability,
|
candidate_availability,
|
||||||
special_conditions,
|
special_conditions,
|
||||||
service_state: RuntimeServiceState::default(),
|
|
||||||
};
|
|
||||||
state.validate()?;
|
|
||||||
|
|
||||||
Ok(RuntimeStateImport {
|
|
||||||
import_id: import_id.to_string(),
|
|
||||||
description,
|
|
||||||
state,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_packed_event_collection(
|
||||||
|
save_slice: &SmpLoadedSaveSlice,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
Option<RuntimePackedEventCollectionSummary>,
|
||||||
|
Vec<RuntimeEventRecord>,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
> {
|
||||||
|
let Some(summary) = save_slice.event_runtime_collection.as_ref() else {
|
||||||
|
return Ok((None, Vec::new()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut imported_runtime_records = Vec::new();
|
||||||
|
let mut imported_record_ids = BTreeSet::new();
|
||||||
|
for record in &summary.records {
|
||||||
|
if let Some(import_result) =
|
||||||
|
smp_packed_record_to_runtime_event_record(record, known_company_ids)
|
||||||
|
{
|
||||||
|
let runtime_record = import_result?;
|
||||||
|
imported_record_ids.insert(record.live_entry_id);
|
||||||
|
imported_runtime_records.push(runtime_record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let records = summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.map(|record| {
|
||||||
|
runtime_packed_event_record_summary_from_smp(
|
||||||
|
record,
|
||||||
|
known_company_ids,
|
||||||
|
imported_record_ids.contains(&record.live_entry_id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Some(RuntimePackedEventCollectionSummary {
|
||||||
|
source_kind: summary.source_kind.clone(),
|
||||||
|
mechanism_family: summary.mechanism_family.clone(),
|
||||||
|
mechanism_confidence: summary.mechanism_confidence.clone(),
|
||||||
|
container_profile_family: summary.container_profile_family.clone(),
|
||||||
|
packed_state_version: summary.packed_state_version,
|
||||||
|
packed_state_version_hex: summary.packed_state_version_hex.clone(),
|
||||||
|
live_id_bound: summary.live_id_bound,
|
||||||
|
live_record_count: summary.live_record_count,
|
||||||
|
live_entry_ids: summary.live_entry_ids.clone(),
|
||||||
|
decoded_record_count: records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.decode_status != "unsupported_framing")
|
||||||
|
.count(),
|
||||||
|
imported_runtime_record_count: imported_runtime_records.len(),
|
||||||
|
records,
|
||||||
|
}),
|
||||||
|
imported_runtime_records,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn runtime_packed_event_record_summary_from_smp(
|
fn runtime_packed_event_record_summary_from_smp(
|
||||||
record: &SmpLoadedPackedEventRecordSummary,
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
imported: bool,
|
||||||
) -> RuntimePackedEventRecordSummary {
|
) -> RuntimePackedEventRecordSummary {
|
||||||
RuntimePackedEventRecordSummary {
|
RuntimePackedEventRecordSummary {
|
||||||
record_index: record.record_index,
|
record_index: record.record_index,
|
||||||
|
|
@ -423,6 +571,11 @@ fn runtime_packed_event_record_summary_from_smp(
|
||||||
grouped_effect_row_counts: record.grouped_effect_row_counts.clone(),
|
grouped_effect_row_counts: record.grouped_effect_row_counts.clone(),
|
||||||
decoded_actions: record.decoded_actions.clone(),
|
decoded_actions: record.decoded_actions.clone(),
|
||||||
executable_import_ready: record.executable_import_ready,
|
executable_import_ready: record.executable_import_ready,
|
||||||
|
import_outcome: Some(determine_packed_event_import_outcome(
|
||||||
|
record,
|
||||||
|
known_company_ids,
|
||||||
|
imported,
|
||||||
|
)),
|
||||||
notes: record.notes.clone(),
|
notes: record.notes.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -442,13 +595,18 @@ fn smp_packed_record_to_runtime_event_record(
|
||||||
record: &SmpLoadedPackedEventRecordSummary,
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
known_company_ids: &BTreeSet<u32>,
|
known_company_ids: &BTreeSet<u32>,
|
||||||
) -> Option<Result<RuntimeEventRecord, String>> {
|
) -> Option<Result<RuntimeEventRecord, String>> {
|
||||||
if !record.executable_import_ready {
|
if record.decode_status == "unsupported_framing" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
let effects =
|
||||||
smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids)
|
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids) {
|
||||||
.and_then(|effects| {
|
Ok(effects) => effects,
|
||||||
|
Err(err) if err.contains("unresolved company ids") => return None,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((|| {
|
||||||
let trigger_kind = record.trigger_kind.ok_or_else(|| {
|
let trigger_kind = record.trigger_kind.ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"packed event record {} is missing trigger_kind",
|
"packed event record {} is missing trigger_kind",
|
||||||
|
|
@ -482,8 +640,7 @@ fn smp_packed_record_to_runtime_event_record(
|
||||||
effects,
|
effects,
|
||||||
}
|
}
|
||||||
.into_runtime_record())
|
.into_runtime_record())
|
||||||
}),
|
})())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn smp_runtime_effects_to_runtime_effects(
|
fn smp_runtime_effects_to_runtime_effects(
|
||||||
|
|
@ -588,6 +745,54 @@ fn company_target_supported_for_import(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn determine_packed_event_import_outcome(
|
||||||
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
imported: bool,
|
||||||
|
) -> String {
|
||||||
|
if imported {
|
||||||
|
return "imported".to_string();
|
||||||
|
}
|
||||||
|
if record.decode_status == "unsupported_framing" {
|
||||||
|
return "blocked_unsupported_decode".to_string();
|
||||||
|
}
|
||||||
|
if packed_record_requires_missing_company_context(record, known_company_ids) {
|
||||||
|
return "blocked_missing_company_context".to_string();
|
||||||
|
}
|
||||||
|
"blocked_unsupported_decode".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packed_record_requires_missing_company_context(
|
||||||
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> bool {
|
||||||
|
record
|
||||||
|
.decoded_actions
|
||||||
|
.iter()
|
||||||
|
.any(|effect| runtime_effect_requires_missing_company_context(effect, known_company_ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_effect_requires_missing_company_context(
|
||||||
|
effect: &RuntimeEffect,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> bool {
|
||||||
|
match effect {
|
||||||
|
RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||||
|
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||||
|
!company_target_supported_for_import(target, known_company_ids)
|
||||||
|
}
|
||||||
|
RuntimeEffect::AppendEventRecord { record } => record.effects.iter().any(|nested| {
|
||||||
|
runtime_effect_requires_missing_company_context(nested, known_company_ids)
|
||||||
|
}),
|
||||||
|
RuntimeEffect::SetWorldFlag { .. }
|
||||||
|
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||||
|
| RuntimeEffect::SetSpecialCondition { .. }
|
||||||
|
| RuntimeEffect::ActivateEventRecord { .. }
|
||||||
|
| RuntimeEffect::DeactivateEventRecord { .. }
|
||||||
|
| RuntimeEffect::RemoveEventRecord { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate_runtime_state_dump_document(
|
pub fn validate_runtime_state_dump_document(
|
||||||
document: &RuntimeStateDumpDocument,
|
document: &RuntimeStateDumpDocument,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
|
@ -655,6 +860,42 @@ pub fn validate_runtime_save_slice_document(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate_runtime_overlay_import_document(
|
||||||
|
document: &RuntimeOverlayImportDocument,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if document.format_version != OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION {
|
||||||
|
return Err(format!(
|
||||||
|
"unsupported overlay import document format_version {} (expected {})",
|
||||||
|
document.format_version, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if document.import_id.trim().is_empty() {
|
||||||
|
return Err("import_id must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if document
|
||||||
|
.source
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|text| text.trim().is_empty())
|
||||||
|
{
|
||||||
|
return Err("overlay import source.description must not be empty".to_string());
|
||||||
|
}
|
||||||
|
for (index, note) in document.source.notes.iter().enumerate() {
|
||||||
|
if note.trim().is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"overlay import source.notes[{index}] must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if document.base_snapshot_path.trim().is_empty() {
|
||||||
|
return Err("base_snapshot_path must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if document.save_slice_path.trim().is_empty() {
|
||||||
|
return Err("save_slice_path must not be empty".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_runtime_save_slice_document(
|
pub fn load_runtime_save_slice_document(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
|
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
|
||||||
|
|
@ -663,6 +904,14 @@ pub fn load_runtime_save_slice_document(
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_runtime_overlay_import_document(
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<RuntimeOverlayImportDocument, Box<dyn std::error::Error>> {
|
||||||
|
let text = std::fs::read_to_string(path)?;
|
||||||
|
let document: RuntimeOverlayImportDocument = serde_json::from_str(&text)?;
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_runtime_save_slice_document(
|
pub fn save_runtime_save_slice_document(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
document: &RuntimeSaveSliceDocument,
|
document: &RuntimeSaveSliceDocument,
|
||||||
|
|
@ -677,21 +926,44 @@ pub fn save_runtime_save_slice_document(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_runtime_overlay_import_document(
|
||||||
|
path: &Path,
|
||||||
|
document: &RuntimeOverlayImportDocument,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
validate_runtime_overlay_import_document(document)
|
||||||
|
.map_err(|err| format!("invalid runtime overlay import document: {err}"))?;
|
||||||
|
let bytes = serde_json::to_vec_pretty(document)?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, bytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_runtime_state_import(
|
pub fn load_runtime_state_import(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
||||||
let text = std::fs::read_to_string(path)?;
|
let text = std::fs::read_to_string(path)?;
|
||||||
load_runtime_state_import_from_str(
|
load_runtime_state_import_from_str_with_base(
|
||||||
&text,
|
&text,
|
||||||
path.file_stem()
|
path.file_stem()
|
||||||
.and_then(|stem| stem.to_str())
|
.and_then(|stem| stem.to_str())
|
||||||
.unwrap_or("runtime-state"),
|
.unwrap_or("runtime-state"),
|
||||||
|
path.parent().unwrap_or_else(|| Path::new(".")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_runtime_state_import_from_str(
|
pub fn load_runtime_state_import_from_str(
|
||||||
text: &str,
|
text: &str,
|
||||||
fallback_id: &str,
|
fallback_id: &str,
|
||||||
|
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
||||||
|
load_runtime_state_import_from_str_with_base(text, fallback_id, Path::new("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_runtime_state_import_from_str_with_base(
|
||||||
|
text: &str,
|
||||||
|
fallback_id: &str,
|
||||||
|
base_dir: &Path,
|
||||||
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
||||||
if let Ok(document) = serde_json::from_str::<RuntimeStateDumpDocument>(text) {
|
if let Ok(document) = serde_json::from_str::<RuntimeStateDumpDocument>(text) {
|
||||||
validate_runtime_state_dump_document(&document)
|
validate_runtime_state_dump_document(&document)
|
||||||
|
|
@ -725,6 +997,51 @@ pub fn load_runtime_state_import_from_str(
|
||||||
return Ok(import);
|
return Ok(import);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(document) = serde_json::from_str::<RuntimeOverlayImportDocument>(text) {
|
||||||
|
validate_runtime_overlay_import_document(&document)
|
||||||
|
.map_err(|err| format!("invalid runtime overlay import document: {err}"))?;
|
||||||
|
let base_snapshot_path = resolve_document_path(base_dir, &document.base_snapshot_path);
|
||||||
|
let save_slice_path = resolve_document_path(base_dir, &document.save_slice_path);
|
||||||
|
|
||||||
|
let snapshot = load_runtime_snapshot_document(&base_snapshot_path)?;
|
||||||
|
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"invalid runtime snapshot {}: {err}",
|
||||||
|
base_snapshot_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let save_slice_document = load_runtime_save_slice_document(&save_slice_path)?;
|
||||||
|
validate_runtime_save_slice_document(&save_slice_document).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"invalid runtime save slice document {}: {err}",
|
||||||
|
save_slice_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut description_parts = Vec::new();
|
||||||
|
if let Some(description) = document.source.description {
|
||||||
|
description_parts.push(description);
|
||||||
|
}
|
||||||
|
if let Some(description) = snapshot.source.description {
|
||||||
|
description_parts.push(format!("base snapshot {description}"));
|
||||||
|
}
|
||||||
|
if let Some(description) = save_slice_document.source.description {
|
||||||
|
description_parts.push(format!("save slice {description}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return project_save_slice_overlay_to_runtime_state_import(
|
||||||
|
&snapshot.state,
|
||||||
|
&save_slice_document.save_slice,
|
||||||
|
&document.import_id,
|
||||||
|
if description_parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(description_parts.join(" | "))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(Into::into);
|
||||||
|
}
|
||||||
|
|
||||||
let state: RuntimeState = serde_json::from_str(text)?;
|
let state: RuntimeState = serde_json::from_str(text)?;
|
||||||
state
|
state
|
||||||
.validate()
|
.validate()
|
||||||
|
|
@ -736,6 +1053,15 @@ pub fn load_runtime_state_import_from_str(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf {
|
||||||
|
let candidate = PathBuf::from(path);
|
||||||
|
if candidate.is_absolute() {
|
||||||
|
candidate
|
||||||
|
} else {
|
||||||
|
base_dir.join(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -1408,5 +1734,278 @@ mod tests {
|
||||||
.map(|summary| summary.imported_runtime_record_count),
|
.map(|summary| summary.imported_runtime_record_count),
|
||||||
Some(0)
|
Some(0)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||||
|
Some("blocked_missing_company_context")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlays_save_slice_events_onto_base_company_context() {
|
||||||
|
let base_state = RuntimeState {
|
||||||
|
calendar: CalendarPoint {
|
||||||
|
year: 1845,
|
||||||
|
month_slot: 2,
|
||||||
|
phase_slot: 1,
|
||||||
|
tick_slot: 3,
|
||||||
|
},
|
||||||
|
world_flags: BTreeMap::from([("base.only".to_string(), true)]),
|
||||||
|
save_profile: RuntimeSaveProfileState::default(),
|
||||||
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
|
metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]),
|
||||||
|
companies: vec![crate::RuntimeCompany {
|
||||||
|
company_id: 42,
|
||||||
|
current_cash: 500,
|
||||||
|
debt: 20,
|
||||||
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
|
event_runtime_records: vec![RuntimeEventRecord {
|
||||||
|
record_id: 1,
|
||||||
|
trigger_kind: 1,
|
||||||
|
active: true,
|
||||||
|
service_count: 0,
|
||||||
|
marks_collection_dirty: false,
|
||||||
|
one_shot: false,
|
||||||
|
has_fired: false,
|
||||||
|
effects: vec![],
|
||||||
|
}],
|
||||||
|
candidate_availability: BTreeMap::new(),
|
||||||
|
special_conditions: BTreeMap::new(),
|
||||||
|
service_state: RuntimeServiceState {
|
||||||
|
periodic_boundary_calls: 9,
|
||||||
|
trigger_dispatch_counts: BTreeMap::new(),
|
||||||
|
total_event_record_services: 4,
|
||||||
|
dirty_rerun_count: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let 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(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
trailer_family: None,
|
||||||
|
bridge_family: None,
|
||||||
|
profile: None,
|
||||||
|
candidate_availability_table: None,
|
||||||
|
special_conditions_table: None,
|
||||||
|
event_runtime_collection: Some(crate::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: 42,
|
||||||
|
live_record_count: 1,
|
||||||
|
live_entry_ids: vec![7],
|
||||||
|
decoded_record_count: 1,
|
||||||
|
imported_runtime_record_count: 0,
|
||||||
|
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 7,
|
||||||
|
payload_offset: Some(0x7202),
|
||||||
|
payload_len: Some(48),
|
||||||
|
decode_status: "parity_only".to_string(),
|
||||||
|
trigger_kind: Some(7),
|
||||||
|
active: Some(true),
|
||||||
|
marks_collection_dirty: Some(false),
|
||||||
|
one_shot: Some(false),
|
||||||
|
text_bands: packed_text_bands(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
|
||||||
|
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
|
||||||
|
delta: 50,
|
||||||
|
}],
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["needs company context".to_string()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
notes: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut import = project_save_slice_overlay_to_runtime_state_import(
|
||||||
|
&base_state,
|
||||||
|
&save_slice,
|
||||||
|
"overlay-smoke",
|
||||||
|
Some("overlay test".to_string()),
|
||||||
|
)
|
||||||
|
.expect("overlay import should project");
|
||||||
|
|
||||||
|
assert_eq!(import.state.calendar, base_state.calendar);
|
||||||
|
assert_eq!(import.state.companies, base_state.companies);
|
||||||
|
assert_eq!(import.state.service_state, base_state.service_state);
|
||||||
|
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.imported_runtime_record_count),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||||
|
Some("imported")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.metadata
|
||||||
|
.get("save_slice.import_projection")
|
||||||
|
.map(String::as_str),
|
||||||
|
Some("overlay-runtime-restore-v1")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import.state.metadata.get("base.note").map(String::as_str),
|
||||||
|
Some("kept")
|
||||||
|
);
|
||||||
|
assert_eq!(import.state.world_flags.get("base.only"), Some(&true));
|
||||||
|
|
||||||
|
execute_step_command(
|
||||||
|
&mut import.state,
|
||||||
|
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||||
|
)
|
||||||
|
.expect("overlay-imported company-targeted record should run");
|
||||||
|
|
||||||
|
assert_eq!(import.state.companies[0].current_cash, 550);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_overlay_import_document_with_relative_paths() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("system time should be after epoch")
|
||||||
|
.as_nanos();
|
||||||
|
let fixture_dir = std::env::temp_dir().join(format!("rrt-overlay-import-{nonce}"));
|
||||||
|
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
|
||||||
|
|
||||||
|
let snapshot_path = fixture_dir.join("base.json");
|
||||||
|
let save_slice_path = fixture_dir.join("slice.json");
|
||||||
|
let overlay_path = fixture_dir.join("overlay.json");
|
||||||
|
|
||||||
|
let snapshot = crate::RuntimeSnapshotDocument {
|
||||||
|
format_version: crate::SNAPSHOT_FORMAT_VERSION,
|
||||||
|
snapshot_id: "base".to_string(),
|
||||||
|
source: crate::RuntimeSnapshotSource {
|
||||||
|
source_fixture_id: None,
|
||||||
|
description: Some("base snapshot".to_string()),
|
||||||
|
},
|
||||||
|
state: RuntimeState {
|
||||||
|
calendar: CalendarPoint {
|
||||||
|
year: 1835,
|
||||||
|
month_slot: 1,
|
||||||
|
phase_slot: 2,
|
||||||
|
tick_slot: 4,
|
||||||
|
},
|
||||||
|
world_flags: BTreeMap::new(),
|
||||||
|
save_profile: RuntimeSaveProfileState::default(),
|
||||||
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
|
metadata: BTreeMap::new(),
|
||||||
|
companies: vec![crate::RuntimeCompany {
|
||||||
|
company_id: 42,
|
||||||
|
current_cash: 100,
|
||||||
|
debt: 0,
|
||||||
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
|
event_runtime_records: Vec::new(),
|
||||||
|
candidate_availability: BTreeMap::new(),
|
||||||
|
special_conditions: BTreeMap::new(),
|
||||||
|
service_state: RuntimeServiceState::default(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
crate::save_runtime_snapshot_document(&snapshot_path, &snapshot)
|
||||||
|
.expect("snapshot should save");
|
||||||
|
|
||||||
|
let save_slice_document = RuntimeSaveSliceDocument {
|
||||||
|
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
||||||
|
save_slice_id: "slice".to_string(),
|
||||||
|
source: RuntimeSaveSliceDocumentSource::default(),
|
||||||
|
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(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
trailer_family: None,
|
||||||
|
bridge_family: None,
|
||||||
|
profile: None,
|
||||||
|
candidate_availability_table: None,
|
||||||
|
special_conditions_table: None,
|
||||||
|
event_runtime_collection: Some(crate::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: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 7,
|
||||||
|
payload_offset: Some(0x7202),
|
||||||
|
payload_len: Some(48),
|
||||||
|
decode_status: "parity_only".to_string(),
|
||||||
|
trigger_kind: Some(7),
|
||||||
|
active: Some(true),
|
||||||
|
marks_collection_dirty: Some(false),
|
||||||
|
one_shot: Some(false),
|
||||||
|
text_bands: packed_text_bands(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
|
||||||
|
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
|
||||||
|
delta: 50,
|
||||||
|
}],
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["needs company context".to_string()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
notes: vec![],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
save_runtime_save_slice_document(&save_slice_path, &save_slice_document)
|
||||||
|
.expect("save slice should save");
|
||||||
|
|
||||||
|
let overlay = RuntimeOverlayImportDocument {
|
||||||
|
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
|
||||||
|
import_id: "overlay-relative".to_string(),
|
||||||
|
source: RuntimeOverlayImportDocumentSource {
|
||||||
|
description: Some("relative overlay".to_string()),
|
||||||
|
notes: vec![],
|
||||||
|
},
|
||||||
|
base_snapshot_path: "base.json".to_string(),
|
||||||
|
save_slice_path: "slice.json".to_string(),
|
||||||
|
};
|
||||||
|
save_runtime_overlay_import_document(&overlay_path, &overlay)
|
||||||
|
.expect("overlay document should save");
|
||||||
|
|
||||||
|
let import =
|
||||||
|
load_runtime_state_import(&overlay_path).expect("overlay runtime import should load");
|
||||||
|
assert_eq!(import.import_id, "overlay-relative");
|
||||||
|
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||||
|
assert_eq!(import.state.companies[0].company_id, 42);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(snapshot_path);
|
||||||
|
let _ = std::fs::remove_file(save_slice_path);
|
||||||
|
let _ = std::fs::remove_file(overlay_path);
|
||||||
|
let _ = std::fs::remove_dir(fixture_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,14 @@ pub use campaign_exe::{
|
||||||
OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file,
|
OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file,
|
||||||
};
|
};
|
||||||
pub use import::{
|
pub use import::{
|
||||||
RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, RuntimeStateDumpDocument,
|
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
||||||
RuntimeStateDumpSource, RuntimeStateImport, SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
|
||||||
STATE_DUMP_FORMAT_VERSION, load_runtime_save_slice_document, load_runtime_state_import,
|
RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport,
|
||||||
project_save_slice_to_runtime_state_import, save_runtime_save_slice_document,
|
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, STATE_DUMP_FORMAT_VERSION,
|
||||||
|
load_runtime_overlay_import_document, load_runtime_save_slice_document,
|
||||||
|
load_runtime_state_import, project_save_slice_overlay_to_runtime_state_import,
|
||||||
|
project_save_slice_to_runtime_state_import, save_runtime_overlay_import_document,
|
||||||
|
save_runtime_save_slice_document, validate_runtime_overlay_import_document,
|
||||||
validate_runtime_save_slice_document, validate_runtime_state_dump_document,
|
validate_runtime_save_slice_document, validate_runtime_state_dump_document,
|
||||||
};
|
};
|
||||||
pub use persistence::{
|
pub use persistence::{
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ pub struct RuntimePackedEventRecordSummary {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub executable_import_ready: bool,
|
pub executable_import_ready: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub import_outcome: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,14 +335,17 @@ impl RuntimeState {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let executable_import_ready_count = summary
|
let importable_or_imported_count = summary
|
||||||
.records
|
.records
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|record| record.executable_import_ready)
|
.filter(|record| {
|
||||||
|
record.executable_import_ready
|
||||||
|
|| record.import_outcome.as_deref() == Some("imported")
|
||||||
|
})
|
||||||
.count();
|
.count();
|
||||||
if summary.imported_runtime_record_count > executable_import_ready_count {
|
if summary.imported_runtime_record_count > importable_or_imported_count {
|
||||||
return Err(
|
return Err(
|
||||||
"packed_event_collection.imported_runtime_record_count must not exceed executable-import-ready records"
|
"packed_event_collection.imported_runtime_record_count must not exceed importable or imported records"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +387,15 @@ impl RuntimeState {
|
||||||
"packed_event_collection.records[{record_index}].decode_status must not be empty"
|
"packed_event_collection.records[{record_index}].decode_status must not be empty"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if record
|
||||||
|
.import_outcome
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|value| value.trim().is_empty())
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].import_outcome must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
if record.grouped_effect_row_counts.len() != 4 {
|
if record.grouped_effect_row_counts.len() != 4 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries"
|
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries"
|
||||||
|
|
@ -767,6 +781,7 @@ mod tests {
|
||||||
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
decoded_actions: Vec::new(),
|
decoded_actions: Vec::new(),
|
||||||
executable_import_ready: false,
|
executable_import_ready: false,
|
||||||
|
import_outcome: None,
|
||||||
notes: vec!["test".to_string()],
|
notes: vec!["test".to_string()],
|
||||||
},
|
},
|
||||||
RuntimePackedEventRecordSummary {
|
RuntimePackedEventRecordSummary {
|
||||||
|
|
@ -784,6 +799,7 @@ mod tests {
|
||||||
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
decoded_actions: Vec::new(),
|
decoded_actions: Vec::new(),
|
||||||
executable_import_ready: false,
|
executable_import_ready: false,
|
||||||
|
import_outcome: None,
|
||||||
notes: vec!["test".to_string()],
|
notes: vec!["test".to_string()],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ pub struct RuntimeSummary {
|
||||||
pub packed_event_imported_runtime_record_count: usize,
|
pub packed_event_imported_runtime_record_count: usize,
|
||||||
pub packed_event_parity_only_record_count: usize,
|
pub packed_event_parity_only_record_count: usize,
|
||||||
pub packed_event_unsupported_record_count: usize,
|
pub packed_event_unsupported_record_count: usize,
|
||||||
|
pub packed_event_blocked_missing_company_context_count: usize,
|
||||||
pub event_runtime_record_count: usize,
|
pub event_runtime_record_count: usize,
|
||||||
pub candidate_availability_count: usize,
|
pub candidate_availability_count: usize,
|
||||||
pub zero_candidate_availability_count: usize,
|
pub zero_candidate_availability_count: usize,
|
||||||
|
|
@ -153,6 +154,20 @@ impl RuntimeSummary {
|
||||||
.count()
|
.count()
|
||||||
})
|
})
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
|
packed_event_blocked_missing_company_context_count: state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| {
|
||||||
|
summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| {
|
||||||
|
record.import_outcome.as_deref()
|
||||||
|
== Some("blocked_missing_company_context")
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0),
|
||||||
event_runtime_record_count: state.event_runtime_records.len(),
|
event_runtime_record_count: state.event_runtime_records.len(),
|
||||||
candidate_availability_count: state.candidate_availability.len(),
|
candidate_availability_count: state.candidate_availability.len(),
|
||||||
zero_candidate_availability_count: state
|
zero_candidate_availability_count: state
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,18 @@ Current local tool status:
|
||||||
The atlas milestone is broad enough that the next implementation focus has already shifted downward
|
The atlas milestone is broad enough that the next implementation focus has already shifted downward
|
||||||
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
||||||
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
||||||
state-diff tooling, tracked save-slice documents for captured-runtime inputs, and a packed-event
|
state-diff tooling, tracked save-slice documents for captured-runtime inputs, overlay import
|
||||||
persistence bridge that now reaches per-record summaries and selective executable import.
|
documents that combine captured snapshots with save-derived state, and a packed-event persistence
|
||||||
|
bridge that now reaches per-record summaries and selective executable import.
|
||||||
|
|
||||||
The highest-value next passes are now:
|
The highest-value next passes are now:
|
||||||
|
|
||||||
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
|
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
|
||||||
avoid shell-first implementation bets
|
avoid shell-first implementation bets
|
||||||
|
- use captured-context overlay imports whenever save-derived packed rows need live runtime context
|
||||||
|
that the save slice does not actually persist
|
||||||
- widen packed-event target-family coverage only where static evidence is strong enough to support
|
- widen packed-event target-family coverage only where static evidence is strong enough to support
|
||||||
deterministic executable import
|
deterministic executable import after the necessary runtime context is present
|
||||||
- add the next imported object/context families needed to turn current parity-only packed rows into
|
|
||||||
executable runtime records
|
|
||||||
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
|
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
|
||||||
environment
|
environment
|
||||||
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice
|
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ Implemented today:
|
||||||
- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the
|
- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the
|
||||||
CLI and fixture layers
|
CLI and fixture layers
|
||||||
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
||||||
service, snapshot-backed inputs, save-slice-backed inputs, normalized state-fragment assertions,
|
service, snapshot-backed inputs, save-slice-backed inputs, overlay-import-backed inputs,
|
||||||
and imported packed-event execution
|
normalized state-fragment assertions, and imported packed-event execution
|
||||||
|
|
||||||
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
|
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
|
||||||
wider packed-event target-family coverage plus company-collection import depth, not another
|
captured-context overlay import for company-targeted packed events, not another persistence
|
||||||
persistence scaffold pass.
|
scaffold pass.
|
||||||
|
|
||||||
## Why This Boundary
|
## Why This Boundary
|
||||||
|
|
||||||
|
|
@ -366,30 +366,32 @@ Checked-in fixture families already include:
|
||||||
|
|
||||||
## Next Slice
|
## Next Slice
|
||||||
|
|
||||||
The recommended next implementation slice is wider packed-event target-family coverage on top of the
|
The recommended next implementation slice is captured-context overlay import on top of the
|
||||||
captured save-slice workflow that now exists today.
|
save-slice and snapshot workflows that already exist today.
|
||||||
|
|
||||||
Target behavior:
|
Target behavior:
|
||||||
|
|
||||||
- expand the executable import subset beyond the current direct-state and follow-on lanes only when
|
- preserve save slices as partial state rather than pretending they reconstruct full live company
|
||||||
target resolution and field semantics are statically grounded enough to preserve headless
|
state
|
||||||
determinism
|
- overlay save-derived packed-event state onto a captured runtime snapshot that already has the
|
||||||
- add the next imported object families needed to make currently parity-only packed rows executable,
|
needed company roster and other live context
|
||||||
starting with company-targeted effects
|
- upgrade currently blocked company-targeted packed rows when the overlaid base snapshot provides
|
||||||
|
every referenced company id
|
||||||
- keep preserving unsupported packed rows as parity summaries instead of guessing executable meaning
|
- keep preserving unsupported packed rows as parity summaries instead of guessing executable meaning
|
||||||
|
|
||||||
Public-model additions for that slice:
|
Public-model additions for that slice:
|
||||||
|
|
||||||
- wider target-family summaries only where imported execution can be justified by current static
|
- tracked overlay import documents that reference one base snapshot plus one save-slice document
|
||||||
evidence
|
- runtime-side import outcome labels for packed records so blocked-missing-context and
|
||||||
- imported company/runtime context needed by the next packed-event target families
|
blocked-unsupported cases stay explicit
|
||||||
- no shell queue/modal behavior in the runtime core
|
- fixture support for generic runtime-import documents, not just snapshots or save slices
|
||||||
|
|
||||||
Fixture work for that slice:
|
Fixture work for that slice:
|
||||||
|
|
||||||
- save-slice-backed fixtures that prove real packed event records survive import and diff paths
|
- overlay-backed fixtures that prove company-targeted packed rows execute deterministically against
|
||||||
- regression fixtures that lock the current selective executable import boundary and the
|
captured company context
|
||||||
unsupported/parity-only counts
|
- regression fixtures that lock the before/after boundary between save-slice-only imports and
|
||||||
|
overlay-backed imports
|
||||||
- state-fragment assertions that lock both packed parity summaries and imported executable records
|
- state-fragment assertions that lock both packed parity summaries and imported executable records
|
||||||
|
|
||||||
Do not mix this slice with:
|
Do not mix this slice with:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "packed-event-selective-import-overlay-base-snapshot",
|
||||||
|
"source": {
|
||||||
|
"description": "Base runtime snapshot supplying company context for selective packed-event overlay import."
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1835,
|
||||||
|
"month_slot": 1,
|
||||||
|
"phase_slot": 2,
|
||||||
|
"tick_slot": 4
|
||||||
|
},
|
||||||
|
"world_flags": {
|
||||||
|
"base.only": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"base.note": "preserve base runtime context"
|
||||||
|
},
|
||||||
|
"companies": [
|
||||||
|
{
|
||||||
|
"company_id": 42,
|
||||||
|
"current_cash": 100,
|
||||||
|
"debt": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event_runtime_records": [],
|
||||||
|
"candidate_availability": {},
|
||||||
|
"special_conditions": {},
|
||||||
|
"service_state": {
|
||||||
|
"periodic_boundary_calls": 0,
|
||||||
|
"trigger_dispatch_counts": {},
|
||||||
|
"total_event_record_services": 0,
|
||||||
|
"dirty_rerun_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"fixture_id": "packed-event-selective-import-overlay-fixture",
|
||||||
|
"source": {
|
||||||
|
"kind": "captured-runtime",
|
||||||
|
"description": "Fixture backed by an overlay import document so save-derived packed events execute against captured company context."
|
||||||
|
},
|
||||||
|
"state_import_path": "packed-event-selective-import-overlay.json",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"kind": "service_trigger_kind",
|
||||||
|
"trigger_kind": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expected_summary": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1835,
|
||||||
|
"month_slot": 1,
|
||||||
|
"phase_slot": 2,
|
||||||
|
"tick_slot": 4
|
||||||
|
},
|
||||||
|
"calendar_projection_source": "base-snapshot-preserved",
|
||||||
|
"calendar_projection_is_placeholder": false,
|
||||||
|
"world_flag_count": 7,
|
||||||
|
"company_count": 1,
|
||||||
|
"packed_event_collection_present": true,
|
||||||
|
"packed_event_record_count": 2,
|
||||||
|
"packed_event_decoded_record_count": 2,
|
||||||
|
"packed_event_imported_runtime_record_count": 2,
|
||||||
|
"packed_event_parity_only_record_count": 1,
|
||||||
|
"packed_event_unsupported_record_count": 0,
|
||||||
|
"packed_event_blocked_missing_company_context_count": 0,
|
||||||
|
"event_runtime_record_count": 3,
|
||||||
|
"special_condition_count": 1,
|
||||||
|
"enabled_special_condition_count": 1,
|
||||||
|
"total_event_record_service_count": 3,
|
||||||
|
"total_trigger_dispatch_count": 2,
|
||||||
|
"dirty_rerun_count": 1,
|
||||||
|
"total_company_cash": 150
|
||||||
|
},
|
||||||
|
"expected_state_fragment": {
|
||||||
|
"world_flags": {
|
||||||
|
"base.only": true,
|
||||||
|
"from_packed_root": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"base.note": "preserve base runtime context",
|
||||||
|
"save_slice.import_projection": "overlay-runtime-restore-v1"
|
||||||
|
},
|
||||||
|
"companies": [
|
||||||
|
{
|
||||||
|
"company_id": 42,
|
||||||
|
"current_cash": 150,
|
||||||
|
"debt": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"special_conditions": {
|
||||||
|
"Imported Follow-On": 1
|
||||||
|
},
|
||||||
|
"packed_event_collection": {
|
||||||
|
"live_entry_ids": [7, 9],
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"decode_status": "executable",
|
||||||
|
"import_outcome": "imported"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"decode_status": "parity_only",
|
||||||
|
"import_outcome": "imported"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event_runtime_records": [
|
||||||
|
{
|
||||||
|
"record_id": 7,
|
||||||
|
"service_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_id": 9,
|
||||||
|
"service_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_id": 99,
|
||||||
|
"service_count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service_state": {
|
||||||
|
"dirty_rerun_count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
fixtures/runtime/packed-event-selective-import-overlay.json
Normal file
12
fixtures/runtime/packed-event-selective-import-overlay.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"import_id": "packed-event-selective-import-overlay",
|
||||||
|
"source": {
|
||||||
|
"description": "Overlay import that combines a captured base snapshot with the selective packed-event save slice.",
|
||||||
|
"notes": [
|
||||||
|
"used to upgrade explicit company-targeted packed rows from blocked parity-only state into executable runtime records"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"base_snapshot_path": "packed-event-selective-import-overlay-base-snapshot.json",
|
||||||
|
"save_slice_path": "packed-event-selective-import-save-slice.json"
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"packed_event_imported_runtime_record_count": 1,
|
"packed_event_imported_runtime_record_count": 1,
|
||||||
"packed_event_parity_only_record_count": 1,
|
"packed_event_parity_only_record_count": 1,
|
||||||
"packed_event_unsupported_record_count": 0,
|
"packed_event_unsupported_record_count": 0,
|
||||||
|
"packed_event_blocked_missing_company_context_count": 1,
|
||||||
"event_runtime_record_count": 2,
|
"event_runtime_record_count": 2,
|
||||||
"special_condition_count": 1,
|
"special_condition_count": 1,
|
||||||
"enabled_special_condition_count": 1,
|
"enabled_special_condition_count": 1,
|
||||||
|
|
@ -40,11 +41,13 @@
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
"decode_status": "executable",
|
"decode_status": "executable",
|
||||||
"executable_import_ready": true
|
"executable_import_ready": true,
|
||||||
|
"import_outcome": "imported"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"decode_status": "parity_only",
|
"decode_status": "parity_only",
|
||||||
"executable_import_ready": false
|
"executable_import_ready": false,
|
||||||
|
"import_outcome": "blocked_missing_company_context"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue