Decode real packed event record structure
This commit is contained in:
parent
fa63cefb70
commit
45f258cf5d
16 changed files with 1011 additions and 44 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(&[]);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue