Execute aggregate cargo packed event conditions

This commit is contained in:
Jan Petykiewicz 2026-04-16 15:26:37 -07:00
commit 377de45631
18 changed files with 1036 additions and 176 deletions

View file

@ -725,6 +725,7 @@ fn project_save_slice_components(
.map(|entry| RuntimeCargoCatalogEntry {
slot_id: entry.slot_id,
label: entry.label.clone(),
cargo_class: entry.cargo_class,
supplied_token_stem: entry
.supplied_cargo_token_probable_high16_ascii_stem
.clone(),
@ -977,7 +978,10 @@ fn runtime_packed_event_condition_row_summary_from_smp(
semantic_preview: row.semantic_preview.clone(),
requires_candidate_name_binding: row.requires_candidate_name_binding,
recovered_cargo_slot: row.recovered_cargo_slot,
recovered_cargo_class: row.recovered_cargo_class.clone(),
recovered_cargo_class: cargo_entry
.map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase())
.map(|value| value.replace("farmmine", "farm_mine"))
.or_else(|| row.recovered_cargo_class.clone()),
recovered_cargo_label: cargo_entry
.map(|entry| entry.label.clone())
.or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)),
@ -1015,7 +1019,10 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
semantic_family: row.semantic_family.clone(),
semantic_preview: row.semantic_preview.clone(),
recovered_cargo_slot: row.recovered_cargo_slot,
recovered_cargo_class: row.recovered_cargo_class.clone(),
recovered_cargo_class: cargo_entry
.map(|entry| format!("{:?}", entry.cargo_class).to_ascii_lowercase())
.map(|value| value.replace("farmmine", "farm_mine"))
.or_else(|| row.recovered_cargo_class.clone()),
recovered_cargo_label: cargo_entry
.map(|entry| entry.label.clone())
.or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)),
@ -1663,6 +1670,24 @@ fn lower_condition_targets_in_condition(
value: *value,
}
}
RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => {
RuntimeCondition::FactoryProductionTotalThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => {
RuntimeCondition::FarmMineProductionTotalThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => {
RuntimeCondition::OtherCargoProductionTotalThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => {
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: *comparator,
@ -1768,6 +1793,9 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionSlotThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
@ -2391,6 +2419,9 @@ fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool {
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionSlotThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
@ -2489,6 +2520,9 @@ fn runtime_condition_company_target_import_blocker(
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionSlotThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
@ -3532,12 +3566,49 @@ mod tests {
}
}
fn save_cargo_catalog(
entries: &[(u32, crate::RuntimeCargoClass)],
) -> crate::SmpLoadedCargoCatalog {
crate::SmpLoadedCargoCatalog {
source_kind: "recipe-book-summary-slot-catalog".to_string(),
semantic_family: "scenario-save-derived-cargo-catalog".to_string(),
root_offset: Some(0x0fe7),
observed_entry_count: entries.len(),
entries: entries
.iter()
.enumerate()
.map(
|(index, (slot_id, cargo_class))| crate::SmpLoadedCargoCatalogEntry {
slot_id: *slot_id,
label: format!("Cargo Production Slot {slot_id}"),
cargo_class: *cargo_class,
book_index: index,
max_annual_production_word: 0,
mode_word: 0,
runtime_import_branch_kind: "zero-mode-skipped".to_string(),
annual_amount_word: 0,
supplied_cargo_token_word: 0,
supplied_cargo_token_probable_high16_ascii_stem: None,
demanded_cargo_token_word: 0,
demanded_cargo_token_probable_high16_ascii_stem: None,
},
)
.collect(),
}
}
fn real_cargo_production_row(
descriptor_id: u32,
value: i32,
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
let slot = descriptor_id.saturating_sub(229);
let descriptor_label = format!("Cargo Production Slot {slot}");
let recovered_cargo_class = match slot {
1..=4 => Some("factory".to_string()),
5..=8 => Some("farm_mine".to_string()),
9..=11 => Some("other".to_string()),
_ => None,
};
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
@ -3557,7 +3628,7 @@ mod tests {
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set {descriptor_label} to {value}")),
recovered_cargo_slot: Some(slot),
recovered_cargo_class: None,
recovered_cargo_class,
recovered_locomotive_id: None,
locomotive_name: None,
notes: vec![],
@ -3957,7 +4028,11 @@ mod tests {
},
),
locomotive_catalog: None,
cargo_catalog: None,
cargo_catalog: Some(save_cargo_catalog(&[
(1, crate::RuntimeCargoClass::Factory),
(5, crate::RuntimeCargoClass::FarmMine),
(9, crate::RuntimeCargoClass::Other),
])),
special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable {
source_kind: "save-fixed-special-conditions-range".to_string(),
table_offset: 0x0d64,
@ -4283,7 +4358,11 @@ mod tests {
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
cargo_catalog: Some(save_cargo_catalog(&[
(1, crate::RuntimeCargoClass::Factory),
(5, crate::RuntimeCargoClass::FarmMine),
(9, crate::RuntimeCargoClass::Other),
])),
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
@ -4399,7 +4478,11 @@ mod tests {
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
cargo_catalog: Some(save_cargo_catalog(&[
(1, crate::RuntimeCargoClass::Factory),
(5, crate::RuntimeCargoClass::FarmMine),
(9, crate::RuntimeCargoClass::Other),
])),
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
@ -4493,7 +4576,11 @@ mod tests {
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
cargo_catalog: Some(save_cargo_catalog(&[
(1, crate::RuntimeCargoClass::Factory),
(5, crate::RuntimeCargoClass::FarmMine),
(9, crate::RuntimeCargoClass::Other),
])),
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
@ -8122,7 +8209,11 @@ mod tests {
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
cargo_catalog: Some(save_cargo_catalog(&[
(1, crate::RuntimeCargoClass::Factory),
(5, crate::RuntimeCargoClass::FarmMine),
(9, crate::RuntimeCargoClass::Other),
])),
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
@ -8156,11 +8247,13 @@ mod tests {
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![5, 0, 0, 0],
grouped_effect_row_counts: vec![7, 0, 0, 0],
grouped_effect_rows: vec![
real_locomotive_availability_row(250, 42),
real_locomotive_cost_row(352, 250000),
real_cargo_production_row(230, 125),
real_cargo_production_row(234, 75),
real_cargo_production_row(238, 30),
real_limited_track_building_amount_row(18),
real_territory_access_cost_row(750000),
],
@ -8178,6 +8271,8 @@ mod tests {
slot: 1,
value: 125,
},
RuntimeEffect::SetCargoProductionSlot { slot: 5, value: 75 },
RuntimeEffect::SetCargoProductionSlot { slot: 9, value: 30 },
RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 },
RuntimeEffect::SetTerritoryAccessCost { value: 750000 },
],
@ -8197,7 +8292,7 @@ mod tests {
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 6,
standalone_condition_row_count: 9,
standalone_condition_rows: vec![
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
@ -8262,7 +8357,7 @@ mod tests {
.to_string(),
),
recovered_cargo_slot: Some(1),
recovered_cargo_class: None,
recovered_cargo_class: Some("factory".to_string()),
requires_candidate_name_binding: false,
notes: vec![],
},
@ -8280,7 +8375,7 @@ mod tests {
metric: Some("Cargo Production Total".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Cargo Production Total == 125".to_string(),
"Test Cargo Production Total == 230".to_string(),
),
recovered_cargo_slot: None,
recovered_cargo_class: None,
@ -8289,6 +8384,69 @@ mod tests {
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 4,
raw_condition_id: 2419,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&125_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Factory Production Total".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Factory Production Total == 125".to_string(),
),
recovered_cargo_slot: None,
recovered_cargo_class: Some("factory".to_string()),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 5,
raw_condition_id: 2420,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&75_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Farm/Mine Production Total".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Farm/Mine Production Total == 75".to_string(),
),
recovered_cargo_slot: None,
recovered_cargo_class: Some("farm_mine".to_string()),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 6,
raw_condition_id: 2421,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&30_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Other Cargo Production Total".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Other Cargo Production Total == 30".to_string(),
),
recovered_cargo_slot: None,
recovered_cargo_class: Some("other".to_string()),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 7,
raw_condition_id: 2547,
subtype: 4,
flag_bytes: {
@ -8309,7 +8467,7 @@ mod tests {
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 5,
row_index: 8,
raw_condition_id: 1516,
subtype: 4,
flag_bytes: {
@ -8355,9 +8513,21 @@ mod tests {
value: 125,
},
RuntimeCondition::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 230,
},
RuntimeCondition::FactoryProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::FarmMineProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 75,
},
RuntimeCondition::OtherCargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 30,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,
@ -8391,6 +8561,76 @@ mod tests {
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("setup trigger should execute");
assert_eq!(
import.state.named_locomotive_availability.get("Big Boy"),
Some(&42)
);
assert_eq!(
import.state.named_locomotive_cost.get("Locomotive 1"),
Some(&250000)
);
assert_eq!(import.state.cargo_production_overrides.get(&1), Some(&125));
assert_eq!(import.state.cargo_production_overrides.get(&5), Some(&75));
assert_eq!(import.state.cargo_production_overrides.get(&9), Some(&30));
assert_eq!(import.state.cargo_catalog.len(), 3);
assert_eq!(
import.state.cargo_catalog[0].cargo_class,
crate::RuntimeCargoClass::Factory
);
assert_eq!(
import.state.cargo_catalog[1].cargo_class,
crate::RuntimeCargoClass::FarmMine
);
assert_eq!(
import.state.cargo_catalog[2].cargo_class,
crate::RuntimeCargoClass::Other
);
assert_eq!(import.state.event_runtime_records.len(), 2);
assert_eq!(
import.state.event_runtime_records[1].conditions,
vec![
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: "Big Boy".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 42,
},
RuntimeCondition::NamedLocomotiveCostThreshold {
name: "Locomotive 1".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 250000,
},
RuntimeCondition::CargoProductionSlotThreshold {
slot: 1,
label: "Cargo Production Slot 1".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 230,
},
RuntimeCondition::FactoryProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::FarmMineProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 75,
},
RuntimeCondition::OtherCargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 30,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,
},
RuntimeCondition::TerritoryAccessCostThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 750000,
},
]
);
crate::execute_step_command(
&mut import.state,
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 },

View file

@ -35,7 +35,7 @@ pub use pk4::{
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
};
pub use runtime::{
RuntimeCargoCatalogEntry, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,

View file

@ -118,11 +118,22 @@ pub struct RuntimeCargoCatalogEntry {
pub slot_id: u32,
pub label: String,
#[serde(default)]
pub cargo_class: RuntimeCargoClass,
#[serde(default)]
pub supplied_token_stem: Option<String>,
#[serde(default)]
pub demanded_token_stem: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeCargoClass {
#[default]
Other,
Factory,
FarmMine,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCompanyTarget {
@ -274,6 +285,18 @@ pub enum RuntimeCondition {
comparator: RuntimeConditionComparator,
value: i64,
},
FactoryProductionTotalThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
FarmMineProductionTotalThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
OtherCargoProductionTotalThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator,
value: i64,
@ -1475,6 +1498,9 @@ fn validate_runtime_condition(
}
}
RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()),
RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()),

View file

@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric,
RuntimeCargoClass, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
};
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -266,6 +266,83 @@ struct RealOrdinaryConditionMetadata {
kind: RealOrdinaryConditionKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KnownCargoSlotDefinition {
slot_id: u32,
label: &'static str,
cargo_class: RuntimeCargoClass,
descriptor_id: u32,
}
const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [
KnownCargoSlotDefinition {
slot_id: 1,
label: "Cargo Production Slot 1",
cargo_class: RuntimeCargoClass::Factory,
descriptor_id: 230,
},
KnownCargoSlotDefinition {
slot_id: 2,
label: "Cargo Production Slot 2",
cargo_class: RuntimeCargoClass::Factory,
descriptor_id: 231,
},
KnownCargoSlotDefinition {
slot_id: 3,
label: "Cargo Production Slot 3",
cargo_class: RuntimeCargoClass::Factory,
descriptor_id: 232,
},
KnownCargoSlotDefinition {
slot_id: 4,
label: "Cargo Production Slot 4",
cargo_class: RuntimeCargoClass::Factory,
descriptor_id: 233,
},
KnownCargoSlotDefinition {
slot_id: 5,
label: "Cargo Production Slot 5",
cargo_class: RuntimeCargoClass::FarmMine,
descriptor_id: 234,
},
KnownCargoSlotDefinition {
slot_id: 6,
label: "Cargo Production Slot 6",
cargo_class: RuntimeCargoClass::FarmMine,
descriptor_id: 235,
},
KnownCargoSlotDefinition {
slot_id: 7,
label: "Cargo Production Slot 7",
cargo_class: RuntimeCargoClass::FarmMine,
descriptor_id: 236,
},
KnownCargoSlotDefinition {
slot_id: 8,
label: "Cargo Production Slot 8",
cargo_class: RuntimeCargoClass::FarmMine,
descriptor_id: 237,
},
KnownCargoSlotDefinition {
slot_id: 9,
label: "Cargo Production Slot 9",
cargo_class: RuntimeCargoClass::Other,
descriptor_id: 238,
},
KnownCargoSlotDefinition {
slot_id: 10,
label: "Cargo Production Slot 10",
cargo_class: RuntimeCargoClass::Other,
descriptor_id: 239,
},
KnownCargoSlotDefinition {
slot_id: 11,
label: "Cargo Production Slot 11",
cargo_class: RuntimeCargoClass::Other,
descriptor_id: 240,
},
];
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435;
const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200;
const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422;
@ -1629,6 +1706,8 @@ pub struct SmpLoadedLocomotiveCatalog {
pub struct SmpLoadedCargoCatalogEntry {
pub slot_id: u32,
pub label: String,
#[serde(default)]
pub cargo_class: RuntimeCargoClass,
pub book_index: usize,
pub max_annual_production_word: u32,
pub mode_word: u32,
@ -2080,9 +2159,11 @@ fn derive_cargo_catalog_from_recipe_book_probe(
.find(|line| line.imports_to_runtime_descriptor)
.or_else(|| book.lines.first())?;
let slot_id = (book.book_index + 1) as u32;
let definition = known_cargo_slot_definition(slot_id)?;
Some(SmpLoadedCargoCatalogEntry {
slot_id,
label: format!("Cargo Production Slot {slot_id}"),
label: definition.label.to_string(),
cargo_class: definition.cargo_class,
book_index: book.book_index,
max_annual_production_word: book.max_annual_production_word,
mode_word: line.mode_word,
@ -2113,6 +2194,30 @@ fn derive_cargo_catalog_from_recipe_book_probe(
})
}
fn known_cargo_slot_definition(slot_id: u32) -> Option<KnownCargoSlotDefinition> {
KNOWN_CARGO_SLOT_DEFINITIONS
.iter()
.copied()
.find(|definition| definition.slot_id == slot_id)
}
fn known_cargo_slot_definition_for_descriptor_id(
descriptor_id: u32,
) -> Option<KnownCargoSlotDefinition> {
KNOWN_CARGO_SLOT_DEFINITIONS
.iter()
.copied()
.find(|definition| definition.descriptor_id == descriptor_id)
}
fn runtime_cargo_class_name(cargo_class: RuntimeCargoClass) -> &'static str {
match cargo_class {
RuntimeCargoClass::Factory => "factory",
RuntimeCargoClass::FarmMine => "farm_mine",
RuntimeCargoClass::Other => "other",
}
}
fn parse_event_runtime_collection_summary(
bytes: &[u8],
container_profile: Option<&SmpContainerProfile>,
@ -2766,6 +2871,12 @@ fn parse_real_condition_row_summary(
_ => None,
}),
recovered_cargo_class: ordinary_metadata.and_then(|metadata| match metadata.kind {
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
candidate_name_ref
.and_then(recovered_cargo_production_slot_from_condition_name)
.and_then(known_cargo_slot_definition)
.map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string())
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::FactoryProductionTotal,
) => Some("factory".to_string()),
@ -3083,7 +3194,9 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x16,
)),
recovered_cargo_slot: recovered_cargo_production_slot(descriptor_id),
recovered_cargo_class: None,
recovered_cargo_class: recovered_cargo_production_slot(descriptor_id)
.and_then(known_cargo_slot_definition)
.map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()),
recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id)
.or_else(|| recovered_locomotive_cost_loco_id(descriptor_id)),
locomotive_name,
@ -3156,9 +3269,12 @@ fn decode_real_condition_row(
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
row.candidate_name.as_ref().and_then(|name| {
recovered_cargo_production_slot_from_condition_name(name).map(|slot| {
let label = known_cargo_slot_definition(slot)
.map(|definition| definition.label.to_string())
.unwrap_or_else(|| name.clone());
RuntimeCondition::CargoProductionSlotThreshold {
slot,
label: name.clone(),
label,
comparator,
value,
}
@ -3185,11 +3301,15 @@ fn decode_real_condition_row(
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => {
Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value })
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => {
Some(RuntimeCondition::FactoryProductionTotalThreshold { comparator, value })
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => {
Some(RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value })
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::FactoryProductionTotal
| RealWorldConditionKind::FarmMineProductionTotal
| RealWorldConditionKind::OtherCargoProductionTotal,
) => None,
RealWorldConditionKind::OtherCargoProductionTotal,
) => Some(RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value }),
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::LimitedTrackBuildingAmount,
) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }),
@ -3295,27 +3415,14 @@ fn recovered_locomotive_availability_loco_id(descriptor_id: u32) -> Option<u32>
}
fn recovered_cargo_production_label(descriptor_id: u32) -> Option<&'static str> {
static LABELS: OnceLock<BTreeMap<u32, &'static str>> = OnceLock::new();
LABELS
.get_or_init(|| {
(230..=240)
.enumerate()
.map(|(slot_index, descriptor_id)| {
let label = Box::leak(
format!("Cargo Production Slot {}", slot_index + 1).into_boxed_str(),
) as &'static str;
(descriptor_id, label)
})
.collect()
})
.get(&descriptor_id)
.copied()
known_cargo_slot_definition_for_descriptor_id(descriptor_id).map(|definition| definition.label)
}
fn recovered_cargo_production_slot_from_condition_name(name: &str) -> Option<u32> {
let suffix = name.strip_prefix("Cargo Production Slot ")?;
let slot = suffix.parse::<u32>().ok()?;
(1..=11).contains(&slot).then_some(slot)
KNOWN_CARGO_SLOT_DEFINITIONS
.iter()
.find(|definition| definition.label == name)
.map(|definition| definition.slot_id)
}
fn recovered_locomotive_cost_loco_id(descriptor_id: u32) -> Option<u32> {
@ -3998,6 +4105,9 @@ fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) ->
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionSlotThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
@ -9979,6 +10089,24 @@ mod tests {
200,
None,
),
build_real_condition_row_with_threshold(
REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID,
4,
125,
None,
),
build_real_condition_row_with_threshold(
REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID,
4,
75,
None,
),
build_real_condition_row_with_threshold(
REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
4,
30,
None,
),
build_real_condition_row_with_threshold(
REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID,
4,
@ -10048,6 +10176,18 @@ mod tests {
comparator: RuntimeConditionComparator::Ge,
value: 200,
},
RuntimeCondition::FactoryProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::FarmMineProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 75,
},
RuntimeCondition::OtherCargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 30,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,

View file

@ -3,10 +3,11 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget,
RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget,
RuntimeTrackMetric, RuntimeTrackPieceCounts, calendar::BoundaryEventKind,
RuntimeCargoClass, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind,
};
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -779,6 +780,24 @@ fn evaluate_record_conditions(
return Ok(None);
}
}
RuntimeCondition::FactoryProductionTotalThreshold { comparator, value } => {
let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Factory);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value } => {
let actual = cargo_production_total_for_class(state, RuntimeCargoClass::FarmMine);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value } => {
let actual = cargo_production_total_for_class(state, RuntimeCargoClass::Other);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => {
let actual = state
.world_restore
@ -1142,6 +1161,17 @@ fn compare_condition_value(
}
}
fn cargo_production_total_for_class(state: &RuntimeState, cargo_class: RuntimeCargoClass) -> i64 {
state
.cargo_catalog
.iter()
.filter(|entry| entry.cargo_class == cargo_class)
.filter_map(|entry| state.cargo_production_overrides.get(&entry.slot_id))
.copied()
.map(i64::from)
.sum()
}
fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result<u64, String> {
if delta >= 0 {
current
@ -2228,9 +2258,32 @@ mod tests {
territory_access_cost: Some(750000),
..RuntimeWorldRestoreState::default()
},
cargo_catalog: vec![
crate::RuntimeCargoCatalogEntry {
slot_id: 1,
label: "Cargo Production Slot 1".to_string(),
cargo_class: RuntimeCargoClass::Factory,
supplied_token_stem: None,
demanded_token_stem: None,
},
crate::RuntimeCargoCatalogEntry {
slot_id: 5,
label: "Cargo Production Slot 5".to_string(),
cargo_class: RuntimeCargoClass::FarmMine,
supplied_token_stem: None,
demanded_token_stem: None,
},
crate::RuntimeCargoCatalogEntry {
slot_id: 9,
label: "Cargo Production Slot 9".to_string(),
cargo_class: RuntimeCargoClass::Other,
supplied_token_stem: None,
demanded_token_stem: None,
},
],
named_locomotive_availability: BTreeMap::from([(String::from("Big Boy"), 42)]),
named_locomotive_cost: BTreeMap::from([(String::from("GP7"), 175000)]),
cargo_production_overrides: BTreeMap::from([(1, 125), (2, 75)]),
cargo_production_overrides: BTreeMap::from([(1, 125), (5, 75), (9, 30)]),
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 25,
@ -2259,7 +2312,19 @@ mod tests {
},
RuntimeCondition::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 200,
value: 230,
},
RuntimeCondition::FactoryProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::FarmMineProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 75,
},
RuntimeCondition::OtherCargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 30,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,