Recover whole-game packed event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-15 22:19:09 -07:00
commit a3f9a73766
8 changed files with 627 additions and 16 deletions

View file

@ -127,7 +127,7 @@ struct RealGroupedEffectDescriptorMetadata {
executable_in_runtime: bool,
}
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 8] = [
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 11] = [
RealGroupedEffectDescriptorMetadata {
descriptor_id: 1,
label: "Player Cash",
@ -156,6 +156,27 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
parameter_family: "whole_game_state_enum",
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 108,
label: "Use Wartime Cargos",
target_mask_bits: 0x08,
parameter_family: "special_condition_scalar",
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 109,
label: "Turbo Diesel Availability",
target_mask_bits: 0x08,
parameter_family: "candidate_availability_scalar",
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 110,
label: "Disable Stock Buying and Selling",
target_mask_bits: 0x08,
parameter_family: "world_flag_toggle",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 9,
label: "Confiscate All",
@ -2421,17 +2442,8 @@ fn parse_real_grouped_effect_row_summary(
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 descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id);
let semantic_family = classify_real_grouped_effect_semantic_family(
let mut row_shape = classify_real_grouped_effect_row_shape(
opcode,
raw_scalar_value,
value_byte_0x11,
@ -2440,6 +2452,28 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x16,
)
.to_string();
let mut semantic_family = classify_real_grouped_effect_semantic_family(
opcode,
raw_scalar_value,
value_byte_0x11,
value_byte_0x12,
value_word_0x14,
value_word_0x16,
)
.to_string();
if descriptor_metadata.is_some_and(|metadata| {
matches!(
metadata.parameter_family,
"special_condition_scalar" | "candidate_availability_scalar"
) && opcode == 3
&& value_byte_0x11 == 0
&& value_byte_0x12 == 0
&& value_word_0x14 == 0
&& value_word_0x16 == 0
}) {
row_shape = "scalar_assignment".to_string();
semantic_family = "scalar_assignment".to_string();
}
let mut notes = Vec::new();
if locomotive_name.is_some() {
@ -2606,6 +2640,13 @@ fn build_real_grouped_effect_semantic_preview(
}
}
fn runtime_candidate_availability_name(label: &str) -> String {
label
.strip_suffix(" Availability")
.unwrap_or(label)
.to_string()
}
fn decode_real_grouped_effect_actions(
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
compact_control: &SmpLoadedPackedEventCompactControlSummary,
@ -2680,6 +2721,26 @@ fn decode_real_grouped_effect_action(
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 108
&& row.row_shape == "scalar_assignment"
{
return Some(RuntimeEffect::SetSpecialCondition {
label: descriptor_metadata.label.to_string(),
value: row.raw_scalar_value as u32,
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 109
&& row.row_shape == "scalar_assignment"
{
return Some(RuntimeEffect::SetCandidateAvailability {
name: runtime_candidate_availability_name(descriptor_metadata.label),
value: row.raw_scalar_value as u32,
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 9
&& row.row_shape == "bool_toggle"
@ -8251,6 +8312,222 @@ mod tests {
);
}
#[test]
fn decodes_real_special_condition_descriptor_from_checked_in_metadata() {
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 108,
opcode: 3,
raw_scalar_value: 1,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
locomotive_name: None,
});
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
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: [1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[],
[&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].grouped_effect_rows[0]
.descriptor_label
.as_deref(),
Some("Use Wartime Cargos")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.parameter_family
.as_deref(),
Some("special_condition_scalar")
);
assert_eq!(
summary.records[0].decoded_actions,
vec![RuntimeEffect::SetSpecialCondition {
label: "Use Wartime Cargos".to_string(),
value: 1,
}]
);
assert!(summary.records[0].executable_import_ready);
}
#[test]
fn decodes_real_candidate_availability_descriptor_from_checked_in_metadata() {
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 109,
opcode: 3,
raw_scalar_value: 1,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
locomotive_name: None,
});
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
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: [1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[],
[&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].grouped_effect_rows[0]
.descriptor_label
.as_deref(),
Some("Turbo Diesel Availability")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.parameter_family
.as_deref(),
Some("candidate_availability_scalar")
);
assert_eq!(
summary.records[0].decoded_actions,
vec![RuntimeEffect::SetCandidateAvailability {
name: "Turbo Diesel".to_string(),
value: 1,
}]
);
assert!(summary.records[0].executable_import_ready);
}
#[test]
fn keeps_real_world_flag_descriptor_parity_only_with_checked_in_metadata() {
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 110,
opcode: 0,
raw_scalar_value: 1,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
locomotive_name: None,
});
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
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: [1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[],
[&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].grouped_effect_rows[0]
.descriptor_label
.as_deref(),
Some("Disable Stock Buying and Selling")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.parameter_family
.as_deref(),
Some("world_flag_toggle")
);
assert!(summary.records[0].decoded_actions.is_empty());
assert!(!summary.records[0].executable_import_ready);
}
#[test]
fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() {
for (value, expected) in [