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

@ -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,