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)] pub credit_rating_score: Option, #[serde(default)] pub prime_rate: Option, #[serde(default = "runtime_company_default_active")] pub active: bool, #[serde(default)] pub available_track_laying_capacity: Option, #[serde(default)] pub controller_kind: RuntimeCompanyControllerKind, #[serde(default)] pub track_piece_counts: RuntimeTrackPieceCounts, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] pub total: u32, #[serde(default)] pub single: u32, #[serde(default)] pub double: u32, #[serde(default)] pub transition: u32, #[serde(default)] pub electric: u32, #[serde(default)] pub non_electric: u32, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeTerritory { pub territory_id: u32, #[serde(default)] pub name: Option, #[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)] pub struct RuntimeCompanyTerritoryAccess { pub company_id: u32, pub territory_id: u32, } fn runtime_player_default_active() -> bool { true } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimePlayer { pub player_id: u32, pub current_cash: i64, #[serde(default = "runtime_player_default_active")] pub active: bool, #[serde(default)] pub controller_kind: RuntimeCompanyControllerKind, } fn runtime_train_default_active() -> bool { true } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeTrain { pub train_id: u32, pub owner_company_id: u32, #[serde(default)] pub territory_id: Option, #[serde(default)] pub locomotive_name: Option, #[serde(default = "runtime_train_default_active")] pub active: bool, #[serde(default)] pub retired: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeLocomotiveCatalogEntry { pub locomotive_id: u32, pub name: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeCompanyTarget { AllActive, HumanCompanies, AiCompanies, SelectedCompany, ConditionTrueCompany, Ids { ids: Vec }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimePlayerTarget { AllActive, HumanPlayers, AiPlayers, SelectedPlayer, ConditionTruePlayer, Ids { ids: Vec }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeTerritoryTarget { AllTerritories, Ids { ids: Vec }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum RuntimeCompanyConditionTestScope { #[default] Disabled, AllCompanies, SelectedCompanyOnly, AiCompaniesOnly, HumanCompaniesOnly, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum RuntimePlayerConditionTestScope { #[default] Disabled, AllPlayers, SelectedPlayerOnly, AiPlayersOnly, 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 { target: RuntimeTerritoryTarget, metric: RuntimeTerritoryMetric, comparator: RuntimeConditionComparator, value: i64, }, CompanyTerritoryNumericThreshold { target: RuntimeCompanyTarget, territory: RuntimeTerritoryTarget, metric: RuntimeTrackMetric, comparator: RuntimeConditionComparator, value: i64, }, SpecialConditionThreshold { label: String, comparator: RuntimeConditionComparator, value: i64, }, CandidateAvailabilityThreshold { name: String, comparator: RuntimeConditionComparator, value: i64, }, NamedLocomotiveAvailabilityThreshold { name: String, comparator: RuntimeConditionComparator, value: i64, }, NamedLocomotiveCostThreshold { name: String, comparator: RuntimeConditionComparator, value: i64, }, CargoProductionTotalThreshold { comparator: RuntimeConditionComparator, value: i64, }, LimitedTrackBuildingAmountThreshold { comparator: RuntimeConditionComparator, value: i64, }, TerritoryAccessCostThreshold { comparator: RuntimeConditionComparator, value: i64, }, EconomicStatusCodeThreshold { comparator: RuntimeConditionComparator, value: i64, }, WorldFlagEquals { key: String, value: bool, }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeEffect { SetWorldFlag { key: String, value: bool, }, SetLimitedTrackBuildingAmount { value: i32, }, SetEconomicStatusCode { value: i32, }, SetCompanyCash { target: RuntimeCompanyTarget, value: i64, }, SetPlayerCash { target: RuntimePlayerTarget, value: i64, }, DeactivatePlayer { target: RuntimePlayerTarget, }, SetCompanyTerritoryAccess { target: RuntimeCompanyTarget, territory: RuntimeTerritoryTarget, value: bool, }, ConfiscateCompanyAssets { target: RuntimeCompanyTarget, }, DeactivateCompany { target: RuntimeCompanyTarget, }, SetCompanyTrackLayingCapacity { target: RuntimeCompanyTarget, value: Option, }, RetireTrains { #[serde(default)] company_target: Option, #[serde(default)] territory_target: Option, #[serde(default)] locomotive_name: Option, }, AdjustCompanyCash { target: RuntimeCompanyTarget, delta: i64, }, AdjustCompanyDebt { target: RuntimeCompanyTarget, delta: i64, }, SetCandidateAvailability { name: String, value: u32, }, SetNamedLocomotiveAvailability { name: String, value: bool, }, SetNamedLocomotiveAvailabilityValue { name: String, value: u32, }, SetNamedLocomotiveCost { name: String, value: u32, }, SetCargoProductionSlot { slot: u32, value: u32, }, SetTerritoryAccessCost { value: u32, }, SetSpecialCondition { label: String, value: u32, }, AppendEventRecord { record: Box, }, 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 conditions: Vec, #[serde(default)] pub effects: Vec, } #[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, #[serde(default)] pub one_shot: bool, #[serde(default)] pub has_fired: bool, #[serde(default)] pub conditions: Vec, #[serde(default)] pub effects: Vec, } #[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, 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, #[serde(default)] pub decoded_record_count: usize, #[serde(default)] pub imported_runtime_record_count: usize, #[serde(default)] pub records: Vec, } #[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, #[serde(default)] pub payload_len: Option, pub decode_status: String, #[serde(default)] pub payload_family: String, #[serde(default)] pub trigger_kind: Option, #[serde(default)] pub active: Option, #[serde(default)] pub marks_collection_dirty: Option, #[serde(default)] pub one_shot: Option, #[serde(default)] pub compact_control: Option, #[serde(default)] pub text_bands: Vec, #[serde(default)] pub standalone_condition_row_count: usize, #[serde(default)] pub standalone_condition_rows: Vec, #[serde(default)] pub negative_sentinel_scope: Option, #[serde(default)] pub grouped_effect_row_counts: Vec, #[serde(default)] pub grouped_effect_rows: Vec, #[serde(default)] pub grouped_company_targets: Vec>, #[serde(default)] pub decoded_conditions: Vec, #[serde(default)] pub decoded_actions: Vec, #[serde(default)] pub executable_import_ready: bool, #[serde(default)] pub import_outcome: Option, #[serde(default)] pub notes: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimePackedEventNegativeSentinelScopeSummary { pub company_test_scope: RuntimeCompanyConditionTestScope, pub player_test_scope: RuntimePlayerConditionTestScope, pub territory_scope_selector_is_0x63: bool, #[serde(default)] pub source_row_indexes: Vec, } #[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, #[serde(default)] pub grouped_scope_checkboxes_0x7ff: Vec, pub summary_toggle_0x800: u8, #[serde(default)] pub grouped_territory_selectors_0x80f: Vec, } #[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, #[serde(default)] pub candidate_name: Option, #[serde(default)] pub comparator: Option, #[serde(default)] pub metric: Option, #[serde(default)] pub semantic_family: Option, #[serde(default)] pub semantic_preview: Option, #[serde(default)] pub requires_candidate_name_binding: bool, #[serde(default)] pub notes: Vec, } #[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, #[serde(default)] pub target_mask_bits: Option, #[serde(default)] pub parameter_family: Option, 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, #[serde(default)] pub semantic_preview: Option, #[serde(default)] pub recovered_locomotive_id: Option, #[serde(default)] pub locomotive_name: Option, #[serde(default)] pub notes: Vec, } 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, conditions: self.conditions, 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, #[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, #[serde(default)] pub profile_family: Option, #[serde(default)] pub map_path: Option, #[serde(default)] pub display_name: Option, #[serde(default)] pub selected_year_profile_lane: Option, #[serde(default)] pub sandbox_enabled: Option, #[serde(default)] pub campaign_scenario_enabled: Option, #[serde(default)] pub staged_profile_copy_on_restore: Option, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct RuntimeWorldRestoreState { #[serde(default)] pub selected_year_profile_lane: Option, #[serde(default)] pub campaign_scenario_enabled: Option, #[serde(default)] pub sandbox_enabled: Option, #[serde(default)] pub seed_tuple_written_from_raw_lane: Option, #[serde(default)] pub absolute_counter_requires_shell_context: Option, #[serde(default)] pub absolute_counter_reconstructible_from_save: Option, #[serde(default)] pub disable_cargo_economy_special_condition_slot: Option, #[serde(default)] pub disable_cargo_economy_special_condition_reconstructible_from_save: Option, #[serde(default)] pub disable_cargo_economy_special_condition_write_side_grounded: Option, #[serde(default)] pub disable_cargo_economy_special_condition_enabled: Option, #[serde(default)] pub use_bio_accelerator_cars_enabled: Option, #[serde(default)] pub use_wartime_cargos_enabled: Option, #[serde(default)] pub disable_train_crashes_enabled: Option, #[serde(default)] pub disable_train_crashes_and_breakdowns_enabled: Option, #[serde(default)] pub ai_ignore_territories_at_startup_enabled: Option, #[serde(default)] pub limited_track_building_amount: Option, #[serde(default)] pub economic_status_code: Option, #[serde(default)] pub territory_access_cost: Option, #[serde(default)] pub absolute_counter_restore_kind: Option, #[serde(default)] pub absolute_counter_adjustment_context: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeState { pub calendar: CalendarPoint, #[serde(default)] pub world_flags: BTreeMap, #[serde(default)] pub save_profile: RuntimeSaveProfileState, #[serde(default)] pub world_restore: RuntimeWorldRestoreState, #[serde(default)] pub metadata: BTreeMap, #[serde(default)] pub companies: Vec, #[serde(default)] pub selected_company_id: Option, #[serde(default)] pub players: Vec, #[serde(default)] pub selected_player_id: Option, #[serde(default)] pub trains: Vec, #[serde(default)] pub locomotive_catalog: Vec, #[serde(default)] pub territories: Vec, #[serde(default)] pub company_territory_track_piece_counts: Vec, #[serde(default)] pub company_territory_access: Vec, #[serde(default)] pub packed_event_collection: Option, #[serde(default)] pub event_runtime_records: Vec, #[serde(default)] pub candidate_availability: BTreeMap, #[serde(default)] pub named_locomotive_availability: BTreeMap, #[serde(default)] pub named_locomotive_cost: BTreeMap, #[serde(default)] pub cargo_production_overrides: BTreeMap, #[serde(default)] pub special_conditions: BTreeMap, #[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_player_ids = BTreeSet::new(); let mut active_player_ids = BTreeSet::new(); for player in &self.players { if !seen_player_ids.insert(player.player_id) { return Err(format!("duplicate player_id {}", player.player_id)); } if player.active { active_player_ids.insert(player.player_id); } } if let Some(selected_player_id) = self.selected_player_id { if !seen_player_ids.contains(&selected_player_id) { return Err(format!( "selected_player_id {} does not reference a live player", selected_player_id )); } if !active_player_ids.contains(&selected_player_id) { return Err(format!( "selected_player_id {} must reference an active player", selected_player_id )); } } let mut seen_territory_ids = BTreeSet::new(); let mut seen_territory_names = BTreeSet::new(); for territory in &self.territories { if !seen_territory_ids.insert(territory.territory_id) { return Err(format!("duplicate territory_id {}", territory.territory_id)); } if let Some(name) = territory.name.as_deref() { if name.trim().is_empty() { return Err(format!( "territory_id {} has an empty name", territory.territory_id )); } if !seen_territory_names.insert(name.to_string()) { return Err(format!("duplicate territory name {name:?}")); } } } let mut seen_train_ids = BTreeSet::new(); for train in &self.trains { if !seen_train_ids.insert(train.train_id) { return Err(format!("duplicate train_id {}", train.train_id)); } if !seen_company_ids.contains(&train.owner_company_id) { return Err(format!( "train_id {} references unknown owner_company_id {}", train.train_id, train.owner_company_id )); } if let Some(territory_id) = train.territory_id { if !seen_territory_ids.contains(&territory_id) { return Err(format!( "train_id {} references unknown territory_id {}", train.train_id, territory_id )); } } if train.retired && train.active { return Err(format!( "train_id {} cannot be active and retired at the same time", train.train_id )); } if train .locomotive_name .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err(format!( "train_id {} has an empty locomotive_name", train.train_id )); } } let mut seen_locomotive_ids = BTreeSet::new(); let mut seen_locomotive_names = BTreeSet::new(); for entry in &self.locomotive_catalog { if !seen_locomotive_ids.insert(entry.locomotive_id) { return Err(format!( "duplicate locomotive_catalog.locomotive_id {}", entry.locomotive_id )); } if entry.name.trim().is_empty() { return Err(format!( "locomotive_catalog entry {} has an empty name", entry.locomotive_id )); } if !seen_locomotive_names.insert(entry.name.clone()) { return Err(format!( "duplicate locomotive_catalog.name {:?}", entry.name )); } } 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_company_territory_access = BTreeSet::new(); for entry in &self.company_territory_access { if !seen_company_ids.contains(&entry.company_id) { return Err(format!( "company_territory_access references unknown company_id {}", entry.company_id )); } if !seen_territory_ids.contains(&entry.territory_id) { return Err(format!( "company_territory_access references unknown territory_id {}", entry.territory_id )); } if !seen_company_territory_access.insert((entry.company_id, entry.territory_id)) { return Err(format!( "duplicate company_territory_access pair ({}, {})", entry.company_id, entry.territory_id )); } } let mut seen_record_ids = BTreeSet::new(); for record in &self.event_runtime_records { if !seen_record_ids.insert(record.record_id) { return Err(format!("duplicate record_id {}", record.record_id)); } for (condition_index, condition) in record.conditions.iter().enumerate() { validate_runtime_condition(condition, &seen_company_ids, &seen_territory_ids) .map_err(|err| { format!( "event_runtime_records[record_id={}].conditions[{condition_index}] {err}", record.record_id ) })?; } for (effect_index, effect) in record.effects.iter().enumerate() { validate_runtime_effect( effect, &seen_company_ids, &seen_player_ids, &seen_territory_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::() { 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" )); } if row .comparator .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err(format!( "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty comparator" )); } if row .metric .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err(format!( "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty metric" )); } if row .semantic_family .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err(format!( "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_family" )); } if row .semantic_preview .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err(format!( "packed_event_collection.records[{record_index}].standalone_condition_rows contains an empty semantic_preview" )); } } for row in &record.grouped_effect_rows { if row.row_shape.trim().is_empty() { 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.named_locomotive_availability.keys() { if key.trim().is_empty() { return Err("named_locomotive_availability contains an empty key".to_string()); } } for key in self.named_locomotive_cost.keys() { if key.trim().is_empty() { return Err("named_locomotive_cost contains an empty key".to_string()); } } for slot in self.cargo_production_overrides.keys() { if !(1..=11).contains(slot) { return Err(format!( "cargo_production_overrides contains out-of-range slot {}", slot )); } } for key in self.special_conditions.keys() { if key.trim().is_empty() { return Err("special_conditions contains an empty key".to_string()); } } Ok(()) } } fn validate_runtime_effect( effect: &RuntimeEffect, valid_company_ids: &BTreeSet, valid_player_ids: &BTreeSet, valid_territory_ids: &BTreeSet, ) -> Result<(), String> { match effect { RuntimeEffect::SetWorldFlag { key, .. } => { if key.trim().is_empty() { return Err("key must not be empty".to_string()); } } RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } => {} RuntimeEffect::SetCompanyCash { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyDebt { target, .. } => { validate_company_target(target, valid_company_ids)?; } RuntimeEffect::SetCompanyTerritoryAccess { target, territory, .. } => { validate_company_target(target, valid_company_ids)?; validate_territory_target(territory, valid_territory_ids)?; } RuntimeEffect::SetPlayerCash { target, .. } | RuntimeEffect::DeactivatePlayer { target } => { validate_player_target(target, valid_player_ids)?; } RuntimeEffect::RetireTrains { company_target, territory_target, locomotive_name, } => { if let Some(company_target) = company_target { validate_company_target(company_target, valid_company_ids)?; } if let Some(territory_target) = territory_target { validate_territory_target(territory_target, valid_territory_ids)?; } if company_target.is_none() && territory_target.is_none() && locomotive_name.is_none() { return Err( "retire_trains requires at least one company_target, territory_target, or locomotive_name filter" .to_string(), ); } if locomotive_name .as_deref() .is_some_and(|value| value.trim().is_empty()) { return Err("locomotive_name must not be empty".to_string()); } } RuntimeEffect::SetCandidateAvailability { name, .. } => { if name.trim().is_empty() { return Err("name must not be empty".to_string()); } } RuntimeEffect::SetNamedLocomotiveAvailability { name, .. } => { if name.trim().is_empty() { return Err("name must not be empty".to_string()); } } RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, .. } => { if name.trim().is_empty() { return Err("name must not be empty".to_string()); } } RuntimeEffect::SetNamedLocomotiveCost { name, .. } => { if name.trim().is_empty() { return Err("name must not be empty".to_string()); } } RuntimeEffect::SetCargoProductionSlot { slot, .. } => { if !(1..=11).contains(slot) { return Err("slot must be in 1..=11".to_string()); } } RuntimeEffect::SetTerritoryAccessCost { .. } => {} 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, valid_player_ids, valid_territory_ids, )?; } RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::RemoveEventRecord { .. } => {} } Ok(()) } fn validate_event_record_template( record: &RuntimeEventRecordTemplate, valid_company_ids: &BTreeSet, valid_player_ids: &BTreeSet, valid_territory_ids: &BTreeSet, ) -> Result<(), String> { for (condition_index, condition) in record.conditions.iter().enumerate() { validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err( |err| { format!( "template record_id={}.conditions[{condition_index}] {err}", record.record_id ) }, )?; } for (effect_index, effect) in record.effects.iter().enumerate() { validate_runtime_effect( effect, valid_company_ids, valid_player_ids, valid_territory_ids, ) .map_err(|err| { format!( "template record_id={}.effects[{effect_index}] {err}", record.record_id ) })?; } Ok(()) } fn validate_runtime_condition( condition: &RuntimeCondition, valid_company_ids: &BTreeSet, valid_territory_ids: &BTreeSet, ) -> Result<(), String> { match condition { RuntimeCondition::CompanyNumericThreshold { target, .. } => { validate_company_target(target, valid_company_ids) } RuntimeCondition::TerritoryNumericThreshold { target, .. } => { validate_territory_target(target, valid_territory_ids) } RuntimeCondition::CompanyTerritoryNumericThreshold { target, territory, .. } => { validate_company_target(target, valid_company_ids)?; validate_territory_target(territory, valid_territory_ids) } RuntimeCondition::SpecialConditionThreshold { label, .. } => { if label.trim().is_empty() { Err("label must not be empty".to_string()) } else { Ok(()) } } RuntimeCondition::CandidateAvailabilityThreshold { name, .. } => { if name.trim().is_empty() { Err("name must not be empty".to_string()) } else { Ok(()) } } RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name, .. } | RuntimeCondition::NamedLocomotiveCostThreshold { name, .. } => { if name.trim().is_empty() { Err("name must not be empty".to_string()) } else { Ok(()) } } RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()), RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()), RuntimeCondition::WorldFlagEquals { key, .. } => { if key.trim().is_empty() { Err("key must not be empty".to_string()) } else { Ok(()) } } } } fn validate_company_target( target: &RuntimeCompanyTarget, valid_company_ids: &BTreeSet, ) -> Result<(), String> { match target { RuntimeCompanyTarget::AllActive | RuntimeCompanyTarget::HumanCompanies | RuntimeCompanyTarget::AiCompanies | RuntimeCompanyTarget::SelectedCompany | RuntimeCompanyTarget::ConditionTrueCompany => Ok(()), 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(()) } } } fn validate_player_target( target: &RuntimePlayerTarget, valid_player_ids: &BTreeSet, ) -> Result<(), String> { match target { RuntimePlayerTarget::AllActive | RuntimePlayerTarget::HumanPlayers | RuntimePlayerTarget::AiPlayers | RuntimePlayerTarget::SelectedPlayer | RuntimePlayerTarget::ConditionTruePlayer => Ok(()), RuntimePlayerTarget::Ids { ids } => { if ids.is_empty() { return Err("target ids must not be empty".to_string()); } for player_id in ids { if !valid_player_ids.contains(player_id) { return Err(format!("target references unknown player_id {player_id}")); } } Ok(()) } } } fn validate_territory_target( target: &RuntimeTerritoryTarget, valid_territory_ids: &BTreeSet, ) -> Result<(), String> { match target { RuntimeTerritoryTarget::AllTerritories => Ok(()), RuntimeTerritoryTarget::Ids { ids } => { if ids.is_empty() { return Err("territory target ids must not be empty".to_string()); } for territory_id in ids { if !valid_territory_ids.contains(territory_id) { return Err(format!( "territory target references unknown territory_id {territory_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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, RuntimeCompany { company_id: 1, current_cash: 200, debt: 0, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, ], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: 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), limited_track_building_amount: None, economic_status_code: None, territory_access_cost: None, 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, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, 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, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyCash { target: RuntimeCompanyTarget::Ids { ids: vec![2] }, delta: 50, }], }], candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, 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, conditions: Vec::new(), effects: vec![RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 8, trigger_kind: 0x0a, active: true, marks_collection_dirty: false, one_shot: false, conditions: Vec::new(), effects: vec![RuntimeEffect::AdjustCompanyCash { target: RuntimeCompanyTarget::Ids { ids: vec![2] }, delta: 50, }], }), }], }], candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: 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, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), 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(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), decoded_conditions: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, import_outcome: None, 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(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), decoded_conditions: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, import_outcome: None, notes: vec!["test".to_string()], }, ], }), event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: Some(2), players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: false, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: Some(1), players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_duplicate_train_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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: vec![ RuntimeTrain { train_id: 7, owner_company_id: 1, territory_id: None, locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }, RuntimeTrain { train_id: 7, owner_company_id: 1, territory_id: None, locomotive_name: Some("Orca".to_string()), active: true, retired: false, }, ], locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_train_with_unknown_owner_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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: vec![RuntimeTrain { train_id: 7, owner_company_id: 2, territory_id: None, locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }], locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_train_with_unknown_territory() { 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: vec![RuntimeTrain { train_id: 7, owner_company_id: 1, territory_id: Some(9), locomotive_name: Some("Mikado".to_string()), active: true, retired: false, }], locomotive_catalog: Vec::new(), territories: vec![RuntimeTerritory { territory_id: 1, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_train_marked_active_and_retired() { 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: vec![RuntimeTrain { train_id: 7, owner_company_id: 1, territory_id: None, locomotive_name: Some("Mikado".to_string()), active: true, retired: true, }], locomotive_catalog: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), company_territory_access: Vec::new(), packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_duplicate_company_territory_access_pairs() { 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: vec![RuntimeTerritory { territory_id: 7, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], company_territory_track_piece_counts: Vec::new(), company_territory_access: vec![ RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 7, }, RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 7, }, ], packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_company_territory_access_with_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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: vec![RuntimeTerritory { territory_id: 7, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], company_territory_track_piece_counts: Vec::new(), company_territory_access: vec![RuntimeCompanyTerritoryAccess { company_id: 2, territory_id: 7, }], packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } #[test] fn rejects_company_territory_access_with_unknown_territory() { 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, credit_rating_score: None, prime_rate: None, track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, players: Vec::new(), selected_player_id: None, trains: Vec::new(), locomotive_catalog: Vec::new(), territories: vec![RuntimeTerritory { territory_id: 7, name: Some("Appalachia".to_string()), track_piece_counts: RuntimeTrackPieceCounts::default(), }], company_territory_track_piece_counts: Vec::new(), company_territory_access: vec![RuntimeCompanyTerritoryAccess { company_id: 1, territory_id: 8, }], packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; assert!(state.validate().is_err()); } }