Decode packed event records for runtime import
This commit is contained in:
parent
83f55fa26e
commit
09b6514dbf
13 changed files with 1801 additions and 50 deletions
|
|
@ -4152,7 +4152,38 @@ mod tests {
|
||||||
"packed_state_version_hex": "0x000003e9",
|
"packed_state_version_hex": "0x000003e9",
|
||||||
"live_id_bound": 5,
|
"live_id_bound": 5,
|
||||||
"live_record_count": 3,
|
"live_record_count": 3,
|
||||||
"live_entry_ids": [1, 3, 5]
|
"live_entry_ids": [1, 3, 5],
|
||||||
|
"decoded_record_count": 0,
|
||||||
|
"imported_runtime_record_count": 0,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 1,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["left fixture"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_index": 1,
|
||||||
|
"live_entry_id": 3,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["left fixture"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_index": 2,
|
||||||
|
"live_entry_id": 5,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["left fixture"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"event_runtime_records": []
|
"event_runtime_records": []
|
||||||
}
|
}
|
||||||
|
|
@ -4178,7 +4209,29 @@ mod tests {
|
||||||
"packed_state_version_hex": "0x000003e9",
|
"packed_state_version_hex": "0x000003e9",
|
||||||
"live_id_bound": 5,
|
"live_id_bound": 5,
|
||||||
"live_record_count": 2,
|
"live_record_count": 2,
|
||||||
"live_entry_ids": [1, 5]
|
"live_entry_ids": [1, 5],
|
||||||
|
"decoded_record_count": 0,
|
||||||
|
"imported_runtime_record_count": 0,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 1,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["right fixture"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_index": 1,
|
||||||
|
"live_entry_id": 5,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["right fixture"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"event_runtime_records": []
|
"event_runtime_records": []
|
||||||
}
|
}
|
||||||
|
|
@ -4210,6 +4263,155 @@ mod tests {
|
||||||
.expect("snapshot-backed packed-event fixture should summarize");
|
.expect("snapshot-backed packed-event fixture should summarize");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarizes_snapshot_backed_fixture_with_imported_packed_event_record() {
|
||||||
|
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../fixtures/runtime/packed-event-record-import-from-snapshot.json");
|
||||||
|
|
||||||
|
run_runtime_summarize_fixture(&fixture_path)
|
||||||
|
.expect("snapshot-backed imported packed-event fixture should summarize");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
|
||||||
|
let left = serde_json::json!({
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "left-packed-import",
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 0
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 7,
|
||||||
|
"live_record_count": 1,
|
||||||
|
"live_entry_ids": [7],
|
||||||
|
"decoded_record_count": 0,
|
||||||
|
"imported_runtime_record_count": 0,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 7,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["left placeholder"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event_runtime_records": []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let right = serde_json::json!({
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "right-packed-import",
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 0
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 7,
|
||||||
|
"live_record_count": 1,
|
||||||
|
"live_entry_ids": [7],
|
||||||
|
"decoded_record_count": 1,
|
||||||
|
"imported_runtime_record_count": 1,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 7,
|
||||||
|
"payload_offset": 29186,
|
||||||
|
"payload_len": 64,
|
||||||
|
"decode_status": "executable",
|
||||||
|
"trigger_kind": 7,
|
||||||
|
"active": true,
|
||||||
|
"marks_collection_dirty": false,
|
||||||
|
"one_shot": false,
|
||||||
|
"text_bands": [
|
||||||
|
{
|
||||||
|
"label": "primary_text_band",
|
||||||
|
"packed_len": 5,
|
||||||
|
"present": true,
|
||||||
|
"preview": "Alpha"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"standalone_condition_row_count": 1,
|
||||||
|
"grouped_effect_row_counts": [0, 1, 0, 0],
|
||||||
|
"decoded_actions": [
|
||||||
|
{
|
||||||
|
"kind": "set_world_flag",
|
||||||
|
"key": "from_packed_root",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executable_import_ready": true,
|
||||||
|
"notes": ["decoded test record"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event_runtime_records": [
|
||||||
|
{
|
||||||
|
"record_id": 7,
|
||||||
|
"trigger_kind": 7,
|
||||||
|
"active": true,
|
||||||
|
"marks_collection_dirty": false,
|
||||||
|
"one_shot": false,
|
||||||
|
"has_fired": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"kind": "set_world_flag",
|
||||||
|
"key": "from_packed_root",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let left_path = write_temp_json("runtime-diff-packed-import-left", &left);
|
||||||
|
let right_path = write_temp_json("runtime-diff-packed-import-right", &right);
|
||||||
|
|
||||||
|
let left_state =
|
||||||
|
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
|
||||||
|
let right_state =
|
||||||
|
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
|
||||||
|
let differences = diff_json_values(&left_state, &right_state);
|
||||||
|
|
||||||
|
assert!(differences.iter().any(|entry| {
|
||||||
|
entry.path == "$.packed_event_collection.records[0].decode_status"
|
||||||
|
|| entry.path == "$.packed_event_collection.records[0].decoded_actions[0]"
|
||||||
|
}));
|
||||||
|
assert!(
|
||||||
|
differences
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.path == "$.event_runtime_records[0]")
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_file(left_path);
|
||||||
|
let _ = fs::remove_file(right_path);
|
||||||
|
}
|
||||||
|
|
||||||
#[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 {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ pub struct ExpectedRuntimeSummary {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub company_count: Option<usize>,
|
pub company_count: Option<usize>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub packed_event_collection_present: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub packed_event_record_count: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub packed_event_decoded_record_count: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub packed_event_imported_runtime_record_count: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
pub event_runtime_record_count: Option<usize>,
|
pub event_runtime_record_count: Option<usize>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub candidate_availability_count: Option<usize>,
|
pub candidate_availability_count: Option<usize>,
|
||||||
|
|
@ -301,6 +309,38 @@ impl ExpectedRuntimeSummary {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(present) = self.packed_event_collection_present {
|
||||||
|
if actual.packed_event_collection_present != present {
|
||||||
|
mismatches.push(format!(
|
||||||
|
"packed_event_collection_present mismatch: expected {present}, got {}",
|
||||||
|
actual.packed_event_collection_present
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(count) = self.packed_event_record_count {
|
||||||
|
if actual.packed_event_record_count != count {
|
||||||
|
mismatches.push(format!(
|
||||||
|
"packed_event_record_count mismatch: expected {count}, got {}",
|
||||||
|
actual.packed_event_record_count
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(count) = self.packed_event_decoded_record_count {
|
||||||
|
if actual.packed_event_decoded_record_count != count {
|
||||||
|
mismatches.push(format!(
|
||||||
|
"packed_event_decoded_record_count mismatch: expected {count}, got {}",
|
||||||
|
actual.packed_event_decoded_record_count
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(count) = self.packed_event_imported_runtime_record_count {
|
||||||
|
if actual.packed_event_imported_runtime_record_count != count {
|
||||||
|
mismatches.push(format!(
|
||||||
|
"packed_event_imported_runtime_record_count mismatch: expected {count}, got {}",
|
||||||
|
actual.packed_event_imported_runtime_record_count
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(count) = self.event_runtime_record_count {
|
if let Some(count) = self.event_runtime_record_count {
|
||||||
if actual.event_runtime_record_count != count {
|
if actual.event_runtime_record_count != count {
|
||||||
mismatches.push(format!(
|
mismatches.push(format!(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||||
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||||
|
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
||||||
|
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
|
||||||
|
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||||
|
|
@ -137,7 +140,27 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
if let Some(family) = &save_slice.bridge_family {
|
if let Some(family) = &save_slice.bridge_family {
|
||||||
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
||||||
}
|
}
|
||||||
|
let known_company_ids = BTreeSet::new();
|
||||||
|
let imported_event_runtime_records = save_slice
|
||||||
|
.event_runtime_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| {
|
||||||
|
summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|record| {
|
||||||
|
smp_packed_record_to_runtime_event_record(record, &known_company_ids)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
let packed_event_collection = save_slice.event_runtime_collection.as_ref().map(|summary| {
|
let packed_event_collection = save_slice.event_runtime_collection.as_ref().map(|summary| {
|
||||||
|
let records = summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.map(runtime_packed_event_record_summary_from_smp)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
RuntimePackedEventCollectionSummary {
|
RuntimePackedEventCollectionSummary {
|
||||||
source_kind: summary.source_kind.clone(),
|
source_kind: summary.source_kind.clone(),
|
||||||
mechanism_family: summary.mechanism_family.clone(),
|
mechanism_family: summary.mechanism_family.clone(),
|
||||||
|
|
@ -148,6 +171,12 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
live_id_bound: summary.live_id_bound,
|
live_id_bound: summary.live_id_bound,
|
||||||
live_record_count: summary.live_record_count,
|
live_record_count: summary.live_record_count,
|
||||||
live_entry_ids: summary.live_entry_ids.clone(),
|
live_entry_ids: summary.live_entry_ids.clone(),
|
||||||
|
decoded_record_count: records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.decode_status != "unsupported_framing")
|
||||||
|
.count(),
|
||||||
|
imported_runtime_record_count: imported_event_runtime_records.len(),
|
||||||
|
records,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if let Some(summary) = &save_slice.event_runtime_collection {
|
if let Some(summary) = &save_slice.event_runtime_collection {
|
||||||
|
|
@ -163,6 +192,14 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
"save_slice.event_runtime_collection_record_count".to_string(),
|
"save_slice.event_runtime_collection_record_count".to_string(),
|
||||||
summary.live_record_count.to_string(),
|
summary.live_record_count.to_string(),
|
||||||
);
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"save_slice.event_runtime_collection_decoded_record_count".to_string(),
|
||||||
|
summary.decoded_record_count.to_string(),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"save_slice.event_runtime_collection_imported_runtime_record_count".to_string(),
|
||||||
|
imported_event_runtime_records.len().to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let save_profile = if let Some(profile) = &save_slice.profile {
|
let save_profile = if let Some(profile) = &save_slice.profile {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
|
|
@ -328,7 +365,7 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
metadata,
|
metadata,
|
||||||
companies: Vec::new(),
|
companies: Vec::new(),
|
||||||
packed_event_collection,
|
packed_event_collection,
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: imported_event_runtime_records,
|
||||||
candidate_availability,
|
candidate_availability,
|
||||||
special_conditions,
|
special_conditions,
|
||||||
service_state: RuntimeServiceState::default(),
|
service_state: RuntimeServiceState::default(),
|
||||||
|
|
@ -342,6 +379,193 @@ pub fn project_save_slice_to_runtime_state_import(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_packed_event_record_summary_from_smp(
|
||||||
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
|
) -> RuntimePackedEventRecordSummary {
|
||||||
|
RuntimePackedEventRecordSummary {
|
||||||
|
record_index: record.record_index,
|
||||||
|
live_entry_id: record.live_entry_id,
|
||||||
|
payload_offset: record.payload_offset,
|
||||||
|
payload_len: record.payload_len,
|
||||||
|
decode_status: record.decode_status.clone(),
|
||||||
|
trigger_kind: record.trigger_kind,
|
||||||
|
active: record.active,
|
||||||
|
marks_collection_dirty: record.marks_collection_dirty,
|
||||||
|
one_shot: record.one_shot,
|
||||||
|
text_bands: record
|
||||||
|
.text_bands
|
||||||
|
.iter()
|
||||||
|
.map(runtime_packed_event_text_band_summary_from_smp)
|
||||||
|
.collect(),
|
||||||
|
standalone_condition_row_count: record.standalone_condition_row_count,
|
||||||
|
grouped_effect_row_counts: record.grouped_effect_row_counts.clone(),
|
||||||
|
decoded_actions: record.decoded_actions.clone(),
|
||||||
|
executable_import_ready: record.executable_import_ready,
|
||||||
|
notes: record.notes.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_packed_event_text_band_summary_from_smp(
|
||||||
|
band: &SmpLoadedPackedEventTextBandSummary,
|
||||||
|
) -> RuntimePackedEventTextBandSummary {
|
||||||
|
RuntimePackedEventTextBandSummary {
|
||||||
|
label: band.label.clone(),
|
||||||
|
packed_len: band.packed_len,
|
||||||
|
present: band.present,
|
||||||
|
preview: band.preview.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smp_packed_record_to_runtime_event_record(
|
||||||
|
record: &SmpLoadedPackedEventRecordSummary,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> Option<Result<RuntimeEventRecord, String>> {
|
||||||
|
if !record.executable_import_ready {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids)
|
||||||
|
.and_then(|effects| {
|
||||||
|
let trigger_kind = record.trigger_kind.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"packed event record {} is missing trigger_kind",
|
||||||
|
record.live_entry_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let active = record.active.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"packed event record {} is missing active flag",
|
||||||
|
record.live_entry_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let marks_collection_dirty = record.marks_collection_dirty.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"packed event record {} is missing dirty flag",
|
||||||
|
record.live_entry_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let one_shot = record.one_shot.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"packed event record {} is missing one_shot flag",
|
||||||
|
record.live_entry_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(RuntimeEventRecordTemplate {
|
||||||
|
record_id: record.live_entry_id,
|
||||||
|
trigger_kind,
|
||||||
|
active,
|
||||||
|
marks_collection_dirty,
|
||||||
|
one_shot,
|
||||||
|
effects,
|
||||||
|
}
|
||||||
|
.into_runtime_record())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smp_runtime_effects_to_runtime_effects(
|
||||||
|
effects: &[RuntimeEffect],
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> Result<Vec<RuntimeEffect>, String> {
|
||||||
|
effects
|
||||||
|
.iter()
|
||||||
|
.map(|effect| smp_runtime_effect_to_runtime_effect(effect, known_company_ids))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smp_runtime_effect_to_runtime_effect(
|
||||||
|
effect: &RuntimeEffect,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> Result<RuntimeEffect, String> {
|
||||||
|
match effect {
|
||||||
|
RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag {
|
||||||
|
key: key.clone(),
|
||||||
|
value: *value,
|
||||||
|
}),
|
||||||
|
RuntimeEffect::AdjustCompanyCash { target, delta } => {
|
||||||
|
if company_target_supported_for_import(target, known_company_ids) {
|
||||||
|
Ok(RuntimeEffect::AdjustCompanyCash {
|
||||||
|
target: target.clone(),
|
||||||
|
delta: *delta,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err("packed company-cash effect requires unresolved company ids".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeEffect::AdjustCompanyDebt { target, delta } => {
|
||||||
|
if company_target_supported_for_import(target, known_company_ids) {
|
||||||
|
Ok(RuntimeEffect::AdjustCompanyDebt {
|
||||||
|
target: target.clone(),
|
||||||
|
delta: *delta,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err("packed company-debt effect requires unresolved company ids".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeEffect::SetCandidateAvailability { name, value } => {
|
||||||
|
Ok(RuntimeEffect::SetCandidateAvailability {
|
||||||
|
name: name.clone(),
|
||||||
|
value: *value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
RuntimeEffect::SetSpecialCondition { label, value } => {
|
||||||
|
Ok(RuntimeEffect::SetSpecialCondition {
|
||||||
|
label: label.clone(),
|
||||||
|
value: *value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord {
|
||||||
|
record: Box::new(smp_runtime_record_template_to_runtime(
|
||||||
|
record,
|
||||||
|
known_company_ids,
|
||||||
|
)?),
|
||||||
|
}),
|
||||||
|
RuntimeEffect::ActivateEventRecord { record_id } => {
|
||||||
|
Ok(RuntimeEffect::ActivateEventRecord {
|
||||||
|
record_id: *record_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
RuntimeEffect::DeactivateEventRecord { record_id } => {
|
||||||
|
Ok(RuntimeEffect::DeactivateEventRecord {
|
||||||
|
record_id: *record_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
RuntimeEffect::RemoveEventRecord { record_id } => Ok(RuntimeEffect::RemoveEventRecord {
|
||||||
|
record_id: *record_id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smp_runtime_record_template_to_runtime(
|
||||||
|
record: &RuntimeEventRecordTemplate,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> Result<RuntimeEventRecordTemplate, String> {
|
||||||
|
Ok(RuntimeEventRecordTemplate {
|
||||||
|
record_id: record.record_id,
|
||||||
|
trigger_kind: record.trigger_kind,
|
||||||
|
active: record.active,
|
||||||
|
marks_collection_dirty: record.marks_collection_dirty,
|
||||||
|
one_shot: record.one_shot,
|
||||||
|
effects: smp_runtime_effects_to_runtime_effects(&record.effects, known_company_ids)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn company_target_supported_for_import(
|
||||||
|
target: &crate::RuntimeCompanyTarget,
|
||||||
|
known_company_ids: &BTreeSet<u32>,
|
||||||
|
) -> bool {
|
||||||
|
match target {
|
||||||
|
crate::RuntimeCompanyTarget::AllActive => true,
|
||||||
|
crate::RuntimeCompanyTarget::Ids { ids } => {
|
||||||
|
!ids.is_empty()
|
||||||
|
&& ids
|
||||||
|
.iter()
|
||||||
|
.all(|company_id| known_company_ids.contains(company_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate_runtime_state_dump_document(
|
pub fn validate_runtime_state_dump_document(
|
||||||
document: &RuntimeStateDumpDocument,
|
document: &RuntimeStateDumpDocument,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
|
@ -397,6 +621,7 @@ pub fn load_runtime_state_import_from_str(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{StepCommand, execute_step_command};
|
||||||
|
|
||||||
fn state() -> RuntimeState {
|
fn state() -> RuntimeState {
|
||||||
RuntimeState {
|
RuntimeState {
|
||||||
|
|
@ -419,6 +644,47 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn packed_text_bands() -> Vec<crate::SmpLoadedPackedEventTextBandSummary> {
|
||||||
|
vec![
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "primary_text_band".to_string(),
|
||||||
|
packed_len: 5,
|
||||||
|
present: true,
|
||||||
|
preview: "Alpha".to_string(),
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "secondary_text_band_0".to_string(),
|
||||||
|
packed_len: 0,
|
||||||
|
present: false,
|
||||||
|
preview: "".to_string(),
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "secondary_text_band_1".to_string(),
|
||||||
|
packed_len: 0,
|
||||||
|
present: false,
|
||||||
|
preview: "".to_string(),
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "secondary_text_band_2".to_string(),
|
||||||
|
packed_len: 0,
|
||||||
|
present: false,
|
||||||
|
preview: "".to_string(),
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "secondary_text_band_3".to_string(),
|
||||||
|
packed_len: 0,
|
||||||
|
present: false,
|
||||||
|
preview: "".to_string(),
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: "secondary_text_band_4".to_string(),
|
||||||
|
packed_len: 0,
|
||||||
|
present: false,
|
||||||
|
preview: "".to_string(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loads_dump_document() {
|
fn loads_dump_document() {
|
||||||
let text = serde_json::to_string(&RuntimeStateDumpDocument {
|
let text = serde_json::to_string(&RuntimeStateDumpDocument {
|
||||||
|
|
@ -548,6 +814,61 @@ mod tests {
|
||||||
live_id_bound: 5,
|
live_id_bound: 5,
|
||||||
live_record_count: 3,
|
live_record_count: 3,
|
||||||
live_entry_ids: vec![1, 3, 5],
|
live_entry_ids: vec![1, 3, 5],
|
||||||
|
decoded_record_count: 0,
|
||||||
|
imported_runtime_record_count: 0,
|
||||||
|
records: vec![
|
||||||
|
crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 1,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["test".to_string()],
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 1,
|
||||||
|
live_entry_id: 3,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["test".to_string()],
|
||||||
|
},
|
||||||
|
crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 2,
|
||||||
|
live_entry_id: 5,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["test".to_string()],
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
notes: vec!["packed profile recovered".to_string()],
|
notes: vec!["packed profile recovered".to_string()],
|
||||||
};
|
};
|
||||||
|
|
@ -709,4 +1030,187 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(import.state.event_runtime_records.is_empty());
|
assert!(import.state.event_runtime_records.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn projects_executable_packed_records_into_runtime_and_services_follow_on() {
|
||||||
|
let save_slice = SmpLoadedSaveSlice {
|
||||||
|
file_extension_hint: Some("gms".to_string()),
|
||||||
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||||
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
trailer_family: None,
|
||||||
|
bridge_family: None,
|
||||||
|
profile: None,
|
||||||
|
candidate_availability_table: None,
|
||||||
|
special_conditions_table: None,
|
||||||
|
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||||
|
metadata_tag_offset: 0x7100,
|
||||||
|
records_tag_offset: 0x7200,
|
||||||
|
close_tag_offset: 0x7600,
|
||||||
|
packed_state_version: 0x3e9,
|
||||||
|
packed_state_version_hex: "0x000003e9".to_string(),
|
||||||
|
live_id_bound: 7,
|
||||||
|
live_record_count: 1,
|
||||||
|
live_entry_ids: vec![7],
|
||||||
|
decoded_record_count: 1,
|
||||||
|
imported_runtime_record_count: 1,
|
||||||
|
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 7,
|
||||||
|
payload_offset: Some(0x7202),
|
||||||
|
payload_len: Some(64),
|
||||||
|
decode_status: "executable".to_string(),
|
||||||
|
trigger_kind: Some(7),
|
||||||
|
active: Some(true),
|
||||||
|
marks_collection_dirty: Some(true),
|
||||||
|
one_shot: Some(false),
|
||||||
|
text_bands: packed_text_bands(),
|
||||||
|
standalone_condition_row_count: 1,
|
||||||
|
grouped_effect_row_counts: vec![0, 1, 0, 0],
|
||||||
|
decoded_actions: vec![
|
||||||
|
RuntimeEffect::SetWorldFlag {
|
||||||
|
key: "from_packed_root".to_string(),
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
RuntimeEffect::AppendEventRecord {
|
||||||
|
record: Box::new(RuntimeEventRecordTemplate {
|
||||||
|
record_id: 99,
|
||||||
|
trigger_kind: 0x0a,
|
||||||
|
active: true,
|
||||||
|
marks_collection_dirty: false,
|
||||||
|
one_shot: false,
|
||||||
|
effects: vec![RuntimeEffect::SetSpecialCondition {
|
||||||
|
label: "Imported Follow-On".to_string(),
|
||||||
|
value: 1,
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executable_import_ready: true,
|
||||||
|
notes: vec!["decoded test record".to_string()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
notes: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut import = project_save_slice_to_runtime_state_import(
|
||||||
|
&save_slice,
|
||||||
|
"packed-events-exec",
|
||||||
|
Some("test packed event import".to_string()),
|
||||||
|
)
|
||||||
|
.expect("save slice should project");
|
||||||
|
|
||||||
|
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.imported_runtime_record_count),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = execute_step_command(
|
||||||
|
&mut import.state,
|
||||||
|
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||||
|
)
|
||||||
|
.expect("trigger service should succeed");
|
||||||
|
|
||||||
|
assert_eq!(result.final_summary.event_runtime_record_count, 2);
|
||||||
|
assert_eq!(result.final_summary.total_event_record_service_count, 2);
|
||||||
|
assert_eq!(result.final_summary.total_trigger_dispatch_count, 2);
|
||||||
|
assert_eq!(result.final_summary.dirty_rerun_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
import.state.world_flags.get("from_packed_root"),
|
||||||
|
Some(&true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import.state.special_conditions.get("Imported Follow-On"),
|
||||||
|
Some(&1)
|
||||||
|
);
|
||||||
|
assert_eq!(import.state.event_runtime_records[0].service_count, 1);
|
||||||
|
assert_eq!(import.state.event_runtime_records[1].record_id, 99);
|
||||||
|
assert_eq!(import.state.event_runtime_records[1].service_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_parity_only_packed_records_out_of_runtime_event_records() {
|
||||||
|
let save_slice = SmpLoadedSaveSlice {
|
||||||
|
file_extension_hint: Some("gms".to_string()),
|
||||||
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||||
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
trailer_family: None,
|
||||||
|
bridge_family: None,
|
||||||
|
profile: None,
|
||||||
|
candidate_availability_table: None,
|
||||||
|
special_conditions_table: None,
|
||||||
|
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
||||||
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||||
|
mechanism_confidence: "grounded".to_string(),
|
||||||
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||||
|
metadata_tag_offset: 0x7100,
|
||||||
|
records_tag_offset: 0x7200,
|
||||||
|
close_tag_offset: 0x7600,
|
||||||
|
packed_state_version: 0x3e9,
|
||||||
|
packed_state_version_hex: "0x000003e9".to_string(),
|
||||||
|
live_id_bound: 7,
|
||||||
|
live_record_count: 1,
|
||||||
|
live_entry_ids: vec![7],
|
||||||
|
decoded_record_count: 1,
|
||||||
|
imported_runtime_record_count: 0,
|
||||||
|
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 7,
|
||||||
|
payload_offset: Some(0x7202),
|
||||||
|
payload_len: Some(48),
|
||||||
|
decode_status: "parity_only".to_string(),
|
||||||
|
trigger_kind: Some(7),
|
||||||
|
active: Some(true),
|
||||||
|
marks_collection_dirty: Some(false),
|
||||||
|
one_shot: Some(false),
|
||||||
|
text_bands: packed_text_bands(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
|
||||||
|
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
|
||||||
|
delta: 50,
|
||||||
|
}],
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["decoded but not importable".to_string()],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
notes: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let import = project_save_slice_to_runtime_state_import(
|
||||||
|
&save_slice,
|
||||||
|
"packed-events-parity-only",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("save slice should project");
|
||||||
|
|
||||||
|
assert!(import.state.event_runtime_records.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.decoded_record_count),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
import
|
||||||
|
.state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.imported_runtime_record_count),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ pub use pk4::{
|
||||||
};
|
};
|
||||||
pub use runtime::{
|
pub use runtime::{
|
||||||
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
|
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
|
||||||
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
|
||||||
|
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState,
|
||||||
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
|
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
|
||||||
};
|
};
|
||||||
pub use smp::{
|
pub use smp::{
|
||||||
|
|
@ -38,7 +39,8 @@ pub use smp::{
|
||||||
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
|
SmpAlignedRuntimeRuleBandProbe, SmpAsciiPreview, SmpClassicPackedProfileBlock,
|
||||||
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
|
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
|
||||||
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
|
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
|
||||||
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, SmpLoadedProfile,
|
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary,
|
||||||
|
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile,
|
||||||
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
|
||||||
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
|
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
|
||||||
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
|
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,51 @@ pub struct RuntimePackedEventCollectionSummary {
|
||||||
pub live_id_bound: u32,
|
pub live_id_bound: u32,
|
||||||
pub live_record_count: usize,
|
pub live_record_count: usize,
|
||||||
pub live_entry_ids: Vec<u32>,
|
pub live_entry_ids: Vec<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub decoded_record_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub imported_runtime_record_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub records: Vec<RuntimePackedEventRecordSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimePackedEventRecordSummary {
|
||||||
|
pub record_index: usize,
|
||||||
|
pub live_entry_id: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub payload_offset: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub payload_len: Option<usize>,
|
||||||
|
pub decode_status: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub trigger_kind: Option<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub active: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub marks_collection_dirty: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub one_shot: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub text_bands: Vec<RuntimePackedEventTextBandSummary>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub standalone_condition_row_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub grouped_effect_row_counts: Vec<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub decoded_actions: Vec<RuntimeEffect>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub executable_import_ready: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimePackedEventTextBandSummary {
|
||||||
|
pub label: String,
|
||||||
|
pub packed_len: usize,
|
||||||
|
pub present: bool,
|
||||||
|
pub preview: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeEventRecordTemplate {
|
impl RuntimeEventRecordTemplate {
|
||||||
|
|
@ -271,9 +316,37 @@ impl RuntimeState {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if summary.live_record_count != summary.records.len() {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.live_record_count must match records length"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let decoded_record_count = summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.decode_status != "unsupported_framing")
|
||||||
|
.count();
|
||||||
|
if summary.decoded_record_count != decoded_record_count {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.decoded_record_count must match decoded records"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let executable_import_ready_count = summary
|
||||||
|
.records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.executable_import_ready)
|
||||||
|
.count();
|
||||||
|
if summary.imported_runtime_record_count > executable_import_ready_count {
|
||||||
|
return Err(
|
||||||
|
"packed_event_collection.imported_runtime_record_count must not exceed executable-import-ready records"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut previous_id = None;
|
let mut previous_id = None;
|
||||||
for entry_id in &summary.live_entry_ids {
|
for (record_index, entry_id) in summary.live_entry_ids.iter().enumerate() {
|
||||||
if *entry_id == 0 {
|
if *entry_id == 0 {
|
||||||
return Err(
|
return Err(
|
||||||
"packed_event_collection.live_entry_ids must not contain id 0".to_string(),
|
"packed_event_collection.live_entry_ids must not contain id 0".to_string(),
|
||||||
|
|
@ -292,6 +365,35 @@ impl RuntimeState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
previous_id = Some(*entry_id);
|
previous_id = Some(*entry_id);
|
||||||
|
|
||||||
|
let record = &summary.records[record_index];
|
||||||
|
if record.live_entry_id != *entry_id {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].live_entry_id must match live_entry_ids"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if record.record_index != record_index {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].record_index must match position"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if record.decode_status.trim().is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].decode_status must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if record.grouped_effect_row_counts.len() != 4 {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for band in &record.text_bands {
|
||||||
|
if band.label.trim().is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"packed_event_collection.records[{record_index}].text_bands contains an empty label"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -647,6 +749,44 @@ mod tests {
|
||||||
live_id_bound: 4,
|
live_id_bound: 4,
|
||||||
live_record_count: 2,
|
live_record_count: 2,
|
||||||
live_entry_ids: vec![3, 3],
|
live_entry_ids: vec![3, 3],
|
||||||
|
decoded_record_count: 0,
|
||||||
|
imported_runtime_record_count: 0,
|
||||||
|
records: vec![
|
||||||
|
RuntimePackedEventRecordSummary {
|
||||||
|
record_index: 0,
|
||||||
|
live_entry_id: 3,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["test".to_string()],
|
||||||
|
},
|
||||||
|
RuntimePackedEventRecordSummary {
|
||||||
|
record_index: 1,
|
||||||
|
live_entry_id: 3,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec!["test".to_string()],
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
event_runtime_records: Vec::new(),
|
event_runtime_records: Vec::new(),
|
||||||
candidate_availability: BTreeMap::new(),
|
candidate_availability: BTreeMap::new(),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ use std::path::Path;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::{RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate};
|
||||||
|
|
||||||
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
||||||
const PREAMBLE_U32_WORD_COUNT: usize = 16;
|
const PREAMBLE_U32_WORD_COUNT: usize = 16;
|
||||||
const MIN_ASCII_RUN_LEN: usize = 8;
|
const MIN_ASCII_RUN_LEN: usize = 8;
|
||||||
|
|
@ -86,6 +88,17 @@ const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
|
||||||
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
||||||
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
||||||
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
||||||
|
const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
|
||||||
|
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
|
||||||
|
const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1";
|
||||||
|
const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [
|
||||||
|
"primary_text_band",
|
||||||
|
"secondary_text_band_0",
|
||||||
|
"secondary_text_band_1",
|
||||||
|
"secondary_text_band_2",
|
||||||
|
"secondary_text_band_3",
|
||||||
|
"secondary_text_band_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 =
|
||||||
|
|
@ -1189,6 +1202,51 @@ pub struct SmpLoadedEventRuntimeCollectionSummary {
|
||||||
pub live_id_bound: u32,
|
pub live_id_bound: u32,
|
||||||
pub live_record_count: usize,
|
pub live_record_count: usize,
|
||||||
pub live_entry_ids: Vec<u32>,
|
pub live_entry_ids: Vec<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub decoded_record_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub imported_runtime_record_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub records: Vec<SmpLoadedPackedEventRecordSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SmpLoadedPackedEventRecordSummary {
|
||||||
|
pub record_index: usize,
|
||||||
|
pub live_entry_id: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub payload_offset: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub payload_len: Option<usize>,
|
||||||
|
pub decode_status: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub trigger_kind: Option<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub active: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub marks_collection_dirty: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub one_shot: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub standalone_condition_row_count: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub grouped_effect_row_counts: Vec<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub decoded_actions: Vec<RuntimeEffect>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub executable_import_ready: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SmpLoadedPackedEventTextBandSummary {
|
||||||
|
pub label: String,
|
||||||
|
pub packed_len: usize,
|
||||||
|
pub present: bool,
|
||||||
|
pub preview: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -1431,6 +1489,20 @@ fn parse_event_runtime_collection_summary(
|
||||||
if live_entry_ids.len() != live_record_count {
|
if live_entry_ids.len() != live_record_count {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let records_payload = bytes.get(records_tag_offset + 2..close_tag_offset)?;
|
||||||
|
let records = parse_event_runtime_record_summaries(
|
||||||
|
records_payload,
|
||||||
|
records_tag_offset + 2,
|
||||||
|
&live_entry_ids,
|
||||||
|
);
|
||||||
|
let decoded_record_count = records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.decode_status != "unsupported_framing")
|
||||||
|
.count();
|
||||||
|
let imported_runtime_record_count = records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.executable_import_ready)
|
||||||
|
.count();
|
||||||
|
|
||||||
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||||
source_kind: "packed-event-runtime-collection".to_string(),
|
source_kind: "packed-event-runtime-collection".to_string(),
|
||||||
|
|
@ -1450,6 +1522,9 @@ fn parse_event_runtime_collection_summary(
|
||||||
live_id_bound,
|
live_id_bound,
|
||||||
live_record_count,
|
live_record_count,
|
||||||
live_entry_ids,
|
live_entry_ids,
|
||||||
|
decoded_record_count,
|
||||||
|
imported_runtime_record_count,
|
||||||
|
records,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1491,6 +1566,313 @@ fn decode_live_entry_ids_with_mapping(
|
||||||
Some(live_entry_ids)
|
Some(live_entry_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_event_runtime_record_summaries(
|
||||||
|
records_payload: &[u8],
|
||||||
|
records_payload_offset: usize,
|
||||||
|
live_entry_ids: &[u32],
|
||||||
|
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
||||||
|
try_parse_synthetic_event_runtime_record_summaries(
|
||||||
|
records_payload,
|
||||||
|
records_payload_offset,
|
||||||
|
live_entry_ids,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
build_unsupported_event_runtime_record_summaries(
|
||||||
|
live_entry_ids,
|
||||||
|
"0x4e9a payload did not match the current packed-event record decode harness",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_synthetic_event_runtime_record_summaries(
|
||||||
|
records_payload: &[u8],
|
||||||
|
records_payload_offset: usize,
|
||||||
|
live_entry_ids: &[u32],
|
||||||
|
) -> Option<Vec<SmpLoadedPackedEventRecordSummary>> {
|
||||||
|
if !records_payload.starts_with(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC.len();
|
||||||
|
let mut records = Vec::with_capacity(live_entry_ids.len());
|
||||||
|
for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() {
|
||||||
|
let record_len = usize::try_from(read_u32_at(records_payload, cursor)?).ok()?;
|
||||||
|
cursor += 4;
|
||||||
|
let record_body = records_payload.get(cursor..cursor + record_len)?;
|
||||||
|
records.push(parse_synthetic_event_runtime_record_summary(
|
||||||
|
record_body,
|
||||||
|
records_payload_offset + cursor,
|
||||||
|
record_index,
|
||||||
|
live_entry_id,
|
||||||
|
)?);
|
||||||
|
cursor += record_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor != records_payload.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_synthetic_event_runtime_record_summary(
|
||||||
|
record_body: &[u8],
|
||||||
|
payload_offset: usize,
|
||||||
|
record_index: usize,
|
||||||
|
live_entry_id: u32,
|
||||||
|
) -> Option<SmpLoadedPackedEventRecordSummary> {
|
||||||
|
if !record_body.starts_with(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = PACKED_EVENT_RECORD_SYNTHETIC_MAGIC.len();
|
||||||
|
let trigger_kind = read_u8_at(record_body, cursor)?;
|
||||||
|
cursor += 1;
|
||||||
|
let flags = read_u8_at(record_body, cursor)?;
|
||||||
|
cursor += 1;
|
||||||
|
let standalone_condition_row_count = usize::from(read_u8_at(record_body, cursor)?);
|
||||||
|
cursor += 1;
|
||||||
|
let action_count = usize::from(read_u8_at(record_body, cursor)?);
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
let mut grouped_effect_row_counts = Vec::with_capacity(4);
|
||||||
|
for _ in 0..4 {
|
||||||
|
grouped_effect_row_counts.push(usize::from(read_u8_at(record_body, cursor)?));
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len());
|
||||||
|
for label in PACKED_EVENT_TEXT_BAND_LABELS {
|
||||||
|
let packed_len = usize::from(read_u16_at(record_body, cursor)?);
|
||||||
|
cursor += 2;
|
||||||
|
let band_bytes = record_body.get(cursor..cursor + packed_len)?;
|
||||||
|
cursor += packed_len;
|
||||||
|
text_bands.push(SmpLoadedPackedEventTextBandSummary {
|
||||||
|
label: label.to_string(),
|
||||||
|
packed_len,
|
||||||
|
present: packed_len != 0,
|
||||||
|
preview: ascii_preview(band_bytes),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut decoded_actions = Vec::with_capacity(action_count);
|
||||||
|
for _ in 0..action_count {
|
||||||
|
decoded_actions.push(parse_synthetic_packed_event_action(
|
||||||
|
record_body,
|
||||||
|
&mut cursor,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor != record_body.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let executable_import_ready = decoded_actions
|
||||||
|
.iter()
|
||||||
|
.all(runtime_effect_supported_for_save_import);
|
||||||
|
|
||||||
|
Some(SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index,
|
||||||
|
live_entry_id,
|
||||||
|
payload_offset: Some(payload_offset),
|
||||||
|
payload_len: Some(record_body.len()),
|
||||||
|
decode_status: if executable_import_ready {
|
||||||
|
"executable".to_string()
|
||||||
|
} else {
|
||||||
|
"parity_only".to_string()
|
||||||
|
},
|
||||||
|
trigger_kind: Some(trigger_kind),
|
||||||
|
active: Some(flags & 0x01 != 0),
|
||||||
|
marks_collection_dirty: Some(flags & 0x02 != 0),
|
||||||
|
one_shot: Some(flags & 0x04 != 0),
|
||||||
|
text_bands,
|
||||||
|
standalone_condition_row_count,
|
||||||
|
grouped_effect_row_counts,
|
||||||
|
decoded_actions,
|
||||||
|
executable_import_ready,
|
||||||
|
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
|
||||||
|
let opcode = read_u8_at(bytes, *cursor)?;
|
||||||
|
*cursor += 1;
|
||||||
|
match opcode {
|
||||||
|
0x01 => {
|
||||||
|
let key = parse_len_prefixed_string(bytes, cursor)?;
|
||||||
|
let value = read_u8_at(bytes, *cursor)? != 0;
|
||||||
|
*cursor += 1;
|
||||||
|
Some(RuntimeEffect::SetWorldFlag { key, value })
|
||||||
|
}
|
||||||
|
0x02 => {
|
||||||
|
let target = parse_synthetic_company_target(bytes, cursor)?;
|
||||||
|
let delta = read_i64_at(bytes, *cursor)?;
|
||||||
|
*cursor += 8;
|
||||||
|
Some(RuntimeEffect::AdjustCompanyCash { target, delta })
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
let target = parse_synthetic_company_target(bytes, cursor)?;
|
||||||
|
let delta = read_i64_at(bytes, *cursor)?;
|
||||||
|
*cursor += 8;
|
||||||
|
Some(RuntimeEffect::AdjustCompanyDebt { target, delta })
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
let name = parse_len_prefixed_string(bytes, cursor)?;
|
||||||
|
let value = read_u32_at(bytes, *cursor)?;
|
||||||
|
*cursor += 4;
|
||||||
|
Some(RuntimeEffect::SetCandidateAvailability { name, value })
|
||||||
|
}
|
||||||
|
0x05 => {
|
||||||
|
let label = parse_len_prefixed_string(bytes, cursor)?;
|
||||||
|
let value = read_u32_at(bytes, *cursor)?;
|
||||||
|
*cursor += 4;
|
||||||
|
Some(RuntimeEffect::SetSpecialCondition { label, value })
|
||||||
|
}
|
||||||
|
0x06 => {
|
||||||
|
let template_len = usize::try_from(read_u32_at(bytes, *cursor)?).ok()?;
|
||||||
|
*cursor += 4;
|
||||||
|
let template_bytes = bytes.get(*cursor..*cursor + template_len)?;
|
||||||
|
let record = parse_synthetic_event_runtime_record_template(template_bytes)?;
|
||||||
|
*cursor += template_len;
|
||||||
|
Some(RuntimeEffect::AppendEventRecord {
|
||||||
|
record: Box::new(record),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
0x07 => {
|
||||||
|
let record_id = read_u32_at(bytes, *cursor)?;
|
||||||
|
*cursor += 4;
|
||||||
|
Some(RuntimeEffect::ActivateEventRecord { record_id })
|
||||||
|
}
|
||||||
|
0x08 => {
|
||||||
|
let record_id = read_u32_at(bytes, *cursor)?;
|
||||||
|
*cursor += 4;
|
||||||
|
Some(RuntimeEffect::DeactivateEventRecord { record_id })
|
||||||
|
}
|
||||||
|
0x09 => {
|
||||||
|
let record_id = read_u32_at(bytes, *cursor)?;
|
||||||
|
*cursor += 4;
|
||||||
|
Some(RuntimeEffect::RemoveEventRecord { record_id })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_synthetic_event_runtime_record_template(
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> Option<RuntimeEventRecordTemplate> {
|
||||||
|
if !bytes.starts_with(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC.len();
|
||||||
|
let record_id = read_u32_at(bytes, cursor)?;
|
||||||
|
cursor += 4;
|
||||||
|
let trigger_kind = read_u8_at(bytes, cursor)?;
|
||||||
|
cursor += 1;
|
||||||
|
let flags = read_u8_at(bytes, cursor)?;
|
||||||
|
cursor += 1;
|
||||||
|
let action_count = usize::from(read_u8_at(bytes, cursor)?);
|
||||||
|
cursor += 1;
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
let mut effects = Vec::with_capacity(action_count);
|
||||||
|
for _ in 0..action_count {
|
||||||
|
effects.push(parse_synthetic_packed_event_action(bytes, &mut cursor)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor != bytes.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(RuntimeEventRecordTemplate {
|
||||||
|
record_id,
|
||||||
|
trigger_kind,
|
||||||
|
active: flags & 0x01 != 0,
|
||||||
|
marks_collection_dirty: flags & 0x02 != 0,
|
||||||
|
one_shot: flags & 0x04 != 0,
|
||||||
|
effects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_synthetic_company_target(
|
||||||
|
bytes: &[u8],
|
||||||
|
cursor: &mut usize,
|
||||||
|
) -> Option<RuntimeCompanyTarget> {
|
||||||
|
let target_kind = read_u8_at(bytes, *cursor)?;
|
||||||
|
*cursor += 1;
|
||||||
|
match target_kind {
|
||||||
|
0x00 => Some(RuntimeCompanyTarget::AllActive),
|
||||||
|
0x01 => {
|
||||||
|
let count = usize::from(read_u8_at(bytes, *cursor)?);
|
||||||
|
*cursor += 1;
|
||||||
|
let mut ids = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
ids.push(read_u32_at(bytes, *cursor)?);
|
||||||
|
*cursor += 4;
|
||||||
|
}
|
||||||
|
Some(RuntimeCompanyTarget::Ids { ids })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<String> {
|
||||||
|
let len = usize::from(read_u8_at(bytes, *cursor)?);
|
||||||
|
*cursor += 1;
|
||||||
|
let text_bytes = bytes.get(*cursor..*cursor + len)?;
|
||||||
|
*cursor += len;
|
||||||
|
Some(String::from_utf8_lossy(text_bytes).into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
||||||
|
match effect {
|
||||||
|
RuntimeEffect::SetWorldFlag { .. }
|
||||||
|
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||||
|
| RuntimeEffect::SetSpecialCondition { .. }
|
||||||
|
| RuntimeEffect::ActivateEventRecord { .. }
|
||||||
|
| RuntimeEffect::DeactivateEventRecord { .. }
|
||||||
|
| RuntimeEffect::RemoveEventRecord { .. } => true,
|
||||||
|
RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||||
|
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||||
|
matches!(target, RuntimeCompanyTarget::AllActive)
|
||||||
|
}
|
||||||
|
RuntimeEffect::AppendEventRecord { record } => record
|
||||||
|
.effects
|
||||||
|
.iter()
|
||||||
|
.all(runtime_effect_supported_for_save_import),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_unsupported_event_runtime_record_summaries(
|
||||||
|
live_entry_ids: &[u32],
|
||||||
|
note: &str,
|
||||||
|
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
||||||
|
live_entry_ids
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
.map(
|
||||||
|
|(record_index, live_entry_id)| SmpLoadedPackedEventRecordSummary {
|
||||||
|
record_index,
|
||||||
|
live_entry_id,
|
||||||
|
payload_offset: None,
|
||||||
|
payload_len: None,
|
||||||
|
decode_status: "unsupported_framing".to_string(),
|
||||||
|
trigger_kind: None,
|
||||||
|
active: None,
|
||||||
|
marks_collection_dirty: None,
|
||||||
|
one_shot: None,
|
||||||
|
text_bands: Vec::new(),
|
||||||
|
standalone_condition_row_count: 0,
|
||||||
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||||
|
decoded_actions: Vec::new(),
|
||||||
|
executable_import_ready: false,
|
||||||
|
notes: vec![note.to_string()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
@ -4846,11 +5228,27 @@ fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec<u32> {
|
||||||
words
|
words
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_u8_at(bytes: &[u8], offset: usize) -> Option<u8> {
|
||||||
|
bytes.get(offset).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16_at(bytes: &[u8], offset: usize) -> Option<u16> {
|
||||||
|
let chunk = bytes.get(offset..offset + 2)?;
|
||||||
|
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
|
fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
|
||||||
let chunk = bytes.get(offset..offset + 4)?;
|
let chunk = bytes.get(offset..offset + 4)?;
|
||||||
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> {
|
||||||
|
let chunk = bytes.get(offset..offset + 8)?;
|
||||||
|
Some(i64::from_le_bytes([
|
||||||
|
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
fn probable_normal_f32_string(value: u32) -> Option<String> {
|
fn probable_normal_f32_string(value: u32) -> Option<String> {
|
||||||
let exponent = (value >> 23) & 0xff;
|
let exponent = (value >> 23) & 0xff;
|
||||||
if exponent == 0 || exponent == 0xff {
|
if exponent == 0 || exponent == 0xff {
|
||||||
|
|
@ -5611,7 +6009,10 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_recipe_token_layouts() {
|
fn classifies_recipe_token_layouts() {
|
||||||
assert_eq!(classify_recipe_token_layout(0x00000000), "zero");
|
assert_eq!(classify_recipe_token_layout(0x00000000), "zero");
|
||||||
assert_eq!(classify_recipe_token_layout(0x72470000), "high16-ascii-stem");
|
assert_eq!(
|
||||||
|
classify_recipe_token_layout(0x72470000),
|
||||||
|
"high16-ascii-stem"
|
||||||
|
);
|
||||||
assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric");
|
assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric");
|
||||||
assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker");
|
assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker");
|
||||||
}
|
}
|
||||||
|
|
@ -5638,9 +6039,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_recipe_runtime_import_branches() {
|
fn classifies_recipe_runtime_import_branches() {
|
||||||
assert_eq!(classify_recipe_runtime_import_branch(0), "zero-mode-skipped");
|
assert_eq!(
|
||||||
assert_eq!(classify_recipe_runtime_import_branch(1), "mode1-demand-branch");
|
classify_recipe_runtime_import_branch(0),
|
||||||
assert_eq!(classify_recipe_runtime_import_branch(3), "mode3-dual-branch");
|
"zero-mode-skipped"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_recipe_runtime_import_branch(1),
|
||||||
|
"mode1-demand-branch"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_recipe_runtime_import_branch(3),
|
||||||
|
"mode3-dual-branch"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_recipe_runtime_import_branch(0x00110000),
|
classify_recipe_runtime_import_branch(0x00110000),
|
||||||
"nonzero-supply-branch"
|
"nonzero-supply-branch"
|
||||||
|
|
@ -6205,6 +6615,189 @@ mod tests {
|
||||||
assert_eq!(summary.live_record_count, 3);
|
assert_eq!(summary.live_record_count, 3);
|
||||||
assert_eq!(summary.live_entry_ids, vec![1, 3, 5]);
|
assert_eq!(summary.live_entry_ids, vec![1, 3, 5]);
|
||||||
assert_eq!(summary.records_tag_offset, 96);
|
assert_eq!(summary.records_tag_offset, 96);
|
||||||
|
assert_eq!(summary.decoded_record_count, 0);
|
||||||
|
assert_eq!(summary.records.len(), 3);
|
||||||
|
assert_eq!(summary.records[0].decode_status, "unsupported_framing");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_len_prefixed_string(text: &str) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::with_capacity(1 + text.len());
|
||||||
|
bytes.push(text.len() as u8);
|
||||||
|
bytes.extend_from_slice(text.as_bytes());
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_template(
|
||||||
|
record_id: u32,
|
||||||
|
trigger_kind: u8,
|
||||||
|
flags: u8,
|
||||||
|
actions: &[Vec<u8>],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC);
|
||||||
|
bytes.extend_from_slice(&record_id.to_le_bytes());
|
||||||
|
bytes.push(trigger_kind);
|
||||||
|
bytes.push(flags);
|
||||||
|
bytes.push(actions.len() as u8);
|
||||||
|
bytes.push(0);
|
||||||
|
for action in actions {
|
||||||
|
bytes.extend_from_slice(action);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_action_set_world_flag(key: &str, value: bool) -> Vec<u8> {
|
||||||
|
let mut bytes = vec![0x01];
|
||||||
|
bytes.extend_from_slice(&encode_len_prefixed_string(key));
|
||||||
|
bytes.push(u8::from(value));
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_action_set_special_condition(label: &str, value: u32) -> Vec<u8> {
|
||||||
|
let mut bytes = vec![0x05];
|
||||||
|
bytes.extend_from_slice(&encode_len_prefixed_string(label));
|
||||||
|
bytes.extend_from_slice(&value.to_le_bytes());
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_action_adjust_company_cash_ids(ids: &[u32], delta: i64) -> Vec<u8> {
|
||||||
|
let mut bytes = vec![0x02, 0x01, ids.len() as u8];
|
||||||
|
for id in ids {
|
||||||
|
bytes.extend_from_slice(&id.to_le_bytes());
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&delta.to_le_bytes());
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_action_append_template(template: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut bytes = vec![0x06];
|
||||||
|
bytes.extend_from_slice(&(template.len() as u32).to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&template);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_synthetic_event_record(
|
||||||
|
trigger_kind: u8,
|
||||||
|
flags: u8,
|
||||||
|
standalone_count: u8,
|
||||||
|
grouped_counts: [u8; 4],
|
||||||
|
text_bands: [&[u8]; 6],
|
||||||
|
actions: &[Vec<u8>],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
bytes.extend_from_slice(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC);
|
||||||
|
bytes.push(trigger_kind);
|
||||||
|
bytes.push(flags);
|
||||||
|
bytes.push(standalone_count);
|
||||||
|
bytes.push(actions.len() as u8);
|
||||||
|
bytes.extend_from_slice(&grouped_counts);
|
||||||
|
for band in text_bands {
|
||||||
|
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
|
||||||
|
bytes.extend_from_slice(band);
|
||||||
|
}
|
||||||
|
for action in actions {
|
||||||
|
bytes.extend_from_slice(action);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_synthetic_event_runtime_record_summaries_and_actions() {
|
||||||
|
let append_template = encode_template(
|
||||||
|
99,
|
||||||
|
0x0a,
|
||||||
|
0x01,
|
||||||
|
&[encode_action_set_special_condition("Imported Follow-On", 1)],
|
||||||
|
);
|
||||||
|
let record_body = build_synthetic_event_record(
|
||||||
|
7,
|
||||||
|
0x03,
|
||||||
|
1,
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[b"Alpha", b"", b"", b"", b"", b""],
|
||||||
|
&[
|
||||||
|
encode_action_set_world_flag("from_packed_root", true),
|
||||||
|
encode_action_append_template(append_template),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
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, 1, 1, 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(&[0x00, 0x00]);
|
||||||
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
||||||
|
bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&record_body);
|
||||||
|
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.decoded_record_count, 1);
|
||||||
|
assert_eq!(summary.imported_runtime_record_count, 1);
|
||||||
|
assert_eq!(summary.records.len(), 1);
|
||||||
|
assert_eq!(summary.records[0].decode_status, "executable");
|
||||||
|
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
|
||||||
|
assert_eq!(summary.records[0].standalone_condition_row_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
summary.records[0].grouped_effect_row_counts,
|
||||||
|
vec![0, 1, 0, 0]
|
||||||
|
);
|
||||||
|
assert_eq!(summary.records[0].decoded_actions.len(), 2);
|
||||||
|
match &summary.records[0].decoded_actions[1] {
|
||||||
|
RuntimeEffect::AppendEventRecord { record } => {
|
||||||
|
assert_eq!(record.record_id, 99);
|
||||||
|
assert_eq!(record.trigger_kind, 0x0a);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected decoded action: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_company_targeted_synthetic_records_as_parity_only() {
|
||||||
|
let record_body = build_synthetic_event_record(
|
||||||
|
8,
|
||||||
|
0x01,
|
||||||
|
0,
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[b"", b"", b"", b"", b"", b""],
|
||||||
|
&[encode_action_adjust_company_cash_ids(&[7], 25)],
|
||||||
|
);
|
||||||
|
|
||||||
|
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, 1, 1, 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(&[0x00, 0x00]);
|
||||||
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
||||||
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
||||||
|
bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&record_body);
|
||||||
|
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.decoded_record_count, 1);
|
||||||
|
assert_eq!(summary.imported_runtime_record_count, 0);
|
||||||
|
assert_eq!(summary.records[0].decode_status, "parity_only");
|
||||||
|
assert!(!summary.records[0].executable_import_ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -6267,6 +6860,9 @@ mod tests {
|
||||||
live_id_bound: 5,
|
live_id_bound: 5,
|
||||||
live_record_count: 3,
|
live_record_count: 3,
|
||||||
live_entry_ids: vec![1, 3, 5],
|
live_entry_ids: vec![1, 3, 5],
|
||||||
|
decoded_record_count: 0,
|
||||||
|
imported_runtime_record_count: 0,
|
||||||
|
records: build_unsupported_event_runtime_record_summaries(&[1, 3, 5], "test summary"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ pub struct RuntimeSummary {
|
||||||
pub company_count: usize,
|
pub company_count: usize,
|
||||||
pub packed_event_collection_present: bool,
|
pub packed_event_collection_present: bool,
|
||||||
pub packed_event_record_count: usize,
|
pub packed_event_record_count: usize,
|
||||||
|
pub packed_event_decoded_record_count: usize,
|
||||||
|
pub packed_event_imported_runtime_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,
|
||||||
|
|
@ -117,6 +119,16 @@ impl RuntimeSummary {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|summary| summary.live_record_count)
|
.map(|summary| summary.live_record_count)
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
|
packed_event_decoded_record_count: state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.decoded_record_count)
|
||||||
|
.unwrap_or(0),
|
||||||
|
packed_event_imported_runtime_record_count: state
|
||||||
|
.packed_event_collection
|
||||||
|
.as_ref()
|
||||||
|
.map(|summary| summary.imported_runtime_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
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,17 @@ Current local tool status:
|
||||||
The atlas milestone is broad enough that the next implementation focus has already shifted downward
|
The atlas milestone is broad enough that the next implementation focus has already shifted downward
|
||||||
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
|
||||||
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
|
||||||
state-diff tooling, and initial persistence surfaces.
|
state-diff tooling, and a packed-event persistence bridge that now reaches per-record summaries and
|
||||||
|
selective executable import.
|
||||||
|
|
||||||
The highest-value next passes are now:
|
The highest-value next passes are now:
|
||||||
|
|
||||||
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
|
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
|
||||||
avoid shell-first implementation bets
|
avoid shell-first implementation bets
|
||||||
- deepen the `.smp` event bridge from collection-level structural summaries toward per-record
|
- deepen captured-runtime and round-trip fixture coverage on top of the packed-event bridge that now
|
||||||
packed-body coverage
|
exists
|
||||||
- deepen captured-runtime and round-trip fixture coverage on top of the existing runtime CLI and
|
- widen packed-event target-family coverage only where static evidence is strong enough to support
|
||||||
fixture surfaces
|
deterministic executable import
|
||||||
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
|
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
|
||||||
environment
|
environment
|
||||||
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice
|
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,12 @@ Implemented today:
|
||||||
- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the
|
- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the
|
||||||
CLI and fixture layers
|
CLI and fixture layers
|
||||||
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
|
||||||
service, snapshot-backed inputs, and normalized state-fragment assertions
|
service, snapshot-backed inputs, normalized state-fragment assertions, and imported packed-event
|
||||||
|
execution
|
||||||
|
|
||||||
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
|
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
|
||||||
the `.smp` event-collection structural bridge across inspection, save-slice loading, import, and
|
captured-runtime depth plus wider packed-event target-family coverage, not another persistence
|
||||||
snapshot-backed fixtures.
|
scaffold pass.
|
||||||
|
|
||||||
## Why This Boundary
|
## Why This Boundary
|
||||||
|
|
||||||
|
|
@ -215,10 +216,12 @@ 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 packed event-collection summary now survives into loaded save slices and projected runtime
|
- the packed event-collection bridge now carries per-record summaries into loaded save slices,
|
||||||
snapshots, but per-record packed bodies are still deferred
|
projected runtime snapshots, normalized diffs, and fixtures
|
||||||
- the remaining gap is broader captured-runtime and round-trip fixture depth plus deeper `.smp`
|
- the first decoded packed-event subset can now import into executable runtime records when the
|
||||||
event-body decoding, not the first persistence surface
|
decoded actions fit the current normalized runtime-effect model
|
||||||
|
- the remaining gap is broader captured-runtime and round-trip fixture depth plus wider packed
|
||||||
|
target-family coverage, not first-pass packed-event decode
|
||||||
|
|
||||||
### Milestone 4: Domain Expansion
|
### Milestone 4: Domain Expansion
|
||||||
|
|
||||||
|
|
@ -347,8 +350,8 @@ The currently implemented normalized runtime surface is:
|
||||||
`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
|
||||||
normalized runtime-effect vocabulary with staged event-record mutation
|
normalized runtime-effect vocabulary with staged event-record mutation
|
||||||
- save-side inspection and partial state projection for `.smp` inputs, including the structural
|
- save-side inspection and partial state projection for `.smp` inputs, including per-record packed
|
||||||
packed event-collection summary
|
event summaries and selective executable import
|
||||||
|
|
||||||
Checked-in fixture families already include:
|
Checked-in fixture families already include:
|
||||||
|
|
||||||
|
|
@ -360,33 +363,35 @@ Checked-in fixture families already include:
|
||||||
|
|
||||||
## Next Slice
|
## Next Slice
|
||||||
|
|
||||||
The recommended next implementation slice is deeper `.smp` event persistence, starting from the
|
The recommended next implementation slice is broader captured-runtime depth on top of the packed
|
||||||
structural bridge that already exists today.
|
event bridge that now exists today.
|
||||||
|
|
||||||
Target behavior:
|
Target behavior:
|
||||||
|
|
||||||
- keep carrying the packed event collection across `inspect-smp`, `load-save-slice`,
|
- keep the packed event bridge grounded against real captured save inputs rather than only synthetic
|
||||||
`import-save-state`, snapshots, diffs, and fixtures
|
parser tests and snapshot fixtures
|
||||||
- deepen that bridge from collection structure into per-record packed-body summaries
|
- expand the executable import subset beyond the current direct-state and follow-on lanes only when
|
||||||
- preserve the separation between parity-shaped packed state and executable normalized runtime state
|
target resolution and field semantics are statically grounded enough to preserve headless
|
||||||
until the packed layout is better decoded
|
determinism
|
||||||
|
- keep preserving unsupported packed rows as parity summaries instead of guessing executable meaning
|
||||||
|
|
||||||
Public-model additions for that slice:
|
Public-model additions for that slice:
|
||||||
|
|
||||||
- packed per-record event summary types on the `.smp` side
|
- additional captured-save fixture material for packed event collections
|
||||||
- optional runtime-side parity summaries for imported packed event records
|
- wider target-family summaries only where imported execution can be justified by current static
|
||||||
- no new executable `RuntimeEffect` variants by default in that slice
|
evidence
|
||||||
|
- no shell queue/modal behavior in the runtime core
|
||||||
|
|
||||||
Fixture work for that slice:
|
Fixture work for that slice:
|
||||||
|
|
||||||
- one or more snapshot-backed fixtures that prove imported packed event state survives normalize and
|
- captured `.smp` or save-slice-backed fixtures that prove real packed event records survive import
|
||||||
diff paths
|
and diff paths
|
||||||
- synthetic report/save-slice tests that lock the first per-record packed-body parse shape
|
- regression fixtures that lock the current selective executable import boundary
|
||||||
- state-fragment assertions that lock imported collection ids, version, and record counts
|
- state-fragment assertions that lock both packed parity summaries and imported executable records
|
||||||
|
|
||||||
Do not mix this slice with:
|
Do not mix this slice with:
|
||||||
|
|
||||||
- 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
|
||||||
- direct translation of packed RT3 event rows into executable normalized effects
|
- broad speculative translation of packed RT3 event rows into executable normalized effects
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
"company_count": 0,
|
"company_count": 0,
|
||||||
"packed_event_collection_present": true,
|
"packed_event_collection_present": true,
|
||||||
"packed_event_record_count": 3,
|
"packed_event_record_count": 3,
|
||||||
|
"packed_event_decoded_record_count": 0,
|
||||||
|
"packed_event_imported_runtime_record_count": 0,
|
||||||
"event_runtime_record_count": 0,
|
"event_runtime_record_count": 0,
|
||||||
"total_event_record_service_count": 0,
|
"total_event_record_service_count": 0,
|
||||||
"periodic_boundary_call_count": 0,
|
"periodic_boundary_call_count": 0,
|
||||||
|
|
@ -37,10 +39,11 @@
|
||||||
"packed_event_collection": {
|
"packed_event_collection": {
|
||||||
"mechanism_family": "classic-save-rehydrate-v1",
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
"live_record_count": 3,
|
"live_record_count": 3,
|
||||||
"live_entry_ids": [
|
"live_entry_ids": [1, 3, 5],
|
||||||
1,
|
"records": [
|
||||||
3,
|
{
|
||||||
5
|
"decode_status": "unsupported_framing"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,37 @@
|
||||||
"packed_state_version_hex": "0x000003e9",
|
"packed_state_version_hex": "0x000003e9",
|
||||||
"live_id_bound": 5,
|
"live_id_bound": 5,
|
||||||
"live_record_count": 3,
|
"live_record_count": 3,
|
||||||
"live_entry_ids": [
|
"live_entry_ids": [1, 3, 5],
|
||||||
1,
|
"decoded_record_count": 0,
|
||||||
3,
|
"imported_runtime_record_count": 0,
|
||||||
5
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 1,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["snapshot fixture placeholder"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_index": 1,
|
||||||
|
"live_entry_id": 3,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["snapshot fixture placeholder"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_index": 2,
|
||||||
|
"live_entry_id": 5,
|
||||||
|
"decode_status": "unsupported_framing",
|
||||||
|
"grouped_effect_row_counts": [0, 0, 0, 0],
|
||||||
|
"decoded_actions": [],
|
||||||
|
"executable_import_ready": false,
|
||||||
|
"notes": ["snapshot fixture placeholder"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"event_runtime_records": [],
|
"event_runtime_records": [],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"fixture_id": "packed-event-record-import-from-snapshot",
|
||||||
|
"source": {
|
||||||
|
"kind": "captured-runtime",
|
||||||
|
"description": "Fixture backed by a runtime snapshot that carries one decoded packed-event record and its imported executable runtime record."
|
||||||
|
},
|
||||||
|
"state_snapshot_path": "packed-event-record-import-snapshot.json",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"kind": "service_trigger_kind",
|
||||||
|
"trigger_kind": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expected_summary": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 0
|
||||||
|
},
|
||||||
|
"world_flag_count": 1,
|
||||||
|
"company_count": 0,
|
||||||
|
"packed_event_collection_present": true,
|
||||||
|
"packed_event_record_count": 1,
|
||||||
|
"packed_event_decoded_record_count": 1,
|
||||||
|
"packed_event_imported_runtime_record_count": 1,
|
||||||
|
"event_runtime_record_count": 2,
|
||||||
|
"special_condition_count": 1,
|
||||||
|
"enabled_special_condition_count": 1,
|
||||||
|
"total_event_record_service_count": 2,
|
||||||
|
"periodic_boundary_call_count": 0,
|
||||||
|
"total_trigger_dispatch_count": 2,
|
||||||
|
"dirty_rerun_count": 1,
|
||||||
|
"total_company_cash": 0
|
||||||
|
},
|
||||||
|
"expected_state_fragment": {
|
||||||
|
"world_flags": {
|
||||||
|
"from_packed_root": true
|
||||||
|
},
|
||||||
|
"special_conditions": {
|
||||||
|
"Imported Follow-On": 1
|
||||||
|
},
|
||||||
|
"packed_event_collection": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"decode_status": "executable",
|
||||||
|
"executable_import_ready": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event_runtime_records": [
|
||||||
|
{
|
||||||
|
"record_id": 7,
|
||||||
|
"service_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_id": 99,
|
||||||
|
"trigger_kind": 10,
|
||||||
|
"service_count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service_state": {
|
||||||
|
"dirty_rerun_count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
fixtures/runtime/packed-event-record-import-snapshot.json
Normal file
152
fixtures/runtime/packed-event-record-import-snapshot.json
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"snapshot_id": "packed-event-record-import-snapshot",
|
||||||
|
"source": {
|
||||||
|
"description": "Snapshot fixture carrying one decoded packed-event record plus its imported executable runtime record."
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"calendar": {
|
||||||
|
"year": 1830,
|
||||||
|
"month_slot": 0,
|
||||||
|
"phase_slot": 0,
|
||||||
|
"tick_slot": 0
|
||||||
|
},
|
||||||
|
"world_flags": {},
|
||||||
|
"companies": [],
|
||||||
|
"packed_event_collection": {
|
||||||
|
"source_kind": "packed-event-runtime-collection",
|
||||||
|
"mechanism_family": "classic-save-rehydrate-v1",
|
||||||
|
"mechanism_confidence": "grounded",
|
||||||
|
"container_profile_family": "rt3-classic-save-container-v1",
|
||||||
|
"packed_state_version": 1001,
|
||||||
|
"packed_state_version_hex": "0x000003e9",
|
||||||
|
"live_id_bound": 7,
|
||||||
|
"live_record_count": 1,
|
||||||
|
"live_entry_ids": [7],
|
||||||
|
"decoded_record_count": 1,
|
||||||
|
"imported_runtime_record_count": 1,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"record_index": 0,
|
||||||
|
"live_entry_id": 7,
|
||||||
|
"payload_offset": 29186,
|
||||||
|
"payload_len": 64,
|
||||||
|
"decode_status": "executable",
|
||||||
|
"trigger_kind": 7,
|
||||||
|
"active": true,
|
||||||
|
"marks_collection_dirty": true,
|
||||||
|
"one_shot": false,
|
||||||
|
"text_bands": [
|
||||||
|
{
|
||||||
|
"label": "primary_text_band",
|
||||||
|
"packed_len": 5,
|
||||||
|
"present": true,
|
||||||
|
"preview": "Alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "secondary_text_band_0",
|
||||||
|
"packed_len": 0,
|
||||||
|
"present": false,
|
||||||
|
"preview": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "secondary_text_band_1",
|
||||||
|
"packed_len": 0,
|
||||||
|
"present": false,
|
||||||
|
"preview": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "secondary_text_band_2",
|
||||||
|
"packed_len": 0,
|
||||||
|
"present": false,
|
||||||
|
"preview": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "secondary_text_band_3",
|
||||||
|
"packed_len": 0,
|
||||||
|
"present": false,
|
||||||
|
"preview": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "secondary_text_band_4",
|
||||||
|
"packed_len": 0,
|
||||||
|
"present": false,
|
||||||
|
"preview": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"standalone_condition_row_count": 1,
|
||||||
|
"grouped_effect_row_counts": [0, 1, 0, 0],
|
||||||
|
"decoded_actions": [
|
||||||
|
{
|
||||||
|
"kind": "set_world_flag",
|
||||||
|
"key": "from_packed_root",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "append_event_record",
|
||||||
|
"record": {
|
||||||
|
"record_id": 99,
|
||||||
|
"trigger_kind": 10,
|
||||||
|
"active": true,
|
||||||
|
"marks_collection_dirty": false,
|
||||||
|
"one_shot": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"kind": "set_special_condition",
|
||||||
|
"label": "Imported Follow-On",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executable_import_ready": true,
|
||||||
|
"notes": ["fixture packed-event record"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event_runtime_records": [
|
||||||
|
{
|
||||||
|
"record_id": 7,
|
||||||
|
"trigger_kind": 7,
|
||||||
|
"active": true,
|
||||||
|
"service_count": 0,
|
||||||
|
"marks_collection_dirty": true,
|
||||||
|
"one_shot": false,
|
||||||
|
"has_fired": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"kind": "set_world_flag",
|
||||||
|
"key": "from_packed_root",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "append_event_record",
|
||||||
|
"record": {
|
||||||
|
"record_id": 99,
|
||||||
|
"trigger_kind": 10,
|
||||||
|
"active": true,
|
||||||
|
"marks_collection_dirty": false,
|
||||||
|
"one_shot": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"kind": "set_special_condition",
|
||||||
|
"label": "Imported Follow-On",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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