rrt/crates/rrt-runtime/src/runtime.rs

1102 lines
40 KiB
Rust
Raw Normal View History

use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::CalendarPoint;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeCompanyControllerKind {
#[default]
Unknown,
Human,
Ai,
}
fn runtime_company_default_active() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompany {
pub company_id: u32,
pub current_cash: i64,
pub debt: u64,
#[serde(default = "runtime_company_default_active")]
pub active: bool,
#[serde(default)]
pub available_track_laying_capacity: Option<u32>,
#[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind,
}
2026-04-14 19:37:53 -07:00
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCompanyTarget {
AllActive,
HumanCompanies,
AiCompanies,
SelectedCompany,
ConditionTrueCompany,
2026-04-14 19:37:53 -07:00
Ids { ids: Vec<u32> },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEffect {
SetWorldFlag {
key: String,
value: bool,
},
SetCompanyCash {
target: RuntimeCompanyTarget,
value: i64,
},
DeactivateCompany {
target: RuntimeCompanyTarget,
},
SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget,
value: Option<u32>,
},
2026-04-14 19:37:53 -07:00
AdjustCompanyCash {
target: RuntimeCompanyTarget,
delta: i64,
},
AdjustCompanyDebt {
target: RuntimeCompanyTarget,
delta: i64,
},
SetCandidateAvailability {
name: String,
value: u32,
},
SetSpecialCondition {
label: String,
value: u32,
},
AppendEventRecord {
record: Box<RuntimeEventRecordTemplate>,
},
ActivateEventRecord {
record_id: u32,
},
DeactivateEventRecord {
record_id: u32,
},
RemoveEventRecord {
record_id: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeEventRecordTemplate {
pub record_id: u32,
pub trigger_kind: u8,
pub active: bool,
#[serde(default)]
pub marks_collection_dirty: bool,
#[serde(default)]
pub one_shot: bool,
#[serde(default)]
pub effects: Vec<RuntimeEffect>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeEventRecord {
pub record_id: u32,
pub trigger_kind: u8,
pub active: bool,
#[serde(default)]
pub service_count: u32,
#[serde(default)]
pub marks_collection_dirty: bool,
2026-04-14 19:37:53 -07:00
#[serde(default)]
pub one_shot: bool,
#[serde(default)]
pub has_fired: bool,
#[serde(default)]
pub effects: Vec<RuntimeEffect>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventCollectionSummary {
pub source_kind: String,
pub mechanism_family: String,
pub mechanism_confidence: String,
#[serde(default)]
pub container_profile_family: Option<String>,
pub packed_state_version: u32,
pub packed_state_version_hex: String,
pub live_id_bound: u32,
pub live_record_count: usize,
pub live_entry_ids: Vec<u32>,
#[serde(default)]
pub decoded_record_count: usize,
#[serde(default)]
pub imported_runtime_record_count: usize,
#[serde(default)]
pub records: Vec<RuntimePackedEventRecordSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventRecordSummary {
pub record_index: usize,
pub live_entry_id: u32,
#[serde(default)]
pub payload_offset: Option<usize>,
#[serde(default)]
pub payload_len: Option<usize>,
pub decode_status: String,
#[serde(default)]
pub payload_family: String,
#[serde(default)]
pub trigger_kind: Option<u8>,
#[serde(default)]
pub active: Option<bool>,
#[serde(default)]
pub marks_collection_dirty: Option<bool>,
#[serde(default)]
pub one_shot: Option<bool>,
#[serde(default)]
pub compact_control: Option<RuntimePackedEventCompactControlSummary>,
#[serde(default)]
pub text_bands: Vec<RuntimePackedEventTextBandSummary>,
#[serde(default)]
pub standalone_condition_row_count: usize,
#[serde(default)]
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
#[serde(default)]
pub import_outcome: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8,
pub primary_selector_0x7f0: u32,
pub grouped_mode_0x7f4: u8,
pub one_shot_header_0x7f5: u32,
pub modifier_flag_0x7f9: u8,
pub modifier_flag_0x7fa: u8,
#[serde(default)]
pub grouped_target_scope_ordinals_0x7fb: Vec<u8>,
#[serde(default)]
pub grouped_scope_checkboxes_0x7ff: Vec<u8>,
pub summary_toggle_0x800: u8,
#[serde(default)]
pub grouped_territory_selectors_0x80f: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventTextBandSummary {
pub label: String,
pub packed_len: usize,
pub present: bool,
pub preview: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventConditionRowSummary {
pub row_index: usize,
pub raw_condition_id: i32,
pub subtype: u8,
#[serde(default)]
pub flag_bytes: Vec<u8>,
#[serde(default)]
pub candidate_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventGroupedEffectRowSummary {
pub group_index: usize,
pub row_index: usize,
pub descriptor_id: u32,
#[serde(default)]
pub descriptor_label: Option<String>,
#[serde(default)]
pub target_mask_bits: Option<u8>,
#[serde(default)]
pub parameter_family: Option<String>,
pub opcode: u8,
pub raw_scalar_value: i32,
pub value_byte_0x09: u8,
pub value_dword_0x0d: u32,
pub value_byte_0x11: u8,
pub value_byte_0x12: u8,
pub value_word_0x14: u16,
pub value_word_0x16: u16,
pub row_shape: String,
#[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
2026-04-14 19:37:53 -07:00
impl RuntimeEventRecordTemplate {
pub fn into_runtime_record(self) -> RuntimeEventRecord {
RuntimeEventRecord {
record_id: self.record_id,
trigger_kind: self.trigger_kind,
active: self.active,
service_count: 0,
marks_collection_dirty: self.marks_collection_dirty,
one_shot: self.one_shot,
has_fired: false,
effects: self.effects,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeServiceState {
#[serde(default)]
pub periodic_boundary_calls: u64,
#[serde(default)]
pub trigger_dispatch_counts: BTreeMap<u8, u64>,
#[serde(default)]
pub total_event_record_services: u64,
#[serde(default)]
pub dirty_rerun_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeSaveProfileState {
#[serde(default)]
pub profile_kind: Option<String>,
#[serde(default)]
pub profile_family: Option<String>,
#[serde(default)]
pub map_path: Option<String>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub sandbox_enabled: Option<bool>,
#[serde(default)]
pub campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub staged_profile_copy_on_restore: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeWorldRestoreState {
#[serde(default)]
pub selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub sandbox_enabled: Option<bool>,
#[serde(default)]
pub seed_tuple_written_from_raw_lane: Option<bool>,
#[serde(default)]
pub absolute_counter_requires_shell_context: Option<bool>,
#[serde(default)]
pub absolute_counter_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_slot: Option<u8>,
#[serde(default)]
pub disable_cargo_economy_special_condition_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
#[serde(default)]
pub disable_cargo_economy_special_condition_enabled: Option<bool>,
#[serde(default)]
pub use_bio_accelerator_cars_enabled: Option<bool>,
#[serde(default)]
pub use_wartime_cargos_enabled: Option<bool>,
#[serde(default)]
pub disable_train_crashes_enabled: Option<bool>,
#[serde(default)]
pub disable_train_crashes_and_breakdowns_enabled: Option<bool>,
#[serde(default)]
pub ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)]
pub absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub absolute_counter_adjustment_context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeState {
pub calendar: CalendarPoint,
#[serde(default)]
pub world_flags: BTreeMap<String, bool>,
#[serde(default)]
pub save_profile: RuntimeSaveProfileState,
#[serde(default)]
pub world_restore: RuntimeWorldRestoreState,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub companies: Vec<RuntimeCompany>,
#[serde(default)]
pub selected_company_id: Option<u32>,
#[serde(default)]
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
#[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>,
#[serde(default)]
pub candidate_availability: BTreeMap<String, u32>,
#[serde(default)]
pub special_conditions: BTreeMap<String, u32>,
#[serde(default)]
pub service_state: RuntimeServiceState,
}
impl RuntimeState {
pub fn validate(&self) -> Result<(), String> {
self.calendar.validate()?;
let mut seen_company_ids = BTreeSet::new();
let mut active_company_ids = BTreeSet::new();
for company in &self.companies {
if !seen_company_ids.insert(company.company_id) {
return Err(format!("duplicate company_id {}", company.company_id));
}
if company.active {
active_company_ids.insert(company.company_id);
}
}
if let Some(selected_company_id) = self.selected_company_id {
if !seen_company_ids.contains(&selected_company_id) {
return Err(format!(
"selected_company_id {} does not reference a live company",
selected_company_id
));
}
if !active_company_ids.contains(&selected_company_id) {
return Err(format!(
"selected_company_id {} must reference an active company",
selected_company_id
));
}
}
let mut seen_record_ids = BTreeSet::new();
for record in &self.event_runtime_records {
if !seen_record_ids.insert(record.record_id) {
return Err(format!("duplicate record_id {}", record.record_id));
}
2026-04-14 19:37:53 -07:00
for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, &seen_company_ids).map_err(|err| {
format!(
"event_runtime_records[record_id={}].effects[{effect_index}] {err}",
record.record_id
)
})?;
}
}
if let Some(summary) = &self.packed_event_collection {
if summary.source_kind.trim().is_empty() {
return Err("packed_event_collection.source_kind must not be empty".to_string());
}
if summary.mechanism_family.trim().is_empty() {
return Err(
"packed_event_collection.mechanism_family must not be empty".to_string()
);
}
if summary.mechanism_confidence.trim().is_empty() {
return Err(
"packed_event_collection.mechanism_confidence must not be empty".to_string(),
);
}
if summary
.container_profile_family
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(
"packed_event_collection.container_profile_family must not be empty"
.to_string(),
);
}
if summary.packed_state_version_hex.trim().is_empty() {
return Err(
"packed_event_collection.packed_state_version_hex must not be empty"
.to_string(),
);
}
if summary.live_record_count != summary.live_entry_ids.len() {
return Err(
"packed_event_collection.live_record_count must match live_entry_ids length"
.to_string(),
);
}
if summary.live_record_count != summary.records.len() {
return Err(
"packed_event_collection.live_record_count must match records length"
.to_string(),
);
}
let decoded_record_count = summary
.records
.iter()
.filter(|record| record.decode_status != "unsupported_framing")
.count();
if summary.decoded_record_count != decoded_record_count {
return Err(
"packed_event_collection.decoded_record_count must match decoded records"
.to_string(),
);
}
let importable_or_imported_count = summary
.records
.iter()
.filter(|record| {
record.executable_import_ready
|| record.import_outcome.as_deref() == Some("imported")
})
.count();
if summary.imported_runtime_record_count > importable_or_imported_count {
return Err(
"packed_event_collection.imported_runtime_record_count must not exceed importable or imported records"
.to_string(),
);
}
let mut previous_id = None;
for (record_index, entry_id) in summary.live_entry_ids.iter().enumerate() {
if *entry_id == 0 {
return Err(
"packed_event_collection.live_entry_ids must not contain id 0".to_string(),
);
}
if *entry_id > summary.live_id_bound {
return Err(format!(
"packed_event_collection.live_entry_id {} exceeds live_id_bound {}",
entry_id, summary.live_id_bound
));
}
if previous_id.is_some_and(|prior| prior >= *entry_id) {
return Err(
"packed_event_collection.live_entry_ids must be strictly ascending"
.to_string(),
);
}
previous_id = Some(*entry_id);
let record = &summary.records[record_index];
if record.live_entry_id != *entry_id {
return Err(format!(
"packed_event_collection.records[{record_index}].live_entry_id must match live_entry_ids"
));
}
if record.record_index != record_index {
return Err(format!(
"packed_event_collection.records[{record_index}].record_index must match position"
));
}
if record.decode_status.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].decode_status must not be empty"
));
}
if record.payload_family.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].payload_family must not be empty"
));
}
if record
.import_outcome
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].import_outcome must not be empty"
));
}
if record.grouped_effect_row_counts.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_row_counts must contain exactly 4 entries"
));
}
if record.payload_family == "real_packed_v1"
&& record.standalone_condition_rows.len()
!= record.standalone_condition_row_count
{
return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count"
));
}
if record.payload_family == "real_packed_v1"
&& record.grouped_effect_rows.len()
!= record.grouped_effect_row_counts.iter().sum::<usize>()
{
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows must match grouped_effect_row_counts"
));
}
for band in &record.text_bands {
if band.label.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].text_bands contains an empty label"
));
}
}
if let Some(control) = &record.compact_control {
if control.grouped_target_scope_ordinals_0x7fb.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_target_scope_ordinals_0x7fb must contain exactly 4 entries"
));
}
if control.grouped_scope_checkboxes_0x7ff.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_scope_checkboxes_0x7ff must contain exactly 4 entries"
));
}
if control.grouped_territory_selectors_0x80f.len() != 4 {
return Err(format!(
"packed_event_collection.records[{record_index}].compact_control.grouped_territory_selectors_0x80f must contain exactly 4 entries"
));
}
}
for row in &record.standalone_condition_rows {
if row
.candidate_name
.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 candidate_name"
));
}
}
for row in &record.grouped_effect_rows {
if row.row_shape.trim().is_empty() {
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty row_shape"
));
}
if row
.locomotive_name
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"packed_event_collection.records[{record_index}].grouped_effect_rows contains an empty locomotive_name"
));
}
}
}
}
for key in self.world_flags.keys() {
if key.trim().is_empty() {
return Err("world_flags contains an empty key".to_string());
}
}
for (label, value) in [
(
"save_profile.profile_kind",
self.save_profile.profile_kind.as_deref(),
),
(
"save_profile.profile_family",
self.save_profile.profile_family.as_deref(),
),
(
"save_profile.map_path",
self.save_profile.map_path.as_deref(),
),
(
"save_profile.display_name",
self.save_profile.display_name.as_deref(),
),
] {
if value.is_some_and(|text| text.trim().is_empty()) {
return Err(format!("{label} must not be empty"));
}
}
if self.world_restore.selected_year_profile_lane.is_none()
&& (self.world_restore.campaign_scenario_enabled.is_some()
|| self.world_restore.sandbox_enabled.is_some())
{
return Err(
"world_restore.selected_year_profile_lane must be present when world restore flags are populated"
.to_string(),
);
}
if self
.world_restore
.absolute_counter_restore_kind
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err(
"world_restore.absolute_counter_restore_kind must not be empty".to_string(),
);
}
if self
.world_restore
.absolute_counter_adjustment_context
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err(
"world_restore.absolute_counter_adjustment_context must not be empty".to_string(),
);
}
for (key, value) in &self.metadata {
if key.trim().is_empty() {
return Err("metadata contains an empty key".to_string());
}
if value.trim().is_empty() {
return Err(format!("metadata[{key}] must not be empty"));
}
}
for key in self.candidate_availability.keys() {
if key.trim().is_empty() {
return Err("candidate_availability contains an empty key".to_string());
}
}
for key in self.special_conditions.keys() {
if key.trim().is_empty() {
return Err("special_conditions contains an empty key".to_string());
}
}
Ok(())
}
}
2026-04-14 19:37:53 -07:00
fn validate_runtime_effect(
effect: &RuntimeEffect,
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match effect {
RuntimeEffect::SetWorldFlag { key, .. } => {
if key.trim().is_empty() {
return Err("key must not be empty".to_string());
}
}
RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::DeactivateCompany { target }
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
2026-04-14 19:37:53 -07:00
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
validate_company_target(target, valid_company_ids)?;
}
RuntimeEffect::SetCandidateAvailability { name, .. } => {
if name.trim().is_empty() {
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetSpecialCondition { label, .. } => {
if label.trim().is_empty() {
return Err("label must not be empty".to_string());
}
}
RuntimeEffect::AppendEventRecord { record } => {
validate_event_record_template(record, valid_company_ids)?;
}
RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => {}
}
Ok(())
}
fn validate_event_record_template(
record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, valid_company_ids).map_err(|err| {
format!(
"template record_id={}.effects[{effect_index}] {err}",
record.record_id
)
})?;
}
Ok(())
}
fn validate_company_target(
target: &RuntimeCompanyTarget,
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match target {
RuntimeCompanyTarget::AllActive
| RuntimeCompanyTarget::HumanCompanies
| RuntimeCompanyTarget::AiCompanies
| RuntimeCompanyTarget::SelectedCompany
| RuntimeCompanyTarget::ConditionTrueCompany => Ok(()),
2026-04-14 19:37:53 -07:00
RuntimeCompanyTarget::Ids { ids } => {
if ids.is_empty() {
return Err("target ids must not be empty".to_string());
}
for company_id in ids {
if !valid_company_ids.contains(company_id) {
return Err(format!("target references unknown company_id {company_id}"));
}
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_duplicate_company_ids() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![
RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
},
RuntimeCompany {
company_id: 1,
current_cash: 200,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
},
],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_partial_world_restore_without_year_lane() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState {
selected_year_profile_lane: None,
campaign_scenario_enabled: Some(false),
sandbox_enabled: Some(true),
seed_tuple_written_from_raw_lane: Some(true),
absolute_counter_requires_shell_context: Some(true),
absolute_counter_reconstructible_from_save: Some(false),
disable_cargo_economy_special_condition_slot: Some(30),
disable_cargo_economy_special_condition_reconstructible_from_save: Some(true),
disable_cargo_economy_special_condition_write_side_grounded: Some(true),
disable_cargo_economy_special_condition_enabled: Some(false),
use_bio_accelerator_cars_enabled: Some(false),
use_wartime_cargos_enabled: Some(false),
disable_train_crashes_enabled: Some(false),
disable_train_crashes_and_breakdowns_enabled: Some(false),
ai_ignore_territories_at_startup_enabled: Some(false),
absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(),
),
absolute_counter_adjustment_context: Some(
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30".to_string(),
),
},
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
2026-04-14 19:37:53 -07:00
#[test]
fn rejects_event_effect_targeting_unknown_company() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
2026-04-14 19:37:53 -07:00
}],
selected_company_id: None,
packed_event_collection: None,
2026-04-14 19:37:53 -07:00
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
trigger_kind: 1,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50,
}],
}],
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_template_effect_targeting_unknown_company() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
2026-04-14 19:37:53 -07:00
}],
selected_company_id: None,
packed_event_collection: None,
2026-04-14 19:37:53 -07:00
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
trigger_kind: 1,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AppendEventRecord {
record: Box::new(RuntimeEventRecordTemplate {
record_id: 8,
trigger_kind: 0x0a,
active: true,
marks_collection_dirty: false,
one_shot: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
delta: 50,
}],
}),
}],
}],
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_invalid_packed_event_collection_summary() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 4,
live_record_count: 2,
live_entry_ids: vec![3, 3],
decoded_record_count: 0,
imported_runtime_record_count: 0,
records: vec![
RuntimePackedEventRecordSummary {
record_index: 0,
live_entry_id: 3,
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
compact_control: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
notes: vec!["test".to_string()],
},
RuntimePackedEventRecordSummary {
record_index: 1,
live_entry_id: 3,
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
compact_control: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
notes: vec!["test".to_string()],
},
],
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_selected_company_id_that_does_not_exist() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(2),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_selected_company_id_that_is_inactive() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
active: false,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(1),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
}