Bridge packed event collection through save import
This commit is contained in:
parent
6ebe5fffeb
commit
83f55fa26e
13 changed files with 653 additions and 35 deletions
|
|
@ -4129,6 +4129,87 @@ mod tests {
|
||||||
let _ = fs::remove_file(right_path);
|
let _ = fs::remove_file(right_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diffs_runtime_states_with_packed_event_collection_changes() {
|
||||||
|
let left = serde_json::json!({
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "left-packed-events",
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 1
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 5,
|
||||||
|
"live_record_count": 3,
|
||||||
|
"live_entry_ids": [1, 3, 5]
|
||||||
|
},
|
||||||
|
"event_runtime_records": []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let right = serde_json::json!({
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "right-packed-events",
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 1
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 5,
|
||||||
|
"live_record_count": 2,
|
||||||
|
"live_entry_ids": [1, 5]
|
||||||
|
},
|
||||||
|
"event_runtime_records": []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let left_path = write_temp_json("runtime-diff-packed-events-left", &left);
|
||||||
|
let right_path = write_temp_json("runtime-diff-packed-events-right", &right);
|
||||||
|
|
||||||
|
let left_state =
|
||||||
|
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
|
||||||
|
let right_state =
|
||||||
|
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
|
||||||
|
let differences = diff_json_values(&left_state, &right_state);
|
||||||
|
|
||||||
|
assert!(differences.iter().any(|entry| {
|
||||||
|
entry.path == "$.packed_event_collection.live_record_count"
|
||||||
|
|| entry.path == "$.packed_event_collection.live_entry_ids[1]"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let _ = fs::remove_file(left_path);
|
||||||
|
let _ = fs::remove_file(right_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarizes_snapshot_backed_fixture_with_packed_event_collection() {
|
||||||
|
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../fixtures/runtime/packed-event-collection-from-snapshot.json");
|
||||||
|
|
||||||
|
run_runtime_summarize_fixture(&fixture_path)
|
||||||
|
.expect("snapshot-backed packed-event fixture should summarize");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diffs_classic_profile_samples_across_multiple_files() {
|
fn diffs_classic_profile_samples_across_multiple_files() {
|
||||||
let sample_a = RuntimeClassicProfileSample {
|
let sample_a = RuntimeClassicProfileSample {
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ mod tests {
|
||||||
world_restore: RuntimeWorldRestoreState::default(),
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
metadata: BTreeMap::new(),
|
metadata: BTreeMap::new(),
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use std::path::Path;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
||||||
RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||||
|
|
@ -56,6 +56,10 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
"save_slice.special_conditions_present".to_string(),
|
"save_slice.special_conditions_present".to_string(),
|
||||||
save_slice.special_conditions_table.is_some(),
|
save_slice.special_conditions_table.is_some(),
|
||||||
);
|
);
|
||||||
|
world_flags.insert(
|
||||||
|
"save_slice.event_runtime_collection_present".to_string(),
|
||||||
|
save_slice.event_runtime_collection.is_some(),
|
||||||
|
);
|
||||||
world_flags.insert(
|
world_flags.insert(
|
||||||
"save_slice.mechanism_confidence_grounded".to_string(),
|
"save_slice.mechanism_confidence_grounded".to_string(),
|
||||||
save_slice.mechanism_confidence == "grounded",
|
save_slice.mechanism_confidence == "grounded",
|
||||||
|
|
@ -133,6 +137,33 @@ 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 packed_event_collection = save_slice.event_runtime_collection.as_ref().map(|summary| {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(summary) = &save_slice.event_runtime_collection {
|
||||||
|
metadata.insert(
|
||||||
|
"save_slice.event_runtime_collection_source_kind".to_string(),
|
||||||
|
summary.source_kind.clone(),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"save_slice.event_runtime_collection_version_hex".to_string(),
|
||||||
|
summary.packed_state_version_hex.clone(),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"save_slice.event_runtime_collection_record_count".to_string(),
|
||||||
|
summary.live_record_count.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(),
|
||||||
|
|
@ -296,6 +327,7 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
world_restore,
|
world_restore,
|
||||||
metadata,
|
metadata,
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
|
packed_event_collection,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability,
|
candidate_availability,
|
||||||
special_conditions,
|
special_conditions,
|
||||||
|
|
@ -379,6 +411,7 @@ mod tests {
|
||||||
world_restore: RuntimeWorldRestoreState::default(),
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
metadata: BTreeMap::new(),
|
metadata: BTreeMap::new(),
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
@ -502,6 +535,20 @@ mod tests {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
||||||
|
mechanism_confidence: "mixed".to_string(),
|
||||||
|
container_profile_family: Some("rt3-105-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: 5,
|
||||||
|
live_record_count: 3,
|
||||||
|
live_entry_ids: vec![1, 3, 5],
|
||||||
|
}),
|
||||||
notes: vec!["packed profile recovered".to_string()],
|
notes: vec!["packed profile recovered".to_string()],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -644,5 +691,22 @@ mod tests {
|
||||||
.get("save_slice.profile_byte_0x82_nonzero"),
|
.get("save_slice.profile_byte_0x82_nonzero"),
|
||||||
Some(&true)
|
Some(&true)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.live_record_count),
|
||||||
|
Some(3)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.live_entry_ids.clone()),
|
||||||
|
Some(vec![1, 3, 5])
|
||||||
|
);
|
||||||
|
assert!(import.state.event_runtime_records.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,16 @@ pub use pk4::{
|
||||||
};
|
};
|
||||||
pub use runtime::{
|
pub use runtime::{
|
||||||
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
|
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
|
||||||
RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
||||||
RuntimeWorldRestoreState,
|
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
|
||||||
};
|
};
|
||||||
pub use smp::{
|
pub use smp::{
|
||||||
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,
|
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,
|
||||||
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
|
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
|
||||||
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
|
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
|
||||||
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
|
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
|
||||||
SmpLoadedCandidateAvailabilityTable, SmpLoadedProfile, SmpLoadedSaveSlice,
|
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, SmpLoadedProfile,
|
||||||
SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
||||||
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
|
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
|
||||||
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
|
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
|
||||||
SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe,
|
SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe,
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ mod tests {
|
||||||
world_restore: RuntimeWorldRestoreState::default(),
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
metadata: BTreeMap::new(),
|
metadata: BTreeMap::new(),
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,20 @@ pub struct RuntimeEventRecord {
|
||||||
pub effects: Vec<RuntimeEffect>,
|
pub effects: Vec<RuntimeEffect>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimePackedEventCollectionSummary {
|
||||||
|
pub source_kind: String,
|
||||||
|
pub mechanism_family: String,
|
||||||
|
pub mechanism_confidence: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub container_profile_family: Option<String>,
|
||||||
|
pub packed_state_version: u32,
|
||||||
|
pub packed_state_version_hex: String,
|
||||||
|
pub live_id_bound: u32,
|
||||||
|
pub live_record_count: usize,
|
||||||
|
pub live_entry_ids: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
impl RuntimeEventRecordTemplate {
|
impl RuntimeEventRecordTemplate {
|
||||||
pub fn into_runtime_record(self) -> RuntimeEventRecord {
|
pub fn into_runtime_record(self) -> RuntimeEventRecord {
|
||||||
RuntimeEventRecord {
|
RuntimeEventRecord {
|
||||||
|
|
@ -184,6 +198,8 @@ pub struct RuntimeState {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub companies: Vec<RuntimeCompany>,
|
pub companies: Vec<RuntimeCompany>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
|
||||||
|
#[serde(default)]
|
||||||
pub event_runtime_records: Vec<RuntimeEventRecord>,
|
pub event_runtime_records: Vec<RuntimeEventRecord>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub candidate_availability: BTreeMap<String, u32>,
|
pub candidate_availability: BTreeMap<String, u32>,
|
||||||
|
|
@ -219,6 +235,66 @@ impl RuntimeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(summary) = &self.packed_event_collection {
|
||||||
|
if summary.source_kind.trim().is_empty() {
|
||||||
|
return Err("packed_event_collection.source_kind must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if summary.mechanism_family.trim().is_empty() {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.mechanism_family must not be empty".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if summary.mechanism_confidence.trim().is_empty() {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.mechanism_confidence must not be empty".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if summary
|
||||||
|
.container_profile_family
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|value| value.trim().is_empty())
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.container_profile_family must not be empty"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if summary.packed_state_version_hex.trim().is_empty() {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.packed_state_version_hex must not be empty"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if summary.live_record_count != summary.live_entry_ids.len() {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.live_record_count must match live_entry_ids length"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut previous_id = None;
|
||||||
|
for entry_id in &summary.live_entry_ids {
|
||||||
|
if *entry_id == 0 {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.live_entry_ids must not contain id 0".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if *entry_id > summary.live_id_bound {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.live_entry_id {} exceeds live_id_bound {}",
|
||||||
|
entry_id, summary.live_id_bound
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if previous_id.is_some_and(|prior| prior >= *entry_id) {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.live_entry_ids must be strictly ascending"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
previous_id = Some(*entry_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for key in self.world_flags.keys() {
|
for key in self.world_flags.keys() {
|
||||||
if key.trim().is_empty() {
|
if key.trim().is_empty() {
|
||||||
return Err("world_flags contains an empty key".to_string());
|
return Err("world_flags contains an empty key".to_string());
|
||||||
|
|
@ -402,6 +478,7 @@ mod tests {
|
||||||
debt: 0,
|
debt: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
@ -447,6 +524,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
metadata: BTreeMap::new(),
|
metadata: BTreeMap::new(),
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
@ -474,6 +552,7 @@ mod tests {
|
||||||
current_cash: 100,
|
current_cash: 100,
|
||||||
debt: 0,
|
debt: 0,
|
||||||
}],
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: vec![RuntimeEventRecord {
|
event_runtime_records: vec![RuntimeEventRecord {
|
||||||
record_id: 7,
|
record_id: 7,
|
||||||
trigger_kind: 1,
|
trigger_kind: 1,
|
||||||
|
|
@ -513,6 +592,7 @@ mod tests {
|
||||||
current_cash: 100,
|
current_cash: 100,
|
||||||
debt: 0,
|
debt: 0,
|
||||||
}],
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: vec![RuntimeEventRecord {
|
event_runtime_records: vec![RuntimeEventRecord {
|
||||||
record_id: 7,
|
record_id: 7,
|
||||||
trigger_kind: 1,
|
trigger_kind: 1,
|
||||||
|
|
@ -542,4 +622,38 @@ mod tests {
|
||||||
|
|
||||||
assert!(state.validate().is_err());
|
assert!(state.validate().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_packed_event_collection_summary() {
|
||||||
|
let state = RuntimeState {
|
||||||
|
calendar: CalendarPoint {
|
||||||
|
year: 1830,
|
||||||
|
month_slot: 0,
|
||||||
|
phase_slot: 0,
|
||||||
|
tick_slot: 0,
|
||||||
|
},
|
||||||
|
world_flags: BTreeMap::new(),
|
||||||
|
save_profile: RuntimeSaveProfileState::default(),
|
||||||
|
world_restore: RuntimeWorldRestoreState::default(),
|
||||||
|
metadata: BTreeMap::new(),
|
||||||
|
companies: Vec::new(),
|
||||||
|
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
|
||||||
|
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()),
|
||||||
|
packed_state_version: 0x3e9,
|
||||||
|
packed_state_version_hex: "0x000003e9".to_string(),
|
||||||
|
live_id_bound: 4,
|
||||||
|
live_record_count: 2,
|
||||||
|
live_entry_ids: vec![3, 3],
|
||||||
|
}),
|
||||||
|
event_runtime_records: Vec::new(),
|
||||||
|
candidate_availability: BTreeMap::new(),
|
||||||
|
special_conditions: BTreeMap::new(),
|
||||||
|
service_state: RuntimeServiceState::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(state.validate().is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ const RECIPE_BOOK_LINE_STRIDE: usize = 0x30;
|
||||||
const RECIPE_BOOK_LINE_AREA_LEN: usize = RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE;
|
const RECIPE_BOOK_LINE_AREA_LEN: usize = RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE;
|
||||||
const RECIPE_BOOK_SUMMARY_END_OFFSET: usize =
|
const RECIPE_BOOK_SUMMARY_END_OFFSET: usize =
|
||||||
RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE;
|
RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE;
|
||||||
|
const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99;
|
||||||
|
const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a;
|
||||||
|
const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b;
|
||||||
|
const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
|
||||||
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
||||||
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
||||||
|
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
||||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize =
|
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize =
|
||||||
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
||||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
||||||
|
|
@ -1167,6 +1174,23 @@ pub struct SmpLoadedSpecialConditionsTable {
|
||||||
pub entries: Vec<SmpSpecialConditionEntry>,
|
pub entries: Vec<SmpSpecialConditionEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
pub source_kind: String,
|
||||||
|
pub mechanism_family: String,
|
||||||
|
pub mechanism_confidence: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub container_profile_family: Option<String>,
|
||||||
|
pub metadata_tag_offset: usize,
|
||||||
|
pub records_tag_offset: usize,
|
||||||
|
pub close_tag_offset: usize,
|
||||||
|
pub packed_state_version: u32,
|
||||||
|
pub packed_state_version_hex: String,
|
||||||
|
pub live_id_bound: u32,
|
||||||
|
pub live_record_count: usize,
|
||||||
|
pub live_entry_ids: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct SmpLoadedSaveSlice {
|
pub struct SmpLoadedSaveSlice {
|
||||||
pub file_extension_hint: Option<String>,
|
pub file_extension_hint: Option<String>,
|
||||||
|
|
@ -1178,6 +1202,7 @@ pub struct SmpLoadedSaveSlice {
|
||||||
pub profile: Option<SmpLoadedProfile>,
|
pub profile: Option<SmpLoadedProfile>,
|
||||||
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
|
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
|
||||||
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
|
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
|
||||||
|
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1220,6 +1245,7 @@ pub struct SmpInspectionReport {
|
||||||
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
||||||
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
||||||
pub save_load_summary: Option<SmpSaveLoadSummary>,
|
pub save_load_summary: Option<SmpSaveLoadSummary>,
|
||||||
|
pub event_runtime_collection_summary: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||||
pub contains_grounded_runtime_tags: bool,
|
pub contains_grounded_runtime_tags: bool,
|
||||||
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
|
|
@ -1343,10 +1369,128 @@ pub fn load_save_slice_from_report(
|
||||||
profile,
|
profile,
|
||||||
candidate_availability_table,
|
candidate_availability_table,
|
||||||
special_conditions_table,
|
special_conditions_table,
|
||||||
|
event_runtime_collection: report.event_runtime_collection_summary.clone(),
|
||||||
notes: summary.notes.clone(),
|
notes: summary.notes.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_event_runtime_collection_summary(
|
||||||
|
bytes: &[u8],
|
||||||
|
container_profile: Option<&SmpContainerProfile>,
|
||||||
|
save_load_summary: Option<&SmpSaveLoadSummary>,
|
||||||
|
) -> Option<SmpLoadedEventRuntimeCollectionSummary> {
|
||||||
|
let metadata_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG);
|
||||||
|
let record_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG);
|
||||||
|
let close_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG);
|
||||||
|
|
||||||
|
for metadata_tag_offset in metadata_offsets {
|
||||||
|
let packed_state_version = read_u32_at(bytes, metadata_tag_offset + 2)?;
|
||||||
|
if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let records_tag_offset = record_offsets
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|offset| *offset > metadata_tag_offset + 6)?;
|
||||||
|
let close_tag_offset = close_offsets
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|offset| *offset > records_tag_offset)?;
|
||||||
|
let metadata_payload = bytes.get(metadata_tag_offset + 6..records_tag_offset)?;
|
||||||
|
if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
|
||||||
|
.map(|index| read_u32_at(metadata_payload, index * 4))
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
let direct_collection_flag = header_words[0];
|
||||||
|
let direct_record_stride = usize::try_from(header_words[1]).ok()?;
|
||||||
|
let live_id_bound = header_words[4];
|
||||||
|
let live_record_count = usize::try_from(header_words[5]).ok()?;
|
||||||
|
if direct_collection_flag == 0 || direct_record_stride == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8;
|
||||||
|
let payload_bytes = direct_record_stride.checked_mul(live_record_count)?;
|
||||||
|
if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if metadata_payload.len() < bitset_len + payload_bytes {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes;
|
||||||
|
if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?;
|
||||||
|
let live_entry_ids = decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?;
|
||||||
|
if live_entry_ids.len() != live_record_count {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
mechanism_family: save_load_summary
|
||||||
|
.map(|summary| summary.mechanism_family.clone())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string()),
|
||||||
|
mechanism_confidence: save_load_summary
|
||||||
|
.map(|summary| summary.mechanism_confidence.clone())
|
||||||
|
.unwrap_or_else(|| "inferred".to_string()),
|
||||||
|
container_profile_family: container_profile
|
||||||
|
.map(|profile| profile.profile_family.clone()),
|
||||||
|
metadata_tag_offset,
|
||||||
|
records_tag_offset,
|
||||||
|
close_tag_offset,
|
||||||
|
packed_state_version,
|
||||||
|
packed_state_version_hex: format!("0x{packed_state_version:08x}"),
|
||||||
|
live_id_bound,
|
||||||
|
live_record_count,
|
||||||
|
live_entry_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_live_entry_ids_from_tombstone_bitset(
|
||||||
|
bitset: &[u8],
|
||||||
|
live_id_bound: u32,
|
||||||
|
) -> Option<Vec<u32>> {
|
||||||
|
let ids = decode_live_entry_ids_with_mapping(bitset, live_id_bound, false);
|
||||||
|
if ids.is_some() {
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
decode_live_entry_ids_with_mapping(bitset, live_id_bound, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_live_entry_ids_with_mapping(
|
||||||
|
bitset: &[u8],
|
||||||
|
live_id_bound: u32,
|
||||||
|
subtract_one: bool,
|
||||||
|
) -> Option<Vec<u32>> {
|
||||||
|
let mut live_entry_ids = Vec::new();
|
||||||
|
|
||||||
|
for entry_id in 1..=live_id_bound {
|
||||||
|
let bit_index = if subtract_one {
|
||||||
|
entry_id.checked_sub(1)?
|
||||||
|
} else {
|
||||||
|
entry_id
|
||||||
|
};
|
||||||
|
let byte_index = usize::try_from(bit_index / 8).ok()?;
|
||||||
|
let bit_mask = 1u8.checked_shl(bit_index % 8).unwrap_or(0);
|
||||||
|
let tombstone_byte = *bitset.get(byte_index)?;
|
||||||
|
if tombstone_byte & bit_mask == 0 {
|
||||||
|
live_entry_ids.push(entry_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(live_entry_ids)
|
||||||
|
}
|
||||||
|
|
||||||
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
||||||
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1475,6 +1619,11 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
||||||
rt3_105_packed_profile_probe.as_ref(),
|
rt3_105_packed_profile_probe.as_ref(),
|
||||||
rt3_105_save_name_table_probe.as_ref(),
|
rt3_105_save_name_table_probe.as_ref(),
|
||||||
);
|
);
|
||||||
|
let event_runtime_collection_summary = parse_event_runtime_collection_summary(
|
||||||
|
bytes,
|
||||||
|
container_profile.as_ref(),
|
||||||
|
save_load_summary.as_ref(),
|
||||||
|
);
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
warnings
|
warnings
|
||||||
|
|
@ -1562,6 +1711,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
||||||
classic_rehydrate_profile_probe,
|
classic_rehydrate_profile_probe,
|
||||||
rt3_105_packed_profile_probe,
|
rt3_105_packed_profile_probe,
|
||||||
save_load_summary,
|
save_load_summary,
|
||||||
|
event_runtime_collection_summary,
|
||||||
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
||||||
known_tag_hits,
|
known_tag_hits,
|
||||||
notes: vec![
|
notes: vec![
|
||||||
|
|
@ -6025,6 +6175,110 @@ mod tests {
|
||||||
assert!(slice.candidate_availability_table.is_none());
|
assert!(slice.candidate_availability_table.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_event_runtime_collection_summary_from_synthetic_chunks() {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
||||||
|
|
||||||
|
let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
for word in header_words {
|
||||||
|
bytes.extend_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.extend_from_slice(&[0x14, 0x00]);
|
||||||
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
||||||
|
bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
|
||||||
|
bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]);
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
|
||||||
|
|
||||||
|
let report = inspect_smp_bytes(&bytes);
|
||||||
|
let summary = report
|
||||||
|
.event_runtime_collection_summary
|
||||||
|
.as_ref()
|
||||||
|
.expect("event runtime collection summary should parse");
|
||||||
|
|
||||||
|
assert_eq!(summary.packed_state_version, 0x3e9);
|
||||||
|
assert_eq!(summary.live_id_bound, 5);
|
||||||
|
assert_eq!(summary.live_record_count, 3);
|
||||||
|
assert_eq!(summary.live_entry_ids, vec![1, 3, 5]);
|
||||||
|
assert_eq!(summary.records_tag_offset, 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_event_runtime_collection_summary_from_report() {
|
||||||
|
let mut report = inspect_smp_bytes(&[]);
|
||||||
|
let classic_probe = SmpClassicRehydrateProfileProbe {
|
||||||
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||||||
|
progress_32dc_offset: 0x76e8,
|
||||||
|
progress_3714_offset: 0x76ec,
|
||||||
|
progress_3715_offset: 0x77f8,
|
||||||
|
packed_profile_offset: 0x76f0,
|
||||||
|
packed_profile_len: 0x108,
|
||||||
|
packed_profile_len_hex: "0x108".to_string(),
|
||||||
|
packed_profile_block: SmpClassicPackedProfileBlock {
|
||||||
|
relative_len: 0x108,
|
||||||
|
relative_len_hex: "0x108".to_string(),
|
||||||
|
leading_word_0: 3,
|
||||||
|
leading_word_0_hex: "0x00000003".to_string(),
|
||||||
|
trailing_zero_word_count_after_leading_word: 3,
|
||||||
|
map_path_offset: 0x13,
|
||||||
|
map_path: Some("British Isles.gmp".to_string()),
|
||||||
|
display_name_offset: 0x46,
|
||||||
|
display_name: Some("British Isles".to_string()),
|
||||||
|
profile_byte_0x77: 0,
|
||||||
|
profile_byte_0x77_hex: "0x00".to_string(),
|
||||||
|
profile_byte_0x82: 0,
|
||||||
|
profile_byte_0x82_hex: "0x00".to_string(),
|
||||||
|
profile_byte_0x97: 0,
|
||||||
|
profile_byte_0x97_hex: "0x00".to_string(),
|
||||||
|
profile_byte_0xc5: 0,
|
||||||
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
||||||
|
stable_nonzero_words: vec![],
|
||||||
|
},
|
||||||
|
ascii_runs: vec![],
|
||||||
|
};
|
||||||
|
report.classic_rehydrate_profile_probe = Some(classic_probe.clone());
|
||||||
|
report.save_load_summary = build_save_load_summary(
|
||||||
|
Some("gms"),
|
||||||
|
Some(&SmpContainerProfile {
|
||||||
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||||||
|
profile_evidence: vec![],
|
||||||
|
is_known_profile: true,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(&classic_probe),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
report.event_runtime_collection_summary = Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||||
|
metadata_tag_offset: 0x7100,
|
||||||
|
records_tag_offset: 0x7200,
|
||||||
|
close_tag_offset: 0x7600,
|
||||||
|
packed_state_version: 0x3e9,
|
||||||
|
packed_state_version_hex: "0x000003e9".to_string(),
|
||||||
|
live_id_bound: 5,
|
||||||
|
live_record_count: 3,
|
||||||
|
live_entry_ids: vec![1, 3, 5],
|
||||||
|
});
|
||||||
|
|
||||||
|
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
||||||
|
assert_eq!(
|
||||||
|
slice
|
||||||
|
.event_runtime_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.live_entry_ids.clone()),
|
||||||
|
Some(vec![1, 3, 5])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loads_rt3_105_save_slice_from_report() {
|
fn loads_rt3_105_save_slice_from_report() {
|
||||||
let mut report = inspect_smp_bytes(&[]);
|
let mut report = inspect_smp_bytes(&[]);
|
||||||
|
|
|
||||||
|
|
@ -473,6 +473,7 @@ mod tests {
|
||||||
current_cash: 10,
|
current_cash: 10,
|
||||||
debt: 0,
|
debt: 0,
|
||||||
}],
|
}],
|
||||||
|
packed_event_collection: None,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
special_conditions: BTreeMap::new(),
|
special_conditions: BTreeMap::new(),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ pub struct RuntimeSummary {
|
||||||
pub world_restore_absolute_counter_adjustment_context: Option<String>,
|
pub world_restore_absolute_counter_adjustment_context: Option<String>,
|
||||||
pub metadata_count: usize,
|
pub metadata_count: usize,
|
||||||
pub company_count: usize,
|
pub company_count: usize,
|
||||||
|
pub packed_event_collection_present: bool,
|
||||||
|
pub packed_event_record_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,
|
||||||
|
|
@ -109,6 +111,12 @@ impl RuntimeSummary {
|
||||||
.clone(),
|
.clone(),
|
||||||
metadata_count: state.metadata.len(),
|
metadata_count: state.metadata.len(),
|
||||||
company_count: state.companies.len(),
|
company_count: state.companies.len(),
|
||||||
|
packed_event_collection_present: state.packed_event_collection.is_some(),
|
||||||
|
packed_event_record_count: state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.live_record_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
|
||||||
|
|
|
||||||
|
|
@ -66,15 +66,15 @@ 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, fixture execution, state-diff tooling, and initial
|
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
||||||
persistence surfaces.
|
state-diff tooling, and initial persistence surfaces.
|
||||||
|
|
||||||
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
|
||||||
- broaden the normalized event-service layer through staged event-record mutation and follow-on
|
- deepen the `.smp` event bridge from collection-level structural summaries toward per-record
|
||||||
record behavior
|
packed-body coverage
|
||||||
- deepen captured-runtime and round-trip fixture coverage on top of the existing runtime CLI and
|
- deepen captured-runtime and round-trip fixture coverage on top of the existing runtime CLI and
|
||||||
fixture surfaces
|
fixture surfaces
|
||||||
- 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
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,15 @@ Implemented today:
|
||||||
- `rrt-runtime` exists with a deterministic calendar model, step commands, runtime summaries, and
|
- `rrt-runtime` exists with a deterministic calendar model, step commands, runtime summaries, and
|
||||||
normalized runtime state validation
|
normalized runtime state validation
|
||||||
- periodic trigger dispatch exists, including ordered periodic maintenance, dirty rerun `0x0a`, and
|
- periodic trigger dispatch exists, including ordered periodic maintenance, dirty rerun `0x0a`, and
|
||||||
a first normalized runtime-effect surface
|
a normalized runtime-effect surface with staged event-record mutation
|
||||||
- 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, and normalized state-fragment assertions
|
service, snapshot-backed inputs, and normalized state-fragment assertions
|
||||||
|
|
||||||
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
|
||||||
normalized event-service breadth through staged event-record mutation and follow-on records.
|
the `.smp` event-collection structural bridge across inspection, save-slice loading, import, and
|
||||||
|
snapshot-backed fixtures.
|
||||||
|
|
||||||
## Why This Boundary
|
## Why This Boundary
|
||||||
|
|
||||||
|
|
@ -189,8 +190,10 @@ Current status:
|
||||||
- periodic trigger ordering is implemented
|
- periodic trigger ordering is implemented
|
||||||
- normalized trigger-side effects already exist for world flags, company cash/debt, candidate
|
- normalized trigger-side effects already exist for world flags, company cash/debt, candidate
|
||||||
availability, and special conditions
|
availability, and special conditions
|
||||||
- one-shot handling and dirty reruns are already covered by synthetic fixtures
|
- one-shot handling, dirty reruns, and staged append/activate/deactivate/remove behavior are
|
||||||
- the missing breadth is event-graph mutation and richer trigger-family behavior
|
already covered by synthetic fixtures
|
||||||
|
- the remaining breadth is richer trigger-family behavior and target resolution, not first-pass
|
||||||
|
event-graph mutation
|
||||||
|
|
||||||
### Milestone 3: Persistence Boundary (partially complete)
|
### Milestone 3: Persistence Boundary (partially complete)
|
||||||
|
|
||||||
|
|
@ -212,8 +215,10 @@ Current status:
|
||||||
|
|
||||||
- runtime snapshots and state dumps are implemented
|
- runtime snapshots and state dumps are implemented
|
||||||
- `.smp` save inspection and partial save-slice projection already feed normalized runtime state
|
- `.smp` save inspection and partial save-slice projection already feed normalized runtime state
|
||||||
- the remaining gap is broader captured-runtime and round-trip fixture depth, not the first
|
- the packed event-collection summary now survives into loaded save slices and projected runtime
|
||||||
persistence surface
|
snapshots, but per-record packed bodies are still deferred
|
||||||
|
- the remaining gap is broader captured-runtime and round-trip fixture depth plus deeper `.smp`
|
||||||
|
event-body decoding, not the first persistence surface
|
||||||
|
|
||||||
### Milestone 4: Domain Expansion
|
### Milestone 4: Domain Expansion
|
||||||
|
|
||||||
|
|
@ -313,6 +318,7 @@ Keep:
|
||||||
- cash, debt, and game-speed-related runtime fields when semantically relevant
|
- cash, debt, and game-speed-related runtime fields when semantically relevant
|
||||||
- collection contents and semantic counts
|
- collection contents and semantic counts
|
||||||
- trigger-side effects
|
- trigger-side effects
|
||||||
|
- packed event-collection structural summaries when present
|
||||||
|
|
||||||
## Risks
|
## Risks
|
||||||
|
|
||||||
|
|
@ -340,47 +346,47 @@ The currently implemented normalized runtime surface is:
|
||||||
- `runtime validate-fixture`, `runtime summarize-fixture`, `runtime export-fixture-state`,
|
- `runtime validate-fixture`, `runtime summarize-fixture`, `runtime export-fixture-state`,
|
||||||
`runtime summarize-state`, `runtime import-state`, and `runtime diff-state`
|
`runtime summarize-state`, `runtime import-state`, and `runtime diff-state`
|
||||||
- deterministic stepping, periodic trigger dispatch, one-shot event handling, dirty reruns, and a
|
- deterministic stepping, periodic trigger dispatch, one-shot event handling, dirty reruns, and a
|
||||||
first normalized runtime-effect vocabulary
|
normalized runtime-effect vocabulary with staged event-record mutation
|
||||||
- save-side inspection and partial state projection for `.smp` inputs
|
- save-side inspection and partial state projection for `.smp` inputs, including the structural
|
||||||
|
packed event-collection summary
|
||||||
|
|
||||||
Checked-in fixture families already include:
|
Checked-in fixture families already include:
|
||||||
|
|
||||||
- deterministic minimal-world stepping
|
- deterministic minimal-world stepping
|
||||||
- periodic boundary service
|
- periodic boundary service
|
||||||
- direct trigger-service mutation
|
- direct trigger-service mutation
|
||||||
|
- staged event-record lifecycle coverage
|
||||||
- snapshot-backed fixture execution
|
- snapshot-backed fixture execution
|
||||||
|
|
||||||
## Next Slice
|
## Next Slice
|
||||||
|
|
||||||
The recommended next implementation slice is normalized event-service breadth through staged
|
The recommended next implementation slice is deeper `.smp` event persistence, starting from the
|
||||||
event-record mutation.
|
structural bridge that already exists today.
|
||||||
|
|
||||||
Target behavior:
|
Target behavior:
|
||||||
|
|
||||||
- allow one serviced record to append a follow-on runtime record
|
- keep carrying the packed event collection across `inspect-smp`, `load-save-slice`,
|
||||||
- allow one serviced record to activate, deactivate, or remove another runtime record
|
`import-save-state`, snapshots, diffs, and fixtures
|
||||||
- stage those graph mutations during the pass and commit them only after the pass finishes
|
- deepen that bridge from collection structure into per-record packed-body summaries
|
||||||
- commit staged mutations in exact emission order
|
- preserve the separation between parity-shaped packed state and executable normalized runtime state
|
||||||
- allow newly appended `0x0a` records to run in the dirty rerun after commit, but never in the
|
until the packed layout is better decoded
|
||||||
original pass snapshot
|
|
||||||
|
|
||||||
Public-model additions for that slice:
|
Public-model additions for that slice:
|
||||||
|
|
||||||
- `RuntimeEventRecordTemplate`
|
- packed per-record event summary types on the `.smp` side
|
||||||
- `RuntimeEffect::AppendEventRecord`
|
- optional runtime-side parity summaries for imported packed event records
|
||||||
- `RuntimeEffect::ActivateEventRecord`
|
- no new executable `RuntimeEffect` variants by default in that slice
|
||||||
- `RuntimeEffect::DeactivateEventRecord`
|
|
||||||
- `RuntimeEffect::RemoveEventRecord`
|
|
||||||
|
|
||||||
Fixture work for that slice:
|
Fixture work for that slice:
|
||||||
|
|
||||||
- one synthetic fixture for append plus dirty rerun behavior
|
- one or more snapshot-backed fixtures that prove imported packed event state survives normalize and
|
||||||
- one synthetic fixture for cross-pass activate/deactivate/remove semantics
|
diff paths
|
||||||
- state-fragment assertions that lock final collection contents and per-record counters
|
- synthetic report/save-slice tests that lock the first per-record packed-body parse shape
|
||||||
|
- state-fragment assertions that lock imported collection ids, version, and record counts
|
||||||
|
|
||||||
Do not mix this slice with:
|
Do not mix this slice with:
|
||||||
|
|
||||||
- territory-access or selected-profile parity
|
- territory-access or selected-profile parity
|
||||||
- placed-structure batch placement parity
|
- placed-structure batch placement parity
|
||||||
- shell queue/modal behavior
|
- shell queue/modal behavior
|
||||||
- packed RT3 event-row import/export parity
|
- direct translation of packed RT3 event rows into executable normalized effects
|
||||||
|
|
|
||||||
47
fixtures/runtime/packed-event-collection-from-snapshot.json
Normal file
47
fixtures/runtime/packed-event-collection-from-snapshot.json
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"fixture_id": "packed-event-collection-from-snapshot",
|
||||||
|
"source": {
|
||||||
|
"kind": "captured-runtime",
|
||||||
|
"description": "Fixture backed by a runtime snapshot that carries the packed event collection summary."
|
||||||
|
},
|
||||||
|
"state_snapshot_path": "packed-event-collection-snapshot.json",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"kind": "step_count",
|
||||||
|
"steps": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expected_summary": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 1
|
||||||
|
},
|
||||||
|
"world_flag_count": 0,
|
||||||
|
"company_count": 0,
|
||||||
|
"packed_event_collection_present": true,
|
||||||
|
"packed_event_record_count": 3,
|
||||||
|
"event_runtime_record_count": 0,
|
||||||
|
"total_event_record_service_count": 0,
|
||||||
|
"periodic_boundary_call_count": 0,
|
||||||
|
"total_trigger_dispatch_count": 0,
|
||||||
|
"dirty_rerun_count": 0,
|
||||||
|
"total_company_cash": 0
|
||||||
|
},
|
||||||
|
"expected_state_fragment": {
|
||||||
|
"calendar": {
|
||||||
|
"tick_slot": 1
|
||||||
|
},
|
||||||
|
"packed_event_collection": {
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"live_record_count": 3,
|
||||||
|
"live_entry_ids": [
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
fixtures/runtime/packed-event-collection-snapshot.json
Normal file
41
fixtures/runtime/packed-event-collection-snapshot.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "packed-event-collection-snapshot",
|
||||||
|
"source": {
|
||||||
|
"description": "Snapshot fixture carrying a projected packed event collection summary."
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 0
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 5,
|
||||||
|
"live_record_count": 3,
|
||||||
|
"live_entry_ids": [
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue