Execute real packed event world and train descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-15 20:20:25 -07:00
commit e481274243
31 changed files with 3287 additions and 206 deletions

View file

@ -6,8 +6,7 @@ use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget,
RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget,
RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind,
RuntimeTrackMetric, RuntimeTrackPieceCounts, calendar::BoundaryEventKind,
};
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -312,6 +311,9 @@ fn apply_runtime_effects(
RuntimeEffect::SetWorldFlag { key, value } => {
state.world_flags.insert(key.clone(), *value);
}
RuntimeEffect::SetEconomicStatusCode { value } => {
state.world_restore.economic_status_code = Some(*value);
}
RuntimeEffect::SetCompanyCash { target, value } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
@ -340,6 +342,28 @@ fn apply_runtime_effects(
mutated_player_ids.insert(player_id);
}
}
RuntimeEffect::ConfiscateCompanyAssets { target } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids.iter().copied() {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying confiscate effect"
)
})?;
company.current_cash = 0;
company.debt = 0;
company.active = false;
mutated_company_ids.insert(company_id);
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
}
}
retire_matching_trains(&mut state.trains, Some(&company_ids), None, None);
}
RuntimeEffect::DeactivateCompany { target } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
@ -375,6 +399,26 @@ fn apply_runtime_effects(
mutated_company_ids.insert(company_id);
}
}
RuntimeEffect::RetireTrains {
company_target,
territory_target,
locomotive_name,
} => {
let company_ids = company_target
.as_ref()
.map(|target| resolve_company_target_ids(state, target, condition_context))
.transpose()?;
let territory_ids = territory_target
.as_ref()
.map(|target| resolve_territory_target_ids(state, target))
.transpose()?;
retire_matching_trains(
&mut state.trains,
company_ids.as_ref(),
territory_ids.as_ref(),
locomotive_name.as_deref(),
);
}
RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
@ -523,13 +567,17 @@ fn evaluate_record_conditions(
let matching = resolved
.into_iter()
.filter(|company_id| {
state.companies.iter().find(|company| company.company_id == *company_id).is_some_and(
|company| compare_condition_value(
company_metric_value(company, *metric),
*comparator,
*value,
),
)
state
.companies
.iter()
.find(|company| company.company_id == *company_id)
.is_some_and(|company| {
compare_condition_value(
company_metric_value(company, *metric),
*comparator,
*value,
)
})
})
.collect::<BTreeSet<_>>();
if matching.is_empty() {
@ -597,10 +645,7 @@ fn evaluate_record_conditions(
}))
}
fn intersect_company_matches(
company_matches: &mut Option<BTreeSet<u32>>,
next: BTreeSet<u32>,
) {
fn intersect_company_matches(company_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
match company_matches {
Some(existing) => {
existing.retain(|company_id| next.contains(company_id));
@ -790,7 +835,11 @@ fn resolve_player_target_ids(
if condition_context.matching_player_ids.is_empty() {
Err("target requires player condition-evaluation context".to_string())
} else {
Ok(condition_context.matching_player_ids.iter().copied().collect())
Ok(condition_context
.matching_player_ids
.iter()
.copied()
.collect())
}
}
}
@ -801,9 +850,11 @@ fn resolve_territory_target_ids(
target: &RuntimeTerritoryTarget,
) -> Result<Vec<u32>, String> {
match target {
RuntimeTerritoryTarget::AllTerritories => {
Ok(state.territories.iter().map(|territory| territory.territory_id).collect())
}
RuntimeTerritoryTarget::AllTerritories => Ok(state
.territories
.iter()
.map(|territory| territory.territory_id)
.collect()),
RuntimeTerritoryTarget::Ids { ids } => {
let known_ids = state
.territories
@ -812,7 +863,9 @@ fn resolve_territory_target_ids(
.collect::<BTreeSet<_>>();
for territory_id in ids {
if !known_ids.contains(territory_id) {
return Err(format!("territory target references unknown territory_id {territory_id}"));
return Err(format!(
"territory target references unknown territory_id {territory_id}"
));
}
}
Ok(ids.clone())
@ -832,9 +885,7 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
RuntimeCompanyMetric::TrackPiecesTransition => {
i64::from(company.track_piece_counts.transition)
}
RuntimeCompanyMetric::TrackPiecesElectric => {
i64::from(company.track_piece_counts.electric)
}
RuntimeCompanyMetric::TrackPiecesElectric => i64::from(company.track_piece_counts.electric),
RuntimeCompanyMetric::TrackPiecesNonElectric => {
i64::from(company.track_piece_counts.non_electric)
}
@ -846,7 +897,8 @@ fn territory_metric_value(
territory_ids: &[u32],
metric: RuntimeTerritoryMetric,
) -> i64 {
state.territories
state
.territories
.iter()
.filter(|territory| territory_ids.contains(&territory.territory_id))
.map(|territory| {
@ -864,9 +916,12 @@ fn company_territory_metric_value(
territory_ids: &[u32],
metric: RuntimeTrackMetric,
) -> i64 {
state.company_territory_track_piece_counts
state
.company_territory_track_piece_counts
.iter()
.filter(|entry| entry.company_id == company_id && territory_ids.contains(&entry.territory_id))
.filter(|entry| {
entry.company_id == company_id && territory_ids.contains(&entry.territory_id)
})
.map(|entry| track_piece_metric_value(entry.track_piece_counts, metric))
.sum()
}
@ -920,6 +975,34 @@ fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result<u64, Str
}
}
fn retire_matching_trains(
trains: &mut [crate::RuntimeTrain],
company_ids: Option<&Vec<u32>>,
territory_ids: Option<&Vec<u32>>,
locomotive_name: Option<&str>,
) {
for train in trains.iter_mut() {
if !train.active || train.retired {
continue;
}
if company_ids.is_some_and(|company_ids| !company_ids.contains(&train.owner_company_id)) {
continue;
}
if territory_ids.is_some_and(|territory_ids| {
!train
.territory_id
.is_some_and(|territory_id| territory_ids.contains(&territory_id))
}) {
continue;
}
if locomotive_name.is_some_and(|name| train.locomotive_name.as_deref() != Some(name)) {
continue;
}
train.active = false;
train.retired = true;
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
@ -928,7 +1011,8 @@ mod tests {
use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeWorldRestoreState,
RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts,
RuntimeTrain, RuntimeWorldRestoreState,
};
fn state() -> RuntimeState {
@ -957,6 +1041,7 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
@ -1927,4 +2012,177 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn applies_economic_status_code_effect() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 90,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetEconomicStatusCode { value: 3 }],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("economic-status effect should succeed");
assert_eq!(state.world_restore.economic_status_code, Some(3));
}
#[test]
fn confiscate_company_assets_zeros_company_and_retires_owned_trains() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 50,
debt: 7,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 80,
debt: 9,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
],
selected_company_id: Some(1),
trains: vec![
RuntimeTrain {
train_id: 10,
owner_company_id: 1,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 11,
owner_company_id: 2,
territory_id: None,
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 91,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::ConfiscateCompanyAssets {
target: RuntimeCompanyTarget::SelectedCompany,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("confiscation effect should succeed");
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert!(!state.companies[0].active);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert!(!state.trains[1].retired);
}
#[test]
fn retire_trains_respects_company_territory_and_locomotive_filters() {
let mut state = RuntimeState {
territories: vec![
RuntimeTerritory {
territory_id: 7,
name: Some("Appalachia".to_string()),
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
RuntimeTerritory {
territory_id: 8,
name: Some("Great Plains".to_string()),
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
],
trains: vec![
RuntimeTrain {
train_id: 10,
owner_company_id: 1,
territory_id: Some(7),
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 11,
owner_company_id: 1,
territory_id: Some(7),
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 12,
owner_company_id: 1,
territory_id: Some(8),
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 92,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::RetireTrains {
company_target: Some(RuntimeCompanyTarget::SelectedCompany),
territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }),
locomotive_name: Some("Mikado".to_string()),
}],
}],
selected_company_id: Some(1),
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("retire-trains effect should succeed");
assert!(state.trains[0].retired);
assert!(!state.trains[1].retired);
assert!(!state.trains[2].retired);
}
}