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

@ -17,10 +17,12 @@ selected-company and controller-role context through overlay imports, and real d
`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and `Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and
execute through the ordinary runtime path. Synthetic packed records still exercise the same service execute through the ordinary runtime path. Synthetic packed records still exercise the same service
engine without a parallel packed executor. The first grounded condition-side unlock now exists for engine without a parallel packed executor. The first grounded condition-side unlock now exists for
negative-sentinel `raw_condition_id = -1` company scopes, while ordinary condition-id semantics and negative-sentinel `raw_condition_id = -1` company scopes, and the first ordinary nonnegative
player/territory runtime ownership remain blocked. Mixed supported/unsupported real rows still stay condition batch now executes too: numeric-threshold company finance, company track, aggregate
parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer territory track, and company-territory track rows can import through overlay-backed runtime
the main execution milestone. context. Named-territory bindings and player-owned condition scope still remain blocked. Mixed
supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and
integration tooling, but it is no longer the main execution milestone.
## Project Docs ## Project Docs

View file

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

View file

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

View file

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

View file

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

View file

@ -22,12 +22,49 @@ pub struct RuntimeCompany {
pub company_id: u32, pub company_id: u32,
pub current_cash: i64, pub current_cash: i64,
pub debt: u64, pub debt: u64,
#[serde(default)]
pub credit_rating_score: Option<i64>,
#[serde(default)]
pub prime_rate: Option<i64>,
#[serde(default = "runtime_company_default_active")] #[serde(default = "runtime_company_default_active")]
pub active: bool, pub active: bool,
#[serde(default)] #[serde(default)]
pub available_track_laying_capacity: Option<u32>, pub available_track_laying_capacity: Option<u32>,
#[serde(default)] #[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -63,6 +100,76 @@ pub enum RuntimePlayerConditionTestScope {
HumanPlayersOnly, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEffect { pub enum RuntimeEffect {
@ -121,6 +228,8 @@ pub struct RuntimeEventRecordTemplate {
#[serde(default)] #[serde(default)]
pub one_shot: bool, pub one_shot: bool,
#[serde(default)] #[serde(default)]
pub conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub effects: Vec<RuntimeEffect>, pub effects: Vec<RuntimeEffect>,
} }
@ -138,6 +247,8 @@ pub struct RuntimeEventRecord {
#[serde(default)] #[serde(default)]
pub has_fired: bool, pub has_fired: bool,
#[serde(default)] #[serde(default)]
pub conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub effects: Vec<RuntimeEffect>, pub effects: Vec<RuntimeEffect>,
} }
@ -197,6 +308,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>, pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
#[serde(default)] #[serde(default)]
pub decoded_conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>, pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)] #[serde(default)]
pub executable_import_ready: bool, pub executable_import_ready: bool,
@ -250,6 +363,16 @@ pub struct RuntimePackedEventConditionRowSummary {
#[serde(default)] #[serde(default)]
pub candidate_name: Option<String>, pub candidate_name: Option<String>,
#[serde(default)] #[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>, pub notes: Vec<String>,
} }
@ -293,6 +416,7 @@ impl RuntimeEventRecordTemplate {
marks_collection_dirty: self.marks_collection_dirty, marks_collection_dirty: self.marks_collection_dirty,
one_shot: self.one_shot, one_shot: self.one_shot,
has_fired: false, has_fired: false,
conditions: self.conditions,
effects: self.effects, effects: self.effects,
} }
} }
@ -384,6 +508,10 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub selected_company_id: Option<u32>, pub selected_company_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub territories: Vec<RuntimeTerritory>,
#[serde(default)]
pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>,
#[serde(default)]
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>, pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
#[serde(default)] #[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>, 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(); let mut seen_record_ids = BTreeSet::new();
for record in &self.event_runtime_records { for record in &self.event_runtime_records {
if !seen_record_ids.insert(record.record_id) { if !seen_record_ids.insert(record.record_id) {
return Err(format!("duplicate record_id {}", 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() { for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, &seen_company_ids).map_err(|err| { validate_runtime_effect(effect, &seen_company_ids).map_err(|err| {
format!( format!(
@ -613,6 +770,42 @@ impl RuntimeState {
"packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty candidate_name" "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 { for row in &record.grouped_effect_rows {
if row.row_shape.trim().is_empty() { if row.row_shape.trim().is_empty() {
@ -758,6 +951,14 @@ fn validate_event_record_template(
record: &RuntimeEventRecordTemplate, record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> 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() { for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, valid_company_ids).map_err(|err| { validate_runtime_effect(effect, valid_company_ids).map_err(|err| {
format!( format!(
@ -770,6 +971,19 @@ fn validate_event_record_template(
Ok(()) 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( fn validate_company_target(
target: &RuntimeCompanyTarget, target: &RuntimeCompanyTarget,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
@ -816,6 +1030,9 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
@ -824,12 +1041,17 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 200, current_cash: 200,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
], ],
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -877,6 +1099,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -904,11 +1128,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 7, record_id: 7,
@ -918,6 +1147,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] }, target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50, delta: 50,
@ -948,11 +1178,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 7, record_id: 7,
@ -962,6 +1197,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord { effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate { record: Box::new(RuntimeEventRecordTemplate {
record_id: 8, record_id: 8,
@ -969,6 +1205,7 @@ mod tests {
active: true, active: true,
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] }, target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50, delta: 50,
@ -999,6 +1236,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".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_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, import_outcome: None,
@ -1055,6 +1295,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, import_outcome: None,
@ -1088,11 +1329,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(2), selected_company_id: Some(2),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -1120,11 +1366,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(1), selected_company_id: Some(1),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),

View file

@ -5,8 +5,9 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::{ use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyTarget, RuntimeEffect, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimeTerritoryMetric, RuntimeTrackMetric,
}; };
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KnownSpecialConditionDefinition { struct KnownSpecialConditionDefinition {
slot_index: u8, slot_index: u8,
@ -1321,6 +1449,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>, pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
#[serde(default)] #[serde(default)]
pub decoded_conditions: Vec<RuntimeCondition>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>, pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)] #[serde(default)]
pub executable_import_ready: bool, pub executable_import_ready: bool,
@ -1369,6 +1499,16 @@ pub struct SmpLoadedPackedEventConditionRowSummary {
#[serde(default)] #[serde(default)]
pub candidate_name: Option<String>, pub candidate_name: Option<String>,
#[serde(default)] #[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>, pub notes: Vec<String>,
} }
@ -1853,6 +1993,7 @@ fn parse_synthetic_event_runtime_record_summary(
negative_sentinel_scope: None, negative_sentinel_scope: None,
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions, decoded_actions,
executable_import_ready, executable_import_ready,
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()], 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| { let negative_sentinel_scope = compact_control.as_ref().and_then(|control| {
derive_negative_sentinel_scope_summary(&standalone_condition_rows, 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 let decoded_actions = compact_control
.as_ref() .as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
.unwrap_or_default(); .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() let executable_import_ready = !grouped_effect_rows.is_empty()
&& decoded_actions.len() == grouped_effect_rows.len() && decoded_actions.len() == grouped_effect_rows.len()
&& decoded_conditions.len() == ordinary_condition_row_count
&& decoded_actions && decoded_actions
.iter() .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; let consumed_len = cursor;
Some(( Some((
SmpLoadedPackedEventRecordSummary { SmpLoadedPackedEventRecordSummary {
@ -1989,6 +2142,7 @@ fn parse_real_event_runtime_record_summary(
negative_sentinel_scope, negative_sentinel_scope,
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows, grouped_effect_rows,
decoded_conditions,
decoded_actions, decoded_actions,
executable_import_ready, executable_import_ready,
notes: vec![ notes: vec![
@ -2074,6 +2228,22 @@ fn parse_real_condition_row_summary(
) -> Option<SmpLoadedPackedEventConditionRowSummary> { ) -> Option<SmpLoadedPackedEventConditionRowSummary> {
let raw_condition_id = read_u32_at(row_bytes, 0)? as i32; let raw_condition_id = read_u32_at(row_bytes, 0)? as i32;
let subtype = read_u8_at(row_bytes, 4)?; 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(); let mut notes = Vec::new();
if raw_condition_id < 0 { if raw_condition_id < 0 {
notes.push("negative sentinel-style condition row id".to_string()); 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() { if candidate_name.is_some() {
notes.push("condition row carries candidate-name side string".to_string()); 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 { Some(SmpLoadedPackedEventConditionRowSummary {
row_index, row_index,
raw_condition_id, raw_condition_id,
subtype, subtype,
flag_bytes: row_bytes flag_bytes,
.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?
.to_vec(),
candidate_name, 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, 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( fn parse_real_grouped_effect_row_summary(
row_bytes: &[u8], row_bytes: &[u8],
group_index: usize, 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( fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32, descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> { ) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -2446,6 +2725,7 @@ fn parse_synthetic_event_runtime_record_template(
active: flags & 0x01 != 0, active: flags & 0x01 != 0,
marks_collection_dirty: flags & 0x02 != 0, marks_collection_dirty: flags & 0x02 != 0,
one_shot: flags & 0x04 != 0, one_shot: flags & 0x04 != 0,
conditions: Vec::new(),
effects, 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( fn build_unsupported_event_runtime_record_summaries(
live_entry_ids: &[u32], live_entry_ids: &[u32],
note: &str, note: &str,
@ -2549,6 +2837,7 @@ fn build_unsupported_event_runtime_record_summaries(
negative_sentinel_scope: None, negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
notes: vec![note.to_string()], notes: vec![note.to_string()],
@ -7830,6 +8119,11 @@ mod tests {
subtype: 4, subtype: 4,
flag_bytes: vec![0x30; 25], flag_bytes: vec![0x30; 25],
candidate_name: Some("AutoPlant".to_string()), candidate_name: Some("AutoPlant".to_string()),
comparator: None,
metric: None,
semantic_family: None,
semantic_preview: None,
requires_candidate_name_binding: false,
notes: vec![], notes: vec![],
}]; }];
let summary = derive_negative_sentinel_scope_summary( let summary = derive_negative_sentinel_scope_summary(

View file

@ -3,8 +3,10 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeState, RuntimeSummary, calendar::BoundaryEventKind, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState,
RuntimeSummary, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind,
}; };
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -79,6 +81,11 @@ struct AppliedEffectsSummary {
removed_record_ids: Vec<u32>, removed_record_ids: Vec<u32>,
} }
#[derive(Debug, Default)]
struct ResolvedConditionContext {
matching_company_ids: BTreeSet<u32>,
}
pub fn execute_step_command( pub fn execute_step_command(
state: &mut RuntimeState, state: &mut RuntimeState,
command: &StepCommand, command: &StepCommand,
@ -212,19 +219,31 @@ fn service_trigger_kind(
.or_insert(0) += 1; .or_insert(0) += 1;
for index in eligible_indices { 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]; let record = &state.event_runtime_records[index];
( (
record.record_id, record.record_id,
record.conditions.clone(),
record.effects.clone(), record.effects.clone(),
record.marks_collection_dirty, record.marks_collection_dirty,
record.one_shot, record.one_shot,
) )
}; };
let Some(condition_context) = evaluate_record_conditions(state, &record_conditions)? else {
continue;
};
let effect_summary = apply_runtime_effects( let effect_summary = apply_runtime_effects(
state, state,
&record_effects, &record_effects,
&condition_context,
&mut mutated_company_ids, &mut mutated_company_ids,
&mut staged_event_graph_mutations, &mut staged_event_graph_mutations,
)?; )?;
@ -275,6 +294,7 @@ fn service_trigger_kind(
fn apply_runtime_effects( fn apply_runtime_effects(
state: &mut RuntimeState, state: &mut RuntimeState,
effects: &[RuntimeEffect], effects: &[RuntimeEffect],
condition_context: &ResolvedConditionContext,
mutated_company_ids: &mut BTreeSet<u32>, mutated_company_ids: &mut BTreeSet<u32>,
staged_event_graph_mutations: &mut Vec<EventGraphMutation>, staged_event_graph_mutations: &mut Vec<EventGraphMutation>,
) -> Result<AppliedEffectsSummary, String> { ) -> Result<AppliedEffectsSummary, String> {
@ -286,7 +306,7 @@ fn apply_runtime_effects(
state.world_flags.insert(key.clone(), *value); state.world_flags.insert(key.clone(), *value);
} }
RuntimeEffect::SetCompanyCash { target, 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 { for company_id in company_ids {
let company = state let company = state
.companies .companies
@ -300,7 +320,7 @@ fn apply_runtime_effects(
} }
} }
RuntimeEffect::DeactivateCompany { target } => { 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 { for company_id in company_ids {
let company = state let company = state
.companies .companies
@ -319,7 +339,7 @@ fn apply_runtime_effects(
} }
} }
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { 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 { for company_id in company_ids {
let company = state let company = state
.companies .companies
@ -335,7 +355,7 @@ fn apply_runtime_effects(
} }
} }
RuntimeEffect::AdjustCompanyCash { target, delta } => { 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 { for company_id in company_ids {
let company = state let company = state
.companies .companies
@ -352,7 +372,7 @@ fn apply_runtime_effects(
} }
} }
RuntimeEffect::AdjustCompanyDebt { target, delta } => { 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 { for company_id in company_ids {
let company = state let company = state
.companies .companies
@ -456,9 +476,114 @@ fn commit_staged_event_graph_mutations(
state.validate() 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( fn resolve_company_target_ids(
state: &RuntimeState, state: &RuntimeState,
target: &RuntimeCompanyTarget, target: &RuntimeCompanyTarget,
condition_context: &ResolvedConditionContext,
) -> Result<Vec<u32>, String> { ) -> Result<Vec<u32>, String> {
match target { match target {
RuntimeCompanyTarget::AllActive => Ok(state RuntimeCompanyTarget::AllActive => Ok(state
@ -538,11 +663,101 @@ fn resolve_company_target_ids(
} }
} }
RuntimeCompanyTarget::ConditionTrueCompany => { 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> { fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result<u64, String> {
if delta >= 0 { if delta >= 0 {
current current
@ -583,10 +798,15 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}], }],
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -647,6 +867,7 @@ mod tests {
marks_collection_dirty: true, marks_collection_dirty: true,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "runtime.effect_fired".to_string(), key: "runtime.effect_fired".to_string(),
value: true, value: true,
@ -660,6 +881,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive, target: RuntimeCompanyTarget::AllActive,
delta: 5, delta: 5,
@ -673,6 +895,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetSpecialCondition { effects: vec![RuntimeEffect::SetSpecialCondition {
label: "Dirty rerun fired".to_string(), label: "Dirty rerun fired".to_string(),
value: 1, value: 1,
@ -747,6 +970,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 5, debt: 5,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -755,6 +981,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20, current_cash: 20,
debt: 8, debt: 8,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -767,6 +996,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![ effects: vec![
RuntimeEffect::AdjustCompanyCash { RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] }, target: RuntimeCompanyTarget::Ids { ids: vec![2] },
@ -803,6 +1033,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -811,6 +1044,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20, current_cash: 20,
debt: 2, debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -825,6 +1061,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies, target: RuntimeCompanyTarget::HumanCompanies,
delta: 5, delta: 5,
@ -838,6 +1075,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt { effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies, target: RuntimeCompanyTarget::AiCompanies,
delta: 3, delta: 3,
@ -851,6 +1089,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany, target: RuntimeCompanyTarget::SelectedCompany,
delta: 7, delta: 7,
@ -884,6 +1123,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany, target: RuntimeCompanyTarget::SelectedCompany,
delta: 1, delta: 1,
@ -912,6 +1152,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies, target: RuntimeCompanyTarget::HumanCompanies,
delta: 1, delta: 1,
@ -938,6 +1179,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10, current_cash: 10,
debt: 1, debt: 1,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -946,6 +1190,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 20, current_cash: 20,
debt: 2, debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -954,6 +1201,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 30, current_cash: 30,
debt: 3, debt: 3,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -967,6 +1217,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive, target: RuntimeCompanyTarget::AllActive,
delta: 5, delta: 5,
@ -980,6 +1231,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt { effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::HumanCompanies, target: RuntimeCompanyTarget::HumanCompanies,
delta: 4, delta: 4,
@ -993,6 +1245,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt { effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies, target: RuntimeCompanyTarget::AiCompanies,
delta: 6, delta: 6,
@ -1024,6 +1277,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: Some(8), available_track_laying_capacity: Some(8),
}], }],
@ -1036,6 +1292,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::DeactivateCompany { effects: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany, target: RuntimeCompanyTarget::SelectedCompany,
}], }],
@ -1063,6 +1320,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -1071,6 +1331,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20, current_cash: 20,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}, },
@ -1083,6 +1346,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::Ids { ids: vec![2] }, target: RuntimeCompanyTarget::Ids { ids: vec![2] },
value: Some(14), value: Some(14),
@ -1113,6 +1377,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyCash { effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany, target: RuntimeCompanyTarget::ConditionTrueCompany,
delta: 1, delta: 1,
@ -1141,6 +1406,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: true, one_shot: true,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "one_shot".to_string(), key: "one_shot".to_string(),
value: true, value: true,
@ -1177,6 +1443,9 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 2, debt: 2,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
}], }],
@ -1188,6 +1457,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AdjustCompanyDebt { effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AllActive, target: RuntimeCompanyTarget::AllActive,
delta: -3, delta: -3,
@ -1215,6 +1485,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: true, one_shot: true,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord { effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate { record: Box::new(RuntimeEventRecordTemplate {
record_id: 41, record_id: 41,
@ -1222,6 +1493,7 @@ mod tests {
active: true, active: true,
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "follow_on_later_pass".to_string(), key: "follow_on_later_pass".to_string(),
value: true, value: true,
@ -1268,6 +1540,7 @@ mod tests {
marks_collection_dirty: true, marks_collection_dirty: true,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord { effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate { record: Box::new(RuntimeEventRecordTemplate {
record_id: 51, record_id: 51,
@ -1275,6 +1548,7 @@ mod tests {
active: true, active: true,
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "dirty_rerun_follow_on".to_string(), key: "dirty_rerun_follow_on".to_string(),
value: true, value: true,
@ -1314,6 +1588,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: true, one_shot: true,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![ effects: vec![
RuntimeEffect::AppendEventRecord { RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate { record: Box::new(RuntimeEventRecordTemplate {
@ -1322,6 +1597,7 @@ mod tests {
active: true, active: true,
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetCandidateAvailability { effects: vec![RuntimeEffect::SetCandidateAvailability {
name: "Appended Industry".to_string(), name: "Appended Industry".to_string(),
value: 1, value: 1,
@ -1341,6 +1617,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "deactivated_after_first_pass".to_string(), key: "deactivated_after_first_pass".to_string(),
value: true, value: true,
@ -1354,6 +1631,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetSpecialCondition { effects: vec![RuntimeEffect::SetSpecialCondition {
label: "Activated On Second Pass".to_string(), label: "Activated On Second Pass".to_string(),
value: 1, value: 1,
@ -1367,6 +1645,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetWorldFlag { effects: vec![RuntimeEffect::SetWorldFlag {
key: "removed_after_first_pass".to_string(), key: "removed_after_first_pass".to_string(),
value: true, value: true,
@ -1436,6 +1715,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::AppendEventRecord { effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate { record: Box::new(RuntimeEventRecordTemplate {
record_id: 71, record_id: 71,
@ -1443,6 +1723,7 @@ mod tests {
active: true, active: true,
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
conditions: Vec::new(),
effects: Vec::new(), effects: Vec::new(),
}), }),
}], }],
@ -1455,6 +1736,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: Vec::new(), effects: Vec::new(),
}, },
], ],
@ -1480,6 +1762,7 @@ mod tests {
marks_collection_dirty: false, marks_collection_dirty: false,
one_shot: false, one_shot: false,
has_fired: false, has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }], effects: vec![RuntimeEffect::ActivateEventRecord { record_id: 999 }],
}], }],
..state() ..state()

View file

@ -29,6 +29,8 @@ pub struct RuntimeSummary {
pub metadata_count: usize, pub metadata_count: usize,
pub company_count: usize, pub company_count: usize,
pub active_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_collection_present: bool,
pub packed_event_record_count: usize, pub packed_event_record_count: usize,
pub packed_event_decoded_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_company_condition_scope_disabled_count: usize,
pub packed_event_blocked_player_condition_scope_count: usize, pub packed_event_blocked_player_condition_scope_count: usize,
pub packed_event_blocked_territory_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_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_structural_only_count: usize, pub packed_event_blocked_structural_only_count: usize,
@ -131,6 +136,8 @@ impl RuntimeSummary {
.iter() .iter()
.filter(|company| company.active) .filter(|company| company.active)
.count(), .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_collection_present: state.packed_event_collection.is_some(),
packed_event_record_count: state packed_event_record_count: state
.packed_event_collection .packed_event_collection
@ -267,6 +274,48 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .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_blocked_missing_compact_control_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -355,6 +404,7 @@ mod tests {
use crate::{ use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeTrackPieceCounts,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
}; };
@ -375,6 +425,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".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_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_missing_compact_control".to_string()), 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_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_missing_company_context".to_string()), 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_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some( import_outcome: Some(
@ -481,6 +536,7 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_player_condition_scope".to_string()), 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_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_territory_condition_scope".to_string()), import_outcome: Some("blocked_territory_condition_scope".to_string()),
@ -573,6 +630,9 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
@ -581,12 +641,17 @@ mod tests {
company_id: 2, company_id: 2,
current_cash: 20, current_cash: 20,
debt: 0, debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: Some(7), available_track_laying_capacity: Some(7),
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
}, },
], ],
selected_company_id: None, selected_company_id: None,
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),

View file

@ -84,8 +84,11 @@ The highest-value next passes are now:
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask, - widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
and normalized effect semantics are all grounded, not just after row framing is parsed and normalized effect semantics are all grounded, not just after row framing is parsed
- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`
company scopes; broader ordinary condition-id evaluation and player/territory runtime ownership company scopes, and the first ordinary nonnegative condition batch now executes too: numeric
are the remaining condition frontier, and mixed supported/unsupported real rows stay parity-only thresholds for company finance, company track, aggregate territory track, and company-territory
track
- named-territory ordinary rows and player-owned condition scope are still the remaining condition
frontier, and mixed supported/unsupported real rows stay parity-only
- keep in mind that the current local `.gms` corpus still exports with no packed event collection, - keep in mind that the current local `.gms` corpus still exports with no packed event collection,
so real descriptor mapping needs to stay plumbing-first until better captures exist so real descriptor mapping needs to stay plumbing-first until better captures exist
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution

View file

@ -35,11 +35,15 @@ Implemented today:
`raw_condition_id = -1` company scope lowers `condition_true_company` into normalized company `raw_condition_id = -1` company scope lowers `condition_true_company` into normalized company
targets during import, while player and territory scope variants remain parity-visible and targets during import, while player and territory scope variants remain parity-visible and
explicitly blocked explicitly blocked
- the first ordinary nonnegative condition-id batch now executes too: numeric-threshold company
finance, company track, aggregate territory track, and company-territory track rows can import
through overlay-backed runtime context, while named-territory bindings stay parity-only and
player-owned condition scope still has no runtime owner
That means the next implementation work is breadth, not bootstrap. The recommended next slice is That means the next implementation work is breadth, not bootstrap. The recommended next slice is
ordinary nonnegative condition-id semantics plus runtime ownership for the still-blocked player and broader ordinary condition-id coverage beyond numeric thresholds, plus runtime ownership for the
territory scope families, alongside broader real grouped-descriptor coverage beyond the current still-blocked player-scoped and named-territory condition families, alongside wider real
company-scoped batch. grouped-descriptor coverage beyond the current company-scoped batch.
## Why This Boundary ## Why This Boundary
@ -236,8 +240,10 @@ Current status:
raw `.smp` binaries raw `.smp` binaries
- overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic - overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic
selected-company and controller-role scopes without inventing company state from save bytes alone selected-company and controller-role scopes without inventing company state from save bytes alone
- the remaining gap is wider real grouped-descriptor semantic coverage plus ordinary condition-id - aggregate territory context and company-territory track counters now flow through tracked overlay
evaluation and player/territory runtime ownership, not first-pass captured-runtime plumbing snapshots, so the remaining gap is broader ordinary condition-id coverage beyond numeric
thresholds, named-territory binding, player runtime ownership, and wider real grouped-descriptor
semantic coverage, not first-pass captured-runtime plumbing
### Milestone 4: Domain Expansion ### Milestone 4: Domain Expansion
@ -380,48 +386,52 @@ Checked-in fixture families already include:
## Next Slice ## Next Slice
The recommended next implementation slice is broader real grouped-descriptor coverage on top of the The recommended next implementation slice is broader ordinary-condition breadth on top of the
now-stable compact-control, symbolic-target, and current company-scoped real-family batch. now-stable numeric-threshold, overlay-context, and current company-scoped real-descriptor batch.
Target behavior: Target behavior:
- keep descriptors `2` `Company Cash`, `13` `Deactivate Company`, and `16` - preserve the current proof set for real ordinary-condition execution:
`Company Track Pieces Buildable` as the proof that real grouped rows can cross the whole path: company finance, company track, aggregate territory track, and company-territory track numeric
parse, semantic summary, overlay-backed import, and ordinary trigger execution thresholds all pass through parse, semantic summary, overlay-backed import, and ordinary trigger
- recover more real descriptor identities from the checked-in effect table and expose their target execution
masks and conservative semantic previews without guessing unsupported behavior - extend ordinary condition coverage beyond numeric thresholds only when comparator semantics,
- widen executable real import only when both descriptor identity and runtime effect semantics are runtime ownership, and binding rules are grounded enough to lower honestly into the normalized
grounded enough to map into the normalized runtime path honestly runtime path
- keep condition-relative company scopes explicit until a real condition evaluator exists, instead - keep named-territory ordinary rows explicit and parity-visible until candidate-name territory
of silently degrading or inventing target resolution binding is grounded
- keep player-owned condition scope explicit and parity-visible until there is a first-class player
runtime model
- continue widening real grouped-descriptor execution only when both descriptor identity and
runtime effect semantics are grounded enough to map into the normalized runtime path honestly
Public-model expectations for that slice: Public-model expectations for that slice:
- additional checked-in grouped-descriptor metadata entries keyed by recovered descriptor id - additional checked-in ordinary-condition metadata entries beyond the current numeric-threshold
- more parity summaries with real descriptor labels, target masks, parameter families, and semantic allowlist
previews - richer runtime ownership for still-blocked condition domains such as named territory and player
- more selective real-row `decoded_actions` only where the descriptor-to-runtime mapping is scope
supported end to end - more selective real-row `decoded_conditions` and `decoded_actions` only where the
condition/effect-to-runtime mapping is supported end to end
Fixture work for that slice: Fixture work for that slice:
- preserve the parity-heavy tracked sample as the condition-relative blocked frontier while it now - preserve the new ordinary-condition tracked overlays for executable company finance, company
carries recovered `Company Cash` semantics with executable import readiness track, aggregate territory track, and company-territory track thresholds
- keep overlay-backed captured fixtures for the executable company-scoped real families: - preserve the named-territory tracked overlay as the explicit binding blocker frontier
`Company Cash`, `Deactivate Company`, and `Company Track Pieces Buildable` - keep the older negative-sentinel, mixed real-row, and company-scoped descriptor fixtures green so
- keep a mixed real-row overlay fixture to lock the all-or-nothing parity rule for partially ordinary-condition breadth does not regress descriptor-side execution
supported real records
- keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens - keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens
Current local constraint: Current local constraint:
- the local checked-in and on-disk `.gms` corpus still does not provide a richer captured packed - the local checked-in and on-disk `.gms` corpus still does not provide a richer captured packed
event save set, so descriptor recovery must continue to rely on the grounded static tables and event save set, so wider ordinary-condition and descriptor recovery still needs to rely on the
tracked JSON artifacts until new captures exist grounded static tables and tracked JSON artifacts until new captures exist
Do not mix this slice with: Do not mix this slice with:
- shell queue/modal behavior - shell queue/modal behavior
- territory-access or selected-profile parity - territory-access or selected-profile parity
- broad condition evaluation without grounded runtime ownership - speculative condition execution without grounded runtime ownership
- speculative executable import for real rows whose descriptor meaning is still weak - speculative executable import for real rows whose descriptor meaning is still weak

View file

@ -0,0 +1,93 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-company-finance-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving real ordinary Current Cash conditions gate Company Cash through the normal runtime path."
},
"state_import_path": "packed-event-ordinary-company-finance-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 3,
"active_company_count": 3,
"territory_count": 1,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_unmapped_ordinary_condition_count": 0,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 623
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 333
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "selected_company"
},
"metric": "current_cash",
"comparator": "ge",
"value": 100
}
]
}
]
},
"event_runtime_records": [
{
"record_id": 41,
"service_count": 1,
"conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "selected_company"
},
"metric": "current_cash",
"comparator": "ge",
"value": 100
}
],
"effects": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 333
}
]
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-company-finance-overlay",
"source": {
"description": "Overlay import combining captured company context with the real ordinary company-finance threshold sample."
},
"base_snapshot_path": "packed-event-ordinary-condition-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-company-finance-save-slice.json"
}

View file

@ -0,0 +1,139 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-company-finance-save-slice",
"source": {
"description": "Tracked save-slice document with a real ordinary company-finance threshold row gating Company Cash.",
"original_save_filename": "captured-ordinary-company-finance.gms",
"original_save_sha256": "ordinary-company-finance-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves ordinary Current Cash threshold import through the real packed-event path"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 41,
"live_record_count": 1,
"live_entry_ids": [41],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 41,
"payload_offset": 29200,
"payload_len": 176,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 42,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 2,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 1802,
"subtype": 0,
"flag_bytes": [100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": null,
"comparator": "ge",
"metric": "Current Cash",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Current Cash >= 100",
"requires_candidate_name_binding": false,
"notes": []
}
],
"negative_sentinel_scope": {
"company_test_scope": "selected_company_only",
"player_test_scope": "disabled",
"territory_scope_selector_is_0x63": false,
"source_row_indexes": [0]
},
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 333,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 333 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "condition_true_company"
},
"metric": "current_cash",
"comparator": "ge",
"value": 100
}
],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 333
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"ordinary Current Cash threshold lowers condition-relative company scope at import time"
]
}
]
},
"notes": [
"real ordinary company-finance threshold sample"
]
}
}

View file

@ -0,0 +1,67 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-company-territory-track-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving real ordinary company-territory thresholds gate Company Cash through the normal runtime path."
},
"state_import_path": "packed-event-ordinary-company-territory-track-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 3,
"active_company_count": 3,
"territory_count": 1,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_missing_territory_context_count": 0,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 845
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 555
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "company_territory_numeric_threshold",
"target": {
"kind": "selected_company"
},
"metric": "total",
"comparator": "ge",
"value": 10
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-company-territory-track-overlay",
"source": {
"description": "Overlay import combining company and territory context with the real ordinary company-territory threshold sample."
},
"base_snapshot_path": "packed-event-ordinary-condition-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-company-territory-track-save-slice.json"
}

View file

@ -0,0 +1,139 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-company-territory-track-save-slice",
"source": {
"description": "Tracked save-slice document with a real ordinary company-territory threshold row gating Company Cash.",
"original_save_filename": "captured-ordinary-company-territory-track.gms",
"original_save_sha256": "ordinary-company-territory-track-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves company-territory thresholds import when both company and territory overlay context exist"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 44,
"live_record_count": 1,
"live_entry_ids": [44],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 44,
"payload_offset": 29296,
"payload_len": 176,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 2,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2323,
"subtype": 0,
"flag_bytes": [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": null,
"comparator": "ge",
"metric": "Company-Territory Track Pieces",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Company-Territory Track Pieces >= 10",
"requires_candidate_name_binding": false,
"notes": []
}
],
"negative_sentinel_scope": {
"company_test_scope": "selected_company_only",
"player_test_scope": "disabled",
"territory_scope_selector_is_0x63": true,
"source_row_indexes": [0]
},
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 555,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 555 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "company_territory_numeric_threshold",
"target": {
"kind": "condition_true_company"
},
"metric": "total",
"comparator": "ge",
"value": 10
}
],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 555
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"company-territory thresholds lower condition-relative company scope when overlay territory context is available"
]
}
]
},
"notes": [
"real ordinary company-territory threshold sample"
]
}
}

View file

@ -0,0 +1,54 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-company-track-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving real ordinary company-track thresholds gate Company Track Pieces Buildable through the normal runtime path."
},
"state_import_path": "packed-event-ordinary-company-track-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 3,
"active_company_count": 3,
"territory_count": 1,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 440
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"available_track_laying_capacity": 12
},
{
"company_id": 2,
"available_track_laying_capacity": null
},
{
"company_id": 3,
"available_track_laying_capacity": null
}
],
"event_runtime_records": [
{
"record_id": 42,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-company-track-overlay",
"source": {
"description": "Overlay import combining captured company context with the real ordinary company-track threshold sample."
},
"base_snapshot_path": "packed-event-ordinary-condition-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-company-track-save-slice.json"
}

View file

@ -0,0 +1,139 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-company-track-save-slice",
"source": {
"description": "Tracked save-slice document with a real ordinary company-track threshold row gating Company Track Pieces Buildable.",
"original_save_filename": "captured-ordinary-company-track.gms",
"original_save_sha256": "ordinary-company-track-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves ordinary Company Track Pieces threshold import through the real packed-event path"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 42,
"live_record_count": 1,
"live_entry_ids": [42],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 42,
"payload_offset": 29232,
"payload_len": 176,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 42,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 2,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2293,
"subtype": 0,
"flag_bytes": [20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": null,
"comparator": "ge",
"metric": "Company Track Pieces",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Company Track Pieces >= 20",
"requires_candidate_name_binding": false,
"notes": []
}
],
"negative_sentinel_scope": {
"company_test_scope": "selected_company_only",
"player_test_scope": "disabled",
"territory_scope_selector_is_0x63": false,
"source_row_indexes": [0]
},
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 16,
"descriptor_label": "Company Track Pieces Buildable",
"target_mask_bits": 1,
"parameter_family": "company_build_limit_scalar",
"opcode": 3,
"raw_scalar_value": 12,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "scalar_assignment",
"semantic_family": "scalar_assignment",
"semantic_preview": "Set Company Track Pieces Buildable to 12",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "condition_true_company"
},
"metric": "track_pieces_total",
"comparator": "ge",
"value": 20
}
],
"decoded_actions": [
{
"kind": "set_company_track_laying_capacity",
"target": {
"kind": "condition_true_company"
},
"value": 12
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"ordinary Company Track Pieces threshold lowers condition-relative company scope at import time"
]
}
]
},
"notes": [
"real ordinary company-track threshold sample"
]
}
}

View file

@ -0,0 +1,132 @@
{
"format_version": 1,
"snapshot_id": "packed-event-ordinary-condition-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying company, selection, and aggregate territory context for ordinary-condition packed-event overlays."
},
"state": {
"calendar": {
"year": 1840,
"month_slot": 1,
"phase_slot": 2,
"tick_slot": 3
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "preserve ordinary-condition overlay context"
},
"companies": [
{
"company_id": 1,
"current_cash": 150,
"debt": 80,
"credit_rating_score": 650,
"prime_rate": 5,
"controller_kind": "human",
"track_piece_counts": {
"total": 20,
"single": 5,
"double": 8,
"transition": 1,
"electric": 3,
"non_electric": 17
}
},
{
"company_id": 2,
"current_cash": 90,
"debt": 40,
"credit_rating_score": 480,
"prime_rate": 6,
"controller_kind": "ai",
"track_piece_counts": {
"total": 8,
"single": 2,
"double": 2,
"transition": 0,
"electric": 1,
"non_electric": 7
}
},
{
"company_id": 3,
"current_cash": 200,
"debt": 10,
"credit_rating_score": 720,
"prime_rate": 4,
"controller_kind": "human",
"track_piece_counts": {
"total": 30,
"single": 10,
"double": 12,
"transition": 2,
"electric": 8,
"non_electric": 22
}
}
],
"selected_company_id": 1,
"territories": [
{
"territory_id": 7,
"track_piece_counts": {
"total": 50,
"single": 10,
"double": 20,
"transition": 5,
"electric": 15,
"non_electric": 35
}
}
],
"company_territory_track_piece_counts": [
{
"company_id": 1,
"territory_id": 7,
"track_piece_counts": {
"total": 12,
"single": 3,
"double": 5,
"transition": 1,
"electric": 4,
"non_electric": 8
}
},
{
"company_id": 2,
"territory_id": 7,
"track_piece_counts": {
"total": 7,
"single": 2,
"double": 2,
"transition": 0,
"electric": 1,
"non_electric": 6
}
},
{
"company_id": 3,
"territory_id": 7,
"track_piece_counts": {
"total": 15,
"single": 5,
"double": 6,
"transition": 2,
"electric": 5,
"non_electric": 10
}
}
],
"event_runtime_records": [],
"candidate_availability": {},
"special_conditions": {},
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
}
}

View file

@ -0,0 +1,63 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-named-territory-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving named-territory ordinary conditions stay parity-only with an explicit blocker."
},
"state_import_path": "packed-event-ordinary-named-territory-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 3,
"active_company_count": 3,
"territory_count": 1,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_named_territory_binding_count": 1,
"event_runtime_record_count": 0,
"total_event_record_service_count": 0,
"total_trigger_dispatch_count": 1,
"total_company_cash": 440
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 150
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_named_territory_binding",
"standalone_condition_rows": [
{
"candidate_name": "Appalachia",
"requires_candidate_name_binding": true
}
]
}
]
},
"event_runtime_records": []
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-named-territory-overlay",
"source": {
"description": "Overlay import combining aggregate territory context with the real named-territory threshold sample."
},
"base_snapshot_path": "packed-event-ordinary-condition-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-named-territory-save-slice.json"
}

View file

@ -0,0 +1,132 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-named-territory-save-slice",
"source": {
"description": "Tracked save-slice document with a real ordinary named-territory threshold row that stays parity-only.",
"original_save_filename": "captured-ordinary-named-territory.gms",
"original_save_sha256": "ordinary-named-territory-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks the named-territory binding blocker for ordinary condition rows"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 45,
"live_record_count": 1,
"live_entry_ids": [45],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 45,
"payload_offset": 29328,
"payload_len": 186,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 0,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2313,
"subtype": 0,
"flag_bytes": [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": "Appalachia",
"comparator": "ge",
"metric": "Territory Track Pieces",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Territory Track Pieces >= 10",
"requires_candidate_name_binding": true,
"notes": [
"condition row carries candidate-name side string"
]
}
],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 777,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 777 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "territory_numeric_threshold",
"metric": "track_pieces_total",
"comparator": "ge",
"value": 10
}
],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "selected_company"
},
"value": 777
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"candidate-name territory binding remains parity-only in this slice"
]
}
]
},
"notes": [
"real ordinary named-territory threshold parity sample"
]
}
}

View file

@ -0,0 +1,64 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-territory-track-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving aggregate territory thresholds can gate real packed-event execution when overlay territory context is present."
},
"state_import_path": "packed-event-ordinary-territory-track-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 3,
"active_company_count": 3,
"territory_count": 1,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_missing_territory_context_count": 0,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 734
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 444
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "territory_numeric_threshold",
"metric": "track_pieces_total",
"comparator": "ge",
"value": 40
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-territory-track-overlay",
"source": {
"description": "Overlay import combining aggregate territory context with the real ordinary territory-track threshold sample."
},
"base_snapshot_path": "packed-event-ordinary-condition-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-territory-track-save-slice.json"
}

View file

@ -0,0 +1,130 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-territory-track-save-slice",
"source": {
"description": "Tracked save-slice document with a real ordinary territory-track threshold row gating Company Cash.",
"original_save_filename": "captured-ordinary-territory-track.gms",
"original_save_sha256": "ordinary-territory-track-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves aggregate territory thresholds import when overlay territory context exists"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 43,
"live_record_count": 1,
"live_entry_ids": [43],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 43,
"payload_offset": 29264,
"payload_len": 176,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 0,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2313,
"subtype": 0,
"flag_bytes": [40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": null,
"comparator": "ge",
"metric": "Territory Track Pieces",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Territory Track Pieces >= 40",
"requires_candidate_name_binding": false,
"notes": []
}
],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 444,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 444 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "territory_numeric_threshold",
"metric": "track_pieces_total",
"comparator": "ge",
"value": 40
}
],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "selected_company"
},
"value": 444
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"aggregate territory thresholds execute only when overlay territory context is available"
]
}
]
},
"notes": [
"real ordinary aggregate territory-track threshold sample"
]
}
}