From d3790c2ae32d894b5d17c990c21239163bb66500 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 08:18:34 -0700 Subject: [PATCH] Execute runtime variable event descriptors --- README.md | 11 +- .../event-effects-semantic-catalog.json | 100 +++--- crates/rrt-cli/src/main.rs | 4 + crates/rrt-fixtures/src/load.rs | 8 + crates/rrt-fixtures/src/schema.rs | 40 +++ crates/rrt-runtime/src/import.rs | 216 ++++++++++-- crates/rrt-runtime/src/persistence.rs | 4 + crates/rrt-runtime/src/runtime.rs | 173 +++++++++- crates/rrt-runtime/src/smp.rs | 94 ++++- crates/rrt-runtime/src/step.rs | 172 ++++++++++ crates/rrt-runtime/src/summary.rs | 125 ++++++- docs/README.md | 5 + docs/runtime-rehost-plan.md | 6 +- ...vent-runtime-variable-overlay-fixture.json | 87 +++++ ...packed-event-runtime-variable-overlay.json | 9 + ...ked-event-runtime-variable-save-slice.json | 320 ++++++++++++++++++ .../py/build_event_effect_semantic_catalog.py | 5 + 17 files changed, 1300 insertions(+), 79 deletions(-) create mode 100644 fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json create mode 100644 fixtures/runtime/packed-event-runtime-variable-overlay.json create mode 100644 fixtures/runtime/packed-event-runtime-variable-save-slice.json diff --git a/README.md b/README.md index d098d52..08ed45c 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,10 @@ descriptor residue. The recovered whole-game scalar economy/performance strip `5 bounded runtime landing surface too: representative descriptors import into `RuntimeState.world_scalar_overrides` through stable normalized keys such as `world.build_stations_cost`, `world.track_maintenance_cost`, `world.all_engine_speeds`, and -`world.hotel_revenue`. The grounded aggregate cargo-economics descriptors now have bounded +`world.hotel_revenue`. The runtime-variable strip `39..54` now executes too through bounded +event-owned scalar maps on world/company/player/territory state, without widening save-native +reconstruction or adding a second packed executor. The grounded aggregate cargo-economics +descriptors now have bounded runtime landing surfaces too: descriptor `105` `All Cargo Prices` plus descriptors `177..179` `All Cargo Production` / `All Factory Production` / `All Farm/Mine Production` import into event-owned cargo override state, and the grounded named cargo-production strip `180..229` now @@ -90,8 +93,10 @@ scalar bands are now save-native too. Raw `.smp` inspection/export reconstructs standalone save-slice imports can now lower the grounded lower locomotive availability and locomotive-cost rows directly into `RuntimeState.named_locomotive_availability` and `RuntimeState.named_locomotive_cost` without needing overlay snapshots when the save carries enough -catalog context; the unresolved lower tail and upper locomotive bands now stay on explicit parity -instead of synthetic execution. The remaining recovered scalar world families execute too: +catalog context, and the grounded executable lower prefix now extends through save-backed +locomotive id `61` (`Zephyr`); the unresolved lower tail and upper locomotive bands now stay on +explicit parity instead of synthetic execution. The remaining recovered scalar world families +execute too: cargo-production slots `230..240` lower into `cargo_production_overrides`, and descriptor `453` lowers into `world_restore.territory_access_cost`. Whole-game ordinary-condition breadth now aligns with those diff --git a/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json b/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json index 24dcc9a..a1581bd 100644 --- a/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json +++ b/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json @@ -360,8 +360,8 @@ "target_mask_bits": 8, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 40, @@ -369,8 +369,8 @@ "target_mask_bits": 8, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 41, @@ -378,8 +378,8 @@ "target_mask_bits": 8, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 42, @@ -387,8 +387,8 @@ "target_mask_bits": 8, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 43, @@ -396,8 +396,8 @@ "target_mask_bits": 1, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 44, @@ -405,8 +405,8 @@ "target_mask_bits": 1, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 45, @@ -414,8 +414,8 @@ "target_mask_bits": 1, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 46, @@ -423,8 +423,8 @@ "target_mask_bits": 1, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 47, @@ -432,8 +432,8 @@ "target_mask_bits": 2, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 48, @@ -441,8 +441,8 @@ "target_mask_bits": 2, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 49, @@ -450,8 +450,8 @@ "target_mask_bits": 2, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 50, @@ -459,8 +459,8 @@ "target_mask_bits": 2, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 51, @@ -468,8 +468,8 @@ "target_mask_bits": 4, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 52, @@ -477,8 +477,8 @@ "target_mask_bits": 4, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 53, @@ -486,8 +486,8 @@ "target_mask_bits": 4, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 54, @@ -495,8 +495,8 @@ "target_mask_bits": 4, "parameter_family": "runtime_variable_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 55, @@ -2696,30 +2696,30 @@ }, { "descriptor_id": 299, - "label": "Lower-Band Locomotive Availability Slot 59", + "label": "GP 35 Availability", "target_mask_bits": 11, "parameter_family": "locomotive_availability_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 300, - "label": "Lower-Band Locomotive Availability Slot 60", + "label": "U1 Availability", "target_mask_bits": 11, "parameter_family": "locomotive_availability_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 301, - "label": "Lower-Band Locomotive Availability Slot 61", + "label": "Zephyr Availability", "target_mask_bits": 11, "parameter_family": "locomotive_availability_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 302, @@ -3695,30 +3695,30 @@ }, { "descriptor_id": 410, - "label": "Lower-Band Locomotive Cost Slot 59", + "label": "GP 35 Cost", "target_mask_bits": 11, "parameter_family": "locomotive_cost_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 411, - "label": "Lower-Band Locomotive Cost Slot 60", + "label": "U1 Cost", "target_mask_bits": 11, "parameter_family": "locomotive_cost_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 412, - "label": "Lower-Band Locomotive Cost Slot 61", + "label": "Zephyr Cost", "target_mask_bits": 11, "parameter_family": "locomotive_cost_scalar", "runtime_key": null, - "runtime_status": "evidence_blocked", - "executable_in_runtime": false + "runtime_status": "executable", + "executable_in_runtime": true }, { "descriptor_id": 413, diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 8e7e951..3a1d124 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4479,6 +4479,8 @@ mod tests { let world_scalar_override_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-world-scalar-override-save-slice-fixture.json", ); + let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json"); let cargo_economics_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-cargo-economics-save-slice-fixture.json"); let cargo_economics_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( @@ -4595,6 +4597,8 @@ mod tests { .expect("save-slice-backed executable world-scalar fixture should summarize"); run_runtime_summarize_fixture(&world_scalar_override_fixture) .expect("save-slice-backed world-scalar override fixture should summarize"); + run_runtime_summarize_fixture(&runtime_variable_overlay_fixture) + .expect("overlay-backed runtime-variable fixture should summarize"); run_runtime_summarize_fixture(&cargo_economics_fixture) .expect("save-slice-backed cargo-economics fixture should summarize"); run_runtime_summarize_fixture(&cargo_economics_parity_fixture) diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 9f0a9e5..5db56fa 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -196,6 +196,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -390,6 +394,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 98f94a2..2bfd10f 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -188,6 +188,14 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub cargo_production_override_count: Option, #[serde(default)] + pub world_runtime_variable_count: Option, + #[serde(default)] + pub company_runtime_variable_owner_count: Option, + #[serde(default)] + pub player_runtime_variable_owner_count: Option, + #[serde(default)] + pub territory_runtime_variable_owner_count: Option, + #[serde(default)] pub world_scalar_override_count: Option, #[serde(default)] pub special_condition_count: Option, @@ -927,6 +935,38 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.world_runtime_variable_count { + if actual.world_runtime_variable_count != count { + mismatches.push(format!( + "world_runtime_variable_count mismatch: expected {count}, got {}", + actual.world_runtime_variable_count + )); + } + } + if let Some(count) = self.company_runtime_variable_owner_count { + if actual.company_runtime_variable_owner_count != count { + mismatches.push(format!( + "company_runtime_variable_owner_count mismatch: expected {count}, got {}", + actual.company_runtime_variable_owner_count + )); + } + } + if let Some(count) = self.player_runtime_variable_owner_count { + if actual.player_runtime_variable_owner_count != count { + mismatches.push(format!( + "player_runtime_variable_owner_count mismatch: expected {count}, got {}", + actual.player_runtime_variable_owner_count + )); + } + } + if let Some(count) = self.territory_runtime_variable_owner_count { + if actual.territory_runtime_variable_owner_count != count { + mismatches.push(format!( + "territory_runtime_variable_owner_count mismatch: expected {count}, got {}", + actual.territory_runtime_variable_owner_count + )); + } + } if let Some(count) = self.world_scalar_override_count { if actual.world_scalar_override_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index cacbbd8..65ecb3f 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -292,6 +292,10 @@ pub fn project_save_slice_to_runtime_state_import( farm_mine_cargo_production_override: projection.farm_mine_cargo_production_override, named_cargo_production_overrides: projection.named_cargo_production_overrides, cargo_production_overrides: projection.cargo_production_overrides, + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: projection.world_scalar_overrides, special_conditions: projection.special_conditions, service_state: RuntimeServiceState::default(), @@ -386,6 +390,10 @@ pub fn project_save_slice_overlay_to_runtime_state_import( farm_mine_cargo_production_override: base_state.farm_mine_cargo_production_override, named_cargo_production_overrides: base_state.named_cargo_production_overrides.clone(), cargo_production_overrides: base_state.cargo_production_overrides.clone(), + world_runtime_variables: base_state.world_runtime_variables.clone(), + company_runtime_variables: base_state.company_runtime_variables.clone(), + player_runtime_variables: base_state.player_runtime_variables.clone(), + territory_runtime_variables: base_state.territory_runtime_variables.clone(), world_scalar_overrides: base_state.world_scalar_overrides.clone(), special_conditions: projection.special_conditions, service_state: base_state.service_state.clone(), @@ -1362,6 +1370,10 @@ fn lower_contextual_real_grouped_effects( effects.push(effect); continue; } + if let Some(effect) = lower_contextual_runtime_variable_effect(row)? { + effects.push(effect); + continue; + } if let Some(effect) = lower_contextual_cargo_production_effect(row)? { effects.push(effect); continue; @@ -1436,6 +1448,25 @@ fn lower_contextual_world_scalar_override_effect( })) } +fn lower_contextual_runtime_variable_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> Result, ImportBlocker> { + if row.parameter_family.as_deref() != Some("runtime_variable_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let value = i64::from(row.raw_scalar_value); + Ok(match row.descriptor_id { + 39..=42 => Some(RuntimeEffect::SetWorldVariable { + index: row.descriptor_id - 38, + value, + }), + _ => None, + }) +} + fn lower_contextual_locomotive_availability_effect( row: &SmpLoadedPackedEventGroupedEffectRowSummary, company_context: &ImportRuntimeContext, @@ -1682,12 +1713,28 @@ fn lower_condition_targets_in_effect( value: *value, } } + RuntimeEffect::SetWorldVariable { index, value } => RuntimeEffect::SetWorldVariable { + index: *index, + value: *value, + }, RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value } } RuntimeEffect::SetEconomicStatusCode { value } => { RuntimeEffect::SetEconomicStatusCode { value: *value } } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => RuntimeEffect::SetCompanyVariable { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + index: *index, + value: *value, + }, RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash { target: lower_condition_true_company_target_in_company_target( target, @@ -1695,6 +1742,18 @@ fn lower_condition_targets_in_effect( )?, value: *value, }, + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => RuntimeEffect::SetPlayerVariable { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + index: *index, + value: *value, + }, RuntimeEffect::SetPlayerCash { target, value } => RuntimeEffect::SetPlayerCash { target: lower_condition_true_player_target_in_player_target( target, @@ -1823,6 +1882,15 @@ fn lower_condition_targets_in_effect( value: *value, } } + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => RuntimeEffect::SetTerritoryVariable { + target: target.clone(), + index: *index, + value: *value, + }, RuntimeEffect::SetCargoProductionOverride { target, value } => { RuntimeEffect::SetCargoProductionOverride { target: target.clone(), @@ -2220,12 +2288,35 @@ fn smp_runtime_effect_to_runtime_effect( key: key.clone(), value: *value, }), + RuntimeEffect::SetWorldVariable { index, value } => Ok(RuntimeEffect::SetWorldVariable { + index: *index, + value: *value, + }), RuntimeEffect::SetLimitedTrackBuildingAmount { value } => { Ok(RuntimeEffect::SetLimitedTrackBuildingAmount { value: *value }) } RuntimeEffect::SetEconomicStatusCode { value } => { Ok(RuntimeEffect::SetEconomicStatusCode { value: *value }) } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => { + if company_target_allowed_for_import( + target, + company_context, + allow_condition_true_company, + ) { + Ok(RuntimeEffect::SetCompanyVariable { + target: target.clone(), + index: *index, + value: *value, + }) + } else { + Err(company_target_import_error_message(target, company_context)) + } + } RuntimeEffect::SetCompanyCash { target, value } => { if company_target_allowed_for_import( target, @@ -2240,6 +2331,25 @@ fn smp_runtime_effect_to_runtime_effect( Err(company_target_import_error_message(target, company_context)) } } + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => { + if player_target_allowed_for_import( + target, + company_context, + allow_condition_true_player, + ) { + Ok(RuntimeEffect::SetPlayerVariable { + target: target.clone(), + index: *index, + value: *value, + }) + } else { + Err(player_target_import_error_message(target, company_context)) + } + } RuntimeEffect::SetPlayerCash { target, value } => { if player_target_allowed_for_import( target, @@ -2254,6 +2364,21 @@ fn smp_runtime_effect_to_runtime_effect( Err(player_target_import_error_message(target, company_context)) } } + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => { + if territory_target_import_blocker(target, company_context).is_none() { + Ok(RuntimeEffect::SetTerritoryVariable { + target: target.clone(), + index: *index, + value: *value, + }) + } else { + Err("packed effect requires territory runtime context".to_string()) + } + } RuntimeEffect::SetChairmanCash { target, value } => { if chairman_target_import_blocker(target, company_context).is_none() { Ok(RuntimeEffect::SetChairmanCash { @@ -2998,6 +3123,7 @@ fn real_grouped_row_is_unsupported_executable_descriptor_variant( 56 | 57 => row.row_shape != "scalar_assignment", _ => match row.parameter_family.as_deref() { Some("world_scalar_override") => row.row_shape != "scalar_assignment", + Some("runtime_variable_scalar") => row.row_shape != "scalar_assignment", Some("locomotive_availability_scalar") => { !(row.row_shape == "scalar_assignment" && row.raw_scalar_value >= 0) } @@ -3190,6 +3316,7 @@ fn real_grouped_row_is_unsupported_retire_train_scope( fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } @@ -3208,10 +3335,12 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { .iter() .any(runtime_effect_uses_condition_true_company), RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } | RuntimeEffect::SetWorldScalarOverride { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetPlayerCash { .. } + | RuntimeEffect::SetPlayerVariable { .. } | RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivatePlayer { .. } | RuntimeEffect::DeactivateChairman { .. } @@ -3222,6 +3351,7 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { | RuntimeEffect::SetCargoPriceOverride { .. } | RuntimeEffect::SetCargoProductionOverride { .. } | RuntimeEffect::SetCargoProductionSlot { .. } + | RuntimeEffect::SetTerritoryVariable { .. } | RuntimeEffect::SetTerritoryAccessCost { .. } | RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::ActivateEventRecord { .. } @@ -3232,7 +3362,8 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool { match effect { - RuntimeEffect::SetPlayerCash { target, .. } => { + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } => { matches!(target, RuntimePlayerTarget::ConditionTruePlayer) } RuntimeEffect::DeactivatePlayer { target } => { @@ -3274,6 +3405,7 @@ fn runtime_effect_company_target_import_blocker( ) -> Option { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } @@ -3293,9 +3425,13 @@ fn runtime_effect_company_target_import_blocker( } } RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } | RuntimeEffect::DeactivatePlayer { target } => { player_target_import_blocker(target, company_context) } + RuntimeEffect::SetTerritoryVariable { target, .. } => { + territory_target_import_blocker(target, company_context) + } RuntimeEffect::SetChairmanCash { target, .. } | RuntimeEffect::DeactivateChairman { target } => { chairman_target_import_blocker(target, company_context) @@ -3324,6 +3460,7 @@ fn runtime_effect_company_target_import_blocker( runtime_effect_company_target_import_blocker(nested, company_context) }), RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } | RuntimeEffect::SetWorldScalarOverride { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } @@ -3689,6 +3826,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -4245,6 +4386,9 @@ mod tests { 56 => Some("Trans-Euro"), 57 => Some("V200"), 58 => Some("VL80T"), + 59 => Some("GP 35"), + 60 => Some("U1"), + 61 => Some("Zephyr"), _ => None, } } @@ -4361,6 +4505,9 @@ mod tests { 56 => Some("Trans-Euro"), 57 => Some("V200"), 58 => Some("VL80T"), + 59 => Some("GP 35"), + 60 => Some("U1"), + 61 => Some("Zephyr"), _ => None, } } @@ -4471,6 +4618,9 @@ mod tests { 55 => "Trans-Euro", 56 => "V200", 57 => "VL80T", + 58 => "GP 35", + 59 => "U1", + 60 => "Zephyr", _ => return format!("Locomotive {}", index + 1), } .to_string() @@ -7125,7 +7275,7 @@ mod tests { bridge_family: None, profile: None, candidate_availability_table: None, - named_locomotive_availability_table: Some(save_named_locomotive_table(58)), + named_locomotive_availability_table: Some(save_named_locomotive_table(61)), locomotive_catalog: None, cargo_catalog: None, company_roster: None, @@ -7165,7 +7315,7 @@ mod tests { grouped_effect_row_counts: vec![2, 0, 0, 0], grouped_effect_rows: vec![ real_locomotive_availability_row(250, 42), - real_locomotive_availability_row(298, 7), + real_locomotive_availability_row(301, 7), ], decoded_conditions: Vec::new(), decoded_actions: vec![], @@ -7186,7 +7336,7 @@ mod tests { ) .expect("save slice should project"); - assert_eq!(import.state.locomotive_catalog.len(), 58); + assert_eq!(import.state.locomotive_catalog.len(), 61); assert_eq!(import.state.event_runtime_records.len(), 1); assert_eq!( import @@ -7211,7 +7361,7 @@ mod tests { Some(&42) ); assert_eq!( - import.state.named_locomotive_availability.get("VL80T"), + import.state.named_locomotive_availability.get("Zephyr"), Some(&7) ); } @@ -7242,8 +7392,8 @@ mod tests { name: "Big Boy 4-8-8-4".to_string(), }, crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 58, - name: "VL80T".to_string(), + locomotive_id: 61, + name: "Zephyr".to_string(), }, ], cargo_catalog: Vec::new(), @@ -7255,7 +7405,7 @@ mod tests { candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::from([ ("Big Boy 4-8-8-4".to_string(), 0), - ("VL80T".to_string(), 1), + ("Zephyr".to_string(), 1), ]), named_locomotive_cost: BTreeMap::new(), all_cargo_price_override: None, @@ -7265,6 +7415,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -7318,7 +7472,7 @@ mod tests { grouped_effect_row_counts: vec![2, 0, 0, 0], grouped_effect_rows: vec![ real_locomotive_availability_row(250, 42), - real_locomotive_availability_row(298, 7), + real_locomotive_availability_row(301, 7), ], decoded_conditions: Vec::new(), decoded_actions: vec![], @@ -7364,7 +7518,7 @@ mod tests { Some(&42) ); assert_eq!( - import.state.named_locomotive_availability.get("VL80T"), + import.state.named_locomotive_availability.get("Zephyr"), Some(&7) ); } @@ -7538,7 +7692,7 @@ mod tests { bridge_family: None, profile: None, candidate_availability_table: None, - named_locomotive_availability_table: Some(save_named_locomotive_table(58)), + named_locomotive_availability_table: Some(save_named_locomotive_table(61)), locomotive_catalog: None, cargo_catalog: None, company_roster: None, @@ -7578,7 +7732,7 @@ mod tests { grouped_effect_row_counts: vec![2, 0, 0, 0], grouped_effect_rows: vec![ real_locomotive_cost_row(352, 250000), - real_locomotive_cost_row(409, 325000), + real_locomotive_cost_row(412, 325000), ], decoded_conditions: Vec::new(), decoded_actions: vec![], @@ -7598,7 +7752,7 @@ mod tests { ) .expect("save slice should project"); - assert_eq!(import.state.locomotive_catalog.len(), 58); + assert_eq!(import.state.locomotive_catalog.len(), 61); assert_eq!(import.state.event_runtime_records.len(), 1); assert_eq!( import @@ -7620,7 +7774,7 @@ mod tests { Some(&250000) ); assert_eq!( - import.state.named_locomotive_cost.get("VL80T"), + import.state.named_locomotive_cost.get("Zephyr"), Some(&325000) ); } @@ -7651,8 +7805,8 @@ mod tests { name: "2-D-2".to_string(), }, crate::RuntimeLocomotiveCatalogEntry { - locomotive_id: 58, - name: "VL80T".to_string(), + locomotive_id: 61, + name: "Zephyr".to_string(), }, ], cargo_catalog: Vec::new(), @@ -7665,7 +7819,7 @@ mod tests { named_locomotive_availability: BTreeMap::new(), named_locomotive_cost: BTreeMap::from([ ("2-D-2".to_string(), 100000), - ("VL80T".to_string(), 200000), + ("Zephyr".to_string(), 200000), ]), all_cargo_price_override: None, named_cargo_price_overrides: BTreeMap::new(), @@ -7674,6 +7828,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -7727,7 +7885,7 @@ mod tests { grouped_effect_row_counts: vec![2, 0, 0, 0], grouped_effect_rows: vec![ real_locomotive_cost_row(352, 250000), - real_locomotive_cost_row(409, 325000), + real_locomotive_cost_row(412, 325000), ], decoded_conditions: Vec::new(), decoded_actions: vec![], @@ -7769,7 +7927,7 @@ mod tests { Some(&250000) ); assert_eq!( - import.state.named_locomotive_cost.get("VL80T"), + import.state.named_locomotive_cost.get("Zephyr"), Some(&325000) ); } @@ -8427,6 +8585,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -10710,6 +10872,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -10890,6 +11056,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -12585,6 +12755,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState { @@ -12776,6 +12950,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), diff --git a/crates/rrt-runtime/src/persistence.rs b/crates/rrt-runtime/src/persistence.rs index 0b66854..50a82c9 100644 --- a/crates/rrt-runtime/src/persistence.rs +++ b/crates/rrt-runtime/src/persistence.rs @@ -116,6 +116,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 8c16fd9..cd5380b 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -491,6 +491,25 @@ pub enum RuntimeEffect { slot: u32, value: u32, }, + SetWorldVariable { + index: u32, + value: i64, + }, + SetCompanyVariable { + target: RuntimeCompanyTarget, + index: u32, + value: i64, + }, + SetPlayerVariable { + target: RuntimePlayerTarget, + index: u32, + value: i64, + }, + SetTerritoryVariable { + target: RuntimeTerritoryTarget, + index: u32, + value: i64, + }, SetWorldScalarOverride { key: String, value: i64, @@ -882,6 +901,14 @@ pub struct RuntimeState { #[serde(default)] pub cargo_production_overrides: BTreeMap, #[serde(default)] + pub world_runtime_variables: BTreeMap, + #[serde(default)] + pub company_runtime_variables: BTreeMap>, + #[serde(default)] + pub player_runtime_variables: BTreeMap>, + #[serde(default)] + pub territory_runtime_variables: BTreeMap>, + #[serde(default)] pub world_scalar_overrides: BTreeMap, #[serde(default)] pub special_conditions: BTreeMap, @@ -1039,7 +1066,6 @@ impl RuntimeState { )); } } - let mut seen_territory_ids = BTreeSet::new(); let mut seen_territory_names = BTreeSet::new(); for territory in &self.territories { @@ -1541,6 +1567,62 @@ impl RuntimeState { )); } } + for index in self.world_runtime_variables.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "world_runtime_variables contains out-of-range index {}", + index + )); + } + } + for (company_id, vars) in &self.company_runtime_variables { + if !seen_company_ids.contains(company_id) { + return Err(format!( + "company_runtime_variables references unknown company_id {}", + company_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "company_runtime_variables[{company_id}] contains out-of-range index {}", + index + )); + } + } + } + for (player_id, vars) in &self.player_runtime_variables { + if !seen_player_ids.contains(player_id) { + return Err(format!( + "player_runtime_variables references unknown player_id {}", + player_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "player_runtime_variables[{player_id}] contains out-of-range index {}", + index + )); + } + } + } + for (territory_id, vars) in &self.territory_runtime_variables { + if !seen_territory_ids.contains(territory_id) { + return Err(format!( + "territory_runtime_variables references unknown territory_id {}", + territory_id + )); + } + for index in vars.keys() { + if !(1..=4).contains(index) { + return Err(format!( + "territory_runtime_variables[{territory_id}] contains out-of-range index {}", + index + )); + } + } + } for key in self.world_scalar_overrides.keys() { if key.trim().is_empty() { return Err("world_scalar_overrides contains an empty key".to_string()); @@ -1569,6 +1651,13 @@ fn validate_runtime_effect( return Err("key must not be empty".to_string()); } } + RuntimeEffect::SetWorldVariable { index, .. } => { + if !(1..=4).contains(index) { + return Err(format!( + "world runtime variable index {index} must be in 1..=4" + )); + } + } RuntimeEffect::SetWorldScalarOverride { key, .. } => { if key.trim().is_empty() { return Err("key must not be empty".to_string()); @@ -1576,6 +1665,12 @@ fn validate_runtime_effect( } RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } => {} + RuntimeEffect::SetCompanyVariable { target, index, .. } => { + validate_company_target(target, valid_company_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } RuntimeEffect::SetCompanyCash { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } @@ -1594,6 +1689,12 @@ fn validate_runtime_effect( validate_company_target(target, valid_company_ids)?; validate_territory_target(territory, valid_territory_ids)?; } + RuntimeEffect::SetPlayerVariable { target, index, .. } => { + validate_player_target(target, valid_player_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } RuntimeEffect::SetPlayerCash { target, .. } | RuntimeEffect::DeactivatePlayer { target } => { validate_player_target(target, valid_player_ids)?; @@ -1669,6 +1770,12 @@ fn validate_runtime_effect( return Err("slot must be in 1..=11".to_string()); } } + RuntimeEffect::SetTerritoryVariable { target, index, .. } => { + validate_territory_target(target, valid_territory_ids)?; + if !(1..=4).contains(index) { + return Err(format!("runtime variable index {index} must be in 1..=4")); + } + } RuntimeEffect::SetTerritoryAccessCost { .. } => {} RuntimeEffect::SetSpecialCondition { label, .. } => { if label.trim().is_empty() { @@ -1987,6 +2094,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2057,6 +2168,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2131,6 +2246,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2215,6 +2334,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2324,6 +2447,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2385,6 +2512,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2446,6 +2577,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2524,6 +2659,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2592,6 +2731,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2664,6 +2807,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2732,6 +2879,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2806,6 +2957,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2874,6 +3029,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2942,6 +3101,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -3003,6 +3166,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -3074,6 +3241,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index caba87a..7ecd532 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -446,7 +446,7 @@ const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [ }, ]; -const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 58] = [ +const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [ "2-D-2", "E-88", "Adler 2-2-2", @@ -505,6 +505,9 @@ const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 58] = [ "Trans-Euro", "V200", "VL80T", + "GP 35", + "U1", + "Zephyr", ]; const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; @@ -4313,6 +4316,7 @@ fn derive_real_grouped_target_subject( match row.target_mask_bits { Some(0x08) => Some(RealGroupedTargetSubject::WholeGame), Some(0x01) => Some(RealGroupedTargetSubject::Company), + Some(0x04) => Some(RealGroupedTargetSubject::Territory), Some(0x02) => match compact_control .grouped_scope_checkboxes_0x7ff .get(row.group_index) @@ -4401,6 +4405,16 @@ fn real_grouped_chairman_scope_name(ordinal: u8) -> &'static str { } } +fn runtime_variable_index(descriptor_id: u32) -> Option { + match descriptor_id { + 39..=42 => Some(descriptor_id - 38), + 43..=46 => Some(descriptor_id - 42), + 47..=50 => Some(descriptor_id - 46), + 51..=54 => Some(descriptor_id - 50), + _ => None, + } +} + fn decode_real_grouped_effect_actions( grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], compact_control: &SmpLoadedPackedEventCompactControlSummary, @@ -4422,6 +4436,42 @@ fn decode_real_grouped_effect_action( .copied()?; let target_subject = derive_real_grouped_target_subject(row, compact_control); + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.parameter_family == "runtime_variable_scalar" + && row.row_shape == "scalar_assignment" + { + let index = runtime_variable_index(descriptor_metadata.descriptor_id)?; + return match target_subject { + Some(RealGroupedTargetSubject::WholeGame) => Some(RuntimeEffect::SetWorldVariable { + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Company) => Some(RuntimeEffect::SetCompanyVariable { + target: real_grouped_company_target(target_scope_ordinal)?, + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Player) => Some(RuntimeEffect::SetPlayerVariable { + target: real_grouped_player_target(target_scope_ordinal)?, + index, + value: i64::from(row.raw_scalar_value), + }), + Some(RealGroupedTargetSubject::Territory) => compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|selector| RuntimeEffect::SetTerritoryVariable { + target: RuntimeTerritoryTarget::Ids { + ids: vec![selector as u32], + }, + index, + value: i64::from(row.raw_scalar_value), + }), + _ => None, + }; + } + if descriptor_metadata.executable_in_runtime && descriptor_metadata.parameter_family == "company_governance_scalar" && row.row_shape == "scalar_assignment" @@ -4858,6 +4908,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimeChairmanTarget::Ids { .. } ), RuntimeEffect::SetWorldFlag { .. } + | RuntimeEffect::SetWorldVariable { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetCompanyGovernanceScalar { .. } @@ -4879,7 +4930,8 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::RemoveEventRecord { .. } => true, - RuntimeEffect::SetPlayerCash { target, .. } => matches!( + RuntimeEffect::SetPlayerCash { target, .. } + | RuntimeEffect::SetPlayerVariable { target, .. } => matches!( target, RuntimePlayerTarget::AllActive | RuntimePlayerTarget::Ids { .. } @@ -4905,6 +4957,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { ) } RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyVariable { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( target, @@ -4915,6 +4968,10 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimeCompanyTarget::SelectedCompany | RuntimeCompanyTarget::ConditionTrueCompany ), + RuntimeEffect::SetTerritoryVariable { target, .. } => matches!( + target, + RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } + ), RuntimeEffect::AppendEventRecord { record } => record .effects .iter() @@ -11514,6 +11571,17 @@ mod tests { assert_eq!(recovered_locomotive_availability_loco_id(457), None); } + #[test] + fn looks_up_extended_lower_band_locomotive_availability_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(301).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Zephyr Availability"); + assert_eq!(metadata.parameter_family, "locomotive_availability_scalar"); + assert_eq!(recovered_locomotive_availability_loco_id(301), Some(61)); + assert!(metadata.executable_in_runtime); + } + #[test] fn parses_recovered_locomotive_availability_row_with_structured_locomotive_id() { let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec { @@ -11568,6 +11636,17 @@ mod tests { assert!(metadata.executable_in_runtime); } + #[test] + fn looks_up_runtime_variable_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(43).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Company Variable 1"); + assert_eq!(metadata.target_mask_bits, 0x01); + assert_eq!(metadata.parameter_family, "runtime_variable_scalar"); + assert!(metadata.executable_in_runtime); + } + #[test] fn looks_up_recovered_aggregate_cargo_production_descriptor_metadata() { let metadata = @@ -11616,6 +11695,17 @@ mod tests { assert!(!metadata.executable_in_runtime); } + #[test] + fn looks_up_extended_lower_band_locomotive_cost_descriptor_metadata() { + let metadata = + real_grouped_effect_descriptor_metadata(412).expect("descriptor metadata should exist"); + + assert_eq!(metadata.label, "Zephyr Cost"); + assert_eq!(metadata.parameter_family, "locomotive_cost_scalar"); + assert_eq!(recovered_locomotive_cost_loco_id(412), Some(61)); + assert!(metadata.executable_in_runtime); + } + #[test] fn looks_up_recovered_territory_access_cost_descriptor_metadata() { let metadata = diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index b728af5..2e9f88b 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -636,6 +636,52 @@ fn apply_runtime_effects( RuntimeEffect::SetCargoProductionSlot { slot, value } => { state.cargo_production_overrides.insert(*slot, *value); } + RuntimeEffect::SetWorldVariable { index, value } => { + state.world_runtime_variables.insert(*index, *value); + } + RuntimeEffect::SetCompanyVariable { + target, + index, + value, + } => { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + state + .company_runtime_variables + .entry(company_id) + .or_default() + .insert(*index, *value); + mutated_company_ids.insert(company_id); + } + } + RuntimeEffect::SetPlayerVariable { + target, + index, + value, + } => { + let player_ids = resolve_player_target_ids(state, target, condition_context)?; + for player_id in player_ids { + state + .player_runtime_variables + .entry(player_id) + .or_default() + .insert(*index, *value); + } + } + RuntimeEffect::SetTerritoryVariable { + target, + index, + value, + } => { + let territory_ids = resolve_territory_target_ids(state, target)?; + for territory_id in territory_ids { + state + .territory_runtime_variables + .entry(territory_id) + .or_default() + .insert(*index, *value); + } + } RuntimeEffect::SetTerritoryAccessCost { value } => { state.world_restore.territory_access_cost = Some(*value); } @@ -1596,6 +1642,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -2008,6 +2058,128 @@ mod tests { assert_eq!(result.service_events[0].applied_effect_count, 7); } + #[test] + fn applies_runtime_variable_effects() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + players: vec![RuntimePlayer { + player_id: 9, + current_cash: 0, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("North".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 14, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetWorldVariable { + index: 1, + value: -5, + }, + RuntimeEffect::SetCompanyVariable { + target: RuntimeCompanyTarget::AllActive, + index: 2, + value: 17, + }, + RuntimeEffect::SetPlayerVariable { + target: RuntimePlayerTarget::Ids { ids: vec![9] }, + index: 3, + value: 99, + }, + RuntimeEffect::SetTerritoryVariable { + target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + index: 4, + value: 1234, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("runtime variable effects should succeed"); + + assert_eq!(state.world_runtime_variables.get(&1), Some(&-5)); + assert_eq!( + state + .company_runtime_variables + .get(&1) + .and_then(|vars| vars.get(&2)), + Some(&17) + ); + assert_eq!( + state + .company_runtime_variables + .get(&2) + .and_then(|vars| vars.get(&2)), + Some(&17) + ); + assert_eq!( + state + .player_runtime_variables + .get(&9) + .and_then(|vars| vars.get(&3)), + Some(&99) + ); + assert_eq!( + state + .territory_runtime_variables + .get(&7) + .and_then(|vars| vars.get(&4)), + Some(&1234) + ); + assert_eq!(result.service_events[0].applied_effect_count, 4); + } + #[test] fn resolves_symbolic_company_targets() { let mut state = RuntimeState { diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 8d79f1b..b8d8aea 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -91,6 +91,10 @@ pub struct RuntimeSummary { pub zero_named_locomotive_availability_count: usize, pub named_locomotive_cost_count: usize, pub cargo_production_override_count: usize, + pub world_runtime_variable_count: usize, + pub company_runtime_variable_owner_count: usize, + pub player_runtime_variable_owner_count: usize, + pub territory_runtime_variable_owner_count: usize, pub world_scalar_override_count: usize, pub special_condition_count: usize, pub enabled_special_condition_count: usize, @@ -707,6 +711,10 @@ impl RuntimeSummary { .count(), named_locomotive_cost_count: state.named_locomotive_cost.len(), cargo_production_override_count: state.cargo_production_overrides.len(), + world_runtime_variable_count: state.world_runtime_variables.len(), + company_runtime_variable_owner_count: state.company_runtime_variables.len(), + player_runtime_variable_owner_count: state.player_runtime_variables.len(), + territory_runtime_variable_owner_count: state.territory_runtime_variables.len(), world_scalar_override_count: state.world_scalar_overrides.len(), special_condition_count: state.special_conditions.len(), enabled_special_condition_count: state @@ -747,9 +755,9 @@ mod tests { use crate::{ CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, - RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, - RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTrackPieceCounts, - RuntimeWorldRestoreState, + RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePlayer, + RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTerritory, + RuntimeTrackPieceCounts, RuntimeWorldRestoreState, }; use super::RuntimeSummary; @@ -941,6 +949,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1064,6 +1076,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1124,6 +1140,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1176,6 +1196,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1226,6 +1250,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::from([(1, 125), (2, 250)]), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1237,6 +1265,85 @@ mod tests { assert_eq!(summary.world_restore_territory_access_cost, Some(750000)); } + #[test] + fn counts_runtime_variable_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::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 0, + debt: 0, + credit_rating_score: None, + prime_rate: None, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + selected_company_id: None, + players: vec![RuntimePlayer { + player_id: 2, + current_cash: 0, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_player_id: None, + chairman_profiles: Vec::new(), + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 3, + name: Some("East".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + 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(), + all_cargo_price_override: None, + named_cargo_price_overrides: BTreeMap::new(), + all_cargo_production_override: None, + factory_cargo_production_override: None, + farm_mine_cargo_production_override: None, + named_cargo_production_overrides: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::from([(1, 9)]), + company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 11)]))]), + player_runtime_variables: BTreeMap::from([(2, BTreeMap::from([(3, 13)]))]), + territory_runtime_variables: BTreeMap::from([(3, BTreeMap::from([(4, 15)]))]), + world_scalar_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.world_runtime_variable_count, 1); + assert_eq!(summary.company_runtime_variable_owner_count, 1); + assert_eq!(summary.player_runtime_variable_owner_count, 1); + assert_eq!(summary.territory_runtime_variable_owner_count, 1); + } + #[test] fn counts_world_frontier_buckets_separately() { let state = RuntimeState { @@ -1338,6 +1445,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1428,6 +1539,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), @@ -1514,6 +1629,10 @@ mod tests { farm_mine_cargo_production_override: None, named_cargo_production_overrides: BTreeMap::new(), cargo_production_overrides: BTreeMap::new(), + world_runtime_variables: BTreeMap::new(), + company_runtime_variables: BTreeMap::new(), + player_runtime_variables: BTreeMap::new(), + territory_runtime_variables: BTreeMap::new(), world_scalar_overrides: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), diff --git a/docs/README.md b/docs/README.md index 3e135d0..aa79fbd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -113,6 +113,9 @@ The highest-value next passes are now: landing surface too: representative rows execute into `RuntimeState.world_scalar_overrides` through stable normalized keys such as `world.build_stations_cost` and `world.track_maintenance_cost` +- the runtime-variable strip `39..54` now executes too through bounded event-owned scalar maps on + world/company/player/territory state; these variables are runtime-owned only in the current + model and are not yet reconstructed from raw saves - the grounded aggregate cargo-economics descriptors now execute too: descriptor `105` `All Cargo Prices` and descriptors `177..179` `All Cargo Production` / `All Factory Production` / `All Farm/Mine Production` land on bounded event-owned cargo override state, and the grounded @@ -168,6 +171,8 @@ The highest-value next passes are now: - recovered scalar locomotive availability and locomotive-cost descriptors now import through that save-native or embedded `RuntimeState.locomotive_catalog` context into the ordinary `named_locomotive_availability` and `named_locomotive_cost` runtime maps +- the grounded executable lower locomotive prefix now extends through save-backed locomotive id + `61` (`Zephyr`); the unresolved lower tail and upper locomotive bands stay on explicit parity - cargo-production `230..240` and territory-access-cost `453` now execute too through minimal world-side scalar landing surfaces: slot-indexed `cargo_production_overrides` and `world_restore.territory_access_cost` diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 51635a1..12c9707 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -75,6 +75,9 @@ Implemented today: - the recovered whole-game scalar economy/performance strip `59..104` now has a bounded runtime landing surface too: representative descriptors import as `SetWorldScalarOverride` and land in `RuntimeState.world_scalar_overrides` +- the runtime-variable strip `39..54` now imports and executes too through bounded event-owned + world/company/player/territory variable maps, so those descriptors no longer depend on generic + evidence parity or ad hoc fixture-only state - the grounded aggregate cargo-economics descriptors now execute too: descriptor `105` `All Cargo Prices` and descriptors `177..179` `All Cargo Production` / `All Factory Production` / `All Farm/Mine Production` import through bounded cargo override surfaces, and the grounded @@ -130,7 +133,8 @@ Implemented today: through `RuntimeState.locomotive_catalog` into `RuntimeState.named_locomotive_availability`; raw `.smp` inspection/export now reconstructs the save-side locomotive row family and derives the catalog directly into save-slice documents, so standalone save-slice imports can execute those - rows whenever the save carries enough catalog entries + rows whenever the save carries enough catalog entries, and the grounded executable lower prefix + now extends through save-backed locomotive id `61` (`Zephyr`) - the grounded lower locomotive-cost band `352..409` now imports too through the same save-native or embedded catalog into the event-owned `RuntimeState.named_locomotive_cost` map when its scalar payloads are nonnegative; the unresolved lower tail and upper cost tail now stay on diff --git a/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json b/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json new file mode 100644 index 0000000..0e93b3f --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json @@ -0,0 +1,87 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-runtime-variable-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving descriptors 39..54 import and execute through bounded world/company/player/territory runtime-variable surfaces." + }, + "state_import_path": "packed-event-runtime-variable-overlay.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "calendar_projection_source": "base-snapshot-preserved", + "calendar_projection_is_placeholder": false, + "company_count": 3, + "player_count": 2, + "territory_count": 2, + "packed_event_collection_present": true, + "packed_event_record_count": 4, + "packed_event_decoded_record_count": 4, + "packed_event_imported_runtime_record_count": 4, + "event_runtime_record_count": 4, + "world_runtime_variable_count": 1, + "company_runtime_variable_owner_count": 1, + "player_runtime_variable_owner_count": 1, + "territory_runtime_variable_owner_count": 1, + "total_event_record_service_count": 4, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "world_runtime_variables": { + "1": 111 + }, + "company_runtime_variables": { + "1": { + "2": 222 + } + }, + "player_runtime_variables": { + "1": { + "3": 333 + } + }, + "territory_runtime_variables": { + "7": { + "4": 444 + } + }, + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + }, + { + "import_outcome": "imported" + }, + { + "import_outcome": "imported" + }, + { + "import_outcome": "imported" + } + ] + }, + "event_runtime_records": [ + { + "record_id": 71, + "service_count": 1 + }, + { + "record_id": 72, + "service_count": 1 + }, + { + "record_id": 73, + "service_count": 1 + }, + { + "record_id": 74, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-runtime-variable-overlay.json b/fixtures/runtime/packed-event-runtime-variable-overlay.json new file mode 100644 index 0000000..dfc095e --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-runtime-variable-overlay", + "source": { + "description": "Overlay import combining company/player/territory runtime context with the runtime-variable descriptor sample." + }, + "base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json", + "save_slice_path": "packed-event-runtime-variable-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-runtime-variable-save-slice.json b/fixtures/runtime/packed-event-runtime-variable-save-slice.json new file mode 100644 index 0000000..643f5d6 --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-save-slice.json @@ -0,0 +1,320 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-runtime-variable-save-slice", + "source": { + "description": "Tracked save-slice document proving runtime-variable descriptors 39..54 execute through bounded world/company/player/territory variable surfaces.", + "original_save_filename": "captured-runtime-variable.gms", + "original_save_sha256": "runtime-variable-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "uses overlay-backed company/player/territory context while leaving world variables save-slice-native" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "named_locomotive_availability_table": null, + "locomotive_catalog": null, + "cargo_catalog": null, + "company_roster": null, + "chairman_profile_table": null, + "special_conditions_table": null, + "event_runtime_collection": { + "source_kind": "packed-event-runtime-collection", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "container_profile_family": "rt3-classic-save-container-v1", + "metadata_tag_offset": 33280, + "records_tag_offset": 33536, + "close_tag_offset": 34560, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 74, + "live_record_count": 4, + "live_entry_ids": [71, 72, 73, 74], + "decoded_record_count": 4, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 71, + "payload_offset": 33568, + "payload_len": 160, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "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": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 39, + "descriptor_label": "Game Variable 1", + "target_mask_bits": 8, + "parameter_family": "runtime_variable_scalar", + "opcode": 3, + "raw_scalar_value": 111, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Game Variable 1 to 111", + "grouped_target_subject": "whole_game", + "grouped_target_scope": "whole_game", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [ + "descriptor recovered from checked-in EventEffects semantic catalog" + ] + } + ], + "decoded_actions": [ + { + "kind": "set_world_variable", + "index": 1, + "value": 111 + } + ], + "executable_import_ready": true, + "notes": [ + "runtime variable world sample" + ] + }, + { + "record_index": 1, + "live_entry_id": 72, + "payload_offset": 33728, + "payload_len": 160, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 44, + "descriptor_label": "Company Variable 2", + "target_mask_bits": 1, + "parameter_family": "runtime_variable_scalar", + "opcode": 3, + "raw_scalar_value": 222, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Company Variable 2 to 222", + "grouped_target_subject": "company", + "grouped_target_scope": "selected_company", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [ + "descriptor recovered from checked-in EventEffects semantic catalog" + ] + } + ], + "decoded_actions": [ + { + "kind": "set_company_variable", + "target": { + "kind": "selected_company" + }, + "index": 2, + "value": 222 + } + ], + "executable_import_ready": true, + "notes": [ + "runtime variable company sample" + ] + }, + { + "record_index": 2, + "live_entry_id": 73, + "payload_offset": 33888, + "payload_len": 160, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 12, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 0, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 49, + "descriptor_label": "Player Variable 3", + "target_mask_bits": 2, + "parameter_family": "runtime_variable_scalar", + "opcode": 3, + "raw_scalar_value": 333, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Player Variable 3 to 333", + "grouped_target_subject": "player", + "grouped_target_scope": "selected_player", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [ + "descriptor recovered from checked-in EventEffects semantic catalog" + ] + } + ], + "decoded_actions": [ + { + "kind": "set_player_variable", + "target": { + "kind": "selected_player" + }, + "index": 3, + "value": 333 + } + ], + "executable_import_ready": true, + "notes": [ + "runtime variable player sample" + ] + }, + { + "record_index": 3, + "live_entry_id": 74, + "payload_offset": 34048, + "payload_len": 160, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 12, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 0, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [7, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 54, + "descriptor_label": "Territory Variable 4", + "target_mask_bits": 4, + "parameter_family": "runtime_variable_scalar", + "opcode": 3, + "raw_scalar_value": 444, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Territory Variable 4 to 444", + "grouped_target_subject": "territory", + "grouped_target_scope": "named_territory", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [ + "descriptor recovered from checked-in EventEffects semantic catalog" + ] + } + ], + "decoded_actions": [ + { + "kind": "set_territory_variable", + "target": { + "kind": "ids", + "ids": [7] + }, + "index": 4, + "value": 444 + } + ], + "executable_import_ready": true, + "notes": [ + "runtime variable territory sample" + ] + } + ] + }, + "notes": [ + "runtime variable descriptor executable sample" + ] + } +} diff --git a/tools/py/build_event_effect_semantic_catalog.py b/tools/py/build_event_effect_semantic_catalog.py index 831d87f..e48744c 100644 --- a/tools/py/build_event_effect_semantic_catalog.py +++ b/tools/py/build_event_effect_semantic_catalog.py @@ -64,6 +64,9 @@ GROUNDED_LOCOMOTIVE_PREFIX = { 56: "Trans-Euro", 57: "V200", 58: "VL80T", + 59: "GP 35", + 60: "U1", + 61: "Zephyr", } @@ -208,6 +211,8 @@ def classify( runtime_status = "shell_owned" elif signature_byte_0x63 == 0 and signature_byte_0x64 == 0x8F: parameter_family = "runtime_variable_scalar" + runtime_status = "executable" + executable_in_runtime = True elif "Earthquake" in label or "Storm" in label: parameter_family = "world_disaster_scalar"