Execute recovered world scalar event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-16 11:39:59 -07:00
commit 13c7268b0d
23 changed files with 675 additions and 98 deletions

View file

@ -4468,6 +4468,9 @@ mod tests {
let scalar_band_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json",
);
let world_scalar_executable_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-executable-save-slice-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize");
@ -4496,6 +4499,8 @@ mod tests {
.expect("overlay-backed locomotive cost fixture should summarize");
run_runtime_summarize_fixture(&scalar_band_parity_fixture)
.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");
}
#[test]

View file

@ -186,6 +186,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
@ -359,6 +360,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},

View file

@ -58,6 +58,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub world_restore_economic_status_code: Option<i32>,
#[serde(default)]
pub world_restore_territory_access_cost: Option<u32>,
#[serde(default)]
pub world_restore_absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub world_restore_absolute_counter_adjustment_context: Option<String>,
@ -160,6 +162,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub named_locomotive_cost_count: Option<usize>,
#[serde(default)]
pub cargo_production_override_count: Option<usize>,
#[serde(default)]
pub special_condition_count: Option<usize>,
#[serde(default)]
pub enabled_special_condition_count: Option<usize>,
@ -373,6 +377,14 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(value) = self.world_restore_territory_access_cost {
if actual.world_restore_territory_access_cost != Some(value) {
mismatches.push(format!(
"world_restore_territory_access_cost mismatch: expected {value}, got {:?}",
actual.world_restore_territory_access_cost
));
}
}
if let Some(kind) = &self.world_restore_absolute_counter_restore_kind {
if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) {
mismatches.push(format!(
@ -785,6 +797,14 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.cargo_production_override_count {
if actual.cargo_production_override_count != count {
mismatches.push(format!(
"cargo_production_override_count mismatch: expected {count}, got {}",
actual.cargo_production_override_count
));
}
}
if let Some(count) = self.special_condition_count {
if actual.special_condition_count != count {
mismatches.push(format!(

View file

@ -96,6 +96,7 @@ struct SaveSliceProjection {
candidate_availability: BTreeMap<String, u32>,
named_locomotive_availability: BTreeMap<String, u32>,
named_locomotive_cost: BTreeMap<String, u32>,
cargo_production_overrides: BTreeMap<u32, u32>,
special_conditions: BTreeMap<String, u32>,
}
@ -251,6 +252,7 @@ pub fn project_save_slice_to_runtime_state_import(
candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
named_locomotive_cost: projection.named_locomotive_cost,
cargo_production_overrides: projection.cargo_production_overrides,
special_conditions: projection.special_conditions,
service_state: RuntimeServiceState::default(),
};
@ -293,7 +295,10 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
calendar: base_state.calendar,
world_flags,
save_profile: projection.save_profile,
world_restore: projection.world_restore,
world_restore: RuntimeWorldRestoreState {
territory_access_cost: base_state.world_restore.territory_access_cost,
..projection.world_restore
},
metadata,
companies: base_state.companies.clone(),
selected_company_id: base_state.selected_company_id,
@ -311,6 +316,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
named_locomotive_cost: base_state.named_locomotive_cost.clone(),
cargo_production_overrides: base_state.cargo_production_overrides.clone(),
special_conditions: projection.special_conditions,
service_state: base_state.service_state.clone(),
};
@ -554,6 +560,7 @@ fn project_save_slice_components(
ai_ignore_territories_at_startup_enabled: special_condition_enabled(34),
limited_track_building_amount: None,
economic_status_code: None,
territory_access_cost: None,
absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(),
),
@ -640,6 +647,7 @@ fn project_save_slice_components(
}
let named_locomotive_cost = BTreeMap::new();
let cargo_production_overrides = BTreeMap::new();
for (index, note) in save_slice.notes.iter().enumerate() {
metadata.insert(format!("save_slice.note.{index}"), note.clone());
@ -655,6 +663,7 @@ fn project_save_slice_components(
candidate_availability,
named_locomotive_availability,
named_locomotive_cost,
cargo_production_overrides,
special_conditions,
})
}
@ -978,6 +987,14 @@ fn lower_contextual_real_grouped_effects(
let mut effects = Vec::with_capacity(record.grouped_effect_rows.len());
for row in &record.grouped_effect_rows {
if let Some(effect) = lower_contextual_cargo_production_effect(row)? {
effects.push(effect);
continue;
}
if let Some(effect) = lower_contextual_territory_access_cost_effect(row)? {
effects.push(effect);
continue;
}
if let Some(effect) = lower_contextual_locomotive_cost_effect(row, company_context)? {
effects.push(effect);
continue;
@ -1011,10 +1028,8 @@ fn lower_contextual_locomotive_availability_effect(
if row.row_shape != "scalar_assignment" {
return Ok(None);
}
let value = match row.raw_scalar_value {
0 => false,
1 => true,
_ => return Ok(None),
let Some(value) = u32::try_from(row.raw_scalar_value).ok() else {
return Ok(None);
};
let Some(locomotive_id) = row.recovered_locomotive_id else {
return Ok(None);
@ -1026,12 +1041,48 @@ fn lower_contextual_locomotive_availability_effect(
else {
return Err(ImportBlocker::MissingLocomotiveCatalogContext);
};
Ok(Some(RuntimeEffect::SetNamedLocomotiveAvailability {
Ok(Some(RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name,
value,
}))
}
fn lower_contextual_cargo_production_effect(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
) -> Result<Option<RuntimeEffect>, ImportBlocker> {
if row.parameter_family.as_deref() != Some("cargo_production_scalar") {
return Ok(None);
}
if row.row_shape != "scalar_assignment" {
return Ok(None);
}
let Some(value) = u32::try_from(row.raw_scalar_value).ok() else {
return Ok(None);
};
let Some(slot) = row.descriptor_id.checked_sub(229) else {
return Ok(None);
};
if !(1..=11).contains(&slot) {
return Ok(None);
}
Ok(Some(RuntimeEffect::SetCargoProductionSlot { slot, value }))
}
fn lower_contextual_territory_access_cost_effect(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
) -> Result<Option<RuntimeEffect>, ImportBlocker> {
if row.parameter_family.as_deref() != Some("territory_access_cost_scalar") {
return Ok(None);
}
if row.row_shape != "scalar_assignment" {
return Ok(None);
}
let Some(value) = u32::try_from(row.raw_scalar_value).ok() else {
return Ok(None);
};
Ok(Some(RuntimeEffect::SetTerritoryAccessCost { value }))
}
fn lower_contextual_locomotive_cost_effect(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
company_context: &ImportRuntimeContext,
@ -1288,12 +1339,27 @@ fn lower_condition_targets_in_effect(
value: *value,
}
}
RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => {
RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name: name.clone(),
value: *value,
}
}
RuntimeEffect::SetNamedLocomotiveCost { name, value } => {
RuntimeEffect::SetNamedLocomotiveCost {
name: name.clone(),
value: *value,
}
}
RuntimeEffect::SetCargoProductionSlot { slot, value } => {
RuntimeEffect::SetCargoProductionSlot {
slot: *slot,
value: *value,
}
}
RuntimeEffect::SetTerritoryAccessCost { value } => {
RuntimeEffect::SetTerritoryAccessCost { value: *value }
}
RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition {
label: label.clone(),
value: *value,
@ -1729,12 +1795,27 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value,
})
}
RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => {
Ok(RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name: name.clone(),
value: *value,
})
}
RuntimeEffect::SetNamedLocomotiveCost { name, value } => {
Ok(RuntimeEffect::SetNamedLocomotiveCost {
name: name.clone(),
value: *value,
})
}
RuntimeEffect::SetCargoProductionSlot { slot, value } => {
Ok(RuntimeEffect::SetCargoProductionSlot {
slot: *slot,
value: *value,
})
}
RuntimeEffect::SetTerritoryAccessCost { value } => {
Ok(RuntimeEffect::SetTerritoryAccessCost { value: *value })
}
RuntimeEffect::SetSpecialCondition { label, value } => {
Ok(RuntimeEffect::SetSpecialCondition {
label: label.clone(),
@ -2296,7 +2377,10 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
| RuntimeEffect::SetNamedLocomotiveCost { .. }
| RuntimeEffect::SetCargoProductionSlot { .. }
| RuntimeEffect::SetTerritoryAccessCost { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
@ -2375,7 +2459,10 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
| RuntimeEffect::SetNamedLocomotiveCost { .. }
| RuntimeEffect::SetCargoProductionSlot { .. }
| RuntimeEffect::SetTerritoryAccessCost { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
@ -2717,6 +2804,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
}
@ -4882,7 +4970,7 @@ mod tests {
}
#[test]
fn leaves_recovered_locomotive_availability_rows_blocked_unmapped_world_descriptor() {
fn blocks_scalar_locomotive_availability_rows_without_catalog_context() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -4953,7 +5041,7 @@ mod tests {
executable_import_ready: false,
notes: vec![
"decoded from grounded real 0x4e9a row framing".to_string(),
"recovered locomotive availability descriptor family remains parity-only"
"scalar locomotive availability rows still need catalog context"
.to_string(),
],
}],
@ -4975,7 +5063,7 @@ mod tests {
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_world_descriptor")
Some("blocked_missing_locomotive_catalog_context")
);
}
@ -5056,7 +5144,7 @@ mod tests {
}
#[test]
fn overlays_boolean_locomotive_availability_rows_into_named_availability_effects() {
fn overlays_scalar_locomotive_availability_rows_into_named_availability_effects() {
let base_state = RuntimeState {
calendar: CalendarPoint {
year: 1845,
@ -5094,6 +5182,7 @@ mod tests {
("Locomotive 112".to_string(), 1),
]),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -5141,14 +5230,14 @@ mod tests {
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![2, 0, 0, 0],
grouped_effect_rows: vec![
real_locomotive_availability_row(250, 1),
real_locomotive_availability_row(457, 0),
real_locomotive_availability_row(250, 42),
real_locomotive_availability_row(457, 7),
],
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec![
"boolean locomotive availability rows use overlay catalog context"
"scalar locomotive availability rows use overlay catalog context"
.to_string(),
],
}],
@ -5185,14 +5274,14 @@ mod tests {
.state
.named_locomotive_availability
.get("Locomotive 10"),
Some(&1)
Some(&42)
);
assert_eq!(
import
.state
.named_locomotive_availability
.get("Locomotive 112"),
Some(&0)
Some(&7)
);
}
@ -5385,6 +5474,7 @@ mod tests {
("Locomotive 1".to_string(), 100000),
("Locomotive 101".to_string(), 200000),
]),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -5554,7 +5644,7 @@ mod tests {
}
#[test]
fn keeps_recovered_cargo_production_rows_parity_only() {
fn imports_recovered_cargo_production_rows_into_runtime_records() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -5602,32 +5692,42 @@ mod tests {
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["cargo production rows remain metadata-only".to_string()],
notes: vec![
"cargo production rows now import through world overrides".to_string(),
],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
let mut import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-cargo-production-frontier",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(import.state.event_runtime_records.len(), 1);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_world_descriptor")
Some("imported")
);
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("cargo production runtime record should run");
assert_eq!(import.state.cargo_production_overrides.get(&1), Some(&125));
}
#[test]
fn keeps_recovered_territory_access_cost_rows_parity_only() {
fn imports_recovered_territory_access_cost_rows_into_runtime_records() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -5675,27 +5775,40 @@ mod tests {
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["territory access cost rows remain metadata-only".to_string()],
notes: vec![
"territory access cost rows now import through world restore".to_string(),
],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
let mut import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-territory-access-cost-frontier",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(import.state.event_runtime_records.len(), 1);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_world_descriptor")
Some("imported")
);
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("territory access cost runtime record should run");
assert_eq!(
import.state.world_restore.territory_access_cost,
Some(750000)
);
}
@ -5736,6 +5849,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -8596,6 +8710,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
periodic_boundary_calls: 9,
@ -8766,6 +8881,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},

View file

@ -106,6 +106,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},

View file

@ -317,10 +317,21 @@ pub enum RuntimeEffect {
name: String,
value: bool,
},
SetNamedLocomotiveAvailabilityValue {
name: String,
value: u32,
},
SetNamedLocomotiveCost {
name: String,
value: u32,
},
SetCargoProductionSlot {
slot: u32,
value: u32,
},
SetTerritoryAccessCost {
value: u32,
},
SetSpecialCondition {
label: String,
value: u32,
@ -614,6 +625,8 @@ pub struct RuntimeWorldRestoreState {
#[serde(default)]
pub economic_status_code: Option<i32>,
#[serde(default)]
pub territory_access_cost: Option<u32>,
#[serde(default)]
pub absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub absolute_counter_adjustment_context: Option<String>,
@ -659,6 +672,8 @@ pub struct RuntimeState {
#[serde(default)]
pub named_locomotive_cost: BTreeMap<String, u32>,
#[serde(default)]
pub cargo_production_overrides: BTreeMap<u32, u32>,
#[serde(default)]
pub special_conditions: BTreeMap<String, u32>,
#[serde(default)]
pub service_state: RuntimeServiceState,
@ -1173,6 +1188,14 @@ impl RuntimeState {
return Err("named_locomotive_cost contains an empty key".to_string());
}
}
for slot in self.cargo_production_overrides.keys() {
if !(1..=11).contains(slot) {
return Err(format!(
"cargo_production_overrides contains out-of-range slot {}",
slot
));
}
}
for key in self.special_conditions.keys() {
if key.trim().is_empty() {
return Err("special_conditions contains an empty key".to_string());
@ -1249,11 +1272,22 @@ fn validate_runtime_effect(
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, .. } => {
if name.trim().is_empty() {
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetNamedLocomotiveCost { name, .. } => {
if name.trim().is_empty() {
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetCargoProductionSlot { slot, .. } => {
if !(1..=11).contains(slot) {
return Err("slot must be in 1..=11".to_string());
}
}
RuntimeEffect::SetTerritoryAccessCost { .. } => {}
RuntimeEffect::SetSpecialCondition { label, .. } => {
if label.trim().is_empty() {
return Err("label must not be empty".to_string());
@ -1476,6 +1510,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1512,6 +1547,7 @@ mod tests {
ai_ignore_territories_at_startup_enabled: Some(false),
limited_track_building_amount: None,
economic_status_code: None,
territory_access_cost: None,
absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(),
),
@ -1534,6 +1570,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1591,6 +1628,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1658,6 +1696,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1756,6 +1795,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1800,6 +1840,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1844,6 +1885,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1905,6 +1947,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1956,6 +1999,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2011,6 +2055,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2062,6 +2107,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2119,6 +2165,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2170,6 +2217,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2221,6 +2269,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};

View file

@ -2838,7 +2838,7 @@ fn recovered_cargo_production_descriptor_metadata(
target_mask_bits: 0x08,
parameter_family: "cargo_production_scalar",
runtime_key: None,
executable_in_runtime: false,
executable_in_runtime: true,
}
})
}
@ -2951,7 +2951,7 @@ fn recovered_territory_access_cost_descriptor_metadata(
target_mask_bits: 0x08,
parameter_family: "territory_access_cost_scalar",
runtime_key: None,
executable_in_runtime: false,
executable_in_runtime: true,
})
}
@ -3241,6 +3241,28 @@ fn decode_real_grouped_effect_action(
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.parameter_family == "cargo_production_scalar"
&& row.row_shape == "scalar_assignment"
&& row.raw_scalar_value >= 0
{
let slot = descriptor_metadata.descriptor_id.checked_sub(229)?;
return Some(RuntimeEffect::SetCargoProductionSlot {
slot,
value: row.raw_scalar_value as u32,
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.parameter_family == "territory_access_cost_scalar"
&& row.row_shape == "scalar_assignment"
&& row.raw_scalar_value >= 0
{
return Some(RuntimeEffect::SetTerritoryAccessCost {
value: row.raw_scalar_value as u32,
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.parameter_family == "world_flag_toggle"
&& row.row_shape == "bool_toggle"
@ -3489,7 +3511,10 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
| RuntimeEffect::SetNamedLocomotiveCost { .. }
| RuntimeEffect::SetCargoProductionSlot { .. }
| RuntimeEffect::SetTerritoryAccessCost { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ConfiscateCompanyAssets { .. }
| RuntimeEffect::DeactivateCompany { .. }
@ -9345,7 +9370,7 @@ mod tests {
assert_eq!(metadata.target_mask_bits, 0x08);
assert_eq!(metadata.parameter_family, "cargo_production_scalar");
assert_eq!(metadata.runtime_key, None);
assert!(!metadata.executable_in_runtime);
assert!(metadata.executable_in_runtime);
}
#[test]
@ -9381,7 +9406,7 @@ mod tests {
assert_eq!(metadata.target_mask_bits, 0x08);
assert_eq!(metadata.parameter_family, "territory_access_cost_scalar");
assert_eq!(metadata.runtime_key, None);
assert!(!metadata.executable_in_runtime);
assert!(metadata.executable_in_runtime);
}
#[test]

View file

@ -495,9 +495,20 @@ fn apply_runtime_effects(
.named_locomotive_availability
.insert(name.clone(), u32::from(*value));
}
RuntimeEffect::SetNamedLocomotiveAvailabilityValue { name, value } => {
state
.named_locomotive_availability
.insert(name.clone(), *value);
}
RuntimeEffect::SetNamedLocomotiveCost { name, value } => {
state.named_locomotive_cost.insert(name.clone(), *value);
}
RuntimeEffect::SetCargoProductionSlot { slot, value } => {
state.cargo_production_overrides.insert(*slot, *value);
}
RuntimeEffect::SetTerritoryAccessCost { value } => {
state.world_restore.territory_access_cost = Some(*value);
}
RuntimeEffect::SetSpecialCondition { label, value } => {
state.special_conditions.insert(label.clone(), *value);
}
@ -1169,6 +1180,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
}
@ -1457,6 +1469,80 @@ mod tests {
assert_eq!(result.service_events[0].applied_effect_count, 2);
}
#[test]
fn applies_scalar_named_locomotive_availability_effects() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 12,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name: "Big Boy".to_string(),
value: 42,
},
RuntimeEffect::SetNamedLocomotiveAvailabilityValue {
name: "GP7".to_string(),
value: 7,
},
],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("scalar named locomotive availability effects should succeed");
assert_eq!(
state.named_locomotive_availability.get("Big Boy"),
Some(&42)
);
assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&7));
assert_eq!(result.service_events[0].applied_effect_count, 2);
}
#[test]
fn applies_world_scalar_override_effects() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 13,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::SetCargoProductionSlot {
slot: 1,
value: 125,
},
RuntimeEffect::SetTerritoryAccessCost { value: 750000 },
],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("world scalar override effects should succeed");
assert_eq!(state.cargo_production_overrides.get(&1), Some(&125));
assert_eq!(state.world_restore.territory_access_cost, Some(750000));
assert_eq!(result.service_events[0].applied_effect_count, 2);
}
#[test]
fn resolves_symbolic_company_targets() {
let mut state = RuntimeState {

View file

@ -26,6 +26,7 @@ pub struct RuntimeSummary {
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
pub world_restore_limited_track_building_amount: Option<i32>,
pub world_restore_economic_status_code: Option<i32>,
pub world_restore_territory_access_cost: Option<u32>,
pub world_restore_absolute_counter_restore_kind: Option<String>,
pub world_restore_absolute_counter_adjustment_context: Option<String>,
pub metadata_count: usize,
@ -77,6 +78,7 @@ pub struct RuntimeSummary {
pub named_locomotive_availability_count: usize,
pub zero_named_locomotive_availability_count: usize,
pub named_locomotive_cost_count: usize,
pub cargo_production_override_count: usize,
pub special_condition_count: usize,
pub enabled_special_condition_count: usize,
pub save_profile_kind: Option<String>,
@ -149,6 +151,7 @@ impl RuntimeSummary {
.world_restore
.limited_track_building_amount,
world_restore_economic_status_code: state.world_restore.economic_status_code,
world_restore_territory_access_cost: state.world_restore.territory_access_cost,
world_restore_absolute_counter_restore_kind: state
.world_restore
.absolute_counter_restore_kind
@ -597,6 +600,7 @@ impl RuntimeSummary {
.filter(|value| **value == 0)
.count(),
named_locomotive_cost_count: state.named_locomotive_cost.len(),
cargo_production_override_count: state.cargo_production_overrides.len(),
special_condition_count: state.special_conditions.len(),
enabled_special_condition_count: state
.special_conditions
@ -820,6 +824,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -920,6 +925,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -969,6 +975,7 @@ mod tests {
("Mikado".to_string(), 0),
]),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1010,6 +1017,7 @@ mod tests {
("Big Boy".to_string(), 250000),
("GP7".to_string(), 175000),
]),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1019,6 +1027,47 @@ mod tests {
assert_eq!(summary.named_locomotive_cost_count, 2);
}
#[test]
fn counts_world_scalar_override_surfaces() {
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 {
territory_access_cost: Some(750000),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::from([(1, 125), (2, 250)]),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.cargo_production_override_count, 2);
assert_eq!(summary.world_restore_territory_access_cost, Some(750000));
}
#[test]
fn counts_world_frontier_buckets_separately() {
let state = RuntimeState {
@ -1110,6 +1159,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1189,6 +1239,7 @@ mod tests {
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};