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);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn diffs_classic_profile_samples_across_multiple_files() {
|
||||
let sample_a = RuntimeClassicProfileSample {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ mod tests {
|
|||
world_restore: RuntimeWorldRestoreState::default(),
|
||||
metadata: BTreeMap::new(),
|
||||
companies: Vec::new(),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use std::path::Path;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
||||
RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
||||
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
||||
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
||||
};
|
||||
|
||||
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_table.is_some(),
|
||||
);
|
||||
world_flags.insert(
|
||||
"save_slice.event_runtime_collection_present".to_string(),
|
||||
save_slice.event_runtime_collection.is_some(),
|
||||
);
|
||||
world_flags.insert(
|
||||
"save_slice.mechanism_confidence_grounded".to_string(),
|
||||
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 {
|
||||
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 {
|
||||
metadata.insert(
|
||||
"save_slice.profile_kind".to_string(),
|
||||
|
|
@ -296,6 +327,7 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
world_restore,
|
||||
metadata,
|
||||
companies: Vec::new(),
|
||||
packed_event_collection,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability,
|
||||
special_conditions,
|
||||
|
|
@ -379,6 +411,7 @@ mod tests {
|
|||
world_restore: RuntimeWorldRestoreState::default(),
|
||||
metadata: BTreeMap::new(),
|
||||
companies: Vec::new(),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: 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()],
|
||||
};
|
||||
|
||||
|
|
@ -644,5 +691,22 @@ mod tests {
|
|||
.get("save_slice.profile_byte_0x82_nonzero"),
|
||||
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::{
|
||||
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
|
||||
RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
||||
RuntimeWorldRestoreState,
|
||||
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
||||
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
|
||||
};
|
||||
pub use smp::{
|
||||
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,
|
||||
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
|
||||
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
|
||||
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
|
||||
SmpLoadedCandidateAvailabilityTable, SmpLoadedProfile, SmpLoadedSaveSlice,
|
||||
SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
||||
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, SmpLoadedProfile,
|
||||
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
||||
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
|
||||
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
|
||||
SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe,
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ mod tests {
|
|||
world_restore: RuntimeWorldRestoreState::default(),
|
||||
metadata: BTreeMap::new(),
|
||||
companies: Vec::new(),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,20 @@ pub struct RuntimeEventRecord {
|
|||
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 {
|
||||
pub fn into_runtime_record(self) -> RuntimeEventRecord {
|
||||
RuntimeEventRecord {
|
||||
|
|
@ -184,6 +198,8 @@ pub struct RuntimeState {
|
|||
#[serde(default)]
|
||||
pub companies: Vec<RuntimeCompany>,
|
||||
#[serde(default)]
|
||||
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
|
||||
#[serde(default)]
|
||||
pub event_runtime_records: Vec<RuntimeEventRecord>,
|
||||
#[serde(default)]
|
||||
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() {
|
||||
if key.trim().is_empty() {
|
||||
return Err("world_flags contains an empty key".to_string());
|
||||
|
|
@ -402,6 +478,7 @@ mod tests {
|
|||
debt: 0,
|
||||
},
|
||||
],
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
|
|
@ -447,6 +524,7 @@ mod tests {
|
|||
},
|
||||
metadata: BTreeMap::new(),
|
||||
companies: Vec::new(),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
|
|
@ -474,6 +552,7 @@ mod tests {
|
|||
current_cash: 100,
|
||||
debt: 0,
|
||||
}],
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 7,
|
||||
trigger_kind: 1,
|
||||
|
|
@ -513,6 +592,7 @@ mod tests {
|
|||
current_cash: 100,
|
||||
debt: 0,
|
||||
}],
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 7,
|
||||
trigger_kind: 1,
|
||||
|
|
@ -542,4 +622,38 @@ mod tests {
|
|||
|
||||
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_SUMMARY_END_OFFSET: usize =
|
||||
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 =
|
||||
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
||||
|
|
@ -1167,6 +1174,23 @@ pub struct SmpLoadedSpecialConditionsTable {
|
|||
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)]
|
||||
pub struct SmpLoadedSaveSlice {
|
||||
pub file_extension_hint: Option<String>,
|
||||
|
|
@ -1178,6 +1202,7 @@ pub struct SmpLoadedSaveSlice {
|
|||
pub profile: Option<SmpLoadedProfile>,
|
||||
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
|
||||
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
|
||||
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -1220,6 +1245,7 @@ pub struct SmpInspectionReport {
|
|||
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
||||
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
||||
pub save_load_summary: Option<SmpSaveLoadSummary>,
|
||||
pub event_runtime_collection_summary: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||
pub contains_grounded_runtime_tags: bool,
|
||||
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
||||
pub notes: Vec<String>,
|
||||
|
|
@ -1343,10 +1369,128 @@ pub fn load_save_slice_from_report(
|
|||
profile,
|
||||
candidate_availability_table,
|
||||
special_conditions_table,
|
||||
event_runtime_collection: report.event_runtime_collection_summary.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 {
|
||||
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
||||
.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_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();
|
||||
if bytes.is_empty() {
|
||||
warnings
|
||||
|
|
@ -1562,6 +1711,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
|||
classic_rehydrate_profile_probe,
|
||||
rt3_105_packed_profile_probe,
|
||||
save_load_summary,
|
||||
event_runtime_collection_summary,
|
||||
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
||||
known_tag_hits,
|
||||
notes: vec![
|
||||
|
|
@ -6025,6 +6175,110 @@ mod tests {
|
|||
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]
|
||||
fn loads_rt3_105_save_slice_from_report() {
|
||||
let mut report = inspect_smp_bytes(&[]);
|
||||
|
|
|
|||
|
|
@ -473,6 +473,7 @@ mod tests {
|
|||
current_cash: 10,
|
||||
debt: 0,
|
||||
}],
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ pub struct RuntimeSummary {
|
|||
pub world_restore_absolute_counter_adjustment_context: Option<String>,
|
||||
pub metadata_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 candidate_availability_count: usize,
|
||||
pub zero_candidate_availability_count: usize,
|
||||
|
|
@ -109,6 +111,12 @@ impl RuntimeSummary {
|
|||
.clone(),
|
||||
metadata_count: state.metadata.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(),
|
||||
candidate_availability_count: state.candidate_availability.len(),
|
||||
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
|
||||
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
||||
trigger dispatch, normalized runtime effects, fixture execution, state-diff tooling, and initial
|
||||
persistence surfaces.
|
||||
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
||||
state-diff tooling, and initial persistence surfaces.
|
||||
|
||||
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
|
||||
- broaden the normalized event-service layer through staged event-record mutation and follow-on
|
||||
record behavior
|
||||
- deepen the `.smp` event bridge from collection-level structural summaries toward per-record
|
||||
packed-body coverage
|
||||
- deepen captured-runtime and round-trip fixture coverage on top of the existing runtime CLI and
|
||||
fixture surfaces
|
||||
- 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
|
||||
normalized runtime state validation
|
||||
- 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
|
||||
CLI and fixture layers
|
||||
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
||||
service, snapshot-backed inputs, and normalized state-fragment assertions
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -189,8 +190,10 @@ Current status:
|
|||
- periodic trigger ordering is implemented
|
||||
- normalized trigger-side effects already exist for world flags, company cash/debt, candidate
|
||||
availability, and special conditions
|
||||
- one-shot handling and dirty reruns are already covered by synthetic fixtures
|
||||
- the missing breadth is event-graph mutation and richer trigger-family behavior
|
||||
- one-shot handling, dirty reruns, and staged append/activate/deactivate/remove behavior are
|
||||
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)
|
||||
|
||||
|
|
@ -212,8 +215,10 @@ Current status:
|
|||
|
||||
- runtime snapshots and state dumps are implemented
|
||||
- `.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
|
||||
persistence surface
|
||||
- the packed event-collection summary now survives into loaded save slices and projected runtime
|
||||
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
|
||||
|
||||
|
|
@ -313,6 +318,7 @@ Keep:
|
|||
- cash, debt, and game-speed-related runtime fields when semantically relevant
|
||||
- collection contents and semantic counts
|
||||
- trigger-side effects
|
||||
- packed event-collection structural summaries when present
|
||||
|
||||
## Risks
|
||||
|
||||
|
|
@ -340,47 +346,47 @@ The currently implemented normalized runtime surface is:
|
|||
- `runtime validate-fixture`, `runtime summarize-fixture`, `runtime export-fixture-state`,
|
||||
`runtime summarize-state`, `runtime import-state`, and `runtime diff-state`
|
||||
- deterministic stepping, periodic trigger dispatch, one-shot event handling, dirty reruns, and a
|
||||
first normalized runtime-effect vocabulary
|
||||
- save-side inspection and partial state projection for `.smp` inputs
|
||||
normalized runtime-effect vocabulary with staged event-record mutation
|
||||
- save-side inspection and partial state projection for `.smp` inputs, including the structural
|
||||
packed event-collection summary
|
||||
|
||||
Checked-in fixture families already include:
|
||||
|
||||
- deterministic minimal-world stepping
|
||||
- periodic boundary service
|
||||
- direct trigger-service mutation
|
||||
- staged event-record lifecycle coverage
|
||||
- snapshot-backed fixture execution
|
||||
|
||||
## Next Slice
|
||||
|
||||
The recommended next implementation slice is normalized event-service breadth through staged
|
||||
event-record mutation.
|
||||
The recommended next implementation slice is deeper `.smp` event persistence, starting from the
|
||||
structural bridge that already exists today.
|
||||
|
||||
Target behavior:
|
||||
|
||||
- allow one serviced record to append a follow-on runtime record
|
||||
- allow one serviced record to activate, deactivate, or remove another runtime record
|
||||
- stage those graph mutations during the pass and commit them only after the pass finishes
|
||||
- commit staged mutations in exact emission order
|
||||
- allow newly appended `0x0a` records to run in the dirty rerun after commit, but never in the
|
||||
original pass snapshot
|
||||
- keep carrying the packed event collection across `inspect-smp`, `load-save-slice`,
|
||||
`import-save-state`, snapshots, diffs, and fixtures
|
||||
- deepen that bridge from collection structure into per-record packed-body summaries
|
||||
- preserve the separation between parity-shaped packed state and executable normalized runtime state
|
||||
until the packed layout is better decoded
|
||||
|
||||
Public-model additions for that slice:
|
||||
|
||||
- `RuntimeEventRecordTemplate`
|
||||
- `RuntimeEffect::AppendEventRecord`
|
||||
- `RuntimeEffect::ActivateEventRecord`
|
||||
- `RuntimeEffect::DeactivateEventRecord`
|
||||
- `RuntimeEffect::RemoveEventRecord`
|
||||
- packed per-record event summary types on the `.smp` side
|
||||
- optional runtime-side parity summaries for imported packed event records
|
||||
- no new executable `RuntimeEffect` variants by default in that slice
|
||||
|
||||
Fixture work for that slice:
|
||||
|
||||
- one synthetic fixture for append plus dirty rerun behavior
|
||||
- one synthetic fixture for cross-pass activate/deactivate/remove semantics
|
||||
- state-fragment assertions that lock final collection contents and per-record counters
|
||||
- one or more snapshot-backed fixtures that prove imported packed event state survives normalize and
|
||||
diff paths
|
||||
- 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:
|
||||
|
||||
- territory-access or selected-profile parity
|
||||
- placed-structure batch placement parity
|
||||
- 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