Recover whole-game packed event descriptors
This commit is contained in:
parent
cc54a00e25
commit
a3f9a73766
8 changed files with 627 additions and 16 deletions
|
|
@ -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 [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue