Implement ordinary packed event conditions

This commit is contained in:
Jan Petykiewicz 2026-04-15 18:27:04 -07:00
commit f73234cb99
28 changed files with 2624 additions and 86 deletions

View file

@ -137,7 +137,7 @@ mod tests {
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState,
RuntimeSnapshotSource, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
save_runtime_snapshot_document,
@ -174,6 +174,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -330,10 +332,15 @@ mod tests {
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -391,6 +398,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,

View file

@ -64,6 +64,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub active_company_count: Option<usize>,
#[serde(default)]
pub territory_count: Option<usize>,
#[serde(default)]
pub company_territory_track_count: Option<usize>,
#[serde(default)]
pub packed_event_collection_present: Option<bool>,
#[serde(default)]
pub packed_event_record_count: Option<usize>,
@ -90,6 +94,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_blocked_territory_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_named_territory_binding_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_ordinary_condition_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_compact_control_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
@ -343,6 +353,22 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.territory_count {
if actual.territory_count != count {
mismatches.push(format!(
"territory_count mismatch: expected {count}, got {}",
actual.territory_count
));
}
}
if let Some(count) = self.company_territory_track_count {
if actual.company_territory_track_count != count {
mismatches.push(format!(
"company_territory_track_count mismatch: expected {count}, got {}",
actual.company_territory_track_count
));
}
}
if let Some(present) = self.packed_event_collection_present {
if actual.packed_event_collection_present != present {
mismatches.push(format!(
@ -447,6 +473,30 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.packed_event_blocked_missing_territory_context_count {
if actual.packed_event_blocked_missing_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_territory_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_named_territory_binding_count {
if actual.packed_event_blocked_named_territory_binding_count != count {
mismatches.push(format!(
"packed_event_blocked_named_territory_binding_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_named_territory_binding_count
));
}
}
if let Some(count) = self.packed_event_blocked_unmapped_ordinary_condition_count {
if actual.packed_event_blocked_unmapped_ordinary_condition_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_ordinary_condition_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_ordinary_condition_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!(

View file

@ -6,12 +6,13 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{
CalendarPoint, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
RuntimePlayerConditionTestScope, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState,
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
};
@ -106,6 +107,7 @@ struct ImportCompanyContext {
known_company_ids: BTreeSet<u32>,
selected_company_id: Option<u32>,
has_complete_controller_context: bool,
has_territory_context: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -117,6 +119,9 @@ enum CompanyTargetImportBlocker {
CompanyConditionScopeDisabled,
PlayerConditionScope,
TerritoryConditionScope,
MissingTerritoryContext,
NamedTerritoryBinding,
UnmappedOrdinaryCondition,
}
impl ImportCompanyContext {
@ -125,6 +130,7 @@ impl ImportCompanyContext {
known_company_ids: BTreeSet::new(),
selected_company_id: None,
has_complete_controller_context: false,
has_territory_context: false,
}
}
@ -140,6 +146,7 @@ impl ImportCompanyContext {
&& state.companies.iter().all(|company| {
company.controller_kind != RuntimeCompanyControllerKind::Unknown
}),
has_territory_context: !state.territories.is_empty(),
}
}
}
@ -171,6 +178,8 @@ pub fn project_save_slice_to_runtime_state_import(
metadata: projection.metadata,
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
@ -220,6 +229,10 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
metadata,
companies: base_state.companies.clone(),
selected_company_id: base_state.selected_company_id,
territories: base_state.territories.clone(),
company_territory_track_piece_counts: base_state
.company_territory_track_piece_counts
.clone(),
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
@ -597,8 +610,10 @@ 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());
let lowered_decoded_conditions = lowered_record_decoded_conditions(record, company_context)
.unwrap_or_else(|_| record.decoded_conditions.clone());
let lowered_decoded_actions = lowered_record_decoded_actions(record, company_context)
.unwrap_or_else(|_| record.decoded_actions.clone());
RuntimePackedEventRecordSummary {
record_index: record.record_index,
live_entry_id: record.live_entry_id,
@ -636,6 +651,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_conditions: lowered_decoded_conditions,
decoded_actions: lowered_decoded_actions,
executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome(
@ -695,6 +711,11 @@ fn runtime_packed_event_condition_row_summary_from_smp(
subtype: row.subtype,
flag_bytes: row.flag_bytes.clone(),
candidate_name: row.candidate_name.clone(),
comparator: row.comparator.clone(),
metric: row.metric.clone(),
semantic_family: row.semantic_family.clone(),
semantic_preview: row.semantic_preview.clone(),
requires_candidate_name_binding: row.requires_candidate_name_binding,
notes: row.notes.clone(),
}
}
@ -738,11 +759,19 @@ fn smp_packed_record_to_runtime_event_record(
}
}
let lowered_effects = match lowered_record_decoded_actions(record) {
let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) {
Ok(conditions) => conditions,
Err(_) => return None,
};
let lowered_effects = match lowered_record_decoded_actions(record, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
let effects = match smp_runtime_effects_to_runtime_effects(&lowered_effects, company_context) {
let effects = match smp_runtime_effects_to_runtime_effects(
&lowered_effects,
company_context,
conditions_provide_company_context(&lowered_conditions),
) {
Ok(effects) => effects,
Err(_) => return None,
};
@ -763,19 +792,42 @@ fn smp_packed_record_to_runtime_event_record(
active,
marks_collection_dirty,
one_shot,
conditions: lowered_conditions,
effects,
}
.into_runtime_record())
})())
}
fn lowered_record_decoded_actions(
fn lowered_record_decoded_conditions(
record: &SmpLoadedPackedEventRecordSummary,
) -> Result<Vec<RuntimeEffect>, CompanyTargetImportBlocker> {
if let Some(blocker) = packed_record_condition_scope_import_blocker(record) {
company_context: &ImportCompanyContext,
) -> Result<Vec<RuntimeCondition>, CompanyTargetImportBlocker> {
if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) {
return Err(blocker);
}
let Some(lowered_target) = lowered_condition_true_company_target(record) else {
return Ok(record.decoded_conditions.clone());
};
Ok(record
.decoded_conditions
.iter()
.map(|condition| lower_condition_true_company_target_in_condition(condition, &lowered_target))
.collect())
}
fn lowered_record_decoded_actions(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> Result<Vec<RuntimeEffect>, CompanyTargetImportBlocker> {
if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) {
return Err(blocker);
}
if !record.decoded_conditions.is_empty() {
return Ok(record.decoded_actions.clone());
}
let Some(lowered_target) = lowered_condition_true_company_target(record) else {
return Ok(record.decoded_actions.clone());
};
@ -788,20 +840,53 @@ fn lowered_record_decoded_actions(
fn packed_record_condition_scope_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
if record.standalone_condition_rows.is_empty() {
return None;
}
let ordinary_condition_row_count = record
.standalone_condition_rows
.iter()
.filter(|row| row.raw_condition_id >= 0)
.count();
if ordinary_condition_row_count != 0 {
if record
.standalone_condition_rows
.iter()
.any(|row| row.requires_candidate_name_binding)
{
return Some(CompanyTargetImportBlocker::NamedTerritoryBinding);
}
if ordinary_condition_row_count != record.decoded_conditions.len() {
return Some(CompanyTargetImportBlocker::UnmappedOrdinaryCondition);
}
if record
.decoded_conditions
.iter()
.any(|condition| matches!(condition, RuntimeCondition::TerritoryNumericThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. }))
&& !company_context.has_territory_context
{
return Some(CompanyTargetImportBlocker::MissingTerritoryContext);
}
}
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);
return if ordinary_condition_row_count == 0 {
Some(CompanyTargetImportBlocker::MissingConditionContext)
} else {
None
};
}
if negative_sentinel_row_count != record.standalone_condition_rows.len() {
if ordinary_condition_row_count == 0
&& negative_sentinel_row_count != record.standalone_condition_rows.len()
{
return Some(CompanyTargetImportBlocker::MissingConditionContext);
}
@ -811,10 +896,22 @@ fn packed_record_condition_scope_import_blocker(
if scope.player_test_scope != RuntimePlayerConditionTestScope::Disabled {
return Some(CompanyTargetImportBlocker::PlayerConditionScope);
}
if scope.territory_scope_selector_is_0x63 {
if ordinary_condition_row_count == 0 && scope.territory_scope_selector_is_0x63 {
return Some(CompanyTargetImportBlocker::TerritoryConditionScope);
}
if scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled {
if record.decoded_conditions.iter().any(|condition| {
matches!(
condition,
RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
)
}) && scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled
{
return Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled);
}
if ordinary_condition_row_count == 0
&& scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled
{
return Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled);
}
@ -890,6 +987,7 @@ fn lower_condition_true_company_target_in_effect(
active: record.active,
marks_collection_dirty: record.marks_collection_dirty,
one_shot: record.one_shot,
conditions: record.conditions.clone(),
effects: record
.effects
.iter()
@ -913,6 +1011,45 @@ fn lower_condition_true_company_target_in_effect(
}
}
fn lower_condition_true_company_target_in_condition(
condition: &RuntimeCondition,
lowered_target: &RuntimeCompanyTarget,
) -> RuntimeCondition {
match condition {
RuntimeCondition::CompanyNumericThreshold {
target,
metric,
comparator,
value,
} => RuntimeCondition::CompanyNumericThreshold {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
metric: *metric,
comparator: *comparator,
value: *value,
},
RuntimeCondition::TerritoryNumericThreshold {
metric,
comparator,
value,
} => RuntimeCondition::TerritoryNumericThreshold {
metric: *metric,
comparator: *comparator,
value: *value,
},
RuntimeCondition::CompanyTerritoryNumericThreshold {
target,
metric,
comparator,
value,
} => RuntimeCondition::CompanyTerritoryNumericThreshold {
target: lower_condition_true_company_target_in_company_target(target, lowered_target),
metric: *metric,
comparator: *comparator,
value: *value,
},
}
}
fn lower_condition_true_company_target_in_company_target(
target: &RuntimeCompanyTarget,
lowered_target: &RuntimeCompanyTarget,
@ -926,16 +1063,24 @@ fn lower_condition_true_company_target_in_company_target(
fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect],
company_context: &ImportCompanyContext,
allow_condition_true_company: bool,
) -> Result<Vec<RuntimeEffect>, String> {
effects
.iter()
.map(|effect| smp_runtime_effect_to_runtime_effect(effect, company_context))
.map(|effect| {
smp_runtime_effect_to_runtime_effect(
effect,
company_context,
allow_condition_true_company,
)
})
.collect()
}
fn smp_runtime_effect_to_runtime_effect(
effect: &RuntimeEffect,
company_context: &ImportCompanyContext,
allow_condition_true_company: bool,
) -> Result<RuntimeEffect, String> {
match effect {
RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag {
@ -943,7 +1088,11 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value,
}),
RuntimeEffect::SetCompanyCash { target, value } => {
if company_target_import_blocker(target, company_context).is_none() {
if company_target_allowed_for_import(
target,
company_context,
allow_condition_true_company,
) {
Ok(RuntimeEffect::SetCompanyCash {
target: target.clone(),
value: *value,
@ -953,7 +1102,11 @@ fn smp_runtime_effect_to_runtime_effect(
}
}
RuntimeEffect::DeactivateCompany { target } => {
if company_target_import_blocker(target, company_context).is_none() {
if company_target_allowed_for_import(
target,
company_context,
allow_condition_true_company,
) {
Ok(RuntimeEffect::DeactivateCompany {
target: target.clone(),
})
@ -962,7 +1115,11 @@ fn smp_runtime_effect_to_runtime_effect(
}
}
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
if company_target_import_blocker(target, company_context).is_none() {
if company_target_allowed_for_import(
target,
company_context,
allow_condition_true_company,
) {
Ok(RuntimeEffect::SetCompanyTrackLayingCapacity {
target: target.clone(),
value: *value,
@ -972,7 +1129,11 @@ fn smp_runtime_effect_to_runtime_effect(
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => {
if company_target_import_blocker(target, company_context).is_none() {
if company_target_allowed_for_import(
target,
company_context,
allow_condition_true_company,
) {
Ok(RuntimeEffect::AdjustCompanyCash {
target: target.clone(),
delta: *delta,
@ -982,7 +1143,11 @@ fn smp_runtime_effect_to_runtime_effect(
}
}
RuntimeEffect::AdjustCompanyDebt { target, delta } => {
if company_target_import_blocker(target, company_context).is_none() {
if company_target_allowed_for_import(
target,
company_context,
allow_condition_true_company,
) {
Ok(RuntimeEffect::AdjustCompanyDebt {
target: target.clone(),
delta: *delta,
@ -1007,6 +1172,7 @@ fn smp_runtime_effect_to_runtime_effect(
record: Box::new(smp_runtime_record_template_to_runtime(
record,
company_context,
allow_condition_true_company,
)?),
}),
RuntimeEffect::ActivateEventRecord { record_id } => {
@ -1028,6 +1194,7 @@ fn smp_runtime_effect_to_runtime_effect(
fn smp_runtime_record_template_to_runtime(
record: &RuntimeEventRecordTemplate,
company_context: &ImportCompanyContext,
allow_condition_true_company: bool,
) -> Result<RuntimeEventRecordTemplate, String> {
Ok(RuntimeEventRecordTemplate {
record_id: record.record_id,
@ -1035,7 +1202,39 @@ fn smp_runtime_record_template_to_runtime(
active: record.active,
marks_collection_dirty: record.marks_collection_dirty,
one_shot: record.one_shot,
effects: smp_runtime_effects_to_runtime_effects(&record.effects, company_context)?,
conditions: record.conditions.clone(),
effects: smp_runtime_effects_to_runtime_effects(
&record.effects,
company_context,
allow_condition_true_company,
)?,
})
}
fn company_target_allowed_for_import(
target: &RuntimeCompanyTarget,
company_context: &ImportCompanyContext,
allow_condition_true_company: bool,
) -> bool {
match company_target_import_blocker(target, company_context) {
None => true,
Some(CompanyTargetImportBlocker::MissingConditionContext)
if allow_condition_true_company
&& matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) =>
{
true
}
Some(_) => false,
}
}
fn conditions_provide_company_context(conditions: &[RuntimeCondition]) -> bool {
conditions.iter().any(|condition| {
matches!(
condition,
RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
)
})
}
@ -1105,6 +1304,15 @@ fn company_target_import_error_message(
"packed company effect requires territory runtime ownership for negative-sentinel scope"
.to_string()
}
Some(CompanyTargetImportBlocker::MissingTerritoryContext) => {
"packed condition requires territory runtime context".to_string()
}
Some(CompanyTargetImportBlocker::NamedTerritoryBinding) => {
"packed condition requires named territory binding".to_string()
}
Some(CompanyTargetImportBlocker::UnmappedOrdinaryCondition) => {
"packed ordinary condition is not yet mapped".to_string()
}
None => "packed company effect is importable".to_string(),
}
}
@ -1125,9 +1333,18 @@ fn determine_packed_event_import_outcome(
return "blocked_missing_compact_control".to_string();
}
if !record.executable_import_ready {
return "blocked_unmapped_real_descriptor".to_string();
return if record
.standalone_condition_rows
.iter()
.any(|row| row.raw_condition_id >= 0)
{
"blocked_unmapped_ordinary_condition".to_string()
} else {
"blocked_unmapped_real_descriptor".to_string()
};
}
if let Some(blocker) = packed_record_condition_scope_import_blocker(record) {
if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context)
{
return company_target_import_outcome(blocker).to_string();
}
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context)
@ -1146,18 +1363,87 @@ fn packed_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
let lowered_effects = match lowered_record_decoded_actions(record) {
if record
.decoded_actions
.iter()
.any(runtime_effect_uses_condition_true_company)
&& !record
.decoded_conditions
.iter()
.any(|condition| matches!(condition, RuntimeCondition::CompanyNumericThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. }))
{
return Some(CompanyTargetImportBlocker::MissingConditionContext);
}
let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) {
Ok(conditions) => conditions,
Err(blocker) => return Some(blocker),
};
let has_company_condition_context = lowered_conditions.iter().any(|condition| {
matches!(
condition,
RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
)
});
if let Some(blocker) = lowered_conditions.iter().find_map(|condition| {
runtime_condition_company_target_import_blocker(condition, company_context)
}) {
return Some(blocker);
}
let lowered_effects = match lowered_record_decoded_actions(record, company_context) {
Ok(effects) => effects,
Err(blocker) => return Some(blocker),
};
lowered_effects
.iter()
.find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context))
.find_map(|effect| {
runtime_effect_company_target_import_blocker(
effect,
company_context,
has_company_condition_context,
)
})
}
fn runtime_condition_company_target_import_blocker(
condition: &RuntimeCondition,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
company_target_import_blocker(target, company_context)
}
RuntimeCondition::TerritoryNumericThreshold { .. } => None,
}
}
fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
match effect {
RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::DeactivateCompany { target }
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
}
RuntimeEffect::AppendEventRecord { record } => record
.effects
.iter()
.any(runtime_effect_uses_condition_true_company),
RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => false,
}
}
fn runtime_effect_company_target_import_blocker(
effect: &RuntimeEffect,
company_context: &ImportCompanyContext,
allow_condition_true_company: bool,
) -> Option<CompanyTargetImportBlocker> {
match effect {
RuntimeEffect::SetCompanyCash { target, .. }
@ -1165,10 +1451,19 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
if allow_condition_true_company
&& matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
{
return None;
}
company_target_import_blocker(target, company_context)
}
RuntimeEffect::AppendEventRecord { record } => record.effects.iter().find_map(|nested| {
runtime_effect_company_target_import_blocker(nested, company_context)
runtime_effect_company_target_import_blocker(
nested,
company_context,
allow_condition_true_company,
)
}),
RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
@ -1226,6 +1521,11 @@ fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'stati
}
CompanyTargetImportBlocker::PlayerConditionScope => "blocked_player_condition_scope",
CompanyTargetImportBlocker::TerritoryConditionScope => "blocked_territory_condition_scope",
CompanyTargetImportBlocker::MissingTerritoryContext => "blocked_missing_territory_context",
CompanyTargetImportBlocker::NamedTerritoryBinding => "blocked_named_territory_binding",
CompanyTargetImportBlocker::UnmappedOrdinaryCondition => {
"blocked_unmapped_ordinary_condition"
}
}
}
@ -1501,7 +1801,7 @@ fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf {
#[cfg(test)]
mod tests {
use super::*;
use crate::{StepCommand, execute_step_command};
use crate::{RuntimeTrackPieceCounts, StepCommand, execute_step_command};
fn state() -> RuntimeState {
RuntimeState {
@ -1517,6 +1817,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -1573,6 +1875,11 @@ mod tests {
subtype: 4,
flag_bytes: vec![0x30; 25],
candidate_name: Some("AutoPlant".to_string()),
comparator: None,
metric: None,
semantic_family: None,
semantic_preview: None,
requires_candidate_name_binding: false,
notes: vec!["negative sentinel-style condition row id".to_string()],
}]
}
@ -1600,6 +1907,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![effect],
executable_import_ready: false,
notes: vec!["synthetic test record".to_string()],
@ -1996,6 +2304,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -2018,6 +2327,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -2040,6 +2350,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["test".to_string()],
@ -2252,6 +2563,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 1, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![
RuntimeEffect::SetWorldFlag {
key: "from_packed_root".to_string(),
@ -2264,6 +2576,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetSpecialCondition {
label: "Imported Follow-On".to_string(),
value: 1,
@ -2363,6 +2676,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,
@ -2498,6 +2812,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 10,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -2506,6 +2823,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 50,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -2640,6 +2960,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
@ -2694,6 +3015,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 10,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -2702,6 +3026,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 50,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -2710,6 +3037,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 70,
debt: 30,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -2763,6 +3093,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
@ -2790,6 +3121,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 8,
@ -2817,6 +3149,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 9,
@ -2844,6 +3177,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 10,
@ -2871,6 +3205,7 @@ mod tests {
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 11,
@ -2995,6 +3330,7 @@ mod tests {
negative_sentinel_scope: Some(player_negative_sentinel_scope()),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
@ -3069,6 +3405,7 @@ mod tests {
negative_sentinel_scope: Some(territory_negative_sentinel_scope()),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
@ -3143,6 +3480,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
@ -3187,10 +3525,15 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -3275,6 +3618,7 @@ mod tests {
"grouped effect row carries locomotive-name side string".to_string(),
],
}],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
value: 250,
@ -3324,6 +3668,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
@ -3384,6 +3731,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(true)],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany,
}],
@ -3469,6 +3817,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(false)],
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
@ -3503,6 +3852,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
@ -3563,6 +3915,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_track_capacity_row(18)],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::SelectedCompany,
value: Some(18),
@ -3602,6 +3955,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
@ -3665,6 +4021,7 @@ mod tests {
real_track_capacity_row(18),
unsupported_real_grouped_row(),
],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::SelectedCompany,
value: Some(18),
@ -3713,10 +4070,15 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 1,
@ -3726,6 +4088,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![],
}],
candidate_availability: BTreeMap::new(),
@ -3780,6 +4143,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,
@ -3878,10 +4242,15 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -3939,6 +4308,7 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 50,

View file

@ -36,12 +36,15 @@ pub use pk4::{
};
pub use runtime::{
RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyTerritoryTrackPieceCount,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
RuntimePlayerConditionTestScope, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimeTrackPieceCounts,
RuntimeWorldRestoreState,
};
pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -94,6 +94,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),

View file

@ -22,12 +22,49 @@ pub struct RuntimeCompany {
pub company_id: u32,
pub current_cash: i64,
pub debt: u64,
#[serde(default)]
pub credit_rating_score: Option<i64>,
#[serde(default)]
pub prime_rate: Option<i64>,
#[serde(default = "runtime_company_default_active")]
pub active: bool,
#[serde(default)]
pub available_track_laying_capacity: Option<u32>,
#[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind,
#[serde(default)]
pub track_piece_counts: RuntimeTrackPieceCounts,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeTrackPieceCounts {
#[serde(default)]
pub total: u32,
#[serde(default)]
pub single: u32,
#[serde(default)]
pub double: u32,
#[serde(default)]
pub transition: u32,
#[serde(default)]
pub electric: u32,
#[serde(default)]
pub non_electric: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeTerritory {
pub territory_id: u32,
#[serde(default)]
pub track_piece_counts: RuntimeTrackPieceCounts,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyTerritoryTrackPieceCount {
pub company_id: u32,
pub territory_id: u32,
#[serde(default)]
pub track_piece_counts: RuntimeTrackPieceCounts,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -63,6 +100,76 @@ pub enum RuntimePlayerConditionTestScope {
HumanPlayersOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeConditionComparator {
Ge,
Le,
Gt,
Lt,
Eq,
Ne,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeCompanyMetric {
CurrentCash,
TotalDebt,
CreditRating,
PrimeRate,
TrackPiecesTotal,
TrackPiecesSingle,
TrackPiecesDouble,
TrackPiecesTransition,
TrackPiecesElectric,
TrackPiecesNonElectric,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTerritoryMetric {
TrackPiecesTotal,
TrackPiecesSingle,
TrackPiecesDouble,
TrackPiecesTransition,
TrackPiecesElectric,
TrackPiecesNonElectric,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTrackMetric {
Total,
Single,
Double,
Transition,
Electric,
NonElectric,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCondition {
CompanyNumericThreshold {
target: RuntimeCompanyTarget,
metric: RuntimeCompanyMetric,
comparator: RuntimeConditionComparator,
value: i64,
},
TerritoryNumericThreshold {
metric: RuntimeTerritoryMetric,
comparator: RuntimeConditionComparator,
value: i64,
},
CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget,
metric: RuntimeTrackMetric,
comparator: RuntimeConditionComparator,
value: i64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEffect {
@ -121,6 +228,8 @@ pub struct RuntimeEventRecordTemplate {
#[serde(default)]
pub one_shot: bool,
#[serde(default)]
pub conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub effects: Vec<RuntimeEffect>,
}
@ -138,6 +247,8 @@ pub struct RuntimeEventRecord {
#[serde(default)]
pub has_fired: bool,
#[serde(default)]
pub conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub effects: Vec<RuntimeEffect>,
}
@ -197,6 +308,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)]
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
#[serde(default)]
pub decoded_conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
@ -250,6 +363,16 @@ pub struct RuntimePackedEventConditionRowSummary {
#[serde(default)]
pub candidate_name: Option<String>,
#[serde(default)]
pub comparator: Option<String>,
#[serde(default)]
pub metric: Option<String>,
#[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub requires_candidate_name_binding: bool,
#[serde(default)]
pub notes: Vec<String>,
}
@ -293,6 +416,7 @@ impl RuntimeEventRecordTemplate {
marks_collection_dirty: self.marks_collection_dirty,
one_shot: self.one_shot,
has_fired: false,
conditions: self.conditions,
effects: self.effects,
}
}
@ -384,6 +508,10 @@ pub struct RuntimeState {
#[serde(default)]
pub selected_company_id: Option<u32>,
#[serde(default)]
pub territories: Vec<RuntimeTerritory>,
#[serde(default)]
pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>,
#[serde(default)]
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
#[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>,
@ -424,11 +552,40 @@ impl RuntimeState {
}
}
let mut seen_territory_ids = BTreeSet::new();
for territory in &self.territories {
if !seen_territory_ids.insert(territory.territory_id) {
return Err(format!("duplicate territory_id {}", territory.territory_id));
}
}
for entry in &self.company_territory_track_piece_counts {
if !seen_company_ids.contains(&entry.company_id) {
return Err(format!(
"company_territory_track_piece_counts references unknown company_id {}",
entry.company_id
));
}
if !seen_territory_ids.contains(&entry.territory_id) {
return Err(format!(
"company_territory_track_piece_counts references unknown territory_id {}",
entry.territory_id
));
}
}
let mut seen_record_ids = BTreeSet::new();
for record in &self.event_runtime_records {
if !seen_record_ids.insert(record.record_id) {
return Err(format!("duplicate record_id {}", record.record_id));
}
for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, &seen_company_ids).map_err(|err| {
format!(
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
record.record_id
)
})?;
}
for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, &seen_company_ids).map_err(|err| {
format!(
@ -613,6 +770,42 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty candidate_name"
));
}
if row
.comparator
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty comparator"
));
}
if row
.metric
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty metric"
));
}
if row
.semantic_family
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_family"
));
}
if row
.semantic_preview
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_preview"
));
}
}
for row in &record.grouped_effect_rows {
if row.row_shape.trim().is_empty() {
@ -758,6 +951,14 @@ fn validate_event_record_template(
record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, valid_company_ids).map_err(|err| {
format!(
"template record_id={}.conditions[{condition_index}] {err}",
record.record_id
)
})?;
}
for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, valid_company_ids).map_err(|err| {
format!(
@ -770,6 +971,19 @@ fn validate_event_record_template(
Ok(())
}
fn validate_runtime_condition(
condition: &RuntimeCondition,
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
validate_company_target(target, valid_company_ids)
}
RuntimeCondition::TerritoryNumericThreshold { .. } => Ok(()),
}
}
fn validate_company_target(
target: &RuntimeCompanyTarget,
valid_company_ids: &BTreeSet<u32>,
@ -816,6 +1030,9 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
@ -824,12 +1041,17 @@ mod tests {
company_id: 1,
current_cash: 200,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
},
],
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -877,6 +1099,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -904,11 +1128,16 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}],
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
@ -918,6 +1147,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50,
@ -948,11 +1178,16 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}],
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
@ -962,6 +1197,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: 8,
@ -969,6 +1205,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50,
@ -999,6 +1236,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -1031,6 +1270,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
@ -1055,6 +1295,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
@ -1088,11 +1329,16 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(2),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -1120,11 +1366,16 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(1),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),

View file

@ -5,8 +5,9 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimeTerritoryMetric, RuntimeTrackMetric,
};
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -184,6 +185,133 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealOrdinaryConditionMetric {
Company(RuntimeCompanyMetric),
Territory(RuntimeTerritoryMetric),
CompanyTerritory(RuntimeTrackMetric),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RealOrdinaryConditionMetadata {
raw_condition_id: i32,
label: &'static str,
metric: RealOrdinaryConditionMetric,
}
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 22] = [
RealOrdinaryConditionMetadata {
raw_condition_id: 1802,
label: "Current Cash",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::CurrentCash),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 951,
label: "Total Debt",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TotalDebt),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2366,
label: "Credit Rating",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::CreditRating),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2368,
label: "Prime Rate",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::PrimeRate),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2293,
label: "Company Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesTotal),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2294,
label: "Company Single Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesSingle),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2295,
label: "Company Double Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesDouble),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2296,
label: "Company Transition Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesTransition),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2297,
label: "Company Electric Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesElectric),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2298,
label: "Company Non-Electric Track Pieces",
metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesNonElectric),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2313,
label: "Territory Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesTotal),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2314,
label: "Territory Single Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesSingle),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2315,
label: "Territory Double Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesDouble),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2316,
label: "Territory Transition Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesTransition),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2317,
label: "Territory Electric Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesElectric),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2318,
label: "Territory Non-Electric Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesNonElectric),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2323,
label: "Company-Territory Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Total),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2324,
label: "Company-Territory Single Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Single),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2325,
label: "Company-Territory Double Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Double),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2326,
label: "Company-Territory Transition Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Transition),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2327,
label: "Company-Territory Electric Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Electric),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2328,
label: "Company-Territory Non-Electric Track Pieces",
metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::NonElectric),
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KnownSpecialConditionDefinition {
slot_index: u8,
@ -1321,6 +1449,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub decoded_conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
@ -1369,6 +1499,16 @@ pub struct SmpLoadedPackedEventConditionRowSummary {
#[serde(default)]
pub candidate_name: Option<String>,
#[serde(default)]
pub comparator: Option<String>,
#[serde(default)]
pub metric: Option<String>,
#[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub requires_candidate_name_binding: bool,
#[serde(default)]
pub notes: Vec<String>,
}
@ -1853,6 +1993,7 @@ fn parse_synthetic_event_runtime_record_summary(
negative_sentinel_scope: None,
grouped_effect_row_counts,
grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions,
executable_import_ready,
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()],
@ -1958,15 +2099,27 @@ 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_conditions = decode_real_condition_rows(
&standalone_condition_rows,
negative_sentinel_scope.as_ref(),
);
let decoded_actions = compact_control
.as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
.unwrap_or_default();
let ordinary_condition_row_count = standalone_condition_rows
.iter()
.filter(|row| row.raw_condition_id >= 0)
.count();
let executable_import_ready = !grouped_effect_rows.is_empty()
&& decoded_actions.len() == grouped_effect_rows.len()
&& decoded_conditions.len() == ordinary_condition_row_count
&& decoded_actions
.iter()
.all(runtime_effect_supported_for_save_import);
.all(runtime_effect_supported_for_save_import)
&& decoded_conditions
.iter()
.all(runtime_condition_supported_for_save_import);
let consumed_len = cursor;
Some((
SmpLoadedPackedEventRecordSummary {
@ -1989,6 +2142,7 @@ fn parse_real_event_runtime_record_summary(
negative_sentinel_scope,
grouped_effect_row_counts,
grouped_effect_rows,
decoded_conditions,
decoded_actions,
executable_import_ready,
notes: vec![
@ -2074,6 +2228,22 @@ fn parse_real_condition_row_summary(
) -> Option<SmpLoadedPackedEventConditionRowSummary> {
let raw_condition_id = read_u32_at(row_bytes, 0)? as i32;
let subtype = read_u8_at(row_bytes, 4)?;
let flag_bytes = row_bytes
.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?
.to_vec();
let ordinary_metadata = real_ordinary_condition_metadata(raw_condition_id);
let comparator = ordinary_metadata
.and_then(|_| decode_real_condition_comparator(subtype))
.map(condition_comparator_label);
let metric = ordinary_metadata.map(|metadata| metadata.label.to_string());
let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes));
let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| {
matches!(
metadata.metric,
RealOrdinaryConditionMetric::Territory(_)
| RealOrdinaryConditionMetric::CompanyTerritory(_)
) && candidate_name.is_some()
});
let mut notes = Vec::new();
if raw_condition_id < 0 {
notes.push("negative sentinel-style condition row id".to_string());
@ -2081,14 +2251,27 @@ fn parse_real_condition_row_summary(
if candidate_name.is_some() {
notes.push("condition row carries candidate-name side string".to_string());
}
if ordinary_metadata.is_none() && raw_condition_id >= 0 {
notes.push("ordinary condition id is not yet recovered in the checked-in condition table".to_string());
}
Some(SmpLoadedPackedEventConditionRowSummary {
row_index,
raw_condition_id,
subtype,
flag_bytes: row_bytes
.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?
.to_vec(),
flag_bytes,
candidate_name,
comparator,
metric,
semantic_family: ordinary_metadata.map(|_| "numeric_threshold".to_string()),
semantic_preview: ordinary_metadata.and_then(|metadata| {
threshold.map(|value| {
let comparator_text = decode_real_condition_comparator(subtype)
.map(condition_comparator_symbol)
.unwrap_or("?");
format!("Test {} {} {}", metadata.label, comparator_text, value)
})
}),
requires_candidate_name_binding,
notes,
})
}
@ -2136,6 +2319,56 @@ fn decode_player_condition_test_scope(value: u8) -> Option<RuntimePlayerConditio
}
}
fn real_ordinary_condition_metadata(
raw_condition_id: i32,
) -> Option<RealOrdinaryConditionMetadata> {
REAL_ORDINARY_CONDITION_METADATA
.iter()
.copied()
.find(|metadata| metadata.raw_condition_id == raw_condition_id)
}
fn decode_real_condition_comparator(subtype: u8) -> Option<RuntimeConditionComparator> {
match subtype {
0 => Some(RuntimeConditionComparator::Ge),
1 => Some(RuntimeConditionComparator::Le),
2 => Some(RuntimeConditionComparator::Gt),
3 => Some(RuntimeConditionComparator::Lt),
4 => Some(RuntimeConditionComparator::Eq),
5 => Some(RuntimeConditionComparator::Ne),
_ => None,
}
}
fn decode_real_condition_threshold(flag_bytes: &[u8]) -> Option<i64> {
let raw = flag_bytes.get(0..4)?;
let mut bytes = [0u8; 4];
bytes.copy_from_slice(raw);
Some(i32::from_le_bytes(bytes).into())
}
fn condition_comparator_label(comparator: RuntimeConditionComparator) -> String {
match comparator {
RuntimeConditionComparator::Ge => "ge".to_string(),
RuntimeConditionComparator::Le => "le".to_string(),
RuntimeConditionComparator::Gt => "gt".to_string(),
RuntimeConditionComparator::Lt => "lt".to_string(),
RuntimeConditionComparator::Eq => "eq".to_string(),
RuntimeConditionComparator::Ne => "ne".to_string(),
}
}
fn condition_comparator_symbol(comparator: RuntimeConditionComparator) -> &'static str {
match comparator {
RuntimeConditionComparator::Ge => ">=",
RuntimeConditionComparator::Le => "<=",
RuntimeConditionComparator::Gt => ">",
RuntimeConditionComparator::Lt => "<",
RuntimeConditionComparator::Eq => "==",
RuntimeConditionComparator::Ne => "!=",
}
}
fn parse_real_grouped_effect_row_summary(
row_bytes: &[u8],
group_index: usize,
@ -2210,6 +2443,52 @@ fn parse_real_grouped_effect_row_summary(
})
}
fn decode_real_condition_rows(
rows: &[SmpLoadedPackedEventConditionRowSummary],
negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>,
) -> Vec<RuntimeCondition> {
rows.iter()
.filter(|row| row.raw_condition_id >= 0)
.filter_map(|row| decode_real_condition_row(row, negative_sentinel_scope))
.collect()
}
fn decode_real_condition_row(
row: &SmpLoadedPackedEventConditionRowSummary,
negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>,
) -> Option<RuntimeCondition> {
let metadata = real_ordinary_condition_metadata(row.raw_condition_id)?;
let comparator = decode_real_condition_comparator(row.subtype)?;
let value = decode_real_condition_threshold(&row.flag_bytes)?;
match metadata.metric {
RealOrdinaryConditionMetric::Company(metric) => Some(RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany,
metric,
comparator,
value,
}),
RealOrdinaryConditionMetric::Territory(metric) => {
negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
metric,
comparator,
value,
})
}
RealOrdinaryConditionMetric::CompanyTerritory(metric) => {
negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany,
metric,
comparator,
value,
})
}
}
}
fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -2446,6 +2725,7 @@ fn parse_synthetic_event_runtime_record_template(
active: flags & 0x01 != 0,
marks_collection_dirty: flags & 0x02 != 0,
one_shot: flags & 0x04 != 0,
conditions: Vec::new(),
effects,
})
}
@ -2522,6 +2802,14 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
}
}
fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool {
match condition {
RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. } => true,
}
}
fn build_unsupported_event_runtime_record_summaries(
live_entry_ids: &[u32],
note: &str,
@ -2549,6 +2837,7 @@ fn build_unsupported_event_runtime_record_summaries(
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec![note.to_string()],
@ -7830,6 +8119,11 @@ mod tests {
subtype: 4,
flag_bytes: vec![0x30; 25],
candidate_name: Some("AutoPlant".to_string()),
comparator: None,
metric: None,
semantic_family: None,
semantic_preview: None,
requires_candidate_name_binding: false,
notes: vec![],
}];
let summary = derive_negative_sentinel_scope_summary(

View file

@ -3,8 +3,10 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary, calendar::BoundaryEventKind,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState,
RuntimeSummary, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind,
};
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -79,6 +81,11 @@ struct AppliedEffectsSummary {
removed_record_ids: Vec<u32>,
}
#[derive(Debug, Default)]
struct ResolvedConditionContext {
matching_company_ids: BTreeSet<u32>,
}
pub fn execute_step_command(
state: &mut RuntimeState,
command: &StepCommand,
@ -212,19 +219,31 @@ fn service_trigger_kind(
.or_insert(0) += 1;
for index in eligible_indices {
let (record_id, record_effects, record_marks_collection_dirty, record_one_shot) = {
let (
record_id,
record_conditions,
record_effects,
record_marks_collection_dirty,
record_one_shot,
) = {
let record = &state.event_runtime_records[index];
(
record.record_id,
record.conditions.clone(),
record.effects.clone(),
record.marks_collection_dirty,
record.one_shot,
)
};
let Some(condition_context) = evaluate_record_conditions(state, &record_conditions)? else {
continue;
};
let effect_summary = apply_runtime_effects(
state,
&record_effects,
&condition_context,
&mut mutated_company_ids,
&mut staged_event_graph_mutations,
)?;
@ -275,6 +294,7 @@ fn service_trigger_kind(
fn apply_runtime_effects(
state: &mut RuntimeState,
effects: &[RuntimeEffect],
condition_context: &ResolvedConditionContext,
mutated_company_ids: &mut BTreeSet<u32>,
staged_event_graph_mutations: &mut Vec<EventGraphMutation>,
) -> Result<AppliedEffectsSummary, String> {
@ -286,7 +306,7 @@ fn apply_runtime_effects(
state.world_flags.insert(key.clone(), *value);
}
RuntimeEffect::SetCompanyCash { target, value } => {
let company_ids = resolve_company_target_ids(state, target)?;
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let company = state
.companies
@ -300,7 +320,7 @@ fn apply_runtime_effects(
}
}
RuntimeEffect::DeactivateCompany { target } => {
let company_ids = resolve_company_target_ids(state, target)?;
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let company = state
.companies
@ -319,7 +339,7 @@ fn apply_runtime_effects(
}
}
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
let company_ids = resolve_company_target_ids(state, target)?;
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let company = state
.companies
@ -335,7 +355,7 @@ fn apply_runtime_effects(
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target)?;
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let company = state
.companies
@ -352,7 +372,7 @@ fn apply_runtime_effects(
}
}
RuntimeEffect::AdjustCompanyDebt { target, delta } => {
let company_ids = resolve_company_target_ids(state, target)?;
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let company = state
.companies
@ -456,9 +476,114 @@ fn commit_staged_event_graph_mutations(
state.validate()
}
fn evaluate_record_conditions(
state: &RuntimeState,
conditions: &[RuntimeCondition],
) -> Result<Option<ResolvedConditionContext>, String> {
if conditions.is_empty() {
return Ok(Some(ResolvedConditionContext::default()));
}
let mut company_matches: Option<BTreeSet<u32>> = None;
for condition in conditions {
match condition {
RuntimeCondition::CompanyNumericThreshold {
target,
metric,
comparator,
value,
} => {
let resolved = resolve_company_target_ids(
state,
target,
&ResolvedConditionContext::default(),
)?;
let matching = resolved
.into_iter()
.filter(|company_id| {
state.companies.iter().find(|company| company.company_id == *company_id).is_some_and(
|company| compare_condition_value(
company_metric_value(company, *metric),
*comparator,
*value,
),
)
})
.collect::<BTreeSet<_>>();
if matching.is_empty() {
return Ok(None);
}
intersect_company_matches(&mut company_matches, matching);
if company_matches.as_ref().is_some_and(BTreeSet::is_empty) {
return Ok(None);
}
}
RuntimeCondition::TerritoryNumericThreshold {
metric,
comparator,
value,
} => {
let actual = territory_metric_value(state, *metric);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::CompanyTerritoryNumericThreshold {
target,
metric,
comparator,
value,
} => {
let resolved = resolve_company_target_ids(
state,
target,
&ResolvedConditionContext::default(),
)?;
let matching = resolved
.into_iter()
.filter(|company_id| {
compare_condition_value(
company_territory_metric_value(state, *company_id, *metric),
*comparator,
*value,
)
})
.collect::<BTreeSet<_>>();
if matching.is_empty() {
return Ok(None);
}
intersect_company_matches(&mut company_matches, matching);
if company_matches.as_ref().is_some_and(BTreeSet::is_empty) {
return Ok(None);
}
}
}
}
Ok(Some(ResolvedConditionContext {
matching_company_ids: company_matches.unwrap_or_default(),
}))
}
fn intersect_company_matches(
company_matches: &mut Option<BTreeSet<u32>>,
next: BTreeSet<u32>,
) {
match company_matches {
Some(existing) => {
existing.retain(|company_id| next.contains(company_id));
}
None => {
*company_matches = Some(next);
}
}
}
fn resolve_company_target_ids(
state: &RuntimeState,
target: &RuntimeCompanyTarget,
condition_context: &ResolvedConditionContext,
) -> Result<Vec<u32>, String> {
match target {
RuntimeCompanyTarget::AllActive => Ok(state
@ -538,11 +663,101 @@ fn resolve_company_target_ids(
}
}
RuntimeCompanyTarget::ConditionTrueCompany => {
Err("target requires condition-evaluation context".to_string())
if condition_context.matching_company_ids.is_empty() {
Err("target requires condition-evaluation context".to_string())
} else {
Ok(condition_context
.matching_company_ids
.iter()
.copied()
.collect())
}
}
}
}
fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyMetric) -> i64 {
match metric {
RuntimeCompanyMetric::CurrentCash => company.current_cash,
RuntimeCompanyMetric::TotalDebt => company.debt as i64,
RuntimeCompanyMetric::CreditRating => company.credit_rating_score.unwrap_or(0),
RuntimeCompanyMetric::PrimeRate => company.prime_rate.unwrap_or(0),
RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total),
RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single),
RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double),
RuntimeCompanyMetric::TrackPiecesTransition => {
i64::from(company.track_piece_counts.transition)
}
RuntimeCompanyMetric::TrackPiecesElectric => {
i64::from(company.track_piece_counts.electric)
}
RuntimeCompanyMetric::TrackPiecesNonElectric => {
i64::from(company.track_piece_counts.non_electric)
}
}
}
fn territory_metric_value(state: &RuntimeState, metric: RuntimeTerritoryMetric) -> i64 {
state.territories
.iter()
.map(|territory| {
track_piece_metric_value(
territory.track_piece_counts,
territory_metric_to_track_metric(metric),
)
})
.sum()
}
fn company_territory_metric_value(
state: &RuntimeState,
company_id: u32,
metric: RuntimeTrackMetric,
) -> i64 {
state.company_territory_track_piece_counts
.iter()
.filter(|entry| entry.company_id == company_id)
.map(|entry| track_piece_metric_value(entry.track_piece_counts, metric))
.sum()
}
fn track_piece_metric_value(counts: RuntimeTrackPieceCounts, metric: RuntimeTrackMetric) -> i64 {
match metric {
RuntimeTrackMetric::Total => i64::from(counts.total),
RuntimeTrackMetric::Single => i64::from(counts.single),
RuntimeTrackMetric::Double => i64::from(counts.double),
RuntimeTrackMetric::Transition => i64::from(counts.transition),
RuntimeTrackMetric::Electric => i64::from(counts.electric),
RuntimeTrackMetric::NonElectric => i64::from(counts.non_electric),
}
}
fn territory_metric_to_track_metric(metric: RuntimeTerritoryMetric) -> RuntimeTrackMetric {
match metric {
RuntimeTerritoryMetric::TrackPiecesTotal => RuntimeTrackMetric::Total,
RuntimeTerritoryMetric::TrackPiecesSingle => RuntimeTrackMetric::Single,
RuntimeTerritoryMetric::TrackPiecesDouble => RuntimeTrackMetric::Double,
RuntimeTerritoryMetric::TrackPiecesTransition => RuntimeTrackMetric::Transition,
RuntimeTerritoryMetric::TrackPiecesElectric => RuntimeTrackMetric::Electric,
RuntimeTerritoryMetric::TrackPiecesNonElectric => RuntimeTrackMetric::NonElectric,
}
}
fn compare_condition_value(
actual: i64,
comparator: RuntimeConditionComparator,
expected: i64,
) -> bool {
match comparator {
RuntimeConditionComparator::Ge => actual >= expected,
RuntimeConditionComparator::Le => actual <= expected,
RuntimeConditionComparator::Gt => actual > expected,
RuntimeConditionComparator::Lt => actual < expected,
RuntimeConditionComparator::Eq => actual == expected,
RuntimeConditionComparator::Ne => actual != expected,
}
}
fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result<u64, String> {
if delta >= 0 {
current
@ -583,10 +798,15 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -647,6 +867,7 @@ mod tests {
marks_collection_dirty: true,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "runtime.effect_fired".to_string(),
value: true,
@ -660,6 +881,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive,
delta: 5,
@ -673,6 +895,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetSpecialCondition {
label: "Dirty rerun fired".to_string(),
value: 1,
@ -747,6 +970,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 5,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -755,6 +981,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20,
debt: 8,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -767,6 +996,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
@ -803,6 +1033,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -811,6 +1044,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -825,6 +1061,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 5,
@ -838,6 +1075,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 3,
@ -851,6 +1089,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 7,
@ -884,6 +1123,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 1,
@ -912,6 +1152,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 1,
@ -938,6 +1179,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 1,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -946,6 +1190,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 20,
debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
available_track_laying_capacity: None,
},
@ -954,6 +1201,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 30,
debt: 3,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -967,6 +1217,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive,
delta: 5,
@ -980,6 +1231,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 4,
@ -993,6 +1245,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 6,
@ -1024,6 +1277,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: Some(8),
}],
@ -1036,6 +1292,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany,
}],
@ -1063,6 +1320,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -1071,6 +1331,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
@ -1083,6 +1346,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
value: Some(14),
@ -1113,6 +1377,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
delta: 1,
@ -1141,6 +1406,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: true,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "one_shot".to_string(),
value: true,
@ -1177,6 +1443,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
}],
@ -1188,6 +1457,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AllActive,
delta: -3,
@ -1215,6 +1485,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: true,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: 41,
@ -1222,6 +1493,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "follow_on_later_pass".to_string(),
value: true,
@ -1268,6 +1540,7 @@ mod tests {
marks_collection_dirty: true,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: 51,
@ -1275,6 +1548,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "dirty_rerun_follow_on".to_string(),
value: true,
@ -1314,6 +1588,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: true,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
@ -1322,6 +1597,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetCandidateAvailability {
name: "Appended Industry".to_string(),
value: 1,
@ -1341,6 +1617,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "deactivated_after_first_pass".to_string(),
value: true,
@ -1354,6 +1631,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetSpecialCondition {
label: "Activated On Second Pass".to_string(),
value: 1,
@ -1367,6 +1645,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag {
key: "removed_after_first_pass".to_string(),
value: true,
@ -1436,6 +1715,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: 71,
@ -1443,6 +1723,7 @@ mod tests {
active: true,
marks_collection_dirty: false,
one_shot: false,
conditions: Vec::new(),
effects: Vec::new(),
}),
}],
@ -1455,6 +1736,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: Vec::new(),
},
],
@ -1480,6 +1762,7 @@ mod tests {
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }],
}],
..state()

View file

@ -29,6 +29,8 @@ pub struct RuntimeSummary {
pub metadata_count: usize,
pub company_count: usize,
pub active_company_count: usize,
pub territory_count: usize,
pub company_territory_track_count: usize,
pub packed_event_collection_present: bool,
pub packed_event_record_count: usize,
pub packed_event_decoded_record_count: usize,
@ -42,6 +44,9 @@ pub struct RuntimeSummary {
pub packed_event_blocked_company_condition_scope_disabled_count: usize,
pub packed_event_blocked_player_condition_scope_count: usize,
pub packed_event_blocked_territory_condition_scope_count: usize,
pub packed_event_blocked_missing_territory_context_count: usize,
pub packed_event_blocked_named_territory_binding_count: usize,
pub packed_event_blocked_unmapped_ordinary_condition_count: usize,
pub packed_event_blocked_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_structural_only_count: usize,
@ -131,6 +136,8 @@ impl RuntimeSummary {
.iter()
.filter(|company| company.active)
.count(),
territory_count: state.territories.len(),
company_territory_track_count: state.company_territory_track_piece_counts.len(),
packed_event_collection_present: state.packed_event_collection.is_some(),
packed_event_record_count: state
.packed_event_collection
@ -267,6 +274,48 @@ impl RuntimeSummary {
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_territory_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_territory_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_named_territory_binding_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_named_territory_binding")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_unmapped_ordinary_condition_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_unmapped_ordinary_condition")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_compact_control_count: state
.packed_event_collection
.as_ref()
@ -355,6 +404,7 @@ mod tests {
use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeTrackPieceCounts,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
};
@ -375,6 +425,8 @@ mod tests {
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -407,6 +459,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_missing_compact_control".to_string()),
@ -431,6 +484,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_missing_company_context".to_string()),
@ -455,6 +509,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some(
@ -481,6 +536,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_player_condition_scope".to_string()),
@ -505,6 +561,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_territory_condition_scope".to_string()),
@ -573,6 +630,9 @@ mod tests {
company_id: 1,
current_cash: 10,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
@ -581,12 +641,17 @@ mod tests {
company_id: 2,
current_cash: 20,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
available_track_laying_capacity: Some(7),
controller_kind: RuntimeCompanyControllerKind::Ai,
},
],
selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),