Implement world-scalar packed event conditions

This commit is contained in:
Jan Petykiewicz 2026-04-16 13:48:55 -07:00
commit 3f2632e330
12 changed files with 1541 additions and 8 deletions

View file

@ -4476,6 +4476,13 @@ mod tests {
let world_scalar_executable_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-executable-save-slice-fixture.json",
);
let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json",
);
let world_scalar_condition_parity_fixture =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize");
@ -4511,6 +4518,10 @@ mod tests {
.expect("save-slice-backed recovered scalar-band parity fixture should summarize");
run_runtime_summarize_fixture(&world_scalar_executable_fixture)
.expect("save-slice-backed executable world-scalar fixture should summarize");
run_runtime_summarize_fixture(&world_scalar_condition_fixture)
.expect("save-slice-backed executable world-scalar condition fixture should summarize");
run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture)
.expect("save-slice-backed parity world-scalar condition fixture should summarize");
}
#[test]

View file

@ -1544,6 +1544,42 @@ fn lower_condition_targets_in_condition(
comparator: *comparator,
value: *value,
},
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name,
comparator,
value,
} => RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: name.clone(),
comparator: *comparator,
value: *value,
},
RuntimeCondition::NamedLocomotiveCostThreshold {
name,
comparator,
value,
} => RuntimeCondition::NamedLocomotiveCostThreshold {
name: name.clone(),
comparator: *comparator,
value: *value,
},
RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => {
RuntimeCondition::CargoProductionTotalThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => {
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => {
RuntimeCondition::TerritoryAccessCostThreshold {
comparator: *comparator,
value: *value,
}
}
RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => {
RuntimeCondition::EconomicStatusCodeThreshold {
comparator: *comparator,
@ -1633,6 +1669,11 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
| RuntimeCondition::WorldFlagEquals { .. } => false,
}
@ -2251,6 +2292,11 @@ fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool {
condition,
RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
| RuntimeCondition::WorldFlagEquals { .. }
)
@ -2262,12 +2308,18 @@ fn ordinary_condition_row_is_world_state_family(
row.metric.as_deref().is_some_and(|metric| {
metric.contains("Special Condition")
|| metric.contains("Candidate Availability")
|| metric.contains("Named Locomotive")
|| metric.contains("Cargo Production")
|| metric.contains("Limited Track Building Amount")
|| metric.contains("Territory Access Cost")
|| metric.contains("Economic Status")
|| metric.contains("World Flag")
}) || row
.semantic_family
.as_deref()
.is_some_and(|family| family == "world_state_threshold" || family == "world_flag_equals")
}) || row.semantic_family.as_deref().is_some_and(|family| {
matches!(
family,
"world_state_threshold" | "world_scalar_threshold" | "world_flag_equals"
)
})
}
fn real_grouped_row_is_world_state_family(
@ -2334,6 +2386,11 @@ fn runtime_condition_company_target_import_blocker(
.or_else(|| territory_target_import_blocker(territory, company_context)),
RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
| RuntimeCondition::WorldFlagEquals { .. } => None,
}
@ -2855,7 +2912,10 @@ fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf {
#[cfg(test)]
mod tests {
use super::*;
use crate::{RuntimeTrackPieceCounts, RuntimeTrain, StepCommand, execute_step_command};
use crate::{
RuntimeConditionComparator, RuntimeTrackPieceCounts, RuntimeTrain, StepCommand,
execute_step_command,
};
fn state() -> RuntimeState {
RuntimeState {
@ -7859,6 +7919,262 @@ mod tests {
);
}
#[test]
fn imports_and_executes_world_scalar_conditions_through_runtime_state() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
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()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7800,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 45,
live_record_count: 2,
live_entry_ids: vec![41, 45],
decoded_record_count: 2,
imported_runtime_record_count: 2,
records: vec![
crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 41,
payload_offset: Some(0x7200),
payload_len: Some(192),
decode_status: "executable".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![5, 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_limited_track_building_amount_row(18),
real_territory_access_cost_row(750000),
],
decoded_conditions: vec![],
decoded_actions: vec![
RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name: "Big Boy".to_string(),
value: 42,
},
RuntimeEffect::SetNamedLocomotiveCost {
name: "Locomotive 1".to_string(),
value: 250000,
},
RuntimeEffect::SetCargoProductionSlot {
slot: 1,
value: 125,
},
RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 },
RuntimeEffect::SetTerritoryAccessCost { value: 750000 },
],
executable_import_ready: true,
notes: vec!["world-scalar setup record".to_string()],
},
crate::SmpLoadedPackedEventRecordSummary {
record_index: 1,
live_entry_id: 45,
payload_offset: Some(0x7300),
payload_len: Some(184),
decode_status: "executable".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 5,
standalone_condition_rows: vec![
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: 2422,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&42_i32.to_le_bytes());
bytes
},
candidate_name: Some("Big Boy".to_string()),
comparator: Some("eq".to_string()),
metric: Some("Named Locomotive Availability: Big Boy".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Named Locomotive Availability: Big Boy == 42".to_string(),
),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 1,
raw_condition_id: 2423,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&250000_i32.to_le_bytes());
bytes
},
candidate_name: Some("Locomotive 1".to_string()),
comparator: Some("eq".to_string()),
metric: Some("Named Locomotive Cost: Locomotive 1".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Named Locomotive Cost: Locomotive 1 == 250000"
.to_string(),
),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 2,
raw_condition_id: 2418,
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("Cargo Production Total".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Cargo Production Total == 125".to_string(),
),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 3,
raw_condition_id: 2547,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&18_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Limited Track Building Amount".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Limited Track Building Amount == 18".to_string(),
),
requires_candidate_name_binding: false,
notes: vec![],
},
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 4,
raw_condition_id: 1516,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&750000_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Territory Access Cost".to_string()),
semantic_family: Some("world_scalar_threshold".to_string()),
semantic_preview: Some(
"Test Territory Access Cost == 750000".to_string(),
),
requires_candidate_name_binding: false,
notes: vec![],
},
],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_world_flag_row(
110,
"Disable Stock Buying and Selling",
true,
)],
decoded_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::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 125,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,
},
RuntimeCondition::TerritoryAccessCostThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 750000,
},
],
decoded_actions: vec![RuntimeEffect::SetWorldFlag {
key: "world.world_scalar_conditions_passed".to_string(),
value: true,
}],
executable_import_ready: true,
notes: vec!["world-scalar conditions gate a world-side effect".to_string()],
},
],
}),
notes: vec![],
};
let mut import = project_save_slice_to_runtime_state_import(
&save_slice,
"world-scalar-condition-save-slice",
None,
)
.expect("save-slice import should project");
crate::execute_step_command(
&mut import.state,
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("setup trigger should execute");
crate::execute_step_command(
&mut import.state,
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("gated trigger should execute");
assert_eq!(
import
.state
.world_flags
.get("world.world_scalar_conditions_passed"),
Some(&true)
);
}
#[test]
fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() {
let base_state = state();

View file

@ -244,6 +244,28 @@ pub enum RuntimeCondition {
comparator: RuntimeConditionComparator,
value: i64,
},
NamedLocomotiveAvailabilityThreshold {
name: String,
comparator: RuntimeConditionComparator,
value: i64,
},
NamedLocomotiveCostThreshold {
name: String,
comparator: RuntimeConditionComparator,
value: i64,
},
CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
TerritoryAccessCostThreshold {
comparator: RuntimeConditionComparator,
value: i64,
},
EconomicStatusCodeThreshold {
comparator: RuntimeConditionComparator,
value: i64,
@ -1375,6 +1397,17 @@ fn validate_runtime_condition(
Ok(())
}
}
RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name, .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { name, .. } => {
if name.trim().is_empty() {
Err("name must not be empty".to_string())
} else {
Ok(())
}
}
RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()),
RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()),
RuntimeCondition::WorldFlagEquals { key, .. } => {
if key.trim().is_empty() {

View file

@ -240,6 +240,11 @@ enum RealOrdinaryConditionMetric {
enum RealWorldConditionKind {
SpecialCondition { label: &'static str },
CandidateAvailability,
NamedLocomotiveAvailability,
NamedLocomotiveCost,
CargoProductionTotal,
LimitedTrackBuildingAmount,
TerritoryAccessCost,
EconomicStatus,
WorldFlag { key: &'static str },
}
@ -258,8 +263,13 @@ struct RealOrdinaryConditionMetadata {
}
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435;
const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422;
const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423;
const REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2418;
const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547;
const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516;
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 24] = [
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 29] = [
RealOrdinaryConditionMetadata {
raw_condition_id: 1802,
label: "Current Cash",
@ -419,6 +429,35 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 24] = [
label: "%1 Avail.",
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID,
label: "Unknown Loco Available",
kind: RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::NamedLocomotiveAvailability,
),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID,
label: "Unknown Loco Cost",
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
label: "All Cargo Production",
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID,
label: "Limited Track Building Amount",
kind: RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::LimitedTrackBuildingAmount,
),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_TERRITORY_ACCESS_COST_CONDITION_ID,
label: "Access Rights Cost:",
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2350,
label: "Economic Status",
@ -2526,6 +2565,17 @@ fn parse_real_condition_row_summary(
.to_string(),
);
}
if ordinary_metadata.is_some_and(|metadata| {
matches!(
metadata.kind,
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::NamedLocomotiveAvailability
| RealWorldConditionKind::NamedLocomotiveCost
)
) && candidate_name.is_none()
}) {
notes.push("named locomotive condition row is missing its side-string binding".to_string());
}
Some(SmpLoadedPackedEventConditionRowSummary {
row_index,
raw_condition_id,
@ -2645,6 +2695,27 @@ fn real_ordinary_condition_metric_label(
None => "Candidate Availability".to_string(),
}
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::NamedLocomotiveAvailability,
) => match candidate_name {
Some(name) => format!("Named Locomotive Availability: {name}"),
None => "Named Locomotive Availability".to_string(),
},
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => {
match candidate_name {
Some(name) => format!("Named Locomotive Cost: {name}"),
None => "Named Locomotive Cost".to_string(),
}
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => {
"Cargo Production Total".to_string()
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::LimitedTrackBuildingAmount,
) => "Limited Track Building Amount".to_string(),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => {
"Territory Access Cost".to_string()
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
"Economic Status".to_string()
}
@ -2662,6 +2733,13 @@ fn real_ordinary_condition_semantic_family(
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => {
"world_flag_equals"
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::NamedLocomotiveAvailability
| RealWorldConditionKind::NamedLocomotiveCost
| RealWorldConditionKind::CargoProductionTotal
| RealWorldConditionKind::LimitedTrackBuildingAmount
| RealWorldConditionKind::TerritoryAccessCost,
) => "world_scalar_threshold",
RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold",
}
}
@ -2868,6 +2946,32 @@ fn decode_real_condition_row(
comparator,
value,
}),
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::NamedLocomotiveAvailability,
) => row.candidate_name.as_ref().map(|name| {
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: name.clone(),
comparator,
value,
}
}),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => row
.candidate_name
.as_ref()
.map(|name| RuntimeCondition::NamedLocomotiveCostThreshold {
name: name.clone(),
comparator,
value,
}),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => {
Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value })
}
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::LimitedTrackBuildingAmount,
) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => {
Some(RuntimeCondition::TerritoryAccessCostThreshold { comparator, value })
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value })
}
@ -3655,6 +3759,11 @@ fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) ->
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
| RuntimeCondition::CargoProductionTotalThreshold { .. }
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
| RuntimeCondition::WorldFlagEquals { .. } => true,
}
@ -9431,6 +9540,224 @@ mod tests {
);
}
#[test]
fn decodes_real_named_locomotive_availability_threshold_from_checked_in_metadata() {
let condition_row = build_real_condition_row_with_threshold(
REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID,
4,
42,
Some("Big Boy"),
);
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[condition_row],
[&[], &[], &[], &[]],
);
let mut bytes = Vec::new();
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for word in header_words {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(&[0x00, 0x00]);
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
bytes.extend_from_slice(&record_body);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
let report = inspect_smp_bytes(&bytes);
let summary = report
.event_runtime_collection_summary
.as_ref()
.expect("event runtime collection summary should parse");
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.metric
.as_deref(),
Some("Named Locomotive Availability: Big Boy")
);
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.semantic_family
.as_deref(),
Some("world_scalar_threshold")
);
assert_eq!(
summary.records[0].decoded_conditions,
vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: "Big Boy".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 42,
}]
);
}
#[test]
fn decodes_real_named_locomotive_cost_threshold_from_checked_in_metadata() {
let condition_row = build_real_condition_row_with_threshold(
REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID,
0,
250000,
Some("Locomotive 1"),
);
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[condition_row],
[&[], &[], &[], &[]],
);
let mut bytes = Vec::new();
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for word in header_words {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(&[0x00, 0x00]);
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
bytes.extend_from_slice(&record_body);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
let report = inspect_smp_bytes(&bytes);
let summary = report
.event_runtime_collection_summary
.as_ref()
.expect("event runtime collection summary should parse");
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.metric
.as_deref(),
Some("Named Locomotive Cost: Locomotive 1")
);
assert_eq!(
summary.records[0].decoded_conditions,
vec![RuntimeCondition::NamedLocomotiveCostThreshold {
name: "Locomotive 1".to_string(),
comparator: RuntimeConditionComparator::Ge,
value: 250000,
}]
);
}
#[test]
fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() {
let condition_rows = vec![
build_real_condition_row_with_threshold(
REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
0,
200,
None,
),
build_real_condition_row_with_threshold(
REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID,
4,
18,
None,
),
build_real_condition_row_with_threshold(
REAL_TERRITORY_ACCESS_COST_CONDITION_ID,
4,
750000,
None,
),
];
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&condition_rows,
[&[], &[], &[], &[]],
);
let mut bytes = Vec::new();
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for word in header_words {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(&[0x00, 0x00]);
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
bytes.extend_from_slice(&record_body);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
let report = inspect_smp_bytes(&bytes);
let summary = report
.event_runtime_collection_summary
.as_ref()
.expect("event runtime collection summary should parse");
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.metric
.as_deref(),
Some("Cargo Production Total")
);
assert_eq!(
summary.records[0].standalone_condition_rows[0]
.semantic_family
.as_deref(),
Some("world_scalar_threshold")
);
assert_eq!(
summary.records[0].decoded_conditions,
vec![
RuntimeCondition::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Ge,
value: 200,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,
},
RuntimeCondition::TerritoryAccessCostThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 750000,
},
]
);
}
#[test]
fn decodes_real_world_flag_condition_from_checked_in_world_condition_metadata() {
let condition_row = build_real_condition_row_with_threshold(2535, 4, 1, None);
@ -9492,6 +9819,31 @@ mod tests {
);
}
#[test]
fn looks_up_checked_in_world_scalar_condition_metadata() {
let availability =
real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID)
.expect("availability condition metadata should exist");
assert_eq!(availability.label, "Unknown Loco Available");
let cost = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID)
.expect("cost condition metadata should exist");
assert_eq!(cost.label, "Unknown Loco Cost");
let cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID)
.expect("cargo condition metadata should exist");
assert_eq!(cargo.label, "All Cargo Production");
let build_limit =
real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID)
.expect("build-limit condition metadata should exist");
assert_eq!(build_limit.label, "Limited Track Building Amount");
let access_cost = real_ordinary_condition_metadata(REAL_TERRITORY_ACCESS_COST_CONDITION_ID)
.expect("territory-access-cost condition metadata should exist");
assert_eq!(access_cost.label, "Access Rights Cost:");
}
#[test]
fn looks_up_checked_in_world_flag_descriptor_metadata() {
let metadata =

View file

@ -722,6 +722,67 @@ fn evaluate_record_conditions(
return Ok(None);
}
}
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name,
comparator,
value,
} => {
let actual = state
.named_locomotive_availability
.get(name)
.copied()
.map(i64::from)
.unwrap_or(0);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::NamedLocomotiveCostThreshold {
name,
comparator,
value,
} => {
let actual = state
.named_locomotive_cost
.get(name)
.copied()
.map(i64::from)
.unwrap_or(0);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => {
let actual = state
.cargo_production_overrides
.values()
.copied()
.map(i64::from)
.sum::<i64>();
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => {
let actual = state
.world_restore
.limited_track_building_amount
.map(i64::from)
.unwrap_or(0);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => {
let actual = state
.world_restore
.territory_access_cost
.map(i64::from)
.unwrap_or(0);
if !compare_condition_value(actual, *comparator, *value) {
return Ok(None);
}
}
RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => {
let actual = state
.world_restore
@ -2142,6 +2203,91 @@ mod tests {
assert_eq!(state.world_flags.get("world_condition_failed"), None);
}
#[test]
fn evaluates_world_scalar_conditions_before_effects_run() {
let mut state = RuntimeState {
world_restore: RuntimeWorldRestoreState {
limited_track_building_amount: Some(18),
territory_access_cost: Some(750000),
..RuntimeWorldRestoreState::default()
},
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)]),
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 25,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: vec![
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: "Big Boy".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 42,
},
RuntimeCondition::NamedLocomotiveCostThreshold {
name: "GP7".to_string(),
comparator: RuntimeConditionComparator::Eq,
value: 175000,
},
RuntimeCondition::CargoProductionTotalThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 200,
},
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 18,
},
RuntimeCondition::TerritoryAccessCostThreshold {
comparator: RuntimeConditionComparator::Eq,
value: 750000,
},
],
effects: vec![RuntimeEffect::SetWorldFlag {
key: "world_scalar_condition_passed".to_string(),
value: true,
}],
},
RuntimeEventRecord {
record_id: 26,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
name: "Missing Loco".to_string(),
comparator: RuntimeConditionComparator::Gt,
value: 0,
}],
effects: vec![RuntimeEffect::SetWorldFlag {
key: "world_scalar_condition_failed".to_string(),
value: true,
}],
},
],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("world-scalar conditions should evaluate successfully");
assert_eq!(result.service_events[0].serviced_record_ids, vec![25]);
assert_eq!(
state.world_flags.get("world_scalar_condition_passed"),
Some(&true)
);
assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None);
}
#[test]
fn one_shot_record_only_fires_once() {
let mut state = RuntimeState {