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

@ -5,12 +5,14 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{
CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
CalendarPoint, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
};
@ -112,6 +114,9 @@ enum CompanyTargetImportBlocker {
MissingSelectionContext,
MissingCompanyRoleContext,
MissingConditionContext,
CompanyConditionScopeDisabled,
PlayerConditionScope,
TerritoryConditionScope,
}
impl ImportCompanyContext {
@ -592,6 +597,8 @@ fn runtime_packed_event_record_summary_from_smp(
company_context: &ImportCompanyContext,
imported: bool,
) -> RuntimePackedEventRecordSummary {
let lowered_decoded_actions =
lowered_record_decoded_actions(record).unwrap_or_else(|_| record.decoded_actions.clone());
RuntimePackedEventRecordSummary {
record_index: record.record_index,
live_entry_id: record.live_entry_id,
@ -618,6 +625,10 @@ fn runtime_packed_event_record_summary_from_smp(
.iter()
.map(runtime_packed_event_condition_row_summary_from_smp)
.collect(),
negative_sentinel_scope: record
.negative_sentinel_scope
.as_ref()
.map(runtime_packed_event_negative_sentinel_scope_summary_from_smp),
grouped_effect_row_counts: record.grouped_effect_row_counts.clone(),
grouped_effect_rows: record
.grouped_effect_rows
@ -625,7 +636,7 @@ fn runtime_packed_event_record_summary_from_smp(
.map(runtime_packed_event_grouped_effect_row_summary_from_smp)
.collect(),
grouped_company_targets: classify_real_grouped_company_targets(record),
decoded_actions: record.decoded_actions.clone(),
decoded_actions: lowered_decoded_actions,
executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome(
record,
@ -636,6 +647,17 @@ fn runtime_packed_event_record_summary_from_smp(
}
}
fn runtime_packed_event_negative_sentinel_scope_summary_from_smp(
scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary,
) -> RuntimePackedEventNegativeSentinelScopeSummary {
RuntimePackedEventNegativeSentinelScopeSummary {
company_test_scope: scope.company_test_scope,
player_test_scope: scope.player_test_scope,
territory_scope_selector_is_0x63: scope.territory_scope_selector_is_0x63,
source_row_indexes: scope.source_row_indexes.clone(),
}
}
fn runtime_packed_event_compact_control_summary_from_smp(
control: &crate::SmpLoadedPackedEventCompactControlSummary,
) -> RuntimePackedEventCompactControlSummary {
@ -710,15 +732,20 @@ fn smp_packed_record_to_runtime_event_record(
if record.decode_status == "unsupported_framing" {
return None;
}
if record.payload_family == "real_packed_v1" && !record.executable_import_ready {
return None;
if record.payload_family == "real_packed_v1" {
if record.compact_control.is_none() || !record.executable_import_ready {
return None;
}
}
let effects =
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
let lowered_effects = match lowered_record_decoded_actions(record) {
Ok(effects) => effects,
Err(_) => return None,
};
let effects = match smp_runtime_effects_to_runtime_effects(&lowered_effects, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
Some((|| {
let trigger_kind = record.trigger_kind.ok_or_else(|| {
@ -742,6 +769,160 @@ fn smp_packed_record_to_runtime_event_record(
})())
}
fn lowered_record_decoded_actions(
record: &SmpLoadedPackedEventRecordSummary,
) -> Result<Vec<RuntimeEffect>, CompanyTargetImportBlocker> {
if let Some(blocker) = packed_record_condition_scope_import_blocker(record) {
return Err(blocker);
}
let Some(lowered_target) = lowered_condition_true_company_target(record) else {
return Ok(record.decoded_actions.clone());
};
Ok(record
.decoded_actions
.iter()
.map(|effect| lower_condition_true_company_target_in_effect(effect, &lowered_target))
.collect())
}
fn packed_record_condition_scope_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
) -> Option<CompanyTargetImportBlocker> {
if record.standalone_condition_rows.is_empty() {
return None;
}
let negative_sentinel_row_count = record
.standalone_condition_rows
.iter()
.filter(|row| row.raw_condition_id == -1)
.count();
if negative_sentinel_row_count == 0 {
return Some(CompanyTargetImportBlocker::MissingConditionContext);
}
if negative_sentinel_row_count != record.standalone_condition_rows.len() {
return Some(CompanyTargetImportBlocker::MissingConditionContext);
}
let Some(scope) = record.negative_sentinel_scope.as_ref() else {
return Some(CompanyTargetImportBlocker::MissingConditionContext);
};
if scope.player_test_scope != RuntimePlayerConditionTestScope::Disabled {
return Some(CompanyTargetImportBlocker::PlayerConditionScope);
}
if scope.territory_scope_selector_is_0x63 {
return Some(CompanyTargetImportBlocker::TerritoryConditionScope);
}
if scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled {
return Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled);
}
None
}
fn lowered_condition_true_company_target(
record: &SmpLoadedPackedEventRecordSummary,
) -> Option<RuntimeCompanyTarget> {
let scope = record.negative_sentinel_scope.as_ref()?;
match scope.company_test_scope {
RuntimeCompanyConditionTestScope::Disabled => None,
RuntimeCompanyConditionTestScope::AllCompanies => Some(RuntimeCompanyTarget::AllActive),
RuntimeCompanyConditionTestScope::SelectedCompanyOnly => {
Some(RuntimeCompanyTarget::SelectedCompany)
}
RuntimeCompanyConditionTestScope::AiCompaniesOnly => {
Some(RuntimeCompanyTarget::AiCompanies)
}
RuntimeCompanyConditionTestScope::HumanCompaniesOnly => {
Some(RuntimeCompanyTarget::HumanCompanies)
}
}
}
fn lower_condition_true_company_target_in_effect(
effect: &RuntimeEffect,
lowered_target: &RuntimeCompanyTarget,
) -> RuntimeEffect {
match effect {
RuntimeEffect::SetWorldFlag { key, value } => RuntimeEffect::SetWorldFlag {
key: key.clone(),
value: *value,
},
RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
value: *value,
},
RuntimeEffect::DeactivateCompany { target } => RuntimeEffect::DeactivateCompany {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
},
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
RuntimeEffect::SetCompanyTrackLayingCapacity {
target: lower_condition_true_company_target_in_company_target(
target,
lowered_target,
),
value: *value,
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => RuntimeEffect::AdjustCompanyCash {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
delta: *delta,
},
RuntimeEffect::AdjustCompanyDebt { target, delta } => RuntimeEffect::AdjustCompanyDebt {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
delta: *delta,
},
RuntimeEffect::SetCandidateAvailability { name, value } => {
RuntimeEffect::SetCandidateAvailability {
name: name.clone(),
value: *value,
}
}
RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition {
label: label.clone(),
value: *value,
},
RuntimeEffect::AppendEventRecord { record } => RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: record.record_id,
trigger_kind: record.trigger_kind,
active: record.active,
marks_collection_dirty: record.marks_collection_dirty,
one_shot: record.one_shot,
effects: record
.effects
.iter()
.map(|nested| {
lower_condition_true_company_target_in_effect(nested, lowered_target)
})
.collect(),
}),
},
RuntimeEffect::ActivateEventRecord { record_id } => RuntimeEffect::ActivateEventRecord {
record_id: *record_id,
},
RuntimeEffect::DeactivateEventRecord { record_id } => {
RuntimeEffect::DeactivateEventRecord {
record_id: *record_id,
}
}
RuntimeEffect::RemoveEventRecord { record_id } => RuntimeEffect::RemoveEventRecord {
record_id: *record_id,
},
}
}
fn lower_condition_true_company_target_in_company_target(
target: &RuntimeCompanyTarget,
lowered_target: &RuntimeCompanyTarget,
) -> RuntimeCompanyTarget {
match target {
RuntimeCompanyTarget::ConditionTrueCompany => lowered_target.clone(),
_ => target.clone(),
}
}
fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect],
company_context: &ImportCompanyContext,
@ -912,6 +1093,18 @@ fn company_target_import_error_message(
Some(CompanyTargetImportBlocker::MissingConditionContext) => {
"packed company effect requires condition-relative context".to_string()
}
Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled) => {
"packed company effect disables company-side negative-sentinel condition scope"
.to_string()
}
Some(CompanyTargetImportBlocker::PlayerConditionScope) => {
"packed company effect requires player runtime ownership for negative-sentinel scope"
.to_string()
}
Some(CompanyTargetImportBlocker::TerritoryConditionScope) => {
"packed company effect requires territory runtime ownership for negative-sentinel scope"
.to_string()
}
None => "packed company effect is importable".to_string(),
}
}
@ -934,6 +1127,9 @@ fn determine_packed_event_import_outcome(
if !record.executable_import_ready {
return "blocked_unmapped_real_descriptor".to_string();
}
if let Some(blocker) = packed_record_condition_scope_import_blocker(record) {
return company_target_import_outcome(blocker).to_string();
}
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context)
{
return company_target_import_outcome(blocker).to_string();
@ -950,8 +1146,11 @@ fn packed_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
record
.decoded_actions
let lowered_effects = match lowered_record_decoded_actions(record) {
Ok(effects) => effects,
Err(blocker) => return Some(blocker),
};
lowered_effects
.iter()
.find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context))
}
@ -1022,6 +1221,11 @@ fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'stati
"blocked_missing_company_role_context"
}
CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
CompanyTargetImportBlocker::CompanyConditionScopeDisabled => {
"blocked_company_condition_scope_disabled"
}
CompanyTargetImportBlocker::PlayerConditionScope => "blocked_player_condition_scope",
CompanyTargetImportBlocker::TerritoryConditionScope => "blocked_territory_condition_scope",
}
}
@ -1393,6 +1597,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![effect],
@ -1401,6 +1606,36 @@ mod tests {
}
}
fn company_negative_sentinel_scope(
company_test_scope: RuntimeCompanyConditionTestScope,
) -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope,
player_test_scope: RuntimePlayerConditionTestScope::Disabled,
territory_scope_selector_is_0x63: false,
source_row_indexes: vec![0],
}
}
fn territory_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary
{
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies,
player_test_scope: RuntimePlayerConditionTestScope::Disabled,
territory_scope_selector_is_0x63: true,
source_row_indexes: vec![0],
}
}
fn player_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies,
player_test_scope: RuntimePlayerConditionTestScope::AllPlayers,
territory_scope_selector_is_0x63: false,
source_row_indexes: vec![0],
}
}
fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
@ -1425,7 +1660,9 @@ mod tests {
}]
}
fn real_deactivate_company_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
fn real_deactivate_company_row(
enabled: bool,
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
@ -1470,9 +1707,7 @@ mod tests {
value_word_0x16: 0,
row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!(
"Set Company Track Pieces Buildable to {value}"
)),
semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")),
locomotive_name: None,
notes: vec![],
}
@ -1758,6 +1993,7 @@ mod tests {
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(),
@ -1779,6 +2015,7 @@ mod tests {
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(),
@ -1800,6 +2037,7 @@ mod tests {
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(),
@ -2011,6 +2249,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 1, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![
@ -2121,6 +2360,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
@ -2395,6 +2635,9 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::AllCompanies,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
@ -2443,8 +2686,272 @@ mod tests {
}
#[test]
fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context()
{
fn lowers_negative_sentinel_company_scopes_into_runtime_company_targets() {
let base_state = RuntimeState {
companies: vec![
crate::RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 10,
active: true,
available_track_laying_capacity: None,
},
crate::RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 50,
debt: 20,
active: true,
available_track_laying_capacity: None,
},
crate::RuntimeCompany {
company_id: 3,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 70,
debt: 30,
active: true,
available_track_laying_capacity: None,
},
],
selected_company_id: Some(3),
..state()
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 11,
live_record_count: 5,
live_entry_ids: vec![7, 8, 9, 10, 11],
decoded_record_count: 5,
imported_runtime_record_count: 0,
records: vec![
crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::AllCompanies,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
crate::SmpLoadedPackedEventRecordSummary {
record_index: 1,
live_entry_id: 8,
payload_offset: Some(0x7282),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::SelectedCompanyOnly,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 8,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
crate::SmpLoadedPackedEventRecordSummary {
record_index: 2,
live_entry_id: 9,
payload_offset: Some(0x7302),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::AiCompaniesOnly,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 9,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
crate::SmpLoadedPackedEventRecordSummary {
record_index: 3,
live_entry_id: 10,
payload_offset: Some(0x7382),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::HumanCompaniesOnly,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 10,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
crate::SmpLoadedPackedEventRecordSummary {
record_index: 4,
live_entry_id: 11,
payload_offset: Some(0x7402),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::Disabled,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 11,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
},
],
}),
notes: vec![],
};
let import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"packed-events-real-descriptor-frontier",
None,
)
.expect("save slice should project");
assert_eq!(import.state.event_runtime_records.len(), 4);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].compact_control.as_ref())
.map(|control| control.mode_byte_0x7ef),
Some(6)
);
let effects = import
.state
.event_runtime_records
.iter()
.map(|record| record.effects[0].clone())
.collect::<Vec<_>>();
assert_eq!(
effects,
vec![
RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::AllActive,
value: 7,
},
RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
value: 8,
},
RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::AiCompanies,
value: 9,
},
RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
value: 10,
},
]
);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.map(|record| record.import_outcome.clone())
.collect::<Vec<_>>()
}),
Some(vec![
Some("imported".to_string()),
Some("imported".to_string()),
Some("imported".to_string()),
Some("imported".to_string()),
Some("blocked_company_condition_scope_disabled".to_string()),
])
);
}
#[test]
fn blocks_negative_sentinel_player_scope_until_player_runtime_exists() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -2485,6 +2992,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(player_negative_sentinel_scope()),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
@ -2500,7 +3008,7 @@ mod tests {
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-real-descriptor-frontier",
"negative-sentinel-player-scope",
None,
)
.expect("save slice should project");
@ -2511,17 +3019,82 @@ mod tests {
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].compact_control.as_ref())
.map(|control| control.mode_byte_0x7ef),
Some(6)
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_player_condition_scope")
);
}
#[test]
fn blocks_negative_sentinel_territory_scope_until_territory_runtime_exists() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7,
live_record_count: 1,
live_entry_ids: vec![7],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: Some(territory_negative_sentinel_scope()),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"negative-sentinel-territory-scope",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_missing_condition_context")
Some("blocked_territory_condition_scope")
);
}
@ -2567,6 +3140,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![],
@ -2674,6 +3248,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
@ -2806,6 +3381,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(true)],
decoded_actions: vec![RuntimeEffect::DeactivateCompany {
@ -2890,6 +3466,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(false)],
decoded_actions: vec![],
@ -2983,6 +3560,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_track_capacity_row(18)],
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
@ -3081,6 +3659,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 1, 0, 0],
grouped_effect_rows: vec![
real_track_capacity_row(18),
@ -3198,6 +3777,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
@ -3356,6 +3936,7 @@ mod tests {
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {