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

@ -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(),
},