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 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 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 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 subsystem breadth without depending on the shell or presentation path. The current packed-event
as capture and integration tooling, but it is no longer the main execution milestone. 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 ## Project Docs

View file

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

View file

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

View file

@ -76,6 +76,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>, pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_structural_only_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>,
@ -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 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!(

View file

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{ use crate::{
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary, RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
@ -558,6 +559,7 @@ fn runtime_packed_event_record_summary_from_smp(
payload_offset: record.payload_offset, payload_offset: record.payload_offset,
payload_len: record.payload_len, payload_len: record.payload_len,
decode_status: record.decode_status.clone(), decode_status: record.decode_status.clone(),
payload_family: record.payload_family.clone(),
trigger_kind: record.trigger_kind, trigger_kind: record.trigger_kind,
active: record.active, active: record.active,
marks_collection_dirty: record.marks_collection_dirty, 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) .map(runtime_packed_event_text_band_summary_from_smp)
.collect(), .collect(),
standalone_condition_row_count: record.standalone_condition_row_count, 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_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(), decoded_actions: record.decoded_actions.clone(),
executable_import_ready: record.executable_import_ready, executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome( 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( fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>, known_company_ids: &BTreeSet<u32>,
) -> Option<Result<RuntimeEventRecord, String>> { ) -> Option<Result<RuntimeEventRecord, String>> {
if record.decode_status == "unsupported_framing" { if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" {
return None; return None;
} }
@ -756,6 +802,9 @@ fn determine_packed_event_import_outcome(
if record.decode_status == "unsupported_framing" { if record.decode_status == "unsupported_framing" {
return "blocked_unsupported_decode".to_string(); 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) { if packed_record_requires_missing_company_context(record, known_company_ids) {
return "blocked_missing_company_context".to_string(); 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] #[test]
fn loads_dump_document() { fn loads_dump_document() {
let text = serde_json::to_string(&RuntimeStateDumpDocument { let text = serde_json::to_string(&RuntimeStateDumpDocument {
@ -1345,13 +1424,16 @@ mod tests {
payload_offset: None, payload_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
notes: vec!["test".to_string()], notes: vec!["test".to_string()],
@ -1362,13 +1444,16 @@ mod tests {
payload_offset: None, payload_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
notes: vec!["test".to_string()], notes: vec!["test".to_string()],
@ -1379,13 +1464,16 @@ mod tests {
payload_offset: None, payload_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
notes: vec!["test".to_string()], notes: vec!["test".to_string()],
@ -1586,13 +1674,16 @@ mod tests {
payload_offset: Some(0x7202), payload_offset: Some(0x7202),
payload_len: Some(64), payload_len: Some(64),
decode_status: "executable".to_string(), decode_status: "executable".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7), trigger_kind: Some(7),
active: Some(true), active: Some(true),
marks_collection_dirty: Some(true), marks_collection_dirty: Some(true),
one_shot: Some(false), one_shot: Some(false),
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 1, 0, 0], grouped_effect_row_counts: vec![0, 1, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![ decoded_actions: vec![
RuntimeEffect::SetWorldFlag { RuntimeEffect::SetWorldFlag {
key: "from_packed_root".to_string(), key: "from_packed_root".to_string(),
@ -1692,13 +1783,16 @@ mod tests {
payload_offset: Some(0x7202), payload_offset: Some(0x7202),
payload_len: Some(48), payload_len: Some(48),
decode_status: "parity_only".to_string(), decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7), trigger_kind: Some(7),
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50, 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] #[test]
fn overlays_save_slice_events_onto_base_company_context() { fn overlays_save_slice_events_onto_base_company_context() {
let base_state = RuntimeState { let base_state = RuntimeState {
@ -1813,13 +1992,16 @@ mod tests {
payload_offset: Some(0x7202), payload_offset: Some(0x7202),
payload_len: Some(48), payload_len: Some(48),
decode_status: "parity_only".to_string(), decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7), trigger_kind: Some(7),
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50, delta: 50,
@ -1963,13 +2145,16 @@ mod tests {
payload_offset: Some(0x7202), payload_offset: Some(0x7202),
payload_len: Some(48), payload_len: Some(48),
decode_status: "parity_only".to_string(), decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7), trigger_kind: Some(7),
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50, delta: 50,

View file

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

View file

@ -115,6 +115,8 @@ pub struct RuntimePackedEventRecordSummary {
pub payload_len: Option<usize>, pub payload_len: Option<usize>,
pub decode_status: String, pub decode_status: String,
#[serde(default)] #[serde(default)]
pub payload_family: String,
#[serde(default)]
pub trigger_kind: Option<u8>, pub trigger_kind: Option<u8>,
#[serde(default)] #[serde(default)]
pub active: Option<bool>, pub active: Option<bool>,
@ -127,8 +129,12 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub standalone_condition_row_count: usize, pub standalone_condition_row_count: usize,
#[serde(default)] #[serde(default)]
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>, pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>, pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)] #[serde(default)]
pub executable_import_ready: bool, pub executable_import_ready: bool,
@ -146,6 +152,39 @@ pub struct RuntimePackedEventTextBandSummary {
pub preview: String, 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 { impl RuntimeEventRecordTemplate {
pub fn into_runtime_record(self) -> RuntimeEventRecord { pub fn into_runtime_record(self) -> RuntimeEventRecord {
RuntimeEventRecord { RuntimeEventRecord {
@ -387,6 +426,11 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].decode_status must not be empty" "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 if record
.import_outcome .import_outcome
.as_deref() .as_deref()
@ -401,6 +445,21 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries" "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 { for band in &record.text_bands {
if band.label.trim().is_empty() { if band.label.trim().is_empty() {
return Err(format!( 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_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, import_outcome: None,
@ -790,13 +879,16 @@ mod tests {
payload_offset: None, payload_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, 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_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1"; const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1"; 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] = [ const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [
"primary_text_band", "primary_text_band",
"secondary_text_band_0", "secondary_text_band_0",
@ -1220,6 +1224,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
pub payload_len: Option<usize>, pub payload_len: Option<usize>,
pub decode_status: String, pub decode_status: String,
#[serde(default)] #[serde(default)]
pub payload_family: String,
#[serde(default)]
pub trigger_kind: Option<u8>, pub trigger_kind: Option<u8>,
#[serde(default)] #[serde(default)]
pub active: Option<bool>, pub active: Option<bool>,
@ -1232,8 +1238,12 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub standalone_condition_row_count: usize, pub standalone_condition_row_count: usize,
#[serde(default)] #[serde(default)]
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>, pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>, pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)] #[serde(default)]
pub executable_import_ready: bool, pub executable_import_ready: bool,
@ -1249,6 +1259,39 @@ pub struct SmpLoadedPackedEventTextBandSummary {
pub preview: String, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSaveSlice { pub struct SmpLoadedSaveSlice {
pub file_extension_hint: Option<String>, pub file_extension_hint: Option<String>,
@ -1576,6 +1619,13 @@ fn parse_event_runtime_record_summaries(
records_payload_offset, records_payload_offset,
live_entry_ids, live_entry_ids,
) )
.or_else(|| {
try_parse_real_event_runtime_record_summaries(
records_payload,
records_payload_offset,
live_entry_ids,
)
})
.unwrap_or_else(|| { .unwrap_or_else(|| {
build_unsupported_event_runtime_record_summaries( build_unsupported_event_runtime_record_summaries(
live_entry_ids, live_entry_ids,
@ -1681,19 +1731,236 @@ fn parse_synthetic_event_runtime_record_summary(
} else { } else {
"parity_only".to_string() "parity_only".to_string()
}, },
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(trigger_kind), trigger_kind: Some(trigger_kind),
active: Some(flags & 0x01 != 0), active: Some(flags & 0x01 != 0),
marks_collection_dirty: Some(flags & 0x02 != 0), marks_collection_dirty: Some(flags & 0x02 != 0),
one_shot: Some(flags & 0x04 != 0), one_shot: Some(flags & 0x04 != 0),
text_bands, text_bands,
standalone_condition_row_count, standalone_condition_row_count,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows: Vec::new(),
decoded_actions, decoded_actions,
executable_import_ready, executable_import_ready,
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()], 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> { fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
let opcode = read_u8_at(bytes, *cursor)?; let opcode = read_u8_at(bytes, *cursor)?;
*cursor += 1; *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()) 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 { fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
match effect { match effect {
RuntimeEffect::SetWorldFlag { .. } RuntimeEffect::SetWorldFlag { .. }
@ -1858,13 +2136,16 @@ fn build_unsupported_event_runtime_record_summaries(
payload_offset: None, payload_offset: None,
payload_len: None, payload_len: None,
decode_status: "unsupported_framing".to_string(), decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None, trigger_kind: None,
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
notes: vec![note.to_string()], notes: vec![note.to_string()],
@ -6701,6 +6982,90 @@ mod tests {
bytes 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] #[test]
fn parses_synthetic_event_runtime_record_summaries_and_actions() { fn parses_synthetic_event_runtime_record_summaries_and_actions() {
let append_template = encode_template( let append_template = encode_template(
@ -6746,6 +7111,7 @@ mod tests {
assert_eq!(summary.imported_runtime_record_count, 1); assert_eq!(summary.imported_runtime_record_count, 1);
assert_eq!(summary.records.len(), 1); assert_eq!(summary.records.len(), 1);
assert_eq!(summary.records[0].decode_status, "executable"); 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].text_bands[0].preview, "Alpha");
assert_eq!(summary.records[0].standalone_condition_row_count, 1); assert_eq!(summary.records[0].standalone_condition_row_count, 1);
assert_eq!( assert_eq!(
@ -6797,9 +7163,143 @@ mod tests {
assert_eq!(summary.decoded_record_count, 1); assert_eq!(summary.decoded_record_count, 1);
assert_eq!(summary.imported_runtime_record_count, 0); assert_eq!(summary.imported_runtime_record_count, 0);
assert_eq!(summary.records[0].decode_status, "parity_only"); 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); 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] #[test]
fn loads_event_runtime_collection_summary_from_report() { fn loads_event_runtime_collection_summary_from_report() {
let mut report = inspect_smp_bytes(&[]); 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_parity_only_record_count: usize,
pub packed_event_unsupported_record_count: usize, pub packed_event_unsupported_record_count: usize,
pub packed_event_blocked_missing_company_context_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 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,
@ -168,6 +169,19 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .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(), 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
@ -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 - 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
- use captured-context overlay imports whenever save-derived packed rows need live runtime context - move the packed-event parser from the synthetic harness onto real `0x4e9a` structural decode so
that the save slice does not actually persist real rows stop collapsing to generic unsupported framing
- widen packed-event target-family coverage only where static evidence is strong enough to support - use overlay imports as the context bridge when selectively executable packed rows still need live
deterministic executable import after the necessary runtime context is present 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 - 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

View file

@ -26,8 +26,7 @@ Implemented today:
normalized state-fragment assertions, and imported packed-event execution 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
captured-context overlay import for company-targeted packed events, not another persistence real `0x4e9a` packed-event structural decode, not another persistence scaffold pass.
scaffold pass.
## Why This Boundary ## Why This Boundary
@ -366,37 +365,37 @@ Checked-in fixture families already include:
## Next Slice ## Next Slice
The recommended next implementation slice is captured-context overlay import on top of the The recommended next implementation slice is real `0x4e9a` packed-event structural decode on top
save-slice and snapshot workflows that already exist today. of the save-slice, snapshot, and overlay workflows that already exist today.
Target behavior: Target behavior:
- preserve save slices as partial state rather than pretending they reconstruct full live company - parse real packed-event record bodies structurally instead of dropping them straight to
state `unsupported_framing`
- overlay save-derived packed-event state onto a captured runtime snapshot that already has the - preserve the first real decode pass as parity-only, exposing text-band and row-family structure
needed company roster and other live context without guessing executable semantics
- upgrade currently blocked company-targeted packed rows when the overlaid base snapshot provides - keep the existing synthetic harness and overlay-backed executable import path working unchanged
every referenced company id - reserve selective executable import from real rows for a later pass once row semantics are tighter
- 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:
- tracked overlay import documents that reference one base snapshot plus one save-slice document - payload-family labeling that distinguishes synthetic harness records from real packed rows and
- runtime-side import outcome labels for packed records so blocked-missing-context and unsupported framing
blocked-unsupported cases stay explicit - structural row summaries for real standalone condition rows and grouped effect rows
- fixture support for generic runtime-import documents, not just snapshots or save slices - runtime-side import outcome labels that distinguish `blocked_structural_only` from
`blocked_missing_company_context` and `blocked_unsupported_decode`
Fixture work for that slice: Fixture work for that slice:
- overlay-backed fixtures that prove company-targeted packed rows execute deterministically against - one parity-heavy tracked sample that now exposes a real structurally decoded row family
captured company context - regression fixtures that keep synthetic executable import and overlay-backed company-context
- regression fixtures that lock the before/after boundary between save-slice-only imports and upgrade behavior green
overlay-backed imports - state-fragment assertions that lock the new structural row summaries and
- state-fragment assertions that lock both packed parity summaries and imported executable records `blocked_structural_only` frontier
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
- 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, "record_index": 0,
"live_entry_id": 1, "live_entry_id": 1,
"decode_status": "unsupported_framing", "decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0], "grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [], "decoded_actions": [],
"executable_import_ready": false, "executable_import_ready": false,
@ -39,6 +40,7 @@
"record_index": 1, "record_index": 1,
"live_entry_id": 3, "live_entry_id": 3,
"decode_status": "unsupported_framing", "decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0], "grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [], "decoded_actions": [],
"executable_import_ready": false, "executable_import_ready": false,
@ -48,6 +50,7 @@
"record_index": 2, "record_index": 2,
"live_entry_id": 5, "live_entry_id": 5,
"decode_status": "unsupported_framing", "decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0], "grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [], "decoded_actions": [],
"executable_import_ready": false, "executable_import_ready": false,

View file

@ -26,6 +26,7 @@
"packed_event_imported_runtime_record_count": 0, "packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1, "packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 1, "packed_event_unsupported_record_count": 1,
"packed_event_blocked_structural_only_count": 1,
"event_runtime_record_count": 0, "event_runtime_record_count": 0,
"total_company_cash": 0 "total_company_cash": 0
}, },
@ -40,10 +41,24 @@
"live_entry_ids": [3, 5], "live_entry_ids": [3, 5],
"records": [ "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_offset": 29186,
"payload_len": 96, "payload_len": 96,
"decode_status": "unsupported_framing", "decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0], "grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [], "decoded_actions": [],
"executable_import_ready": false, "executable_import_ready": false,
@ -55,10 +56,7 @@
"payload_offset": 29290, "payload_offset": 29290,
"payload_len": 72, "payload_len": 72,
"decode_status": "parity_only", "decode_status": "parity_only",
"trigger_kind": 7, "payload_family": "real_packed_v1",
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [ "text_bands": [
{ {
"label": "primary_text_band", "label": "primary_text_band",
@ -97,21 +95,49 @@
"preview": "" "preview": ""
} }
], ],
"standalone_condition_row_count": 0, "standalone_condition_row_count": 1,
"grouped_effect_row_counts": [0, 0, 0, 0], "standalone_condition_rows": [
"decoded_actions": [
{ {
"kind": "adjust_company_cash", "row_index": 0,
"target": { "raw_condition_id": -1,
"kind": "ids", "subtype": 4,
"ids": [42] "flag_bytes": [
}, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
"delta": 75 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, "executable_import_ready": false,
"notes": [ "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_offset": 29186,
"payload_len": 64, "payload_len": 64,
"decode_status": "executable", "decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7, "trigger_kind": 7,
"active": true, "active": true,
"marks_collection_dirty": true, "marks_collection_dirty": true,
@ -75,7 +76,9 @@
} }
], ],
"standalone_condition_row_count": 1, "standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0], "grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [ "decoded_actions": [
{ {
"kind": "set_world_flag", "kind": "set_world_flag",

View file

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