Decode real packed event record structure
This commit is contained in:
parent
fa63cefb70
commit
45f258cf5d
16 changed files with 1011 additions and 44 deletions
|
|
@ -91,6 +91,10 @@ const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
|||
const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
|
||||
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
|
||||
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] = [
|
||||
"primary_text_band",
|
||||
"secondary_text_band_0",
|
||||
|
|
@ -1220,6 +1224,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
|
|||
pub payload_len: Option<usize>,
|
||||
pub decode_status: String,
|
||||
#[serde(default)]
|
||||
pub payload_family: String,
|
||||
#[serde(default)]
|
||||
pub trigger_kind: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub active: Option<bool>,
|
||||
|
|
@ -1232,8 +1238,12 @@ pub struct SmpLoadedPackedEventRecordSummary {
|
|||
#[serde(default)]
|
||||
pub standalone_condition_row_count: usize,
|
||||
#[serde(default)]
|
||||
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
|
||||
#[serde(default)]
|
||||
pub grouped_effect_row_counts: Vec<usize>,
|
||||
#[serde(default)]
|
||||
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
|
||||
#[serde(default)]
|
||||
pub decoded_actions: Vec<RuntimeEffect>,
|
||||
#[serde(default)]
|
||||
pub executable_import_ready: bool,
|
||||
|
|
@ -1249,6 +1259,39 @@ pub struct SmpLoadedPackedEventTextBandSummary {
|
|||
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)]
|
||||
pub struct SmpLoadedSaveSlice {
|
||||
pub file_extension_hint: Option<String>,
|
||||
|
|
@ -1576,6 +1619,13 @@ fn parse_event_runtime_record_summaries(
|
|||
records_payload_offset,
|
||||
live_entry_ids,
|
||||
)
|
||||
.or_else(|| {
|
||||
try_parse_real_event_runtime_record_summaries(
|
||||
records_payload,
|
||||
records_payload_offset,
|
||||
live_entry_ids,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
build_unsupported_event_runtime_record_summaries(
|
||||
live_entry_ids,
|
||||
|
|
@ -1681,19 +1731,236 @@ fn parse_synthetic_event_runtime_record_summary(
|
|||
} else {
|
||||
"parity_only".to_string()
|
||||
},
|
||||
payload_family: "synthetic_harness".to_string(),
|
||||
trigger_kind: Some(trigger_kind),
|
||||
active: Some(flags & 0x01 != 0),
|
||||
marks_collection_dirty: Some(flags & 0x02 != 0),
|
||||
one_shot: Some(flags & 0x04 != 0),
|
||||
text_bands,
|
||||
standalone_condition_row_count,
|
||||
standalone_condition_rows: Vec::new(),
|
||||
grouped_effect_row_counts,
|
||||
grouped_effect_rows: Vec::new(),
|
||||
decoded_actions,
|
||||
executable_import_ready,
|
||||
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> {
|
||||
let opcode = read_u8_at(bytes, *cursor)?;
|
||||
*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())
|
||||
}
|
||||
|
||||
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 {
|
||||
match effect {
|
||||
RuntimeEffect::SetWorldFlag { .. }
|
||||
|
|
@ -1858,13 +2136,16 @@ 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,
|
||||
standalone_condition_rows: Vec::new(),
|
||||
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||
grouped_effect_rows: Vec::new(),
|
||||
decoded_actions: Vec::new(),
|
||||
executable_import_ready: false,
|
||||
notes: vec![note.to_string()],
|
||||
|
|
@ -6701,6 +6982,90 @@ mod tests {
|
|||
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]
|
||||
fn parses_synthetic_event_runtime_record_summaries_and_actions() {
|
||||
let append_template = encode_template(
|
||||
|
|
@ -6746,6 +7111,7 @@ mod tests {
|
|||
assert_eq!(summary.imported_runtime_record_count, 1);
|
||||
assert_eq!(summary.records.len(), 1);
|
||||
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].standalone_condition_row_count, 1);
|
||||
assert_eq!(
|
||||
|
|
@ -6797,9 +7163,143 @@ mod tests {
|
|||
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, "synthetic_harness");
|
||||
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]
|
||||
fn loads_event_runtime_collection_summary_from_report() {
|
||||
let mut report = inspect_smp_bytes(&[]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue