Decode packed event records for runtime import
This commit is contained in:
parent
83f55fa26e
commit
09b6514dbf
13 changed files with 1801 additions and 50 deletions
|
|
@ -4,6 +4,8 @@ use std::path::Path;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate};
|
||||
|
||||
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
||||
const PREAMBLE_U32_WORD_COUNT: usize = 16;
|
||||
const MIN_ASCII_RUN_LEN: usize = 8;
|
||||
|
|
@ -86,6 +88,17 @@ const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
|
|||
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
||||
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
||||
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
||||
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_TEXT_BAND_LABELS: [&str; 6] = [
|
||||
"primary_text_band",
|
||||
"secondary_text_band_0",
|
||||
"secondary_text_band_1",
|
||||
"secondary_text_band_2",
|
||||
"secondary_text_band_3",
|
||||
"secondary_text_band_4",
|
||||
];
|
||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize =
|
||||
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
||||
|
|
@ -1189,6 +1202,51 @@ pub struct SmpLoadedEventRuntimeCollectionSummary {
|
|||
pub live_id_bound: u32,
|
||||
pub live_record_count: usize,
|
||||
pub live_entry_ids: Vec<u32>,
|
||||
#[serde(default)]
|
||||
pub decoded_record_count: usize,
|
||||
#[serde(default)]
|
||||
pub imported_runtime_record_count: usize,
|
||||
#[serde(default)]
|
||||
pub records: Vec<SmpLoadedPackedEventRecordSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpLoadedPackedEventRecordSummary {
|
||||
pub record_index: usize,
|
||||
pub live_entry_id: u32,
|
||||
#[serde(default)]
|
||||
pub payload_offset: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub payload_len: Option<usize>,
|
||||
pub decode_status: String,
|
||||
#[serde(default)]
|
||||
pub trigger_kind: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub active: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub marks_collection_dirty: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub one_shot: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>,
|
||||
#[serde(default)]
|
||||
pub standalone_condition_row_count: usize,
|
||||
#[serde(default)]
|
||||
pub grouped_effect_row_counts: Vec<usize>,
|
||||
#[serde(default)]
|
||||
pub decoded_actions: Vec<RuntimeEffect>,
|
||||
#[serde(default)]
|
||||
pub executable_import_ready: bool,
|
||||
#[serde(default)]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpLoadedPackedEventTextBandSummary {
|
||||
pub label: String,
|
||||
pub packed_len: usize,
|
||||
pub present: bool,
|
||||
pub preview: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -1431,6 +1489,20 @@ fn parse_event_runtime_collection_summary(
|
|||
if live_entry_ids.len() != live_record_count {
|
||||
continue;
|
||||
}
|
||||
let records_payload = bytes.get(records_tag_offset + 2..close_tag_offset)?;
|
||||
let records = parse_event_runtime_record_summaries(
|
||||
records_payload,
|
||||
records_tag_offset + 2,
|
||||
&live_entry_ids,
|
||||
);
|
||||
let decoded_record_count = records
|
||||
.iter()
|
||||
.filter(|record| record.decode_status != "unsupported_framing")
|
||||
.count();
|
||||
let imported_runtime_record_count = records
|
||||
.iter()
|
||||
.filter(|record| record.executable_import_ready)
|
||||
.count();
|
||||
|
||||
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||
source_kind: "packed-event-runtime-collection".to_string(),
|
||||
|
|
@ -1450,6 +1522,9 @@ fn parse_event_runtime_collection_summary(
|
|||
live_id_bound,
|
||||
live_record_count,
|
||||
live_entry_ids,
|
||||
decoded_record_count,
|
||||
imported_runtime_record_count,
|
||||
records,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1491,6 +1566,313 @@ fn decode_live_entry_ids_with_mapping(
|
|||
Some(live_entry_ids)
|
||||
}
|
||||
|
||||
fn parse_event_runtime_record_summaries(
|
||||
records_payload: &[u8],
|
||||
records_payload_offset: usize,
|
||||
live_entry_ids: &[u32],
|
||||
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
||||
try_parse_synthetic_event_runtime_record_summaries(
|
||||
records_payload,
|
||||
records_payload_offset,
|
||||
live_entry_ids,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
build_unsupported_event_runtime_record_summaries(
|
||||
live_entry_ids,
|
||||
"0x4e9a payload did not match the current packed-event record decode harness",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn try_parse_synthetic_event_runtime_record_summaries(
|
||||
records_payload: &[u8],
|
||||
records_payload_offset: usize,
|
||||
live_entry_ids: &[u32],
|
||||
) -> Option<Vec<SmpLoadedPackedEventRecordSummary>> {
|
||||
if !records_payload.starts_with(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC.len();
|
||||
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_len = usize::try_from(read_u32_at(records_payload, cursor)?).ok()?;
|
||||
cursor += 4;
|
||||
let record_body = records_payload.get(cursor..cursor + record_len)?;
|
||||
records.push(parse_synthetic_event_runtime_record_summary(
|
||||
record_body,
|
||||
records_payload_offset + cursor,
|
||||
record_index,
|
||||
live_entry_id,
|
||||
)?);
|
||||
cursor += record_len;
|
||||
}
|
||||
|
||||
if cursor != records_payload.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(records)
|
||||
}
|
||||
|
||||
fn parse_synthetic_event_runtime_record_summary(
|
||||
record_body: &[u8],
|
||||
payload_offset: usize,
|
||||
record_index: usize,
|
||||
live_entry_id: u32,
|
||||
) -> Option<SmpLoadedPackedEventRecordSummary> {
|
||||
if !record_body.starts_with(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = PACKED_EVENT_RECORD_SYNTHETIC_MAGIC.len();
|
||||
let trigger_kind = read_u8_at(record_body, cursor)?;
|
||||
cursor += 1;
|
||||
let flags = read_u8_at(record_body, cursor)?;
|
||||
cursor += 1;
|
||||
let standalone_condition_row_count = usize::from(read_u8_at(record_body, cursor)?);
|
||||
cursor += 1;
|
||||
let action_count = usize::from(read_u8_at(record_body, cursor)?);
|
||||
cursor += 1;
|
||||
|
||||
let mut grouped_effect_row_counts = Vec::with_capacity(4);
|
||||
for _ in 0..4 {
|
||||
grouped_effect_row_counts.push(usize::from(read_u8_at(record_body, cursor)?));
|
||||
cursor += 1;
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
let mut decoded_actions = Vec::with_capacity(action_count);
|
||||
for _ in 0..action_count {
|
||||
decoded_actions.push(parse_synthetic_packed_event_action(
|
||||
record_body,
|
||||
&mut cursor,
|
||||
)?);
|
||||
}
|
||||
|
||||
if cursor != record_body.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let executable_import_ready = decoded_actions
|
||||
.iter()
|
||||
.all(runtime_effect_supported_for_save_import);
|
||||
|
||||
Some(SmpLoadedPackedEventRecordSummary {
|
||||
record_index,
|
||||
live_entry_id,
|
||||
payload_offset: Some(payload_offset),
|
||||
payload_len: Some(record_body.len()),
|
||||
decode_status: if executable_import_ready {
|
||||
"executable".to_string()
|
||||
} else {
|
||||
"parity_only".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,
|
||||
grouped_effect_row_counts,
|
||||
decoded_actions,
|
||||
executable_import_ready,
|
||||
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
|
||||
let opcode = read_u8_at(bytes, *cursor)?;
|
||||
*cursor += 1;
|
||||
match opcode {
|
||||
0x01 => {
|
||||
let key = parse_len_prefixed_string(bytes, cursor)?;
|
||||
let value = read_u8_at(bytes, *cursor)? != 0;
|
||||
*cursor += 1;
|
||||
Some(RuntimeEffect::SetWorldFlag { key, value })
|
||||
}
|
||||
0x02 => {
|
||||
let target = parse_synthetic_company_target(bytes, cursor)?;
|
||||
let delta = read_i64_at(bytes, *cursor)?;
|
||||
*cursor += 8;
|
||||
Some(RuntimeEffect::AdjustCompanyCash { target, delta })
|
||||
}
|
||||
0x03 => {
|
||||
let target = parse_synthetic_company_target(bytes, cursor)?;
|
||||
let delta = read_i64_at(bytes, *cursor)?;
|
||||
*cursor += 8;
|
||||
Some(RuntimeEffect::AdjustCompanyDebt { target, delta })
|
||||
}
|
||||
0x04 => {
|
||||
let name = parse_len_prefixed_string(bytes, cursor)?;
|
||||
let value = read_u32_at(bytes, *cursor)?;
|
||||
*cursor += 4;
|
||||
Some(RuntimeEffect::SetCandidateAvailability { name, value })
|
||||
}
|
||||
0x05 => {
|
||||
let label = parse_len_prefixed_string(bytes, cursor)?;
|
||||
let value = read_u32_at(bytes, *cursor)?;
|
||||
*cursor += 4;
|
||||
Some(RuntimeEffect::SetSpecialCondition { label, value })
|
||||
}
|
||||
0x06 => {
|
||||
let template_len = usize::try_from(read_u32_at(bytes, *cursor)?).ok()?;
|
||||
*cursor += 4;
|
||||
let template_bytes = bytes.get(*cursor..*cursor + template_len)?;
|
||||
let record = parse_synthetic_event_runtime_record_template(template_bytes)?;
|
||||
*cursor += template_len;
|
||||
Some(RuntimeEffect::AppendEventRecord {
|
||||
record: Box::new(record),
|
||||
})
|
||||
}
|
||||
0x07 => {
|
||||
let record_id = read_u32_at(bytes, *cursor)?;
|
||||
*cursor += 4;
|
||||
Some(RuntimeEffect::ActivateEventRecord { record_id })
|
||||
}
|
||||
0x08 => {
|
||||
let record_id = read_u32_at(bytes, *cursor)?;
|
||||
*cursor += 4;
|
||||
Some(RuntimeEffect::DeactivateEventRecord { record_id })
|
||||
}
|
||||
0x09 => {
|
||||
let record_id = read_u32_at(bytes, *cursor)?;
|
||||
*cursor += 4;
|
||||
Some(RuntimeEffect::RemoveEventRecord { record_id })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_synthetic_event_runtime_record_template(
|
||||
bytes: &[u8],
|
||||
) -> Option<RuntimeEventRecordTemplate> {
|
||||
if !bytes.starts_with(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC.len();
|
||||
let record_id = read_u32_at(bytes, cursor)?;
|
||||
cursor += 4;
|
||||
let trigger_kind = read_u8_at(bytes, cursor)?;
|
||||
cursor += 1;
|
||||
let flags = read_u8_at(bytes, cursor)?;
|
||||
cursor += 1;
|
||||
let action_count = usize::from(read_u8_at(bytes, cursor)?);
|
||||
cursor += 1;
|
||||
cursor += 1;
|
||||
|
||||
let mut effects = Vec::with_capacity(action_count);
|
||||
for _ in 0..action_count {
|
||||
effects.push(parse_synthetic_packed_event_action(bytes, &mut cursor)?);
|
||||
}
|
||||
|
||||
if cursor != bytes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(RuntimeEventRecordTemplate {
|
||||
record_id,
|
||||
trigger_kind,
|
||||
active: flags & 0x01 != 0,
|
||||
marks_collection_dirty: flags & 0x02 != 0,
|
||||
one_shot: flags & 0x04 != 0,
|
||||
effects,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_synthetic_company_target(
|
||||
bytes: &[u8],
|
||||
cursor: &mut usize,
|
||||
) -> Option<RuntimeCompanyTarget> {
|
||||
let target_kind = read_u8_at(bytes, *cursor)?;
|
||||
*cursor += 1;
|
||||
match target_kind {
|
||||
0x00 => Some(RuntimeCompanyTarget::AllActive),
|
||||
0x01 => {
|
||||
let count = usize::from(read_u8_at(bytes, *cursor)?);
|
||||
*cursor += 1;
|
||||
let mut ids = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
ids.push(read_u32_at(bytes, *cursor)?);
|
||||
*cursor += 4;
|
||||
}
|
||||
Some(RuntimeCompanyTarget::Ids { ids })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<String> {
|
||||
let len = usize::from(read_u8_at(bytes, *cursor)?);
|
||||
*cursor += 1;
|
||||
let text_bytes = bytes.get(*cursor..*cursor + len)?;
|
||||
*cursor += len;
|
||||
Some(String::from_utf8_lossy(text_bytes).into_owned())
|
||||
}
|
||||
|
||||
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
||||
match effect {
|
||||
RuntimeEffect::SetWorldFlag { .. }
|
||||
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||
| RuntimeEffect::SetSpecialCondition { .. }
|
||||
| RuntimeEffect::ActivateEventRecord { .. }
|
||||
| RuntimeEffect::DeactivateEventRecord { .. }
|
||||
| RuntimeEffect::RemoveEventRecord { .. } => true,
|
||||
RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||
matches!(target, RuntimeCompanyTarget::AllActive)
|
||||
}
|
||||
RuntimeEffect::AppendEventRecord { record } => record
|
||||
.effects
|
||||
.iter()
|
||||
.all(runtime_effect_supported_for_save_import),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_unsupported_event_runtime_record_summaries(
|
||||
live_entry_ids: &[u32],
|
||||
note: &str,
|
||||
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
||||
live_entry_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(
|
||||
|(record_index, live_entry_id)| SmpLoadedPackedEventRecordSummary {
|
||||
record_index,
|
||||
live_entry_id,
|
||||
payload_offset: None,
|
||||
payload_len: None,
|
||||
decode_status: "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,
|
||||
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
||||
decoded_actions: Vec::new(),
|
||||
executable_import_ready: false,
|
||||
notes: vec![note.to_string()],
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
||||
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
||||
.iter()
|
||||
|
|
@ -4846,11 +5228,27 @@ fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec<u32> {
|
|||
words
|
||||
}
|
||||
|
||||
fn read_u8_at(bytes: &[u8], offset: usize) -> Option<u8> {
|
||||
bytes.get(offset).copied()
|
||||
}
|
||||
|
||||
fn read_u16_at(bytes: &[u8], offset: usize) -> Option<u16> {
|
||||
let chunk = bytes.get(offset..offset + 2)?;
|
||||
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||
}
|
||||
|
||||
fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
|
||||
let chunk = bytes.get(offset..offset + 4)?;
|
||||
Some(u32::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([
|
||||
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
||||
]))
|
||||
}
|
||||
|
||||
fn probable_normal_f32_string(value: u32) -> Option<String> {
|
||||
let exponent = (value >> 23) & 0xff;
|
||||
if exponent == 0 || exponent == 0xff {
|
||||
|
|
@ -5611,7 +6009,10 @@ mod tests {
|
|||
#[test]
|
||||
fn classifies_recipe_token_layouts() {
|
||||
assert_eq!(classify_recipe_token_layout(0x00000000), "zero");
|
||||
assert_eq!(classify_recipe_token_layout(0x72470000), "high16-ascii-stem");
|
||||
assert_eq!(
|
||||
classify_recipe_token_layout(0x72470000),
|
||||
"high16-ascii-stem"
|
||||
);
|
||||
assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric");
|
||||
assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker");
|
||||
}
|
||||
|
|
@ -5638,9 +6039,18 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn classifies_recipe_runtime_import_branches() {
|
||||
assert_eq!(classify_recipe_runtime_import_branch(0), "zero-mode-skipped");
|
||||
assert_eq!(classify_recipe_runtime_import_branch(1), "mode1-demand-branch");
|
||||
assert_eq!(classify_recipe_runtime_import_branch(3), "mode3-dual-branch");
|
||||
assert_eq!(
|
||||
classify_recipe_runtime_import_branch(0),
|
||||
"zero-mode-skipped"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_recipe_runtime_import_branch(1),
|
||||
"mode1-demand-branch"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_recipe_runtime_import_branch(3),
|
||||
"mode3-dual-branch"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_recipe_runtime_import_branch(0x00110000),
|
||||
"nonzero-supply-branch"
|
||||
|
|
@ -6205,6 +6615,189 @@ mod tests {
|
|||
assert_eq!(summary.live_record_count, 3);
|
||||
assert_eq!(summary.live_entry_ids, vec![1, 3, 5]);
|
||||
assert_eq!(summary.records_tag_offset, 96);
|
||||
assert_eq!(summary.decoded_record_count, 0);
|
||||
assert_eq!(summary.records.len(), 3);
|
||||
assert_eq!(summary.records[0].decode_status, "unsupported_framing");
|
||||
}
|
||||
|
||||
fn encode_len_prefixed_string(text: &str) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(1 + text.len());
|
||||
bytes.push(text.len() as u8);
|
||||
bytes.extend_from_slice(text.as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn encode_template(
|
||||
record_id: u32,
|
||||
trigger_kind: u8,
|
||||
flags: u8,
|
||||
actions: &[Vec<u8>],
|
||||
) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC);
|
||||
bytes.extend_from_slice(&record_id.to_le_bytes());
|
||||
bytes.push(trigger_kind);
|
||||
bytes.push(flags);
|
||||
bytes.push(actions.len() as u8);
|
||||
bytes.push(0);
|
||||
for action in actions {
|
||||
bytes.extend_from_slice(action);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn encode_action_set_world_flag(key: &str, value: bool) -> Vec<u8> {
|
||||
let mut bytes = vec![0x01];
|
||||
bytes.extend_from_slice(&encode_len_prefixed_string(key));
|
||||
bytes.push(u8::from(value));
|
||||
bytes
|
||||
}
|
||||
|
||||
fn encode_action_set_special_condition(label: &str, value: u32) -> Vec<u8> {
|
||||
let mut bytes = vec![0x05];
|
||||
bytes.extend_from_slice(&encode_len_prefixed_string(label));
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn encode_action_adjust_company_cash_ids(ids: &[u32], delta: i64) -> Vec<u8> {
|
||||
let mut bytes = vec![0x02, 0x01, ids.len() as u8];
|
||||
for id in ids {
|
||||
bytes.extend_from_slice(&id.to_le_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(&delta.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn encode_action_append_template(template: Vec<u8>) -> Vec<u8> {
|
||||
let mut bytes = vec![0x06];
|
||||
bytes.extend_from_slice(&(template.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&template);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn build_synthetic_event_record(
|
||||
trigger_kind: u8,
|
||||
flags: u8,
|
||||
standalone_count: u8,
|
||||
grouped_counts: [u8; 4],
|
||||
text_bands: [&[u8]; 6],
|
||||
actions: &[Vec<u8>],
|
||||
) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC);
|
||||
bytes.push(trigger_kind);
|
||||
bytes.push(flags);
|
||||
bytes.push(standalone_count);
|
||||
bytes.push(actions.len() as u8);
|
||||
bytes.extend_from_slice(&grouped_counts);
|
||||
for band in text_bands {
|
||||
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
|
||||
bytes.extend_from_slice(band);
|
||||
}
|
||||
for action in actions {
|
||||
bytes.extend_from_slice(action);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_synthetic_event_runtime_record_summaries_and_actions() {
|
||||
let append_template = encode_template(
|
||||
99,
|
||||
0x0a,
|
||||
0x01,
|
||||
&[encode_action_set_special_condition("Imported Follow-On", 1)],
|
||||
);
|
||||
let record_body = build_synthetic_event_record(
|
||||
7,
|
||||
0x03,
|
||||
1,
|
||||
[0, 1, 0, 0],
|
||||
[b"Alpha", b"", b"", b"", b"", b""],
|
||||
&[
|
||||
encode_action_set_world_flag("from_packed_root", true),
|
||||
encode_action_append_template(append_template),
|
||||
],
|
||||
);
|
||||
|
||||
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(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
||||
bytes.extend_from_slice(&(record_body.len() as u32).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, 1);
|
||||
assert_eq!(summary.records.len(), 1);
|
||||
assert_eq!(summary.records[0].decode_status, "executable");
|
||||
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].grouped_effect_row_counts,
|
||||
vec![0, 1, 0, 0]
|
||||
);
|
||||
assert_eq!(summary.records[0].decoded_actions.len(), 2);
|
||||
match &summary.records[0].decoded_actions[1] {
|
||||
RuntimeEffect::AppendEventRecord { record } => {
|
||||
assert_eq!(record.record_id, 99);
|
||||
assert_eq!(record.trigger_kind, 0x0a);
|
||||
}
|
||||
other => panic!("unexpected decoded action: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_company_targeted_synthetic_records_as_parity_only() {
|
||||
let record_body = build_synthetic_event_record(
|
||||
8,
|
||||
0x01,
|
||||
0,
|
||||
[0, 0, 0, 0],
|
||||
[b"", b"", b"", b"", b"", b""],
|
||||
&[encode_action_adjust_company_cash_ids(&[7], 25)],
|
||||
);
|
||||
|
||||
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(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
||||
bytes.extend_from_slice(&(record_body.len() as u32).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!(!summary.records[0].executable_import_ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -6267,6 +6860,9 @@ mod tests {
|
|||
live_id_bound: 5,
|
||||
live_record_count: 3,
|
||||
live_entry_ids: vec![1, 3, 5],
|
||||
decoded_record_count: 0,
|
||||
imported_runtime_record_count: 0,
|
||||
records: build_unsupported_event_runtime_record_summaries(&[1, 3, 5], "test summary"),
|
||||
});
|
||||
|
||||
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue