Decode real packed event record structure

This commit is contained in:
Jan Petykiewicz 2026-04-14 22:09:09 -07:00
commit 45f258cf5d
16 changed files with 1011 additions and 44 deletions

View file

@ -10,8 +10,10 @@ and stand up Rust tooling that can validate artifacts and later host replacement
The long-term direction is still a DLL we can inject into the original executable, patching in
individual functions as we build them out. The active implementation milestone is now a headless
runtime rehost layer that can execute deterministic world work, compare normalized state, and grow
subsystem breadth without depending on the shell or presentation path. The PE32 hook remains useful
as capture and integration tooling, but it is no longer the main execution milestone.
subsystem breadth without depending on the shell or presentation path. The current packed-event
frontier is real `0x4e9a` structural decode on top of the existing save-slice, snapshot, and
overlay-import workflows. The PE32 hook remains useful as capture and integration tooling, but it
is no longer the main execution milestone.
## Project Docs

View file

@ -1,3 +1,5 @@
#![recursion_limit = "256"]
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
@ -4313,6 +4315,7 @@ mod tests {
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4322,6 +4325,7 @@ mod tests {
"record_index": 1,
"live_entry_id": 3,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4331,6 +4335,7 @@ mod tests {
"record_index": 2,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4370,6 +4375,7 @@ mod tests {
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4379,6 +4385,7 @@ mod tests {
"record_index": 1,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4542,6 +4549,7 @@ mod tests {
"record_index": 0,
"live_entry_id": 7,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -4583,6 +4591,7 @@ mod tests {
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
@ -4596,7 +4605,9 @@ mod tests {
}
],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_world_flag",

View file

@ -374,13 +374,16 @@ mod tests {
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,

View file

@ -76,6 +76,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_structural_only_count: Option<usize>,
#[serde(default)]
pub event_runtime_record_count: Option<usize>,
#[serde(default)]
pub candidate_availability_count: Option<usize>,
@ -371,6 +373,14 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.packed_event_blocked_structural_only_count {
if actual.packed_event_blocked_structural_only_count != count {
mismatches.push(format!(
"packed_event_blocked_structural_only_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_structural_only_count
));
}
}
if let Some(count) = self.event_runtime_record_count {
if actual.event_runtime_record_count != count {
mismatches.push(format!(

View file

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
@ -558,6 +559,7 @@ fn runtime_packed_event_record_summary_from_smp(
payload_offset: record.payload_offset,
payload_len: record.payload_len,
decode_status: record.decode_status.clone(),
payload_family: record.payload_family.clone(),
trigger_kind: record.trigger_kind,
active: record.active,
marks_collection_dirty: record.marks_collection_dirty,
@ -568,7 +570,17 @@ fn runtime_packed_event_record_summary_from_smp(
.map(runtime_packed_event_text_band_summary_from_smp)
.collect(),
standalone_condition_row_count: record.standalone_condition_row_count,
standalone_condition_rows: record
.standalone_condition_rows
.iter()
.map(runtime_packed_event_condition_row_summary_from_smp)
.collect(),
grouped_effect_row_counts: record.grouped_effect_row_counts.clone(),
grouped_effect_rows: record
.grouped_effect_rows
.iter()
.map(runtime_packed_event_grouped_effect_row_summary_from_smp)
.collect(),
decoded_actions: record.decoded_actions.clone(),
executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome(
@ -591,11 +603,45 @@ fn runtime_packed_event_text_band_summary_from_smp(
}
}
fn runtime_packed_event_condition_row_summary_from_smp(
row: &crate::SmpLoadedPackedEventConditionRowSummary,
) -> RuntimePackedEventConditionRowSummary {
RuntimePackedEventConditionRowSummary {
row_index: row.row_index,
raw_condition_id: row.raw_condition_id,
subtype: row.subtype,
flag_bytes: row.flag_bytes.clone(),
candidate_name: row.candidate_name.clone(),
notes: row.notes.clone(),
}
}
fn runtime_packed_event_grouped_effect_row_summary_from_smp(
row: &crate::SmpLoadedPackedEventGroupedEffectRowSummary,
) -> RuntimePackedEventGroupedEffectRowSummary {
RuntimePackedEventGroupedEffectRowSummary {
group_index: row.group_index,
row_index: row.row_index,
descriptor_id: row.descriptor_id,
opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value,
value_byte_0x09: row.value_byte_0x09,
value_dword_0x0d: row.value_dword_0x0d,
value_byte_0x11: row.value_byte_0x11,
value_byte_0x12: row.value_byte_0x12,
value_word_0x14: row.value_word_0x14,
value_word_0x16: row.value_word_0x16,
row_shape: row.row_shape.clone(),
locomotive_name: row.locomotive_name.clone(),
notes: row.notes.clone(),
}
}
fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>,
) -> Option<Result<RuntimeEventRecord, String>> {
if record.decode_status == "unsupported_framing" {
if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" {
return None;
}
@ -756,6 +802,9 @@ fn determine_packed_event_import_outcome(
if record.decode_status == "unsupported_framing" {
return "blocked_unsupported_decode".to_string();
}
if record.payload_family == "real_packed_v1" {
return "blocked_structural_only".to_string();
}
if packed_record_requires_missing_company_context(record, known_company_ids) {
return "blocked_missing_company_context".to_string();
}
@ -1129,6 +1178,36 @@ mod tests {
]
}
fn real_condition_rows() -> Vec<crate::SmpLoadedPackedEventConditionRowSummary> {
vec![crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: -1,
subtype: 4,
flag_bytes: vec![0x30; 25],
candidate_name: Some("AutoPlant".to_string()),
notes: vec!["negative sentinel-style condition row id".to_string()],
}]
}
fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id: 2,
opcode: 8,
raw_scalar_value: 7,
value_byte_0x09: 1,
value_dword_0x0d: 12,
value_byte_0x11: 2,
value_byte_0x12: 3,
value_word_0x14: 24,
value_word_0x16: 36,
row_shape: "multivalue_scalar".to_string(),
locomotive_name: Some("Mikado".to_string()),
notes: vec!["grouped effect row carries locomotive-name side string".to_string()],
}]
}
#[test]
fn loads_dump_document() {
let text = serde_json::to_string(&RuntimeStateDumpDocument {
@ -1345,13 +1424,16 @@ mod tests {
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -1362,13 +1444,16 @@ mod tests {
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -1379,13 +1464,16 @@ mod tests {
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -1586,13 +1674,16 @@ mod tests {
payload_offset: Some(0x7202),
payload_len: Some(64),
decode_status: "executable".to_string(),
payload_family: "synthetic_harness".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,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 1, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![
RuntimeEffect::SetWorldFlag {
key: "from_packed_root".to_string(),
@ -1692,13 +1783,16 @@ mod tests {
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".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,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,
@ -1744,6 +1838,91 @@ mod tests {
);
}
#[test]
fn leaves_real_structural_records_blocked_structural_only() {
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(96),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-structural-only",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_structural_only")
);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.map(|summary| summary.records[0].standalone_condition_rows.len()),
Some(1)
);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.map(|summary| summary.records[0].grouped_effect_rows.len()),
Some(1)
);
}
#[test]
fn overlays_save_slice_events_onto_base_company_context() {
let base_state = RuntimeState {
@ -1813,13 +1992,16 @@ mod tests {
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".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,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,
@ -1963,13 +2145,16 @@ mod tests {
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".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,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,

View file

@ -37,6 +37,7 @@ pub use pk4::{
pub use runtime::{
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
};
@ -46,6 +47,7 @@ pub use smp::{
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary,
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile,
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,

View file

@ -115,6 +115,8 @@ pub struct RuntimePackedEventRecordSummary {
pub payload_len: Option<usize>,
pub decode_status: String,
#[serde(default)]
pub payload_family: String,
#[serde(default)]
pub trigger_kind: Option<u8>,
#[serde(default)]
pub active: Option<bool>,
@ -127,8 +129,12 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)]
pub standalone_condition_row_count: usize,
#[serde(default)]
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
@ -146,6 +152,39 @@ pub struct RuntimePackedEventTextBandSummary {
pub preview: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventConditionRowSummary {
pub row_index: usize,
pub raw_condition_id: i32,
pub subtype: u8,
#[serde(default)]
pub flag_bytes: Vec<u8>,
#[serde(default)]
pub candidate_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventGroupedEffectRowSummary {
pub group_index: usize,
pub row_index: usize,
pub descriptor_id: u32,
pub opcode: u8,
pub raw_scalar_value: i32,
pub value_byte_0x09: u8,
pub value_dword_0x0d: u32,
pub value_byte_0x11: u8,
pub value_byte_0x12: u8,
pub value_word_0x14: u16,
pub value_word_0x16: u16,
pub row_shape: String,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
impl RuntimeEventRecordTemplate {
pub fn into_runtime_record(self) -> RuntimeEventRecord {
RuntimeEventRecord {
@ -387,6 +426,11 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].decode_status must not be empty"
));
}
if record.payload_family.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].payload_family must not be empty"
));
}
if record
.import_outcome
.as_deref()
@ -401,6 +445,21 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries"
));
}
if record.payload_family == "real_packed_v1"
&& record.standalone_condition_rows.len() != record.standalone_condition_row_count
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count"
));
}
if record.payload_family == "real_packed_v1"
&& record.grouped_effect_rows.len()
!= record.grouped_effect_row_counts.iter().sum::<usize>()
{
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows must match grouped_effect_row_counts"
));
}
for band in &record.text_bands {
if band.label.trim().is_empty() {
return Err(format!(
@ -408,6 +467,33 @@ impl RuntimeState {
));
}
}
for row in &record.standalone_condition_rows {
if row
.candidate_name
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty candidate_name"
));
}
}
for row in &record.grouped_effect_rows {
if row.row_shape.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty row_shape"
));
}
if row
.locomotive_name
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty locomotive_name"
));
}
}
}
}
@ -772,13 +858,16 @@ mod tests {
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
@ -790,13 +879,16 @@ mod tests {
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,

View file

@ -91,6 +91,10 @@ const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
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_REAL_CONDITION_MARKER: u16 = 0x526f;
const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8;
const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e;
const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28;
const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [
"primary_text_band",
"secondary_text_band_0",
@ -1220,6 +1224,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
pub payload_len: Option<usize>,
pub decode_status: String,
#[serde(default)]
pub payload_family: String,
#[serde(default)]
pub trigger_kind: Option<u8>,
#[serde(default)]
pub active: Option<bool>,
@ -1232,8 +1238,12 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)]
pub standalone_condition_row_count: usize,
#[serde(default)]
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
@ -1249,6 +1259,39 @@ pub struct SmpLoadedPackedEventTextBandSummary {
pub preview: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventConditionRowSummary {
pub row_index: usize,
pub raw_condition_id: i32,
pub subtype: u8,
#[serde(default)]
pub flag_bytes: Vec<u8>,
#[serde(default)]
pub candidate_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub group_index: usize,
pub row_index: usize,
pub descriptor_id: u32,
pub opcode: u8,
pub raw_scalar_value: i32,
pub value_byte_0x09: u8,
pub value_dword_0x0d: u32,
pub value_byte_0x11: u8,
pub value_byte_0x12: u8,
pub value_word_0x14: u16,
pub value_word_0x16: u16,
pub row_shape: String,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSaveSlice {
pub file_extension_hint: Option<String>,
@ -1576,6 +1619,13 @@ fn parse_event_runtime_record_summaries(
records_payload_offset,
live_entry_ids,
)
.or_else(|| {
try_parse_real_event_runtime_record_summaries(
records_payload,
records_payload_offset,
live_entry_ids,
)
})
.unwrap_or_else(|| {
build_unsupported_event_runtime_record_summaries(
live_entry_ids,
@ -1681,19 +1731,236 @@ fn parse_synthetic_event_runtime_record_summary(
} else {
"parity_only".to_string()
},
payload_family: "synthetic_harness".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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts,
grouped_effect_rows: Vec::new(),
decoded_actions,
executable_import_ready,
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()],
})
}
fn try_parse_real_event_runtime_record_summaries(
records_payload: &[u8],
records_payload_offset: usize,
live_entry_ids: &[u32],
) -> Option<Vec<SmpLoadedPackedEventRecordSummary>> {
let mut cursor = 0usize;
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, consumed_len) = parse_real_event_runtime_record_summary(
records_payload.get(cursor..)?,
records_payload_offset + cursor,
record_index,
live_entry_id,
)?;
records.push(record);
cursor += consumed_len;
}
if cursor != records_payload.len() {
return None;
}
Some(records)
}
fn parse_real_event_runtime_record_summary(
record_body: &[u8],
payload_offset: usize,
record_index: usize,
live_entry_id: u32,
) -> Option<(SmpLoadedPackedEventRecordSummary, usize)> {
let mut cursor = 0usize;
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),
});
}
if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER {
return None;
}
cursor += 2;
let standalone_condition_row_count = usize::from(read_u16_at(record_body, cursor)?);
cursor += 2;
let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count);
for row_index in 0..standalone_condition_row_count {
let row_bytes = record_body.get(cursor..cursor + PACKED_EVENT_REAL_CONDITION_ROW_LEN)?;
cursor += PACKED_EVENT_REAL_CONDITION_ROW_LEN;
let candidate_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
standalone_condition_rows.push(parse_real_condition_row_summary(
row_bytes,
row_index,
candidate_name,
)?);
}
if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER {
return None;
}
cursor += 2;
let mut grouped_effect_row_counts = Vec::with_capacity(4);
for _ in 0..4 {
grouped_effect_row_counts.push(usize::from(read_u16_at(record_body, cursor)?));
cursor += 2;
}
let mut grouped_effect_rows =
Vec::with_capacity(grouped_effect_row_counts.iter().sum::<usize>());
for (group_index, row_count) in grouped_effect_row_counts.iter().copied().enumerate() {
for row_index in 0..row_count {
let row_bytes =
record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?;
cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN;
let locomotive_name =
parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
grouped_effect_rows.push(parse_real_grouped_effect_row_summary(
row_bytes,
group_index,
row_index,
locomotive_name,
)?);
}
}
let consumed_len = cursor;
Some((
SmpLoadedPackedEventRecordSummary {
record_index,
live_entry_id,
payload_offset: Some(payload_offset),
payload_len: Some(consumed_len),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
text_bands,
standalone_condition_row_count,
standalone_condition_rows,
grouped_effect_row_counts,
grouped_effect_rows,
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
consumed_len,
))
}
fn parse_real_condition_row_summary(
row_bytes: &[u8],
row_index: usize,
candidate_name: Option<String>,
) -> Option<SmpLoadedPackedEventConditionRowSummary> {
let raw_condition_id = read_u32_at(row_bytes, 0)? as i32;
let subtype = read_u8_at(row_bytes, 4)?;
let mut notes = Vec::new();
if raw_condition_id < 0 {
notes.push("negative sentinel-style condition row id".to_string());
}
if candidate_name.is_some() {
notes.push("condition row carries candidate-name side string".to_string());
}
Some(SmpLoadedPackedEventConditionRowSummary {
row_index,
raw_condition_id,
subtype,
flag_bytes: row_bytes.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?.to_vec(),
candidate_name,
notes,
})
}
fn parse_real_grouped_effect_row_summary(
row_bytes: &[u8],
group_index: usize,
row_index: usize,
locomotive_name: Option<String>,
) -> Option<SmpLoadedPackedEventGroupedEffectRowSummary> {
let descriptor_id = read_u32_at(row_bytes, 0)?;
let raw_scalar_value = read_u32_at(row_bytes, 4)? as i32;
let opcode = read_u8_at(row_bytes, 8)?;
let value_byte_0x09 = read_u8_at(row_bytes, 9)?;
let value_dword_0x0d = read_u32_at(row_bytes, 0x0d)?;
let value_byte_0x11 = read_u8_at(row_bytes, 0x11)?;
let value_byte_0x12 = read_u8_at(row_bytes, 0x12)?;
let value_word_0x14 = read_u16_at(row_bytes, 0x14)?;
let value_word_0x16 = read_u16_at(row_bytes, 0x16)?;
let row_shape = classify_real_grouped_effect_row_shape(
opcode,
raw_scalar_value,
value_byte_0x11,
value_byte_0x12,
value_word_0x14,
value_word_0x16,
)
.to_string();
let mut notes = Vec::new();
if locomotive_name.is_some() {
notes.push("grouped effect row carries locomotive-name side string".to_string());
}
Some(SmpLoadedPackedEventGroupedEffectRowSummary {
group_index,
row_index,
descriptor_id,
opcode,
raw_scalar_value,
value_byte_0x09,
value_dword_0x0d,
value_byte_0x11,
value_byte_0x12,
value_word_0x14,
value_word_0x16,
row_shape,
locomotive_name,
notes,
})
}
fn classify_real_grouped_effect_row_shape(
opcode: u8,
raw_scalar_value: i32,
value_byte_0x11: u8,
value_byte_0x12: u8,
value_word_0x14: u16,
value_word_0x16: u16,
) -> &'static str {
if opcode == 8 {
return "multivalue_scalar";
}
if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0
{
return "timed_duration";
}
if raw_scalar_value == 0 || raw_scalar_value == 1 {
return "bool_toggle";
}
"raw_other"
}
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
let opcode = read_u8_at(bytes, *cursor)?;
*cursor += 1;
@ -1824,6 +2091,17 @@ fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<String>
Some(String::from_utf8_lossy(text_bytes).into_owned())
}
fn parse_optional_u16_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<Option<String>> {
let len = usize::from(read_u16_at(bytes, *cursor)?);
*cursor += 2;
if len == 0 {
return Some(None);
}
let text_bytes = bytes.get(*cursor..*cursor + len)?;
*cursor += len;
Some(Some(String::from_utf8_lossy(text_bytes).into_owned()))
}
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
match effect {
RuntimeEffect::SetWorldFlag { .. }
@ -1858,13 +2136,16 @@ fn build_unsupported_event_runtime_record_summaries(
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "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,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec![note.to_string()],
@ -6701,6 +6982,90 @@ mod tests {
bytes
}
fn encode_real_optional_string(text: &str) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(text.len() as u16).to_le_bytes());
bytes.extend_from_slice(text.as_bytes());
bytes
}
fn build_real_condition_row(
raw_condition_id: i32,
subtype: u8,
flag_seed: u8,
candidate_name: Option<&str>,
) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(raw_condition_id as u32).to_le_bytes());
bytes.push(subtype);
while bytes.len() < PACKED_EVENT_REAL_CONDITION_ROW_LEN {
bytes.push(flag_seed.wrapping_add(bytes.len() as u8));
}
match candidate_name {
Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)),
None => bytes.extend_from_slice(&0u16.to_le_bytes()),
}
bytes
}
struct RealGroupedEffectRowSpec<'a> {
descriptor_id: u32,
opcode: u8,
raw_scalar_value: i32,
value_byte_0x09: u8,
value_dword_0x0d: u32,
value_byte_0x11: u8,
value_byte_0x12: u8,
value_word_0x14: u16,
value_word_0x16: u16,
locomotive_name: Option<&'a str>,
}
fn build_real_grouped_effect_row(spec: RealGroupedEffectRowSpec<'_>) -> Vec<u8> {
let mut bytes = vec![0; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN];
bytes[0..4].copy_from_slice(&spec.descriptor_id.to_le_bytes());
bytes[4..8].copy_from_slice(&(spec.raw_scalar_value as u32).to_le_bytes());
bytes[8] = spec.opcode;
bytes[9] = spec.value_byte_0x09;
bytes[0x0d..0x11].copy_from_slice(&spec.value_dword_0x0d.to_le_bytes());
bytes[0x11] = spec.value_byte_0x11;
bytes[0x12] = spec.value_byte_0x12;
bytes[0x14..0x16].copy_from_slice(&spec.value_word_0x14.to_le_bytes());
bytes[0x16..0x18].copy_from_slice(&spec.value_word_0x16.to_le_bytes());
match spec.locomotive_name {
Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)),
None => bytes.extend_from_slice(&0u16.to_le_bytes()),
}
bytes
}
fn build_real_event_record(
text_bands: [&[u8]; 6],
condition_rows: &[Vec<u8>],
grouped_rows: [&[Vec<u8>]; 4],
) -> Vec<u8> {
let mut bytes = Vec::new();
for band in text_bands {
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
bytes.extend_from_slice(band);
}
bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes());
bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes());
for row in condition_rows {
bytes.extend_from_slice(row);
}
bytes.extend_from_slice(&PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER.to_le_bytes());
for rows in grouped_rows {
bytes.extend_from_slice(&(rows.len() as u16).to_le_bytes());
}
for rows in grouped_rows {
for row in rows {
bytes.extend_from_slice(row);
}
}
bytes
}
#[test]
fn parses_synthetic_event_runtime_record_summaries_and_actions() {
let append_template = encode_template(
@ -6746,6 +7111,7 @@ mod tests {
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].payload_family, "synthetic_harness");
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
assert_eq!(summary.records[0].standalone_condition_row_count, 1);
assert_eq!(
@ -6797,9 +7163,143 @@ mod tests {
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_eq!(summary.records[0].payload_family, "synthetic_harness");
assert!(!summary.records[0].executable_import_ready);
}
#[test]
fn parses_real_style_event_runtime_record_with_zero_rows() {
let record_body = build_real_event_record(
[b"Alpha", b"", b"", b"", b"", b""],
&[],
[&[], &[], &[], &[]],
);
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(&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_eq!(summary.records[0].payload_family, "real_packed_v1");
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
assert_eq!(summary.records[0].standalone_condition_row_count, 0);
assert_eq!(summary.records[0].standalone_condition_rows.len(), 0);
assert_eq!(summary.records[0].grouped_effect_row_counts, vec![0, 0, 0, 0]);
assert_eq!(summary.records[0].grouped_effect_rows.len(), 0);
}
#[test]
fn parses_real_style_rows_and_side_strings() {
let condition_row = build_real_condition_row(-1, 4, 0x30, Some("AutoPlant"));
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 2,
opcode: 8,
raw_scalar_value: 7,
value_byte_0x09: 1,
value_dword_0x0d: 12,
value_byte_0x11: 2,
value_byte_0x12: 3,
value_word_0x14: 24,
value_word_0x16: 36,
locomotive_name: Some("Mikado"),
});
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[b"Gamma", b"", b"", b"", b"", b""],
&[condition_row],
[&group0_rows, &[], &[], &[]],
);
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(&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.records[0].standalone_condition_rows.len(), 1);
assert_eq!(summary.records[0].standalone_condition_rows[0].raw_condition_id, -1);
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.candidate_name
.as_deref(),
Some("AutoPlant")
);
assert_eq!(summary.records[0].grouped_effect_rows.len(), 1);
assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8);
assert_eq!(
summary.records[0].grouped_effect_rows[0].row_shape,
"multivalue_scalar"
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.locomotive_name
.as_deref(),
Some("Mikado")
);
}
#[test]
fn rejects_truncated_real_style_event_runtime_record() {
let mut record_body = build_real_event_record(
[b"Oops", b"", b"", b"", b"", b""],
&[],
[&[], &[], &[], &[]],
);
record_body.pop();
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(&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.records[0].decode_status, "unsupported_framing");
assert_eq!(summary.records[0].payload_family, "unsupported_framing");
}
#[test]
fn loads_event_runtime_collection_summary_from_report() {
let mut report = inspect_smp_bytes(&[]);

View file

@ -35,6 +35,7 @@ pub struct RuntimeSummary {
pub packed_event_parity_only_record_count: usize,
pub packed_event_unsupported_record_count: usize,
pub packed_event_blocked_missing_company_context_count: usize,
pub packed_event_blocked_structural_only_count: usize,
pub event_runtime_record_count: usize,
pub candidate_availability_count: usize,
pub zero_candidate_availability_count: usize,
@ -168,6 +169,19 @@ impl RuntimeSummary {
.count()
})
.unwrap_or(0),
packed_event_blocked_structural_only_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref() == Some("blocked_structural_only")
})
.count()
})
.unwrap_or(0),
event_runtime_record_count: state.event_runtime_records.len(),
candidate_availability_count: state.candidate_availability.len(),
zero_candidate_availability_count: state
@ -207,3 +221,97 @@ impl RuntimeSummary {
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::{
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
};
use super::RuntimeSummary;
#[test]
fn counts_structural_only_and_missing_context_frontiers() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7,
live_record_count: 2,
live_entry_ids: vec![3, 7],
decoded_record_count: 2,
imported_runtime_record_count: 0,
records: vec![
RuntimePackedEventRecordSummary {
record_index: 0,
live_entry_id: 3,
payload_offset: Some(0x7202),
payload_len: Some(96),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_structural_only".to_string()),
notes: Vec::new(),
},
RuntimePackedEventRecordSummary {
record_index: 1,
live_entry_id: 7,
payload_offset: Some(0x7262),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_missing_company_context".to_string()),
notes: Vec::new(),
},
],
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.packed_event_blocked_structural_only_count, 1);
assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1);
}
}

View file

@ -75,10 +75,12 @@ The highest-value next passes are now:
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
avoid shell-first implementation bets
- use captured-context overlay imports whenever save-derived packed rows need live runtime context
that the save slice does not actually persist
- widen packed-event target-family coverage only where static evidence is strong enough to support
deterministic executable import after the necessary runtime context is present
- move the packed-event parser from the synthetic harness onto real `0x4e9a` structural decode so
real rows stop collapsing to generic unsupported framing
- use overlay imports as the context bridge when selectively executable packed rows still need live
company state that save slices do not persist
- widen real packed-event executable coverage only after the structural decode frontier and row
summaries are stable
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
environment
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice

View file

@ -26,8 +26,7 @@ Implemented today:
normalized state-fragment assertions, and imported packed-event execution
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
captured-context overlay import for company-targeted packed events, not another persistence
scaffold pass.
real `0x4e9a` packed-event structural decode, not another persistence scaffold pass.
## Why This Boundary
@ -366,37 +365,37 @@ Checked-in fixture families already include:
## Next Slice
The recommended next implementation slice is captured-context overlay import on top of the
save-slice and snapshot workflows that already exist today.
The recommended next implementation slice is real `0x4e9a` packed-event structural decode on top
of the save-slice, snapshot, and overlay workflows that already exist today.
Target behavior:
- preserve save slices as partial state rather than pretending they reconstruct full live company
state
- overlay save-derived packed-event state onto a captured runtime snapshot that already has the
needed company roster and other live context
- upgrade currently blocked company-targeted packed rows when the overlaid base snapshot provides
every referenced company id
- keep preserving unsupported packed rows as parity summaries instead of guessing executable meaning
- parse real packed-event record bodies structurally instead of dropping them straight to
`unsupported_framing`
- preserve the first real decode pass as parity-only, exposing text-band and row-family structure
without guessing executable semantics
- keep the existing synthetic harness and overlay-backed executable import path working unchanged
- reserve selective executable import from real rows for a later pass once row semantics are tighter
Public-model additions for that slice:
- tracked overlay import documents that reference one base snapshot plus one save-slice document
- runtime-side import outcome labels for packed records so blocked-missing-context and
blocked-unsupported cases stay explicit
- fixture support for generic runtime-import documents, not just snapshots or save slices
- payload-family labeling that distinguishes synthetic harness records from real packed rows and
unsupported framing
- structural row summaries for real standalone condition rows and grouped effect rows
- runtime-side import outcome labels that distinguish `blocked_structural_only` from
`blocked_missing_company_context` and `blocked_unsupported_decode`
Fixture work for that slice:
- overlay-backed fixtures that prove company-targeted packed rows execute deterministically against
captured company context
- regression fixtures that lock the before/after boundary between save-slice-only imports and
overlay-backed imports
- state-fragment assertions that lock both packed parity summaries and imported executable records
- one parity-heavy tracked sample that now exposes a real structurally decoded row family
- regression fixtures that keep synthetic executable import and overlay-backed company-context
upgrade behavior green
- state-fragment assertions that lock the new structural row summaries and
`blocked_structural_only` frontier
Do not mix this slice with:
- territory-access or selected-profile parity
- placed-structure batch placement parity
- shell queue/modal behavior
- broad speculative translation of packed RT3 event rows into executable normalized effects
- broad speculative translation of real packed RT3 event rows into executable normalized effects

