Decode real packed event compact control

This commit is contained in:
Jan Petykiewicz 2026-04-14 23:01:18 -07:00
commit 4ff6d65774
12 changed files with 483 additions and 41 deletions

View file

@ -11,9 +11,9 @@ The long-term direction is still a DLL we can inject into the original executabl
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 current packed-event subsystem breadth without depending on the shell or presentation path. The current packed-event
frontier is real `0x4e9a` structural decode on top of the existing save-slice, snapshot, and frontier is real `0x4e9a` compact-control decode and descriptor-frontier tightening on top of the
overlay-import workflows. The PE32 hook remains useful as capture and integration tooling, but it existing save-slice, snapshot, and overlay-import workflows. The PE32 hook remains useful as
is no longer the main execution milestone. capture and integration tooling, but it is no longer the main execution milestone.
## Project Docs ## Project Docs

View file

@ -379,6 +379,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: vec![], text_bands: vec![],
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],

View file

@ -76,6 +76,10 @@ 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_missing_compact_control_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_structural_only_count: Option<usize>, pub packed_event_blocked_structural_only_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub event_runtime_record_count: Option<usize>, pub event_runtime_record_count: Option<usize>,
@ -373,6 +377,22 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_compact_control_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_compact_control_count
));
}
}
if let Some(count) = self.packed_event_blocked_unmapped_real_descriptor_count {
if actual.packed_event_blocked_unmapped_real_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_real_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_real_descriptor_count
));
}
}
if let Some(count) = self.packed_event_blocked_structural_only_count { if let Some(count) = self.packed_event_blocked_structural_only_count {
if actual.packed_event_blocked_structural_only_count != count { if actual.packed_event_blocked_structural_only_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,
RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
@ -564,6 +565,10 @@ fn runtime_packed_event_record_summary_from_smp(
active: record.active, active: record.active,
marks_collection_dirty: record.marks_collection_dirty, marks_collection_dirty: record.marks_collection_dirty,
one_shot: record.one_shot, one_shot: record.one_shot,
compact_control: record
.compact_control
.as_ref()
.map(runtime_packed_event_compact_control_summary_from_smp),
text_bands: record text_bands: record
.text_bands .text_bands
.iter() .iter()
@ -592,6 +597,23 @@ fn runtime_packed_event_record_summary_from_smp(
} }
} }
fn runtime_packed_event_compact_control_summary_from_smp(
control: &crate::SmpLoadedPackedEventCompactControlSummary,
) -> RuntimePackedEventCompactControlSummary {
RuntimePackedEventCompactControlSummary {
mode_byte_0x7ef: control.mode_byte_0x7ef,
primary_selector_0x7f0: control.primary_selector_0x7f0,
grouped_mode_0x7f4: control.grouped_mode_0x7f4,
one_shot_header_0x7f5: control.one_shot_header_0x7f5,
modifier_flag_0x7f9: control.modifier_flag_0x7f9,
modifier_flag_0x7fa: control.modifier_flag_0x7fa,
grouped_target_scope_ordinals_0x7fb: control.grouped_target_scope_ordinals_0x7fb.clone(),
grouped_scope_checkboxes_0x7ff: control.grouped_scope_checkboxes_0x7ff.clone(),
summary_toggle_0x800: control.summary_toggle_0x800,
grouped_territory_selectors_0x80f: control.grouped_territory_selectors_0x80f.clone(),
}
}
fn runtime_packed_event_text_band_summary_from_smp( fn runtime_packed_event_text_band_summary_from_smp(
band: &SmpLoadedPackedEventTextBandSummary, band: &SmpLoadedPackedEventTextBandSummary,
) -> RuntimePackedEventTextBandSummary { ) -> RuntimePackedEventTextBandSummary {
@ -803,7 +825,10 @@ fn determine_packed_event_import_outcome(
return "blocked_unsupported_decode".to_string(); return "blocked_unsupported_decode".to_string();
} }
if record.payload_family == "real_packed_v1" { if record.payload_family == "real_packed_v1" {
return "blocked_structural_only".to_string(); if record.compact_control.is_none() {
return "blocked_missing_compact_control".to_string();
}
return "blocked_unmapped_real_descriptor".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();
@ -1208,6 +1233,21 @@ mod tests {
}] }]
} }
fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary {
crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 1,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22],
}
}
#[test] #[test]
fn loads_dump_document() { fn loads_dump_document() {
let text = serde_json::to_string(&RuntimeStateDumpDocument { let text = serde_json::to_string(&RuntimeStateDumpDocument {
@ -1429,6 +1469,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -1449,6 +1490,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -1469,6 +1511,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -1679,6 +1722,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(true), marks_collection_dirty: Some(true),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
@ -1788,6 +1832,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
@ -1839,7 +1884,7 @@ mod tests {
} }
#[test] #[test]
fn leaves_real_structural_records_blocked_structural_only() { fn leaves_real_records_without_compact_control_blocked_missing_compact_control() {
let save_slice = SmpLoadedSaveSlice { let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()), file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -1876,6 +1921,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(), standalone_condition_rows: real_condition_rows(),
@ -1903,7 +1949,7 @@ mod tests {
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()), .and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_structural_only") Some("blocked_missing_compact_control")
); );
assert_eq!( assert_eq!(
import import
@ -1923,6 +1969,85 @@ mod tests {
); );
} }
#[test]
fn leaves_real_records_with_compact_control_blocked_unmapped_real_descriptor() {
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(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
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-real-descriptor-frontier",
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].compact_control.as_ref())
.map(|control| control.mode_byte_0x7ef),
Some(6)
);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_real_descriptor")
);
}
#[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 {
@ -1997,6 +2122,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
@ -2150,6 +2276,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],

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,
RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
@ -47,6 +48,7 @@ pub use smp::{
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit, SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary,
SmpLoadedPackedEventCompactControlSummary,
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile,
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,

View file

@ -125,6 +125,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub one_shot: Option<bool>, pub one_shot: Option<bool>,
#[serde(default)] #[serde(default)]
pub compact_control: Option<RuntimePackedEventCompactControlSummary>,
#[serde(default)]
pub text_bands: Vec<RuntimePackedEventTextBandSummary>, pub text_bands: Vec<RuntimePackedEventTextBandSummary>,
#[serde(default)] #[serde(default)]
pub standalone_condition_row_count: usize, pub standalone_condition_row_count: usize,
@ -144,6 +146,23 @@ pub struct RuntimePackedEventRecordSummary {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8,
pub primary_selector_0x7f0: u32,
pub grouped_mode_0x7f4: u8,
pub one_shot_header_0x7f5: u32,
pub modifier_flag_0x7f9: u8,
pub modifier_flag_0x7fa: u8,
#[serde(default)]
pub grouped_target_scope_ordinals_0x7fb: Vec<u8>,
#[serde(default)]
pub grouped_scope_checkboxes_0x7ff: Vec<u8>,
pub summary_toggle_0x800: u8,
#[serde(default)]
pub grouped_territory_selectors_0x80f: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventTextBandSummary { pub struct RuntimePackedEventTextBandSummary {
pub label: String, pub label: String,
@ -467,6 +486,23 @@ impl RuntimeState {
)); ));
} }
} }
if let Some(control) = &record.compact_control {
if control.grouped_target_scope_ordinals_0x7fb.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_target_scope_ordinals_0x7fb must contain exactly 4 entries"
));
}
if control.grouped_scope_checkboxes_0x7ff.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_scope_checkboxes_0x7ff must contain exactly 4 entries"
));
}
if control.grouped_territory_selectors_0x80f.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_territory_selectors_0x80f must contain exactly 4 entries"
));
}
}
for row in &record.standalone_condition_rows { for row in &record.standalone_condition_rows {
if row if row
.candidate_name .candidate_name
@ -863,6 +899,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -884,6 +921,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),

View file

@ -95,6 +95,8 @@ const PACKED_EVENT_REAL_CONDITION_MARKER: u16 = 0x526f;
const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8; const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8;
const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e; const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e;
const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28; const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28;
const PACKED_EVENT_REAL_GROUP_COUNT: usize = 4;
const PACKED_EVENT_REAL_COMPACT_CONTROL_LEN: usize = 37;
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",
@ -1234,6 +1236,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub one_shot: Option<bool>, pub one_shot: Option<bool>,
#[serde(default)] #[serde(default)]
pub compact_control: Option<SmpLoadedPackedEventCompactControlSummary>,
#[serde(default)]
pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>, pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>,
#[serde(default)] #[serde(default)]
pub standalone_condition_row_count: usize, pub standalone_condition_row_count: usize,
@ -1251,6 +1255,20 @@ pub struct SmpLoadedPackedEventRecordSummary {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8,
pub primary_selector_0x7f0: u32,
pub grouped_mode_0x7f4: u8,
pub one_shot_header_0x7f5: u32,
pub modifier_flag_0x7f9: u8,
pub modifier_flag_0x7fa: u8,
pub grouped_target_scope_ordinals_0x7fb: Vec<u8>,
pub grouped_scope_checkboxes_0x7ff: Vec<u8>,
pub summary_toggle_0x800: u8,
pub grouped_territory_selectors_0x80f: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventTextBandSummary { pub struct SmpLoadedPackedEventTextBandSummary {
pub label: String, pub label: String,
@ -1736,6 +1754,7 @@ fn parse_synthetic_event_runtime_record_summary(
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),
compact_control: None,
text_bands, text_bands,
standalone_condition_row_count, standalone_condition_row_count,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -1794,6 +1813,8 @@ fn parse_real_event_runtime_record_summary(
}); });
} }
let compact_control = parse_optional_real_compact_control_summary(record_body, &mut cursor)?;
if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER { if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER {
return None; return None;
} }
@ -1851,10 +1872,13 @@ fn parse_real_event_runtime_record_summary(
payload_len: Some(consumed_len), payload_len: Some(consumed_len),
decode_status: "parity_only".to_string(), decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(), payload_family: "real_packed_v1".to_string(),
trigger_kind: None, trigger_kind: compact_control.as_ref().map(|control| control.mode_byte_0x7ef),
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: compact_control
.as_ref()
.map(|control| control.one_shot_header_0x7f5 != 0),
compact_control,
text_bands, text_bands,
standalone_condition_row_count, standalone_condition_row_count,
standalone_condition_rows, standalone_condition_rows,
@ -1868,6 +1892,74 @@ fn parse_real_event_runtime_record_summary(
)) ))
} }
fn parse_optional_real_compact_control_summary(
record_body: &[u8],
cursor: &mut usize,
) -> Option<Option<SmpLoadedPackedEventCompactControlSummary>> {
if read_u16_at(record_body, *cursor)? == PACKED_EVENT_REAL_CONDITION_MARKER {
return Some(None);
}
let end = cursor.checked_add(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN)?;
let bytes = record_body.get(*cursor..end)?;
let mut local = 0usize;
let mode_byte_0x7ef = read_u8_at(bytes, local)?;
local += 1;
let primary_selector_0x7f0 = read_u32_at(bytes, local)?;
local += 4;
let grouped_mode_0x7f4 = read_u8_at(bytes, local)?;
local += 1;
let one_shot_header_0x7f5 = read_u32_at(bytes, local)?;
local += 4;
let modifier_flag_0x7f9 = read_u8_at(bytes, local)?;
local += 1;
let modifier_flag_0x7fa = read_u8_at(bytes, local)?;
local += 1;
let mut grouped_target_scope_ordinals_0x7fb = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
grouped_target_scope_ordinals_0x7fb.push(read_u8_at(bytes, local)?);
local += 1;
}
let mut grouped_scope_checkboxes_0x7ff = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
grouped_scope_checkboxes_0x7ff.push(read_u8_at(bytes, local)?);
local += 1;
}
let summary_toggle_0x800 = read_u8_at(bytes, local)?;
local += 1;
let mut grouped_territory_selectors_0x80f =
Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?);
local += 4;
}
if local != bytes.len() {
return None;
}
if read_u16_at(record_body, end)? != PACKED_EVENT_REAL_CONDITION_MARKER {
return None;
}
*cursor = end;
Some(Some(SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef,
primary_selector_0x7f0,
grouped_mode_0x7f4,
one_shot_header_0x7f5,
modifier_flag_0x7f9,
modifier_flag_0x7fa,
grouped_target_scope_ordinals_0x7fb,
grouped_scope_checkboxes_0x7ff,
summary_toggle_0x800,
grouped_territory_selectors_0x80f,
}))
}
fn parse_real_condition_row_summary( fn parse_real_condition_row_summary(
row_bytes: &[u8], row_bytes: &[u8],
row_index: usize, row_index: usize,
@ -2136,13 +2228,14 @@ 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(), 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(), compact_control: None,
standalone_condition_row_count: 0, text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), 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(), grouped_effect_rows: Vec::new(),
@ -5523,6 +5616,11 @@ fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
} }
fn read_i32_at(bytes: &[u8], offset: usize) -> Option<i32> {
let chunk = bytes.get(offset..offset + 4)?;
Some(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
}
fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> { fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> {
let chunk = bytes.get(offset..offset + 8)?; let chunk = bytes.get(offset..offset + 8)?;
Some(i64::from_le_bytes([ Some(i64::from_le_bytes([
@ -7039,8 +7137,40 @@ mod tests {
bytes bytes
} }
#[derive(Clone, Copy)]
struct RealCompactControlSpec {
mode_byte_0x7ef: u8,
primary_selector_0x7f0: u32,
grouped_mode_0x7f4: u8,
one_shot_header_0x7f5: u32,
modifier_flag_0x7f9: u8,
modifier_flag_0x7fa: u8,
grouped_target_scope_ordinals_0x7fb: [u8; PACKED_EVENT_REAL_GROUP_COUNT],
grouped_scope_checkboxes_0x7ff: [u8; PACKED_EVENT_REAL_GROUP_COUNT],
summary_toggle_0x800: u8,
grouped_territory_selectors_0x80f: [i32; PACKED_EVENT_REAL_GROUP_COUNT],
}
fn build_real_compact_control(spec: RealCompactControlSpec) -> Vec<u8> {
let mut bytes = Vec::with_capacity(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN);
bytes.push(spec.mode_byte_0x7ef);
bytes.extend_from_slice(&spec.primary_selector_0x7f0.to_le_bytes());
bytes.push(spec.grouped_mode_0x7f4);
bytes.extend_from_slice(&spec.one_shot_header_0x7f5.to_le_bytes());
bytes.push(spec.modifier_flag_0x7f9);
bytes.push(spec.modifier_flag_0x7fa);
bytes.extend_from_slice(&spec.grouped_target_scope_ordinals_0x7fb);
bytes.extend_from_slice(&spec.grouped_scope_checkboxes_0x7ff);
bytes.push(spec.summary_toggle_0x800);
for selector in spec.grouped_territory_selectors_0x80f {
bytes.extend_from_slice(&selector.to_le_bytes());
}
bytes
}
fn build_real_event_record( fn build_real_event_record(
text_bands: [&[u8]; 6], text_bands: [&[u8]; 6],
compact_control: Option<RealCompactControlSpec>,
condition_rows: &[Vec<u8>], condition_rows: &[Vec<u8>],
grouped_rows: [&[Vec<u8>]; 4], grouped_rows: [&[Vec<u8>]; 4],
) -> Vec<u8> { ) -> Vec<u8> {
@ -7049,6 +7179,9 @@ mod tests {
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
bytes.extend_from_slice(band); bytes.extend_from_slice(band);
} }
if let Some(spec) = compact_control {
bytes.extend_from_slice(&build_real_compact_control(spec));
}
bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes()); bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes());
bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes()); bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes());
for row in condition_rows { for row in condition_rows {
@ -7171,6 +7304,18 @@ mod tests {
fn parses_real_style_event_runtime_record_with_zero_rows() { fn parses_real_style_event_runtime_record_with_zero_rows() {
let record_body = build_real_event_record( let record_body = build_real_event_record(
[b"Alpha", b"", b"", b"", b"", b""], [b"Alpha", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 1,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [0, 1, 2, 3],
grouped_scope_checkboxes_0x7ff: [1, 0, 1, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, 10, -1, 22],
}),
&[], &[],
[&[], &[], &[], &[]], [&[], &[], &[], &[]],
); );
@ -7198,6 +7343,16 @@ mod tests {
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, "real_packed_v1"); assert_eq!(summary.records[0].payload_family, "real_packed_v1");
assert_eq!(summary.records[0].trigger_kind, Some(7));
assert_eq!(summary.records[0].one_shot, Some(true));
assert_eq!(
summary.records[0]
.compact_control
.as_ref()
.expect("real compact control should parse")
.primary_selector_0x7f0,
0x63
);
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, 0); 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].standalone_condition_rows.len(), 0);
@ -7223,6 +7378,18 @@ mod tests {
let group0_rows = vec![grouped_row]; let group0_rows = vec![grouped_row];
let record_body = build_real_event_record( let record_body = build_real_event_record(
[b"Gamma", b"", b"", b"", b"", b""], [b"Gamma", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x2a,
grouped_mode_0x7f4: 1,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 2,
modifier_flag_0x7fa: 3,
grouped_target_scope_ordinals_0x7fb: [1, 4, 7, 8],
grouped_scope_checkboxes_0x7ff: [0, 1, 0, 1],
summary_toggle_0x800: 0,
grouped_territory_selectors_0x80f: [11, -1, 33, -1],
}),
&[condition_row], &[condition_row],
[&group0_rows, &[], &[], &[]], [&group0_rows, &[], &[], &[]],
); );
@ -7247,6 +7414,14 @@ mod tests {
.expect("event runtime collection summary should parse"); .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.len(), 1);
assert_eq!(
summary.records[0]
.compact_control
.as_ref()
.expect("real compact control should parse")
.grouped_target_scope_ordinals_0x7fb,
vec![1, 4, 7, 8]
);
assert_eq!(summary.records[0].standalone_condition_rows[0].raw_condition_id, -1); assert_eq!(summary.records[0].standalone_condition_rows[0].raw_condition_id, -1);
assert_eq!( assert_eq!(
summary.records[0].standalone_condition_rows[0] summary.records[0].standalone_condition_rows[0]
@ -7272,6 +7447,18 @@ mod tests {
fn rejects_truncated_real_style_event_runtime_record() { fn rejects_truncated_real_style_event_runtime_record() {
let mut record_body = build_real_event_record( let mut record_body = build_real_event_record(
[b"Oops", b"", b"", b"", b"", b""], [b"Oops", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 5,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 0,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
summary_toggle_0x800: 0,
grouped_territory_selectors_0x80f: [0, 0, 0, 0],
}),
&[], &[],
[&[], &[], &[], &[]], [&[], &[], &[], &[]],
); );

View file

@ -35,6 +35,8 @@ 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_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_structural_only_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,
@ -169,6 +171,34 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_compact_control_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_compact_control")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_unmapped_real_descriptor_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_unmapped_real_descriptor")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_structural_only_count: state packed_event_blocked_structural_only_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -271,6 +301,7 @@ mod tests {
active: None, active: None,
marks_collection_dirty: None, marks_collection_dirty: None,
one_shot: None, one_shot: None,
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -278,7 +309,7 @@ mod tests {
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_structural_only".to_string()), import_outcome: Some("blocked_missing_compact_control".to_string()),
notes: Vec::new(), notes: Vec::new(),
}, },
RuntimePackedEventRecordSummary { RuntimePackedEventRecordSummary {
@ -292,6 +323,7 @@ mod tests {
active: Some(true), active: Some(true),
marks_collection_dirty: Some(false), marks_collection_dirty: Some(false),
one_shot: Some(false), one_shot: Some(false),
compact_control: None,
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
@ -311,7 +343,9 @@ mod tests {
}; };
let summary = RuntimeSummary::from_state(&state); let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.packed_event_blocked_structural_only_count, 1); assert_eq!(summary.packed_event_blocked_missing_compact_control_count, 1);
assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0);
assert_eq!(summary.packed_event_blocked_structural_only_count, 0);
assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1); assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1);
} }
} }

View file

@ -75,12 +75,14 @@ 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
- move the packed-event parser from the synthetic harness onto real `0x4e9a` structural decode so - tighten the packed-event frontier from generic real-row structure into decoded real compact
real rows stop collapsing to generic unsupported framing control, so parity rows carry mode, selector, one-shot, and grouped target-scope state directly
- use overlay imports as the context bridge when selectively executable packed rows still need live - use overlay imports as the context bridge when selectively executable packed rows still need live
company state that save slices do not persist company state that save slices do not persist
- widen real packed-event executable coverage only after the structural decode frontier and row - widen real packed-event executable coverage only after the compact-control and descriptor frontier
summaries are stable is stable, not just after row framing is parsed
- keep in mind that the current local `.gms` corpus still exports with no packed event collection,
so real descriptor mapping needs to stay plumbing-first until better captures exist
- 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,7 +26,8 @@ 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
real `0x4e9a` packed-event structural decode, not another persistence scaffold pass. real `0x4e9a` compact-control decode and descriptor-frontier tightening, not another persistence
scaffold pass.
## Why This Boundary ## Why This Boundary
@ -365,33 +366,41 @@ Checked-in fixture families already include:
## Next Slice ## Next Slice
The recommended next implementation slice is real `0x4e9a` packed-event structural decode on top The recommended next implementation slice is real `0x4e9a` compact-control decode on top of the
of the save-slice, snapshot, and overlay workflows that already exist today. existing real-row structural parse.
Target behavior: Target behavior:
- parse real packed-event record bodies structurally instead of dropping them straight to - decode the compact control block that sits above the real standalone-condition and grouped-effect
`unsupported_framing` row families, carrying through raw grounded lanes such as mode byte `0x7ef`, primary selector
- preserve the first real decode pass as parity-only, exposing text-band and row-family structure `0x7f0`, grouped mode `0x7f4`, one-shot header `0x7f5`, modifier bytes `0x7f9/0x7fa`, grouped
without guessing executable semantics target-scope ordinals `0x7fb`, grouped scope checkboxes `0x7ff`, summary toggle `0x800`, and
grouped territory selectors `0x80f`
- keep real rows parity-only in runtime import, but replace the coarse `blocked_structural_only`
frontier with narrower outcomes such as `blocked_missing_compact_control` and
`blocked_unmapped_real_descriptor`
- keep the existing synthetic harness and overlay-backed executable import path working unchanged - keep the existing synthetic harness and overlay-backed executable import path working unchanged
- reserve selective executable import from real rows for a later pass once row semantics are tighter - reserve the first real descriptor-to-effect mapping for a later slice once captured evidence is
tighter
Public-model additions for that slice: Public-model additions for that slice:
- payload-family labeling that distinguishes synthetic harness records from real packed rows and - compact-control summaries on packed-event records in both the save-side and runtime-side models
unsupported framing - runtime summary counts for compact-control-missing and unmapped-real-descriptor blockers
- structural row summaries for real standalone condition rows and grouped effect rows - trigger-kind and one-shot derivation only where the compact-control mapping is already grounded
- 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:
- one parity-heavy tracked sample that now exposes a real structurally decoded row family - update the parity-heavy tracked sample so the real row includes compact-control state
- regression fixtures that keep synthetic executable import and overlay-backed company-context - regression fixtures that keep synthetic executable import and overlay-backed company-context
upgrade behavior green upgrade behavior green
- state-fragment assertions that lock the new structural row summaries and - state-fragment assertions that lock the new compact-control summary and narrower import blockers
`blocked_structural_only` frontier
Current local constraint:
- the local checked-in and on-disk `.gms` corpus currently exports with
`packed_event_collection_present = false`, so this slice must not depend on a newly captured real
packed-event-bearing save for acceptance
Do not mix this slice with: Do not mix this slice with:

View file

@ -26,7 +26,9 @@
"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, "packed_event_blocked_missing_compact_control_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 1,
"packed_event_blocked_structural_only_count": 0,
"event_runtime_record_count": 0, "event_runtime_record_count": 0,
"total_company_cash": 0 "total_company_cash": 0
}, },
@ -47,7 +49,13 @@
{ {
"decode_status": "parity_only", "decode_status": "parity_only",
"payload_family": "real_packed_v1", "payload_family": "real_packed_v1",
"import_outcome": "blocked_structural_only", "trigger_kind": 6,
"one_shot": true,
"import_outcome": "blocked_unmapped_real_descriptor",
"compact_control": {
"primary_selector_0x7f0": 99,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3]
},
"standalone_condition_rows": [ "standalone_condition_rows": [
{ {
"candidate_name": "AutoPlant" "candidate_name": "AutoPlant"

View file

@ -54,9 +54,23 @@
"record_index": 1, "record_index": 1,
"live_entry_id": 5, "live_entry_id": 5,
"payload_offset": 29290, "payload_offset": 29290,
"payload_len": 72, "payload_len": 109,
"decode_status": "parity_only", "decode_status": "parity_only",
"payload_family": "real_packed_v1", "payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": true,
"compact_control": {
"mode_byte_0x7ef": 6,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 1,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3],
"grouped_scope_checkboxes_0x7ff": [1, 0, 1, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, 10, -1, 22]
},
"text_bands": [ "text_bands": [
{ {
"label": "primary_text_band", "label": "primary_text_band",
@ -137,7 +151,7 @@
"decoded_actions": [], "decoded_actions": [],
"executable_import_ready": false, "executable_import_ready": false,
"notes": [ "notes": [
"decoded from grounded real 0x4e9a row framing" "decoded from grounded real 0x4e9a row framing with compact control"
] ]
} }
] ]