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::{
|
||||
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,
|
||||
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
|
||||
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
|
||||
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
|
||||
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
|
||||
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
|
||||
save_runtime_save_slice_document, save_runtime_snapshot_document,
|
||||
validate_runtime_snapshot_document,
|
||||
save_runtime_overlay_import_document, save_runtime_save_slice_document,
|
||||
save_runtime_snapshot_document, validate_runtime_snapshot_document,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
|
@ -128,6 +129,11 @@ enum Command {
|
|||
smp_path: PathBuf,
|
||||
output_path: PathBuf,
|
||||
},
|
||||
RuntimeExportOverlayImport {
|
||||
snapshot_path: PathBuf,
|
||||
save_slice_path: PathBuf,
|
||||
output_path: PathBuf,
|
||||
},
|
||||
RuntimeInspectPk4 {
|
||||
pk4_path: PathBuf,
|
||||
},
|
||||
|
|
@ -250,6 +256,14 @@ struct RuntimeSaveSliceExportOutput {
|
|||
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)]
|
||||
struct RuntimePk4InspectionOutput {
|
||||
path: String,
|
||||
|
|
@ -789,6 +803,13 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} => {
|
||||
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 } => {
|
||||
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),
|
||||
})
|
||||
}
|
||||
[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" => {
|
||||
Ok(Command::RuntimeInspectPk4 {
|
||||
pk4_path: PathBuf::from(path),
|
||||
|
|
@ -1096,7 +1126,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
|||
})
|
||||
}
|
||||
_ => Err(
|
||||
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime 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(),
|
||||
),
|
||||
}
|
||||
|
|
@ -1215,13 +1245,21 @@ fn run_runtime_export_fixture_state(
|
|||
}
|
||||
|
||||
fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let snapshot = load_runtime_snapshot_document(snapshot_path)?;
|
||||
validate_runtime_snapshot_document(&snapshot)
|
||||
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
||||
let summary = snapshot.summary();
|
||||
if let Ok(snapshot) = load_runtime_snapshot_document(snapshot_path) {
|
||||
validate_runtime_snapshot_document(&snapshot)
|
||||
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
|
||||
let report = RuntimeStateSummaryReport {
|
||||
snapshot_id: snapshot.snapshot_id.clone(),
|
||||
summary: snapshot.summary(),
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let import = load_runtime_state_import(snapshot_path)?;
|
||||
let report = RuntimeStateSummaryReport {
|
||||
snapshot_id: snapshot.snapshot_id,
|
||||
summary,
|
||||
snapshot_id: import.import_id,
|
||||
summary: RuntimeSummary::from_state(&import.state),
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
Ok(())
|
||||
|
|
@ -1363,6 +1401,17 @@ fn run_runtime_export_save_slice(
|
|||
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(
|
||||
smp_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>> {
|
||||
let report = RuntimePk4InspectionOutput {
|
||||
path: pk4_path.display().to_string(),
|
||||
|
|
@ -4349,11 +4431,15 @@ mod tests {
|
|||
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
|
||||
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
|
||||
let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json");
|
||||
|
||||
run_runtime_summarize_fixture(&parity_fixture)
|
||||
.expect("save-slice-backed parity fixture should summarize");
|
||||
run_runtime_summarize_fixture(&selective_fixture)
|
||||
.expect("save-slice-backed selective-import fixture should summarize");
|
||||
run_runtime_summarize_fixture(&overlay_fixture)
|
||||
.expect("overlay-backed selective-import fixture should summarize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -4396,6 +4482,35 @@ mod tests {
|
|||
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]
|
||||
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
|
||||
let left = serde_json::json!({
|
||||
|
|
@ -4536,6 +4651,27 @@ mod tests {
|
|||
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]
|
||||
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
|
||||
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
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,
|
||||
validate_runtime_snapshot_document,
|
||||
};
|
||||
|
|
@ -34,10 +34,11 @@ fn resolve_raw_fixture_document(
|
|||
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
|
||||
let specified_state_inputs = usize::from(raw.state.is_some())
|
||||
+ usize::from(raw.state_snapshot_path.is_some())
|
||||
+ usize::from(raw.state_save_slice_path.is_some());
|
||||
+ usize::from(raw.state_save_slice_path.is_some())
|
||||
+ usize::from(raw.state_import_path.is_some());
|
||||
if specified_state_inputs != 1 {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -46,9 +47,10 @@ fn resolve_raw_fixture_document(
|
|||
&raw.state,
|
||||
&raw.state_snapshot_path,
|
||||
&raw.state_save_slice_path,
|
||||
&raw.state_import_path,
|
||||
) {
|
||||
(Some(state), None, None) => state.clone(),
|
||||
(None, Some(snapshot_path), None) => {
|
||||
(Some(state), None, None, None) => state.clone(),
|
||||
(None, Some(snapshot_path), None, None) => {
|
||||
let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
|
||||
let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
|
||||
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
|
||||
|
|
@ -59,7 +61,7 @@ fn resolve_raw_fixture_document(
|
|||
})?;
|
||||
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 document = load_runtime_save_slice_document(&save_slice_path)?;
|
||||
validate_runtime_save_slice_document(&document).map_err(|err| {
|
||||
|
|
@ -81,12 +83,28 @@ fn resolve_raw_fixture_document(
|
|||
})?
|
||||
.state
|
||||
}
|
||||
(None, None, None, Some(import_path)) => {
|
||||
let import_path = resolve_snapshot_path(base_dir, import_path);
|
||||
load_runtime_state_import(&import_path)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to load runtime import {}: {err}",
|
||||
import_path.display()
|
||||
)
|
||||
})?
|
||||
.state
|
||||
}
|
||||
_ => unreachable!("state input exclusivity checked above"),
|
||||
};
|
||||
|
||||
let state_origin = match (raw.state_snapshot_path, raw.state_save_slice_path) {
|
||||
(Some(snapshot_path), None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
|
||||
(None, Some(save_slice_path)) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
|
||||
let state_origin = match (
|
||||
raw.state_snapshot_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,
|
||||
};
|
||||
|
||||
|
|
@ -116,11 +134,13 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::FixtureStateOrigin;
|
||||
use rrt_runtime::{
|
||||
CalendarPoint, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
|
||||
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
||||
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
|
||||
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
|
||||
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState,
|
||||
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;
|
||||
|
||||
|
|
@ -275,4 +295,150 @@ mod tests {
|
|||
let _ = std::fs::remove_file(save_slice_path);
|
||||
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)]
|
||||
pub packed_event_unsupported_record_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub packed_event_blocked_missing_company_context_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub event_runtime_record_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
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 actual.event_runtime_record_count != count {
|
||||
mismatches.push(format!(
|
||||
|
|
@ -531,6 +541,7 @@ pub enum FixtureStateOrigin {
|
|||
Inline,
|
||||
SnapshotPath(String),
|
||||
SaveSlicePath(String),
|
||||
ImportPath(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -546,6 +557,8 @@ pub struct RawFixtureDocument {
|
|||
#[serde(default)]
|
||||
pub state_save_slice_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub state_import_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub commands: Vec<StepCommand>,
|
||||
#[serde(default)]
|
||||
pub expected_summary: ExpectedRuntimeSummary,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||
|
|
@ -13,6 +14,7 @@ use crate::{
|
|||
|
||||
pub const STATE_DUMP_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)]
|
||||
pub struct RuntimeStateDumpSource {
|
||||
|
|
@ -52,6 +54,24 @@ pub struct RuntimeSaveSliceDocument {
|
|||
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)]
|
||||
pub struct RuntimeStateImport {
|
||||
pub import_id: String,
|
||||
|
|
@ -59,6 +79,24 @@ pub struct RuntimeStateImport {
|
|||
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(
|
||||
save_slice: &SmpLoadedSaveSlice,
|
||||
import_id: &str,
|
||||
|
|
@ -67,7 +105,96 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
if import_id.trim().is_empty() {
|
||||
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();
|
||||
world_flags.insert(
|
||||
"save_slice.profile_present".to_string(),
|
||||
|
|
@ -107,11 +234,19 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
let mut metadata = BTreeMap::new();
|
||||
metadata.insert(
|
||||
"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(
|
||||
"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(
|
||||
"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 {
|
||||
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
||||
}
|
||||
let known_company_ids = BTreeSet::new();
|
||||
let imported_event_runtime_records = save_slice
|
||||
.event_runtime_collection
|
||||
.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,
|
||||
}
|
||||
});
|
||||
|
||||
let (packed_event_collection, event_runtime_records) =
|
||||
project_packed_event_collection(save_slice, known_company_ids)?;
|
||||
if let Some(summary) = &save_slice.event_runtime_collection {
|
||||
metadata.insert(
|
||||
"save_slice.event_runtime_collection_source_kind".to_string(),
|
||||
|
|
@ -220,9 +319,10 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
);
|
||||
metadata.insert(
|
||||
"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 {
|
||||
metadata.insert(
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
||||
let mut special_conditions = BTreeMap::new();
|
||||
if let Some(table) = &save_slice.special_conditions_table {
|
||||
metadata.insert(
|
||||
|
|
@ -374,35 +475,82 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
metadata.insert(format!("save_slice.note.{index}"), note.clone());
|
||||
}
|
||||
|
||||
let state = RuntimeState {
|
||||
calendar: CalendarPoint {
|
||||
year: 1830,
|
||||
month_slot: 0,
|
||||
phase_slot: 0,
|
||||
tick_slot: 0,
|
||||
},
|
||||
Ok(SaveSliceProjection {
|
||||
world_flags,
|
||||
save_profile,
|
||||
world_restore,
|
||||
metadata,
|
||||
companies: Vec::new(),
|
||||
packed_event_collection,
|
||||
event_runtime_records: imported_event_runtime_records,
|
||||
event_runtime_records,
|
||||
candidate_availability,
|
||||
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(
|
||||
record: &SmpLoadedPackedEventRecordSummary,
|
||||
known_company_ids: &BTreeSet<u32>,
|
||||
imported: bool,
|
||||
) -> RuntimePackedEventRecordSummary {
|
||||
RuntimePackedEventRecordSummary {
|
||||
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(),
|
||||
decoded_actions: record.decoded_actions.clone(),
|
||||
executable_import_ready: record.executable_import_ready,
|
||||
import_outcome: Some(determine_packed_event_import_outcome(
|
||||
record,
|
||||
known_company_ids,
|
||||
imported,
|
||||
)),
|
||||
notes: record.notes.clone(),
|
||||
}
|
||||
}
|
||||
|
|
@ -442,48 +595,52 @@ fn smp_packed_record_to_runtime_event_record(
|
|||
record: &SmpLoadedPackedEventRecordSummary,
|
||||
known_company_ids: &BTreeSet<u32>,
|
||||
) -> Option<Result<RuntimeEventRecord, String>> {
|
||||
if !record.executable_import_ready {
|
||||
if record.decode_status == "unsupported_framing" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids)
|
||||
.and_then(|effects| {
|
||||
let trigger_kind = record.trigger_kind.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing trigger_kind",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let active = record.active.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing active flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let marks_collection_dirty = record.marks_collection_dirty.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing dirty flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let one_shot = record.one_shot.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing one_shot flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
Ok(RuntimeEventRecordTemplate {
|
||||
record_id: record.live_entry_id,
|
||||
trigger_kind,
|
||||
active,
|
||||
marks_collection_dirty,
|
||||
one_shot,
|
||||
effects,
|
||||
}
|
||||
.into_runtime_record())
|
||||
}),
|
||||
)
|
||||
let effects =
|
||||
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids) {
|
||||
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(|| {
|
||||
format!(
|
||||
"packed event record {} is missing trigger_kind",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let active = record.active.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing active flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let marks_collection_dirty = record.marks_collection_dirty.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing dirty flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let one_shot = record.one_shot.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing one_shot flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
Ok(RuntimeEventRecordTemplate {
|
||||
record_id: record.live_entry_id,
|
||||
trigger_kind,
|
||||
active,
|
||||
marks_collection_dirty,
|
||||
one_shot,
|
||||
effects,
|
||||
}
|
||||
.into_runtime_record())
|
||||
})())
|
||||
}
|
||||
|
||||
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(
|
||||
document: &RuntimeStateDumpDocument,
|
||||
) -> Result<(), String> {
|
||||
|
|
@ -655,6 +860,42 @@ pub fn validate_runtime_save_slice_document(
|
|||
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(
|
||||
path: &Path,
|
||||
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
|
||||
|
|
@ -663,6 +904,14 @@ pub fn load_runtime_save_slice_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(
|
||||
path: &Path,
|
||||
document: &RuntimeSaveSliceDocument,
|
||||
|
|
@ -677,21 +926,44 @@ pub fn save_runtime_save_slice_document(
|
|||
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(
|
||||
path: &Path,
|
||||
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
||||
let text = std::fs::read_to_string(path)?;
|
||||
load_runtime_state_import_from_str(
|
||||
load_runtime_state_import_from_str_with_base(
|
||||
&text,
|
||||
path.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or("runtime-state"),
|
||||
path.parent().unwrap_or_else(|| Path::new(".")),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn load_runtime_state_import_from_str(
|
||||
text: &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>> {
|
||||
if let Ok(document) = serde_json::from_str::<RuntimeStateDumpDocument>(text) {
|
||||
validate_runtime_state_dump_document(&document)
|
||||
|
|
@ -725,6 +997,51 @@ pub fn load_runtime_state_import_from_str(
|
|||
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)?;
|
||||
state
|
||||
.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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1408,5 +1734,278 @@ mod tests {
|
|||
.map(|summary| summary.imported_runtime_record_count),
|
||||
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,
|
||||
};
|
||||
pub use import::{
|
||||
RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, RuntimeStateDumpDocument,
|
||||
RuntimeStateDumpSource, RuntimeStateImport, SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
|
||||
STATE_DUMP_FORMAT_VERSION, load_runtime_save_slice_document, load_runtime_state_import,
|
||||
project_save_slice_to_runtime_state_import, save_runtime_save_slice_document,
|
||||
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
|
||||
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
|
||||
RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport,
|
||||
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,
|
||||
};
|
||||
pub use persistence::{
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ pub struct RuntimePackedEventRecordSummary {
|
|||
#[serde(default)]
|
||||
pub executable_import_ready: bool,
|
||||
#[serde(default)]
|
||||
pub import_outcome: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -333,14 +335,17 @@ impl RuntimeState {
|
|||
.to_string(),
|
||||
);
|
||||
}
|
||||
let executable_import_ready_count = summary
|
||||
let importable_or_imported_count = summary
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| record.executable_import_ready)
|
||||
.filter(|record| {
|
||||
record.executable_import_ready
|
||||
|| record.import_outcome.as_deref() == Some("imported")
|
||||
})
|
||||
.count();
|
||||
if summary.imported_runtime_record_count > executable_import_ready_count {
|
||||
if summary.imported_runtime_record_count > importable_or_imported_count {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -382,6 +387,15 @@ impl RuntimeState {
|
|||
"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 {
|
||||
return Err(format!(
|
||||
"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],
|
||||
decoded_actions: Vec::new(),
|
||||
executable_import_ready: false,
|
||||
import_outcome: None,
|
||||
notes: vec!["test".to_string()],
|
||||
},
|
||||
RuntimePackedEventRecordSummary {
|
||||
|
|
@ -784,6 +799,7 @@ mod tests {
|
|||
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||
decoded_actions: Vec::new(),
|
||||
executable_import_ready: false,
|
||||
import_outcome: None,
|
||||
notes: vec!["test".to_string()],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ pub struct RuntimeSummary {
|
|||
pub packed_event_imported_runtime_record_count: usize,
|
||||
pub packed_event_parity_only_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 candidate_availability_count: usize,
|
||||
pub zero_candidate_availability_count: usize,
|
||||
|
|
@ -153,6 +154,20 @@ impl RuntimeSummary {
|
|||
.count()
|
||||
})
|
||||
.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(),
|
||||
candidate_availability_count: state.candidate_availability.len(),
|
||||
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
|
||||
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
||||
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
|
||||
persistence bridge that now reaches per-record summaries and selective executable import.
|
||||
state-diff tooling, tracked save-slice documents for captured-runtime inputs, overlay 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:
|
||||
|
||||
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
|
||||
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
|
||||
deterministic executable import
|
||||
- add the next imported object/context families needed to turn current parity-only packed rows into
|
||||
executable runtime records
|
||||
deterministic executable import after the necessary runtime context is present
|
||||
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
|
||||
environment
|
||||
- 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
|
||||
CLI and fixture layers
|
||||
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
||||
service, snapshot-backed inputs, save-slice-backed inputs, normalized state-fragment assertions,
|
||||
and imported packed-event execution
|
||||
service, snapshot-backed inputs, save-slice-backed inputs, overlay-import-backed inputs,
|
||||
normalized state-fragment assertions, and imported packed-event execution
|
||||
|
||||
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
|
||||
persistence scaffold pass.
|
||||
captured-context overlay import for company-targeted packed events, not another persistence
|
||||
scaffold pass.
|
||||
|
||||
## Why This Boundary
|
||||
|
||||
|
|
@ -366,30 +366,32 @@ Checked-in fixture families already include:
|
|||
|
||||
## Next Slice
|
||||
|
||||
The recommended next implementation slice is wider packed-event target-family coverage on top of the
|
||||
captured save-slice workflow that now exists today.
|
||||
The recommended next implementation slice is captured-context overlay import on top of the
|
||||
save-slice and snapshot workflows that already exist today.
|
||||
|
||||
Target behavior:
|
||||
|
||||
- expand the executable import subset beyond the current direct-state and follow-on lanes only when
|
||||
target resolution and field semantics are statically grounded enough to preserve headless
|
||||
determinism
|
||||
- add the next imported object families needed to make currently parity-only packed rows executable,
|
||||
starting with company-targeted effects
|
||||
- preserve save slices as partial state rather than pretending they reconstruct full live company
|
||||
state
|
||||
- overlay save-derived packed-event state onto a captured runtime snapshot that already has the
|
||||
needed company roster and other live context
|
||||
- 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
|
||||
|
||||
Public-model additions for that slice:
|
||||
|
||||
- wider target-family summaries only where imported execution can be justified by current static
|
||||
evidence
|
||||
- imported company/runtime context needed by the next packed-event target families
|
||||
- no shell queue/modal behavior in the runtime core
|
||||
- tracked overlay import documents that reference one base snapshot plus one save-slice document
|
||||
- runtime-side import outcome labels for packed records so blocked-missing-context and
|
||||
blocked-unsupported cases stay explicit
|
||||
- fixture support for generic runtime-import documents, not just snapshots or save slices
|
||||
|
||||
Fixture work for that slice:
|
||||
|
||||
- save-slice-backed fixtures that prove real packed event records survive import and diff paths
|
||||
- regression fixtures that lock the current selective executable import boundary and the
|
||||
unsupported/parity-only counts
|
||||
- overlay-backed fixtures that prove company-targeted packed rows execute deterministically against
|
||||
captured company context
|
||||
- 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
|
||||
|
||||
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_parity_only_record_count": 1,
|
||||
"packed_event_unsupported_record_count": 0,
|
||||
"packed_event_blocked_missing_company_context_count": 1,
|
||||
"event_runtime_record_count": 2,
|
||||
"special_condition_count": 1,
|
||||
"enabled_special_condition_count": 1,
|
||||
|
|
@ -40,11 +41,13 @@
|
|||
"records": [
|
||||
{
|
||||
"decode_status": "executable",
|
||||
"executable_import_ready": true
|
||||
"executable_import_ready": true,
|
||||
"import_outcome": "imported"
|
||||
},
|
||||
{
|
||||
"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