Unlock negative-sentinel company condition scopes

This commit is contained in:
Jan Petykiewicz 2026-04-15 14:21:12 -07:00
commit 087ebf1097
18 changed files with 1315 additions and 79 deletions

View file

@ -4,7 +4,10 @@ use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate};
use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
};
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
const PREAMBLE_U32_WORD_COUNT: usize = 16;
@ -1312,6 +1315,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)]
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
#[serde(default)]
pub negative_sentinel_scope: Option<SmpLoadedPackedEventNegativeSentinelScopeSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
@ -1323,6 +1328,15 @@ pub struct SmpLoadedPackedEventRecordSummary {
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventNegativeSentinelScopeSummary {
pub company_test_scope: RuntimeCompanyConditionTestScope,
pub player_test_scope: RuntimePlayerConditionTestScope,
pub territory_scope_selector_is_0x63: bool,
#[serde(default)]
pub source_row_indexes: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8,
@ -1836,6 +1850,7 @@ fn parse_synthetic_event_runtime_record_summary(
text_bands,
standalone_condition_row_count,
standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts,
grouped_effect_rows: Vec::new(),
decoded_actions,
@ -1940,6 +1955,9 @@ fn parse_real_event_runtime_record_summary(
}
}
let negative_sentinel_scope = compact_control.as_ref().and_then(|control| {
derive_negative_sentinel_scope_summary(&standalone_condition_rows, control)
});
let decoded_actions = compact_control
.as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
@ -1968,6 +1986,7 @@ fn parse_real_event_runtime_record_summary(
text_bands,
standalone_condition_row_count,
standalone_condition_rows,
negative_sentinel_scope,
grouped_effect_row_counts,
grouped_effect_rows,
decoded_actions,
@ -2074,6 +2093,49 @@ fn parse_real_condition_row_summary(
})
}
fn derive_negative_sentinel_scope_summary(
rows: &[SmpLoadedPackedEventConditionRowSummary],
control: &SmpLoadedPackedEventCompactControlSummary,
) -> Option<SmpLoadedPackedEventNegativeSentinelScopeSummary> {
let source_row_indexes = rows
.iter()
.filter(|row| row.raw_condition_id == -1)
.map(|row| row.row_index)
.collect::<Vec<_>>();
if source_row_indexes.is_empty() {
return None;
}
Some(SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope: decode_company_condition_test_scope(control.modifier_flag_0x7f9)?,
player_test_scope: decode_player_condition_test_scope(control.modifier_flag_0x7fa)?,
territory_scope_selector_is_0x63: control.primary_selector_0x7f0 == 0x63,
source_row_indexes,
})
}
fn decode_company_condition_test_scope(value: u8) -> Option<RuntimeCompanyConditionTestScope> {
match value {
0 => Some(RuntimeCompanyConditionTestScope::Disabled),
1 => Some(RuntimeCompanyConditionTestScope::AllCompanies),
2 => Some(RuntimeCompanyConditionTestScope::SelectedCompanyOnly),
3 => Some(RuntimeCompanyConditionTestScope::AiCompaniesOnly),
4 => Some(RuntimeCompanyConditionTestScope::HumanCompaniesOnly),
_ => None,
}
}
fn decode_player_condition_test_scope(value: u8) -> Option<RuntimePlayerConditionTestScope> {
match value {
0 => Some(RuntimePlayerConditionTestScope::Disabled),
1 => Some(RuntimePlayerConditionTestScope::AllPlayers),
2 => Some(RuntimePlayerConditionTestScope::SelectedPlayerOnly),
3 => Some(RuntimePlayerConditionTestScope::AiPlayersOnly),
4 => Some(RuntimePlayerConditionTestScope::HumanPlayersOnly),
_ => None,
}
}
fn parse_real_grouped_effect_row_summary(
row_bytes: &[u8],
group_index: usize,
@ -2484,6 +2546,7 @@ fn build_unsupported_event_runtime_record_summaries(
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(),
@ -7603,6 +7666,7 @@ mod tests {
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!(summary.records[0].negative_sentinel_scope.is_none());
assert_eq!(
summary.records[0].grouped_effect_row_counts,
vec![0, 0, 0, 0]
@ -7682,6 +7746,20 @@ mod tests {
.as_deref(),
Some("AutoPlant")
);
let negative_sentinel_scope = summary.records[0]
.negative_sentinel_scope
.as_ref()
.expect("negative-sentinel scope summary should decode");
assert_eq!(
negative_sentinel_scope.company_test_scope,
RuntimeCompanyConditionTestScope::SelectedCompanyOnly
);
assert_eq!(
negative_sentinel_scope.player_test_scope,
RuntimePlayerConditionTestScope::AiPlayersOnly
);
assert!(!negative_sentinel_scope.territory_scope_selector_is_0x63);
assert_eq!(negative_sentinel_scope.source_row_indexes, vec![0]);
assert_eq!(summary.records[0].grouped_effect_rows.len(), 1);
assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8);
assert_eq!(
@ -7725,6 +7803,63 @@ mod tests {
);
}
#[test]
fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() {
for (value, expected) in [
(0, RuntimeCompanyConditionTestScope::Disabled),
(1, RuntimeCompanyConditionTestScope::AllCompanies),
(2, RuntimeCompanyConditionTestScope::SelectedCompanyOnly),
(3, RuntimeCompanyConditionTestScope::AiCompaniesOnly),
(4, RuntimeCompanyConditionTestScope::HumanCompaniesOnly),
] {
assert_eq!(decode_company_condition_test_scope(value), Some(expected));
}
for (value, expected) in [
(0, RuntimePlayerConditionTestScope::Disabled),
(1, RuntimePlayerConditionTestScope::AllPlayers),
(2, RuntimePlayerConditionTestScope::SelectedPlayerOnly),
(3, RuntimePlayerConditionTestScope::AiPlayersOnly),
(4, RuntimePlayerConditionTestScope::HumanPlayersOnly),
] {
assert_eq!(decode_player_condition_test_scope(value), Some(expected));
}
let rows = vec![SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: -1,
subtype: 4,
flag_bytes: vec![0x30; 25],
candidate_name: Some("AutoPlant".to_string()),
notes: vec![],
}];
let summary = derive_negative_sentinel_scope_summary(
&rows,
&SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 1,
modifier_flag_0x7f9: 4,
modifier_flag_0x7fa: 2,
grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
},
)
.expect("negative sentinel summary should derive");
assert_eq!(
summary.company_test_scope,
RuntimeCompanyConditionTestScope::HumanCompaniesOnly
);
assert_eq!(
summary.player_test_scope,
RuntimePlayerConditionTestScope::SelectedPlayerOnly
);
assert!(summary.territory_scope_selector_is_0x63);
assert_eq!(summary.source_row_indexes, vec![0]);
}
#[test]
fn classifies_real_grouped_row_semantic_families() {
let grouped_rows = vec![