2026-04-10 01:22:47 -07:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use crate::CalendarPoint;
|
|
|
|
|
|
2026-04-15 09:13:51 -07:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum RuntimeCompanyControllerKind {
|
|
|
|
|
#[default]
|
|
|
|
|
Unknown,
|
|
|
|
|
Human,
|
|
|
|
|
Ai,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 12:11:29 -07:00
|
|
|
fn runtime_company_default_active() -> bool {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeCompany {
|
|
|
|
|
pub company_id: u32,
|
|
|
|
|
pub current_cash: i64,
|
|
|
|
|
pub debt: u64,
|
2026-04-15 18:27:04 -07:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub credit_rating_score: Option<i64>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub prime_rate: Option<i64>,
|
2026-04-15 12:11:29 -07:00
|
|
|
#[serde(default = "runtime_company_default_active")]
|
|
|
|
|
pub active: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub available_track_laying_capacity: Option<u32>,
|
2026-04-15 09:13:51 -07:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub controller_kind: RuntimeCompanyControllerKind,
|
2026-04-15 18:27:04 -07:00
|
|
|
#[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)]
|
2026-04-15 19:15:47 -07:00
|
|
|
pub name: Option<String>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 18:27:04 -07:00
|
|
|
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,
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 20:53:35 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeCompanyTerritoryAccess {
|
|
|
|
|
pub company_id: u32,
|
|
|
|
|
pub territory_id: u32,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:15:47 -07:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 20:20:25 -07:00
|
|
|
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<u32>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub locomotive_name: Option<String>,
|
|
|
|
|
#[serde(default = "runtime_train_default_active")]
|
|
|
|
|
pub active: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub retired: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 10:50:13 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeLocomotiveCatalogEntry {
|
|
|
|
|
pub locomotive_id: u32,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
2026-04-15 09:13:51 -07:00
|
|
|
HumanCompanies,
|
|
|
|
|
AiCompanies,
|
|
|
|
|
SelectedCompany,
|
|
|
|
|
ConditionTrueCompany,
|
2026-04-14 19:37:53 -07:00
|
|
|
Ids { ids: Vec<u32> },
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:15:47 -07:00
|
|
|
#[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<u32> },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
|
|
|
pub enum RuntimeTerritoryTarget {
|
|
|
|
|
AllTerritories,
|
|
|
|
|
Ids { ids: Vec<u32> },
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:21:12 -07:00
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 18:27:04 -07:00
|
|
|
#[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 {
|
2026-04-15 19:15:47 -07:00
|
|
|
target: RuntimeTerritoryTarget,
|
2026-04-15 18:27:04 -07:00
|
|
|
metric: RuntimeTerritoryMetric,
|
|
|
|
|
comparator: RuntimeConditionComparator,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
|
|
|
|
CompanyTerritoryNumericThreshold {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
2026-04-15 19:15:47 -07:00
|
|
|
territory: RuntimeTerritoryTarget,
|
2026-04-15 18:27:04 -07:00
|
|
|
metric: RuntimeTrackMetric,
|
|
|
|
|
comparator: RuntimeConditionComparator,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
2026-04-15 21:41:40 -07:00
|
|
|
SpecialConditionThreshold {
|
|
|
|
|
label: String,
|
|
|
|
|
comparator: RuntimeConditionComparator,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
|
|
|
|
CandidateAvailabilityThreshold {
|
|
|
|
|
name: String,
|
|
|
|
|
comparator: RuntimeConditionComparator,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
2026-04-16 13:48:55 -07:00
|
|
|
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,
|
|
|
|
|
},
|
2026-04-15 21:41:40 -07:00
|
|
|
EconomicStatusCodeThreshold {
|
|
|
|
|
comparator: RuntimeConditionComparator,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
2026-04-16 08:28:50 -07:00
|
|
|
WorldFlagEquals {
|
|
|
|
|
key: String,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
2026-04-15 18:27:04 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 19:37:53 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
|
|
|
pub enum RuntimeEffect {
|
|
|
|
|
SetWorldFlag {
|
|
|
|
|
key: String,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
2026-04-16 09:20:49 -07:00
|
|
|
SetLimitedTrackBuildingAmount {
|
|
|
|
|
value: i32,
|
|
|
|
|
},
|
2026-04-15 20:20:25 -07:00
|
|
|
SetEconomicStatusCode {
|
|
|
|
|
value: i32,
|
|
|
|
|
},
|
2026-04-15 09:50:58 -07:00
|
|
|
SetCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
2026-04-15 19:15:47 -07:00
|
|
|
SetPlayerCash {
|
|
|
|
|
target: RuntimePlayerTarget,
|
|
|
|
|
value: i64,
|
|
|
|
|
},
|
2026-04-15 23:24:08 -07:00
|
|
|
DeactivatePlayer {
|
|
|
|
|
target: RuntimePlayerTarget,
|
|
|
|
|
},
|
2026-04-15 20:53:35 -07:00
|
|
|
SetCompanyTerritoryAccess {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
territory: RuntimeTerritoryTarget,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
2026-04-15 20:20:25 -07:00
|
|
|
ConfiscateCompanyAssets {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
},
|
2026-04-15 12:11:29 -07:00
|
|
|
DeactivateCompany {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
},
|
|
|
|
|
SetCompanyTrackLayingCapacity {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
value: Option<u32>,
|
|
|
|
|
},
|
2026-04-15 20:20:25 -07:00
|
|
|
RetireTrains {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
company_target: Option<RuntimeCompanyTarget>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
territory_target: Option<RuntimeTerritoryTarget>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
locomotive_name: Option<String>,
|
|
|
|
|
},
|
2026-04-14 19:37:53 -07:00
|
|
|
AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
delta: i64,
|
|
|
|
|
},
|
|
|
|
|
AdjustCompanyDebt {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
delta: i64,
|
|
|
|
|
},
|
|
|
|
|
SetCandidateAvailability {
|
|
|
|
|
name: String,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
2026-04-16 10:23:29 -07:00
|
|
|
SetNamedLocomotiveAvailability {
|
|
|
|
|
name: String,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
2026-04-16 11:39:59 -07:00
|
|
|
SetNamedLocomotiveAvailabilityValue {
|
|
|
|
|
name: String,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
2026-04-16 11:19:53 -07:00
|
|
|
SetNamedLocomotiveCost {
|
|
|
|
|
name: String,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
2026-04-16 11:39:59 -07:00
|
|
|
SetCargoProductionSlot {
|
|
|
|
|
slot: u32,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
|
|
|
|
SetTerritoryAccessCost {
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
2026-04-14 19:37:53 -07:00
|
|
|
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)]
|
2026-04-15 18:27:04 -07:00
|
|
|
pub conditions: Vec<RuntimeCondition>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 19:37:53 -07:00
|
|
|
pub effects: Vec<RuntimeEffect>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
#[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)]
|
2026-04-15 18:27:04 -07:00
|
|
|
pub conditions: Vec<RuntimeCondition>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 19:37:53 -07:00
|
|
|
pub effects: Vec<RuntimeEffect>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:01:43 -07:00
|
|
|
#[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>,
|
2026-04-14 20:35:07 -07:00
|
|
|
#[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)]
|
2026-04-14 22:09:09 -07:00
|
|
|
pub payload_family: String,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:35:07 -07:00
|
|
|
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)]
|
2026-04-14 23:01:18 -07:00
|
|
|
pub compact_control: Option<RuntimePackedEventCompactControlSummary>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:35:07 -07:00
|
|
|
pub text_bands: Vec<RuntimePackedEventTextBandSummary>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub standalone_condition_row_count: usize,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 22:09:09 -07:00
|
|
|
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 14:21:12 -07:00
|
|
|
pub negative_sentinel_scope: Option<RuntimePackedEventNegativeSentinelScopeSummary>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:35:07 -07:00
|
|
|
pub grouped_effect_row_counts: Vec<usize>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 22:09:09 -07:00
|
|
|
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 09:13:51 -07:00
|
|
|
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 18:27:04 -07:00
|
|
|
pub decoded_conditions: Vec<RuntimeCondition>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:35:07 -07:00
|
|
|
pub decoded_actions: Vec<RuntimeEffect>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub executable_import_ready: bool,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 21:19:08 -07:00
|
|
|
pub import_outcome: Option<String>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:35:07 -07:00
|
|
|
pub notes: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:21:12 -07:00
|
|
|
#[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<usize>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:01:18 -07:00
|
|
|
#[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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:35:07 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimePackedEventTextBandSummary {
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub packed_len: usize,
|
|
|
|
|
pub present: bool,
|
|
|
|
|
pub preview: String,
|
2026-04-14 20:01:43 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 22:09:09 -07:00
|
|
|
#[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)]
|
2026-04-15 18:27:04 -07:00
|
|
|
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)]
|
2026-04-14 22:09:09 -07:00
|
|
|
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,
|
2026-04-15 09:50:58 -07:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub descriptor_label: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub target_mask_bits: Option<u8>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub parameter_family: Option<String>,
|
2026-04-14 22:09:09 -07:00
|
|
|
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)]
|
2026-04-15 09:50:58 -07:00
|
|
|
pub semantic_family: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub semantic_preview: Option<String>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 10:50:13 -07:00
|
|
|
pub recovered_locomotive_id: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 22:09:09 -07:00
|
|
|
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,
|
2026-04-15 18:27:04 -07:00
|
|
|
conditions: self.conditions,
|
2026-04-14 19:37:53 -07:00
|
|
|
effects: self.effects,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:12:25 -07:00
|
|
|
#[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)]
|
2026-04-16 09:20:49 -07:00
|
|
|
pub limited_track_building_amount: Option<i32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 20:20:25 -07:00
|
|
|
pub economic_status_code: Option<i32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 11:39:59 -07:00
|
|
|
pub territory_access_cost: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-11 18:12:25 -07:00
|
|
|
pub absolute_counter_restore_kind: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub absolute_counter_adjustment_context: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeState {
|
|
|
|
|
pub calendar: CalendarPoint,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub world_flags: BTreeMap<String, bool>,
|
|
|
|
|
#[serde(default)]
|
2026-04-11 18:12:25 -07:00
|
|
|
pub save_profile: RuntimeSaveProfileState,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub world_restore: RuntimeWorldRestoreState,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub metadata: BTreeMap<String, String>,
|
|
|
|
|
#[serde(default)]
|
2026-04-10 01:22:47 -07:00
|
|
|
pub companies: Vec<RuntimeCompany>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 09:13:51 -07:00
|
|
|
pub selected_company_id: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 19:15:47 -07:00
|
|
|
pub players: Vec<RuntimePlayer>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub selected_player_id: Option<u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 20:20:25 -07:00
|
|
|
pub trains: Vec<RuntimeTrain>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 10:50:13 -07:00
|
|
|
pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 18:27:04 -07:00
|
|
|
pub territories: Vec<RuntimeTerritory>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>,
|
|
|
|
|
#[serde(default)]
|
2026-04-15 20:53:35 -07:00
|
|
|
pub company_territory_access: Vec<RuntimeCompanyTerritoryAccess>,
|
|
|
|
|
#[serde(default)]
|
2026-04-14 20:01:43 -07:00
|
|
|
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
|
|
|
|
|
#[serde(default)]
|
2026-04-10 01:22:47 -07:00
|
|
|
pub event_runtime_records: Vec<RuntimeEventRecord>,
|
|
|
|
|
#[serde(default)]
|
2026-04-11 18:12:25 -07:00
|
|
|
pub candidate_availability: BTreeMap<String, u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 10:23:29 -07:00
|
|
|
pub named_locomotive_availability: BTreeMap<String, u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 11:19:53 -07:00
|
|
|
pub named_locomotive_cost: BTreeMap<String, u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-16 11:39:59 -07:00
|
|
|
pub cargo_production_overrides: BTreeMap<u32, u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-11 18:12:25 -07:00
|
|
|
pub special_conditions: BTreeMap<String, u32>,
|
|
|
|
|
#[serde(default)]
|
2026-04-10 01:22:47 -07:00
|
|
|
pub service_state: RuntimeServiceState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RuntimeState {
|
|
|
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
|
|
|
self.calendar.validate()?;
|
|
|
|
|
|
|
|
|
|
let mut seen_company_ids = BTreeSet::new();
|
2026-04-15 12:11:29 -07:00
|
|
|
let mut active_company_ids = BTreeSet::new();
|
2026-04-10 01:22:47 -07:00
|
|
|
for company in &self.companies {
|
|
|
|
|
if !seen_company_ids.insert(company.company_id) {
|
|
|
|
|
return Err(format!("duplicate company_id {}", company.company_id));
|
|
|
|
|
}
|
2026-04-15 12:11:29 -07:00
|
|
|
if company.active {
|
|
|
|
|
active_company_ids.insert(company.company_id);
|
|
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|
2026-04-15 09:13:51 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-15 12:11:29 -07:00
|
|
|
if !active_company_ids.contains(&selected_company_id) {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"selected_company_id {} must reference an active company",
|
|
|
|
|
selected_company_id
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-15 09:13:51 -07:00
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
|
2026-04-15 19:15:47 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 18:27:04 -07:00
|
|
|
let mut seen_territory_ids = BTreeSet::new();
|
2026-04-15 19:15:47 -07:00
|
|
|
let mut seen_territory_names = BTreeSet::new();
|
2026-04-15 18:27:04 -07:00
|
|
|
for territory in &self.territories {
|
|
|
|
|
if !seen_territory_ids.insert(territory.territory_id) {
|
|
|
|
|
return Err(format!("duplicate territory_id {}", territory.territory_id));
|
|
|
|
|
}
|
2026-04-15 19:15:47 -07:00
|
|
|
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:?}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 18:27:04 -07:00
|
|
|
}
|
2026-04-15 20:20:25 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 10:50:13 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 18:27:04 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 20:53:35 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 18:27:04 -07:00
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
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-15 18:27:04 -07:00
|
|
|
for (condition_index, condition) in record.conditions.iter().enumerate() {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_runtime_condition(condition, &seen_company_ids, &seen_territory_ids)
|
|
|
|
|
.map_err(|err| {
|
2026-04-15 18:27:04 -07:00
|
|
|
format!(
|
|
|
|
|
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
2026-04-15 19:15:47 -07:00
|
|
|
})?;
|
2026-04-15 18:27:04 -07:00
|
|
|
}
|
2026-04-14 19:37:53 -07:00
|
|
|
for (effect_index, effect) in record.effects.iter().enumerate() {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_runtime_effect(
|
|
|
|
|
effect,
|
|
|
|
|
&seen_company_ids,
|
|
|
|
|
&seen_player_ids,
|
|
|
|
|
&seen_territory_ids,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|err| {
|
2026-04-14 19:37:53 -07:00
|
|
|
format!(
|
|
|
|
|
"event_runtime_records[record_id={}].effects[{effect_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:01:43 -07:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 20:35:07 -07:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 21:19:08 -07:00
|
|
|
let importable_or_imported_count = summary
|
2026-04-14 20:35:07 -07:00
|
|
|
.records
|
|
|
|
|
.iter()
|
2026-04-14 21:19:08 -07:00
|
|
|
.filter(|record| {
|
|
|
|
|
record.executable_import_ready
|
|
|
|
|
|| record.import_outcome.as_deref() == Some("imported")
|
|
|
|
|
})
|
2026-04-14 20:35:07 -07:00
|
|
|
.count();
|
2026-04-14 21:19:08 -07:00
|
|
|
if summary.imported_runtime_record_count > importable_or_imported_count {
|
2026-04-14 20:35:07 -07:00
|
|
|
return Err(
|
2026-04-14 21:19:08 -07:00
|
|
|
"packed_event_collection.imported_runtime_record_count must not exceed importable or imported records"
|
2026-04-14 20:35:07 -07:00
|
|
|
.to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 20:01:43 -07:00
|
|
|
|
|
|
|
|
let mut previous_id = None;
|
2026-04-14 20:35:07 -07:00
|
|
|
for (record_index, entry_id) in summary.live_entry_ids.iter().enumerate() {
|
2026-04-14 20:01:43 -07:00
|
|
|
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);
|
2026-04-14 20:35:07 -07:00
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 22:09:09 -07:00
|
|
|
if record.payload_family.trim().is_empty() {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"packed_event_collection.records[{record_index}].payload_family must not be empty"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 21:19:08 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 20:35:07 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 22:09:09 -07:00
|
|
|
if record.payload_family == "real_packed_v1"
|
2026-04-15 09:50:58 -07:00
|
|
|
&& record.standalone_condition_rows.len()
|
|
|
|
|
!= record.standalone_condition_row_count
|
2026-04-14 22:09:09 -07:00
|
|
|
{
|
|
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 20:35:07 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 23:01:18 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 22:09:09 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-15 18:27:04 -07:00
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-14 22:09:09 -07:00
|
|
|
}
|
|
|
|
|
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"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 20:01:43 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
for key in self.world_flags.keys() {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("world_flags contains an empty key".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:12:25 -07:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 10:23:29 -07:00
|
|
|
for key in self.named_locomotive_availability.keys() {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("named_locomotive_availability contains an empty key".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 11:19:53 -07:00
|
|
|
for key in self.named_locomotive_cost.keys() {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("named_locomotive_cost contains an empty key".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 11:39:59 -07:00
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 18:12:25 -07:00
|
|
|
for key in self.special_conditions.keys() {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("special_conditions contains an empty key".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 19:37:53 -07:00
|
|
|
fn validate_runtime_effect(
|
|
|
|
|
effect: &RuntimeEffect,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
2026-04-15 19:15:47 -07:00
|
|
|
valid_player_ids: &BTreeSet<u32>,
|
|
|
|
|
valid_territory_ids: &BTreeSet<u32>,
|
2026-04-14 19:37:53 -07:00
|
|
|
) -> Result<(), String> {
|
|
|
|
|
match effect {
|
|
|
|
|
RuntimeEffect::SetWorldFlag { key, .. } => {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("key must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 09:20:49 -07:00
|
|
|
RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
|
|
|
|
|
| RuntimeEffect::SetEconomicStatusCode { .. } => {}
|
2026-04-15 09:50:58 -07:00
|
|
|
RuntimeEffect::SetCompanyCash { target, .. }
|
2026-04-15 20:20:25 -07:00
|
|
|
| RuntimeEffect::ConfiscateCompanyAssets { target }
|
2026-04-15 12:11:29 -07:00
|
|
|
| RuntimeEffect::DeactivateCompany { target }
|
|
|
|
|
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
|
2026-04-15 09:50:58 -07:00
|
|
|
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
2026-04-14 19:37:53 -07:00
|
|
|
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
|
|
|
|
validate_company_target(target, valid_company_ids)?;
|
|
|
|
|
}
|
2026-04-15 20:53:35 -07:00
|
|
|
RuntimeEffect::SetCompanyTerritoryAccess {
|
|
|
|
|
target, territory, ..
|
|
|
|
|
} => {
|
|
|
|
|
validate_company_target(target, valid_company_ids)?;
|
|
|
|
|
validate_territory_target(territory, valid_territory_ids)?;
|
|
|
|
|
}
|
2026-04-15 23:24:08 -07:00
|
|
|
RuntimeEffect::SetPlayerCash { target, .. }
|
|
|
|
|
| RuntimeEffect::DeactivatePlayer { target } => {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_player_target(target, valid_player_ids)?;
|
|
|
|
|
}
|
2026-04-15 20:20:25 -07:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:37:53 -07:00
|
|
|
RuntimeEffect::SetCandidateAvailability { name, .. } => {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err("name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 10:23:29 -07:00
|
|
|
RuntimeEffect::SetNamedLocomotiveAvailability { name, .. } => {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err("name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 11:39:59 -07:00
|
|
|
RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, .. } => {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err("name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 11:19:53 -07:00
|
|
|
RuntimeEffect::SetNamedLocomotiveCost { name, .. } => {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err("name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 11:39:59 -07:00
|
|
|
RuntimeEffect::SetCargoProductionSlot { slot, .. } => {
|
|
|
|
|
if !(1..=11).contains(slot) {
|
|
|
|
|
return Err("slot must be in 1..=11".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::SetTerritoryAccessCost { .. } => {}
|
2026-04-14 19:37:53 -07:00
|
|
|
RuntimeEffect::SetSpecialCondition { label, .. } => {
|
|
|
|
|
if label.trim().is_empty() {
|
|
|
|
|
return Err("label must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::AppendEventRecord { record } => {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_event_record_template(
|
|
|
|
|
record,
|
|
|
|
|
valid_company_ids,
|
|
|
|
|
valid_player_ids,
|
|
|
|
|
valid_territory_ids,
|
|
|
|
|
)?;
|
2026-04-14 19:37:53 -07:00
|
|
|
}
|
|
|
|
|
RuntimeEffect::ActivateEventRecord { .. }
|
|
|
|
|
| RuntimeEffect::DeactivateEventRecord { .. }
|
|
|
|
|
| RuntimeEffect::RemoveEventRecord { .. } => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_event_record_template(
|
|
|
|
|
record: &RuntimeEventRecordTemplate,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
2026-04-15 19:15:47 -07:00
|
|
|
valid_player_ids: &BTreeSet<u32>,
|
|
|
|
|
valid_territory_ids: &BTreeSet<u32>,
|
2026-04-14 19:37:53 -07:00
|
|
|
) -> Result<(), String> {
|
2026-04-15 18:27:04 -07:00
|
|
|
for (condition_index, condition) in record.conditions.iter().enumerate() {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err(
|
|
|
|
|
|err| {
|
2026-04-15 20:20:25 -07:00
|
|
|
format!(
|
|
|
|
|
"template record_id={}.conditions[{condition_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
2026-04-15 19:15:47 -07:00
|
|
|
},
|
|
|
|
|
)?;
|
2026-04-15 18:27:04 -07:00
|
|
|
}
|
2026-04-14 19:37:53 -07:00
|
|
|
for (effect_index, effect) in record.effects.iter().enumerate() {
|
2026-04-15 19:15:47 -07:00
|
|
|
validate_runtime_effect(
|
|
|
|
|
effect,
|
|
|
|
|
valid_company_ids,
|
|
|
|
|
valid_player_ids,
|
|
|
|
|
valid_territory_ids,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|err| {
|
2026-04-14 19:37:53 -07:00
|
|
|
format!(
|
|
|
|
|
"template record_id={}.effects[{effect_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 18:27:04 -07:00
|
|
|
fn validate_runtime_condition(
|
|
|
|
|
condition: &RuntimeCondition,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
2026-04-15 19:15:47 -07:00
|
|
|
valid_territory_ids: &BTreeSet<u32>,
|
2026-04-15 18:27:04 -07:00
|
|
|
) -> Result<(), String> {
|
|
|
|
|
match condition {
|
2026-04-15 19:15:47 -07:00
|
|
|
RuntimeCondition::CompanyNumericThreshold { target, .. } => {
|
2026-04-15 18:27:04 -07:00
|
|
|
validate_company_target(target, valid_company_ids)
|
|
|
|
|
}
|
2026-04-15 19:15:47 -07:00
|
|
|
RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
|
|
|
|
|
validate_territory_target(target, valid_territory_ids)
|
|
|
|
|
}
|
|
|
|
|
RuntimeCondition::CompanyTerritoryNumericThreshold {
|
2026-04-15 20:20:25 -07:00
|
|
|
target, territory, ..
|
2026-04-15 19:15:47 -07:00
|
|
|
} => {
|
|
|
|
|
validate_company_target(target, valid_company_ids)?;
|
|
|
|
|
validate_territory_target(territory, valid_territory_ids)
|
|
|
|
|
}
|
2026-04-15 21:41:40 -07:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 13:48:55 -07:00
|
|
|
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(()),
|
2026-04-15 21:41:40 -07:00
|
|
|
RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()),
|
2026-04-16 08:28:50 -07:00
|
|
|
RuntimeCondition::WorldFlagEquals { key, .. } => {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
Err("key must not be empty".to_string())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 18:27:04 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 19:37:53 -07:00
|
|
|
fn validate_company_target(
|
|
|
|
|
target: &RuntimeCompanyTarget,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
match target {
|
2026-04-15 09:13:51 -07:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:15:47 -07:00
|
|
|
fn validate_player_target(
|
|
|
|
|
target: &RuntimePlayerTarget,
|
|
|
|
|
valid_player_ids: &BTreeSet<u32>,
|
|
|
|
|
) -> 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<u32>,
|
|
|
|
|
) -> 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(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
#[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(),
|
2026-04-11 18:12:25 -07:00
|
|
|
save_profile: RuntimeSaveProfileState::default(),
|
|
|
|
|
world_restore: RuntimeWorldRestoreState::default(),
|
|
|
|
|
metadata: BTreeMap::new(),
|
2026-04-10 01:22:47 -07:00
|
|
|
companies: vec![
|
|
|
|
|
RuntimeCompany {
|
|
|
|
|
company_id: 1,
|
|
|
|
|
current_cash: 100,
|
|
|
|
|
debt: 0,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: true,
|
|
|
|
|
available_track_laying_capacity: None,
|
2026-04-15 09:13:51 -07:00
|
|
|
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
2026-04-10 01:22:47 -07:00
|
|
|
},
|
|
|
|
|
RuntimeCompany {
|
|
|
|
|
company_id: 1,
|
|
|
|
|
current_cash: 200,
|
|
|
|
|
debt: 0,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: true,
|
|
|
|
|
available_track_laying_capacity: None,
|
2026-04-15 09:13:51 -07:00
|
|
|
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
2026-04-10 01:22:47 -07:00
|
|
|
},
|
|
|
|
|
],
|
2026-04-15 09:13:51 -07:00
|
|
|
selected_company_id: None,
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
packed_event_collection: None,
|
2026-04-10 01:22:47 -07:00
|
|
|
event_runtime_records: Vec::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
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),
|
2026-04-16 09:20:49 -07:00
|
|
|
limited_track_building_amount: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
economic_status_code: None,
|
2026-04-16 11:39:59 -07:00
|
|
|
territory_access_cost: None,
|
2026-04-11 18:12:25 -07:00
|
|
|
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(),
|
2026-04-15 09:13:51 -07:00
|
|
|
selected_company_id: None,
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
packed_event_collection: None,
|
2026-04-11 18:12:25 -07:00
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
special_conditions: BTreeMap::new(),
|
2026-04-10 01:22:47 -07:00
|
|
|
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,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: true,
|
|
|
|
|
available_track_laying_capacity: None,
|
2026-04-15 09:13:51 -07:00
|
|
|
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
2026-04-14 19:37:53 -07:00
|
|
|
}],
|
2026-04-15 09:13:51 -07:00
|
|
|
selected_company_id: None,
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
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,
|
2026-04-15 18:27:04 -07:00
|
|
|
conditions: Vec::new(),
|
2026-04-14 19:37:53 -07:00
|
|
|
effects: vec![RuntimeEffect::AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
|
|
|
|
|
delta: 50,
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-14 19:37:53 -07:00
|
|
|
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,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: true,
|
|
|
|
|
available_track_laying_capacity: None,
|
2026-04-15 09:13:51 -07:00
|
|
|
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
2026-04-14 19:37:53 -07:00
|
|
|
}],
|
2026-04-15 09:13:51 -07:00
|
|
|
selected_company_id: None,
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
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,
|
2026-04-15 18:27:04 -07:00
|
|
|
conditions: Vec::new(),
|
2026-04-14 19:37:53 -07:00
|
|
|
effects: vec![RuntimeEffect::AppendEventRecord {
|
|
|
|
|
record: Box::new(RuntimeEventRecordTemplate {
|
|
|
|
|
record_id: 8,
|
|
|
|
|
trigger_kind: 0x0a,
|
|
|
|
|
active: true,
|
|
|
|
|
marks_collection_dirty: false,
|
|
|
|
|
one_shot: false,
|
2026-04-15 18:27:04 -07:00
|
|
|
conditions: Vec::new(),
|
2026-04-14 19:37:53 -07:00
|
|
|
effects: vec![RuntimeEffect::AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
|
|
|
|
|
delta: 50,
|
|
|
|
|
}],
|
|
|
|
|
}),
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-14 19:37:53 -07:00
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
2026-04-14 20:01:43 -07:00
|
|
|
|
|
|
|
|
#[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(),
|
2026-04-15 09:13:51 -07:00
|
|
|
selected_company_id: None,
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
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],
|
2026-04-14 20:35:07 -07:00
|
|
|
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(),
|
2026-04-14 22:09:09 -07:00
|
|
|
payload_family: "unsupported_framing".to_string(),
|
2026-04-14 20:35:07 -07:00
|
|
|
trigger_kind: None,
|
|
|
|
|
active: None,
|
|
|
|
|
marks_collection_dirty: None,
|
|
|
|
|
one_shot: None,
|
2026-04-14 23:01:18 -07:00
|
|
|
compact_control: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
text_bands: Vec::new(),
|
|
|
|
|
standalone_condition_row_count: 0,
|
2026-04-14 22:09:09 -07:00
|
|
|
standalone_condition_rows: Vec::new(),
|
2026-04-15 14:21:12 -07:00
|
|
|
negative_sentinel_scope: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
2026-04-14 22:09:09 -07:00
|
|
|
grouped_effect_rows: Vec::new(),
|
2026-04-15 09:13:51 -07:00
|
|
|
grouped_company_targets: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
decoded_conditions: Vec::new(),
|
2026-04-14 20:35:07 -07:00
|
|
|
decoded_actions: Vec::new(),
|
|
|
|
|
executable_import_ready: false,
|
2026-04-14 21:19:08 -07:00
|
|
|
import_outcome: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
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(),
|
2026-04-14 22:09:09 -07:00
|
|
|
payload_family: "unsupported_framing".to_string(),
|
2026-04-14 20:35:07 -07:00
|
|
|
trigger_kind: None,
|
|
|
|
|
active: None,
|
|
|
|
|
marks_collection_dirty: None,
|
|
|
|
|
one_shot: None,
|
2026-04-14 23:01:18 -07:00
|
|
|
compact_control: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
text_bands: Vec::new(),
|
|
|
|
|
standalone_condition_row_count: 0,
|
2026-04-14 22:09:09 -07:00
|
|
|
standalone_condition_rows: Vec::new(),
|
2026-04-15 14:21:12 -07:00
|
|
|
negative_sentinel_scope: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
grouped_effect_row_counts: vec![0, 0, 0, 0],
|
2026-04-14 22:09:09 -07:00
|
|
|
grouped_effect_rows: Vec::new(),
|
2026-04-15 09:13:51 -07:00
|
|
|
grouped_company_targets: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
decoded_conditions: Vec::new(),
|
2026-04-14 20:35:07 -07:00
|
|
|
decoded_actions: Vec::new(),
|
|
|
|
|
executable_import_ready: false,
|
2026-04-14 21:19:08 -07:00
|
|
|
import_outcome: None,
|
2026-04-14 20:35:07 -07:00
|
|
|
notes: vec!["test".to_string()],
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-04-14 20:01:43 -07:00
|
|
|
}),
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
2026-04-15 09:13:51 -07:00
|
|
|
|
|
|
|
|
#[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,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: true,
|
|
|
|
|
available_track_laying_capacity: None,
|
2026-04-15 09:13:51 -07:00
|
|
|
controller_kind: RuntimeCompanyControllerKind::Human,
|
|
|
|
|
}],
|
|
|
|
|
selected_company_id: Some(2),
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-15 09:13:51 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 09:13:51 -07:00
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
2026-04-15 12:11:29 -07:00
|
|
|
|
|
|
|
|
#[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,
|
2026-04-15 18:27:04 -07:00
|
|
|
credit_rating_score: None,
|
|
|
|
|
prime_rate: None,
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
2026-04-15 12:11:29 -07:00
|
|
|
active: false,
|
|
|
|
|
available_track_laying_capacity: None,
|
|
|
|
|
controller_kind: RuntimeCompanyControllerKind::Human,
|
|
|
|
|
}],
|
|
|
|
|
selected_company_id: Some(1),
|
2026-04-15 19:15:47 -07:00
|
|
|
players: Vec::new(),
|
|
|
|
|
selected_player_id: None,
|
2026-04-15 20:20:25 -07:00
|
|
|
trains: Vec::new(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
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,
|
|
|
|
|
}],
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
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,
|
|
|
|
|
}],
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
territories: vec![RuntimeTerritory {
|
|
|
|
|
territory_id: 1,
|
|
|
|
|
name: Some("Appalachia".to_string()),
|
|
|
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
|
|
|
|
}],
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:20:25 -07:00
|
|
|
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,
|
|
|
|
|
}],
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 18:27:04 -07:00
|
|
|
territories: Vec::new(),
|
|
|
|
|
company_territory_track_piece_counts: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
company_territory_access: Vec::new(),
|
|
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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(),
|
2026-04-16 10:50:13 -07:00
|
|
|
locomotive_catalog: Vec::new(),
|
2026-04-15 20:53:35 -07:00
|
|
|
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,
|
|
|
|
|
}],
|
2026-04-15 12:11:29 -07:00
|
|
|
packed_event_collection: None,
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
2026-04-16 10:23:29 -07:00
|
|
|
named_locomotive_availability: BTreeMap::new(),
|
2026-04-16 11:19:53 -07:00
|
|
|
named_locomotive_cost: BTreeMap::new(),
|
2026-04-16 11:39:59 -07:00
|
|
|
cargo_production_overrides: BTreeMap::new(),
|
2026-04-15 12:11:29 -07:00
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|