2026-04-10 01:22:47 -07:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use crate::CalendarPoint;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeCompany {
|
|
|
|
|
pub company_id: u32,
|
|
|
|
|
pub current_cash: i64,
|
|
|
|
|
pub debt: u64,
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
Ids { ids: Vec<u32> },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
|
|
|
pub enum RuntimeEffect {
|
|
|
|
|
SetWorldFlag {
|
|
|
|
|
key: String,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
|
|
|
|
AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
delta: i64,
|
|
|
|
|
},
|
|
|
|
|
AdjustCompanyDebt {
|
|
|
|
|
target: RuntimeCompanyTarget,
|
|
|
|
|
delta: i64,
|
|
|
|
|
},
|
|
|
|
|
SetCandidateAvailability {
|
|
|
|
|
name: String,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
|
|
|
|
SetSpecialCondition {
|
|
|
|
|
label: String,
|
|
|
|
|
value: u32,
|
|
|
|
|
},
|
|
|
|
|
AppendEventRecord {
|
|
|
|
|
record: Box<RuntimeEventRecordTemplate>,
|
|
|
|
|
},
|
|
|
|
|
ActivateEventRecord {
|
|
|
|
|
record_id: u32,
|
|
|
|
|
},
|
|
|
|
|
DeactivateEventRecord {
|
|
|
|
|
record_id: u32,
|
|
|
|
|
},
|
|
|
|
|
RemoveEventRecord {
|
|
|
|
|
record_id: u32,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeEventRecordTemplate {
|
|
|
|
|
pub record_id: u32,
|
|
|
|
|
pub trigger_kind: u8,
|
|
|
|
|
pub active: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub marks_collection_dirty: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub one_shot: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub effects: Vec<RuntimeEffect>,
|
|
|
|
|
}
|
|
|
|
|
|
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)]
|
|
|
|
|
pub effects: Vec<RuntimeEffect>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RuntimeEventRecordTemplate {
|
|
|
|
|
pub fn into_runtime_record(self) -> RuntimeEventRecord {
|
|
|
|
|
RuntimeEventRecord {
|
|
|
|
|
record_id: self.record_id,
|
|
|
|
|
trigger_kind: self.trigger_kind,
|
|
|
|
|
active: self.active,
|
|
|
|
|
service_count: 0,
|
|
|
|
|
marks_collection_dirty: self.marks_collection_dirty,
|
|
|
|
|
one_shot: self.one_shot,
|
|
|
|
|
has_fired: false,
|
|
|
|
|
effects: self.effects,
|
|
|
|
|
}
|
|
|
|
|
}
|
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)]
|
|
|
|
|
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)]
|
|
|
|
|
pub event_runtime_records: Vec<RuntimeEventRecord>,
|
|
|
|
|
#[serde(default)]
|
2026-04-11 18:12:25 -07:00
|
|
|
pub candidate_availability: BTreeMap<String, u32>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
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();
|
|
|
|
|
for company in &self.companies {
|
|
|
|
|
if !seen_company_ids.insert(company.company_id) {
|
|
|
|
|
return Err(format!("duplicate company_id {}", company.company_id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut seen_record_ids = BTreeSet::new();
|
|
|
|
|
for record in &self.event_runtime_records {
|
|
|
|
|
if !seen_record_ids.insert(record.record_id) {
|
|
|
|
|
return Err(format!("duplicate record_id {}", record.record_id));
|
|
|
|
|
}
|
2026-04-14 19:37:53 -07:00
|
|
|
for (effect_index, effect) in record.effects.iter().enumerate() {
|
|
|
|
|
validate_runtime_effect(effect, &seen_company_ids).map_err(|err| {
|
|
|
|
|
format!(
|
|
|
|
|
"event_runtime_records[record_id={}].effects[{effect_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
match effect {
|
|
|
|
|
RuntimeEffect::SetWorldFlag { key, .. } => {
|
|
|
|
|
if key.trim().is_empty() {
|
|
|
|
|
return Err("key must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::AdjustCompanyCash { target, .. }
|
|
|
|
|
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
|
|
|
|
validate_company_target(target, valid_company_ids)?;
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::SetCandidateAvailability { name, .. } => {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err("name must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::SetSpecialCondition { label, .. } => {
|
|
|
|
|
if label.trim().is_empty() {
|
|
|
|
|
return Err("label must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::AppendEventRecord { record } => {
|
|
|
|
|
validate_event_record_template(record, valid_company_ids)?;
|
|
|
|
|
}
|
|
|
|
|
RuntimeEffect::ActivateEventRecord { .. }
|
|
|
|
|
| RuntimeEffect::DeactivateEventRecord { .. }
|
|
|
|
|
| RuntimeEffect::RemoveEventRecord { .. } => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_event_record_template(
|
|
|
|
|
record: &RuntimeEventRecordTemplate,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
for (effect_index, effect) in record.effects.iter().enumerate() {
|
|
|
|
|
validate_runtime_effect(effect, valid_company_ids).map_err(|err| {
|
|
|
|
|
format!(
|
|
|
|
|
"template record_id={}.effects[{effect_index}] {err}",
|
|
|
|
|
record.record_id
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_company_target(
|
|
|
|
|
target: &RuntimeCompanyTarget,
|
|
|
|
|
valid_company_ids: &BTreeSet<u32>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
match target {
|
|
|
|
|
RuntimeCompanyTarget::AllActive => Ok(()),
|
|
|
|
|
RuntimeCompanyTarget::Ids { ids } => {
|
|
|
|
|
if ids.is_empty() {
|
|
|
|
|
return Err("target ids must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
for company_id in ids {
|
|
|
|
|
if !valid_company_ids.contains(company_id) {
|
|
|
|
|
return Err(format!("target references unknown company_id {company_id}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
RuntimeCompany {
|
|
|
|
|
company_id: 1,
|
|
|
|
|
current_cash: 200,
|
|
|
|
|
debt: 0,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
event_runtime_records: Vec::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
candidate_availability: BTreeMap::new(),
|
|
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rejects_partial_world_restore_without_year_lane() {
|
|
|
|
|
let state = RuntimeState {
|
|
|
|
|
calendar: CalendarPoint {
|
|
|
|
|
year: 1830,
|
|
|
|
|
month_slot: 0,
|
|
|
|
|
phase_slot: 0,
|
|
|
|
|
tick_slot: 0,
|
|
|
|
|
},
|
|
|
|
|
world_flags: BTreeMap::new(),
|
|
|
|
|
save_profile: RuntimeSaveProfileState::default(),
|
|
|
|
|
world_restore: RuntimeWorldRestoreState {
|
|
|
|
|
selected_year_profile_lane: None,
|
|
|
|
|
campaign_scenario_enabled: Some(false),
|
|
|
|
|
sandbox_enabled: Some(true),
|
|
|
|
|
seed_tuple_written_from_raw_lane: Some(true),
|
|
|
|
|
absolute_counter_requires_shell_context: Some(true),
|
|
|
|
|
absolute_counter_reconstructible_from_save: Some(false),
|
|
|
|
|
disable_cargo_economy_special_condition_slot: Some(30),
|
|
|
|
|
disable_cargo_economy_special_condition_reconstructible_from_save: Some(true),
|
|
|
|
|
disable_cargo_economy_special_condition_write_side_grounded: Some(true),
|
|
|
|
|
disable_cargo_economy_special_condition_enabled: Some(false),
|
|
|
|
|
use_bio_accelerator_cars_enabled: Some(false),
|
|
|
|
|
use_wartime_cargos_enabled: Some(false),
|
|
|
|
|
disable_train_crashes_enabled: Some(false),
|
|
|
|
|
disable_train_crashes_and_breakdowns_enabled: Some(false),
|
|
|
|
|
ai_ignore_territories_at_startup_enabled: Some(false),
|
|
|
|
|
absolute_counter_restore_kind: Some(
|
|
|
|
|
"mode-adjusted-selected-year-lane".to_string(),
|
|
|
|
|
),
|
|
|
|
|
absolute_counter_adjustment_context: Some(
|
|
|
|
|
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30".to_string(),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
metadata: BTreeMap::new(),
|
|
|
|
|
companies: Vec::new(),
|
|
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
|
|
|
|
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,
|
|
|
|
|
}],
|
|
|
|
|
event_runtime_records: vec![RuntimeEventRecord {
|
|
|
|
|
record_id: 7,
|
|
|
|
|
trigger_kind: 1,
|
|
|
|
|
active: true,
|
|
|
|
|
service_count: 0,
|
|
|
|
|
marks_collection_dirty: false,
|
|
|
|
|
one_shot: false,
|
|
|
|
|
has_fired: false,
|
|
|
|
|
effects: vec![RuntimeEffect::AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
|
|
|
|
|
delta: 50,
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
|
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rejects_template_effect_targeting_unknown_company() {
|
|
|
|
|
let state = RuntimeState {
|
|
|
|
|
calendar: CalendarPoint {
|
|
|
|
|
year: 1830,
|
|
|
|
|
month_slot: 0,
|
|
|
|
|
phase_slot: 0,
|
|
|
|
|
tick_slot: 0,
|
|
|
|
|
},
|
|
|
|
|
world_flags: BTreeMap::new(),
|
|
|
|
|
save_profile: RuntimeSaveProfileState::default(),
|
|
|
|
|
world_restore: RuntimeWorldRestoreState::default(),
|
|
|
|
|
metadata: BTreeMap::new(),
|
|
|
|
|
companies: vec![RuntimeCompany {
|
|
|
|
|
company_id: 1,
|
|
|
|
|
current_cash: 100,
|
|
|
|
|
debt: 0,
|
|
|
|
|
}],
|
|
|
|
|
event_runtime_records: vec![RuntimeEventRecord {
|
|
|
|
|
record_id: 7,
|
|
|
|
|
trigger_kind: 1,
|
|
|
|
|
active: true,
|
|
|
|
|
service_count: 0,
|
|
|
|
|
marks_collection_dirty: false,
|
|
|
|
|
one_shot: false,
|
|
|
|
|
has_fired: false,
|
|
|
|
|
effects: vec![RuntimeEffect::AppendEventRecord {
|
|
|
|
|
record: Box::new(RuntimeEventRecordTemplate {
|
|
|
|
|
record_id: 8,
|
|
|
|
|
trigger_kind: 0x0a,
|
|
|
|
|
active: true,
|
|
|
|
|
marks_collection_dirty: false,
|
|
|
|
|
one_shot: false,
|
|
|
|
|
effects: vec![RuntimeEffect::AdjustCompanyCash {
|
|
|
|
|
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
|
|
|
|
|
delta: 50,
|
|
|
|
|
}],
|
|
|
|
|
}),
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
candidate_availability: BTreeMap::new(),
|
|
|
|
|
special_conditions: BTreeMap::new(),
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(state.validate().is_err());
|
|
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|