View file

@ -30,6 +30,7 @@
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -39,6 +40,7 @@
"record_index": 1,
"live_entry_id": 3,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -48,6 +50,7 @@
"record_index": 2,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,

View file

@ -26,6 +26,7 @@
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 1,
"packed_event_blocked_structural_only_count": 1,
"event_runtime_record_count": 0,
"total_company_cash": 0
},
@ -40,10 +41,24 @@
"live_entry_ids": [3, 5],
"records": [
{
"decode_status": "unsupported_framing"
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing"
},
{
"decode_status": "parity_only"
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"import_outcome": "blocked_structural_only",
"standalone_condition_rows": [
{
"candidate_name": "AutoPlant"
}
],
"grouped_effect_rows": [
{
"row_shape": "multivalue_scalar",
"locomotive_name": "Mikado"
}
]
}
]
}

View file

@ -42,6 +42,7 @@
"payload_offset": 29186,
"payload_len": 96,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
@ -55,10 +56,7 @@
"payload_offset": 29290,
"payload_len": 72,
"decode_status": "parity_only",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"payload_family": "real_packed_v1",
"text_bands": [
{
"label": "primary_text_band",
@ -97,21 +95,49 @@
"preview": ""
}
],
"standalone_condition_row_count": 0,
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"kind": "adjust_company_cash",
"target": {
"kind": "ids",
"ids": [42]
},
"delta": 75
"row_index": 0,
"raw_condition_id": -1,
"subtype": 4,
"flag_bytes": [
48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
68, 69, 70, 71, 72
],
"candidate_name": "AutoPlant",
"notes": [
"negative sentinel-style condition row id",
"condition row carries candidate-name side string"
]
}
],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"opcode": 8,
"raw_scalar_value": 7,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"locomotive_name": "Mikado",
"notes": [
"grouped effect row carries locomotive-name side string"
]
}
],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"decoded action requires explicit imported company ids before execution"
"decoded from grounded real 0x4e9a row framing"
]
}
]

View file

@ -32,6 +32,7 @@
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": true,
@ -75,7 +76,9 @@
}
],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_world_flag",

View file

@ -42,6 +42,7 @@
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": true,
@ -85,7 +86,9 @@
}
],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_world_flag",
@ -121,6 +124,7 @@
"payload_offset": 29260,
"payload_len": 72,
"decode_status": "parity_only",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
@ -164,7 +168,9 @@
}
],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 0, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "adjust_company_cash",