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

@ -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_CONDITION_ROW_LEN: usize = 0x1e;
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] = [
"primary_text_band",
"secondary_text_band_0",
@ -1234,6 +1236,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)]
pub one_shot: Option<bool>,
#[serde(default)]
pub compact_control: Option<SmpLoadedPackedEventCompactControlSummary>,
#[serde(default)]
pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>,
#[serde(default)]
pub standalone_condition_row_count: usize,
@ -1251,6 +1255,20 @@ pub struct SmpLoadedPackedEventRecordSummary {
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)]
pub struct SmpLoadedPackedEventTextBandSummary {
pub label: String,
@ -1736,6 +1754,7 @@ fn parse_synthetic_event_runtime_record_summary(
active: Some(flags & 0x01 != 0),
marks_collection_dirty: Some(flags & 0x02 != 0),
one_shot: Some(flags & 0x04 != 0),
compact_control: None,
text_bands,
standalone_condition_row_count,
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 {
return None;
}
@ -1851,10 +1872,13 @@ fn parse_real_event_runtime_record_summary(
payload_len: Some(consumed_len),
decode_status: "parity_only".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,
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,
standalone_condition_row_count,
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(
row_bytes: &[u8],
row_index: usize,
@ -2136,13 +2228,14 @@ fn build_unsupported_event_runtime_record_summaries(
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
compact_control: 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(),
@ -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]]))
}
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> {
let chunk = bytes.get(offset..offset + 8)?;
Some(i64::from_le_bytes([
@ -7039,8 +7137,40 @@ mod tests {
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(
text_bands: [&[u8]; 6],
compact_control: Option<RealCompactControlSpec>,
condition_rows: &[Vec<u8>],
grouped_rows: [&[Vec<u8>]; 4],
) -> Vec<u8> {
@ -7049,6 +7179,9 @@ mod tests {
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
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(&(condition_rows.len() as u16).to_le_bytes());
for row in condition_rows {
@ -7171,6 +7304,18 @@ mod tests {
fn parses_real_style_event_runtime_record_with_zero_rows() {
let record_body = build_real_event_record(
[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.records[0].decode_status, "parity_only");
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].standalone_condition_row_count, 0);
assert_eq!(summary.records[0].standalone_condition_rows.len(), 0);
@ -7223,6 +7378,18 @@ mod tests {
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[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],
[&group0_rows, &[], &[], &[]],
);
@ -7247,6 +7414,14 @@ mod tests {
.expect("event runtime collection summary should parse");
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]
@ -7272,6 +7447,18 @@ mod tests {
fn rejects_truncated_real_style_event_runtime_record() {
let mut record_body = build_real_event_record(
[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],
}),
&[],
[&[], &[], &[], &[]],
);