From bd9e1421a1be1ae0e3242ced450b25b0fb93ec7f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 08:50:35 -0700 Subject: [PATCH] Implement runtime variable event conditions --- README.md | 6 +- .../event-effects-semantic-catalog.json | 34 +- crates/rrt-cli/src/main.rs | 10 + crates/rrt-runtime/src/import.rs | 71 ++- crates/rrt-runtime/src/runtime.rs | 57 +++ crates/rrt-runtime/src/smp.rs | 279 +++++++++- crates/rrt-runtime/src/step.rs | 230 ++++++++- docs/README.md | 7 +- docs/runtime-rehost-plan.md | 7 +- ...add-building-shell-save-slice-fixture.json | 37 ++ ...d-event-add-building-shell-save-slice.json | 110 ++++ ...me-variable-condition-overlay-fixture.json | 134 +++++ ...nt-runtime-variable-condition-overlay.json | 9 + ...runtime-variable-condition-save-slice.json | 479 ++++++++++++++++++ .../py/build_event_effect_semantic_catalog.py | 1 + 15 files changed, 1442 insertions(+), 29 deletions(-) create mode 100644 fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json create mode 100644 fixtures/runtime/packed-event-add-building-shell-save-slice.json create mode 100644 fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json create mode 100644 fixtures/runtime/packed-event-runtime-variable-condition-overlay.json create mode 100644 fixtures/runtime/packed-event-runtime-variable-condition-save-slice.json diff --git a/README.md b/README.md index 08ed45c..ddb9081 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ 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 runtime-variable strip `39..54` now executes too through bounded -event-owned scalar maps on world/company/player/territory state, without widening save-native +event-owned scalar maps on world/company/player/territory state, and the matching ordinary +condition strip now gates records through those same maps too, 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` @@ -54,7 +55,8 @@ runtime landing surfaces too: descriptor `105` `All Cargo Prices` plus descripto event-owned cargo override state, and the grounded named cargo-production strip `180..229` now imports into named cargo production overrides too. The named cargo-price strip `106..176` remains explicit `blocked_evidence_blocked_descriptor` parity until descriptor ordering is pinned -more strongly. The first grounded +more strongly. The add-building strip `503..519` is now explicitly classified as recovered +shell-owned descriptor parity rather than generic unresolved residue. The first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and the first ordinary nonnegative condition batch now executes too: numeric-threshold company finance, company track, aggregate territory track, and company-territory track rows can import 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 a1581bd..f94539d 100644 --- a/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json +++ b/artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json @@ -4532,7 +4532,7 @@ }, { "descriptor_id": 503, - "label": "Unknown Add Building", + "label": "Add Building Slot 1", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4541,7 +4541,7 @@ }, { "descriptor_id": 504, - "label": "Unknown Add Building", + "label": "Add Building Slot 2", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4550,7 +4550,7 @@ }, { "descriptor_id": 505, - "label": "Unknown Add Building", + "label": "Add Building Slot 3", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4559,7 +4559,7 @@ }, { "descriptor_id": 506, - "label": "Unknown Add Building", + "label": "Add Building Slot 4", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4568,7 +4568,7 @@ }, { "descriptor_id": 507, - "label": "Unknown Add Building", + "label": "Add Building Slot 5", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4577,7 +4577,7 @@ }, { "descriptor_id": 508, - "label": "Unknown Add Building", + "label": "Add Building Slot 6", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4586,7 +4586,7 @@ }, { "descriptor_id": 509, - "label": "Unknown Add Building", + "label": "Add Building Slot 7", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4595,7 +4595,7 @@ }, { "descriptor_id": 510, - "label": "Unknown Add Building", + "label": "Add Building Slot 8", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4604,7 +4604,7 @@ }, { "descriptor_id": 511, - "label": "Unknown Add Building", + "label": "Add Building Slot 9", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4613,7 +4613,7 @@ }, { "descriptor_id": 512, - "label": "Unknown Add Building", + "label": "Add Building Slot 10", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4622,7 +4622,7 @@ }, { "descriptor_id": 513, - "label": "Unknown Add Building", + "label": "Add Building Slot 11", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4631,7 +4631,7 @@ }, { "descriptor_id": 514, - "label": "Unknown Add Building", + "label": "Add Building Slot 12", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4640,7 +4640,7 @@ }, { "descriptor_id": 515, - "label": "Unknown Add Building", + "label": "Add Building Slot 13", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4649,7 +4649,7 @@ }, { "descriptor_id": 516, - "label": "Unknown Add Building", + "label": "Add Building Slot 14", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4658,7 +4658,7 @@ }, { "descriptor_id": 517, - "label": "Unknown Add Building", + "label": "Add Building Slot 15", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4667,7 +4667,7 @@ }, { "descriptor_id": 518, - "label": "Unknown Add Building", + "label": "Add Building Slot 16", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, @@ -4676,7 +4676,7 @@ }, { "descriptor_id": 519, - "label": "Unknown Add Building", + "label": "Add Building Slot 17", "target_mask_bits": 8, "parameter_family": "world_building_spawn", "runtime_key": null, diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 3a1d124..3661f58 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4481,11 +4481,17 @@ mod tests { ); let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json"); + let runtime_variable_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join( + "../../fixtures/runtime/packed-event-runtime-variable-condition-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( "../../fixtures/runtime/packed-event-cargo-economics-parity-save-slice-fixture.json", ); + let add_building_shell_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json"); let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json", ); @@ -4599,10 +4605,14 @@ mod tests { .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(&runtime_variable_condition_overlay_fixture) + .expect("overlay-backed runtime-variable condition 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) .expect("save-slice-backed cargo-economics parity fixture should summarize"); + run_runtime_summarize_fixture(&add_building_shell_fixture) + .expect("save-slice-backed add-building shell fixture should summarize"); run_runtime_summarize_fixture(&world_scalar_condition_fixture) .expect("save-slice-backed executable world-scalar condition fixture should summarize"); run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture) diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 65ecb3f..58a0f98 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -1302,6 +1302,7 @@ fn lowered_record_decoded_conditions( } let lowered_company_target = lowered_condition_true_company_target(record)?; + let lowered_player_target = lowered_condition_true_player_target(record)?; let ordinary_rows = record .standalone_condition_rows .iter() @@ -1313,6 +1314,7 @@ fn lowered_record_decoded_conditions( condition, row, lowered_company_target.as_ref(), + lowered_player_target.as_ref(), company_context, ) }) @@ -1949,9 +1951,19 @@ fn lower_condition_targets_in_condition( condition: &RuntimeCondition, row: &SmpLoadedPackedEventConditionRowSummary, lowered_company_target: Option<&RuntimeCompanyTarget>, + lowered_player_target: Option<&RuntimePlayerTarget>, company_context: &ImportRuntimeContext, ) -> Result { Ok(match condition { + RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + } => RuntimeCondition::WorldVariableThreshold { + index: *index, + comparator: *comparator, + value: *value, + }, RuntimeCondition::CompanyNumericThreshold { target, metric, @@ -1966,6 +1978,20 @@ fn lower_condition_targets_in_condition( comparator: *comparator, value: *value, }, + RuntimeCondition::CompanyVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::CompanyVariableThreshold { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + index: *index, + comparator: *comparator, + value: *value, + }, RuntimeCondition::ChairmanNumericThreshold { target, metric, @@ -1977,6 +2003,20 @@ fn lower_condition_targets_in_condition( comparator: *comparator, value: *value, }, + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::PlayerVariableThreshold { + target: lower_condition_true_player_target_in_player_target( + target, + lowered_player_target, + )?, + index: *index, + comparator: *comparator, + value: *value, + }, RuntimeCondition::TerritoryNumericThreshold { target, metric, @@ -1988,6 +2028,17 @@ fn lower_condition_targets_in_condition( comparator: *comparator, value: *value, }, + RuntimeCondition::TerritoryVariableThreshold { + target, + index, + comparator, + value, + } => RuntimeCondition::TerritoryVariableThreshold { + target: lower_territory_target_in_condition(target, row, company_context)?, + index: *index, + comparator: *comparator, + value: *value, + }, RuntimeCondition::CompanyTerritoryNumericThreshold { target, territory, @@ -2187,11 +2238,17 @@ fn record_uses_condition_true_player(record: &SmpLoadedPackedEventRecordSummary) fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { match condition { RuntimeCondition::CompanyNumericThreshold { target, .. } + | RuntimeCondition::CompanyVariableThreshold { target, .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => { matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) } + RuntimeCondition::PlayerVariableThreshold { target, .. } => { + matches!(target, RuntimePlayerTarget::ConditionTruePlayer) + } RuntimeCondition::ChairmanNumericThreshold { .. } => false, RuntimeCondition::TerritoryNumericThreshold { .. } + | RuntimeCondition::TerritoryVariableThreshold { .. } + | RuntimeCondition::WorldVariableThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } @@ -2719,6 +2776,7 @@ fn conditions_provide_company_context(conditions: &[RuntimeCondition]) -> bool { matches!( condition, RuntimeCondition::CompanyNumericThreshold { .. } + | RuntimeCondition::CompanyVariableThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } ) }) @@ -3026,7 +3084,8 @@ fn record_has_world_state_condition_rows(record: &SmpLoadedPackedEventRecordSumm fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { matches!( condition, - RuntimeCondition::SpecialConditionThreshold { .. } + RuntimeCondition::WorldVariableThreshold { .. } + | RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. } @@ -3180,15 +3239,25 @@ fn runtime_condition_company_target_import_blocker( company_context: &ImportRuntimeContext, ) -> Option { match condition { + RuntimeCondition::WorldVariableThreshold { .. } => None, RuntimeCondition::CompanyNumericThreshold { target, .. } => { company_target_import_blocker(target, company_context) } + RuntimeCondition::CompanyVariableThreshold { target, .. } => { + company_target_import_blocker(target, company_context) + } + RuntimeCondition::PlayerVariableThreshold { target, .. } => { + player_target_import_blocker(target, company_context) + } RuntimeCondition::ChairmanNumericThreshold { target, .. } => { chairman_target_import_blocker(target, company_context) } RuntimeCondition::TerritoryNumericThreshold { target, .. } => { territory_target_import_blocker(target, company_context) } + RuntimeCondition::TerritoryVariableThreshold { target, .. } => { + territory_target_import_blocker(target, company_context) + } RuntimeCondition::CompanyTerritoryNumericThreshold { target, territory, .. } => company_target_import_blocker(target, company_context) diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index cd5380b..56e388e 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -311,24 +311,47 @@ pub enum RuntimeTrackMetric { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeCondition { + WorldVariableThreshold { + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, CompanyNumericThreshold { target: RuntimeCompanyTarget, metric: RuntimeCompanyMetric, comparator: RuntimeConditionComparator, value: i64, }, + CompanyVariableThreshold { + target: RuntimeCompanyTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, ChairmanNumericThreshold { target: RuntimeChairmanTarget, metric: RuntimeChairmanMetric, comparator: RuntimeConditionComparator, value: i64, }, + PlayerVariableThreshold { + target: RuntimePlayerTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, TerritoryNumericThreshold { target: RuntimeTerritoryTarget, metric: RuntimeTerritoryMetric, comparator: RuntimeConditionComparator, value: i64, }, + TerritoryVariableThreshold { + target: RuntimeTerritoryTarget, + index: u32, + comparator: RuntimeConditionComparator, + value: i64, + }, CompanyTerritoryNumericThreshold { target: RuntimeCompanyTarget, territory: RuntimeTerritoryTarget, @@ -1209,6 +1232,7 @@ impl RuntimeState { validate_runtime_condition( condition, &seen_company_ids, + &seen_player_ids, &seen_chairman_profile_ids, &seen_territory_ids, ) @@ -1810,6 +1834,7 @@ fn validate_event_record_template( validate_runtime_condition( condition, valid_company_ids, + valid_player_ids, valid_chairman_profile_ids, valid_territory_ids, ) @@ -1842,19 +1867,51 @@ fn validate_event_record_template( fn validate_runtime_condition( condition: &RuntimeCondition, valid_company_ids: &BTreeSet, + valid_player_ids: &BTreeSet, valid_chairman_profile_ids: &BTreeSet, valid_territory_ids: &BTreeSet, ) -> Result<(), String> { match condition { + RuntimeCondition::WorldVariableThreshold { index, .. } => { + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } RuntimeCondition::CompanyNumericThreshold { target, .. } => { validate_company_target(target, valid_company_ids) } + RuntimeCondition::CompanyVariableThreshold { target, index, .. } => { + validate_company_target(target, valid_company_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } RuntimeCondition::ChairmanNumericThreshold { target, .. } => { validate_chairman_target(target, valid_chairman_profile_ids) } + RuntimeCondition::PlayerVariableThreshold { target, index, .. } => { + validate_player_target(target, valid_player_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } RuntimeCondition::TerritoryNumericThreshold { target, .. } => { validate_territory_target(target, valid_territory_ids) } + RuntimeCondition::TerritoryVariableThreshold { target, index, .. } => { + validate_territory_target(target, valid_territory_ids)?; + if !(1..=4).contains(index) { + Err("index must be in 1..=4".to_string()) + } else { + Ok(()) + } + } RuntimeCondition::CompanyTerritoryNumericThreshold { target, territory, .. } => { diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 7ecd532..24fbfa1 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -333,9 +333,13 @@ pub(crate) fn grouped_effect_descriptor_runtime_status_name( #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RealOrdinaryConditionMetric { + WorldVariable(u32), Company(RuntimeCompanyMetric), + CompanyVariable(u32), + PlayerVariable(u32), Chairman(RuntimeChairmanMetric), Territory(RuntimeTerritoryMetric), + TerritoryVariable(u32), CompanyTerritory(RuntimeTrackMetric), } @@ -511,6 +515,22 @@ const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [ ]; const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; +const REAL_WORLD_VARIABLE_1_CONDITION_ID: i32 = 2241; +const REAL_WORLD_VARIABLE_2_CONDITION_ID: i32 = 2242; +const REAL_WORLD_VARIABLE_3_CONDITION_ID: i32 = 2243; +const REAL_WORLD_VARIABLE_4_CONDITION_ID: i32 = 2244; +const REAL_COMPANY_VARIABLE_1_CONDITION_ID: i32 = 2245; +const REAL_COMPANY_VARIABLE_2_CONDITION_ID: i32 = 2246; +const REAL_COMPANY_VARIABLE_3_CONDITION_ID: i32 = 2247; +const REAL_COMPANY_VARIABLE_4_CONDITION_ID: i32 = 2248; +const REAL_PLAYER_VARIABLE_1_CONDITION_ID: i32 = 2249; +const REAL_PLAYER_VARIABLE_2_CONDITION_ID: i32 = 2250; +const REAL_PLAYER_VARIABLE_3_CONDITION_ID: i32 = 2251; +const REAL_PLAYER_VARIABLE_4_CONDITION_ID: i32 = 2252; +const REAL_TERRITORY_VARIABLE_1_CONDITION_ID: i32 = 2253; +const REAL_TERRITORY_VARIABLE_2_CONDITION_ID: i32 = 2254; +const REAL_TERRITORY_VARIABLE_3_CONDITION_ID: i32 = 2255; +const REAL_TERRITORY_VARIABLE_4_CONDITION_ID: i32 = 2256; const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218; const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239; const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240; @@ -530,7 +550,87 @@ const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421; const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547; const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; -const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 40] = [ +const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 56] = [ + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, + label: "Game Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_2_CONDITION_ID, + label: "Game Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_3_CONDITION_ID, + label: "Game Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_WORLD_VARIABLE_4_CONDITION_ID, + label: "Game Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_1_CONDITION_ID, + label: "Company Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_2_CONDITION_ID, + label: "Company Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_3_CONDITION_ID, + label: "Company Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_COMPANY_VARIABLE_4_CONDITION_ID, + label: "Company Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_1_CONDITION_ID, + label: "Player Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_2_CONDITION_ID, + label: "Player Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, + label: "Player Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_PLAYER_VARIABLE_4_CONDITION_ID, + label: "Player Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(4)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_1_CONDITION_ID, + label: "Territory Variable 1", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(1)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_2_CONDITION_ID, + label: "Territory Variable 2", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(2)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_3_CONDITION_ID, + label: "Territory Variable 3", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(3)), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, + label: "Territory Variable 4", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(4)), + }, RealOrdinaryConditionMetadata { raw_condition_id: 1802, label: "Current Cash", @@ -3580,6 +3680,13 @@ fn decode_real_condition_row( let comparator = decode_real_condition_comparator(row.subtype)?; let value = decode_real_condition_threshold(&row.flag_bytes)?; match metadata.kind { + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(index)) => { + Some(RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + }) + } RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => { Some(RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::ConditionTrueCompany, @@ -3588,6 +3695,26 @@ fn decode_real_condition_row( value, }) } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(index)) => { + Some(RuntimeCondition::CompanyVariableThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + index, + comparator, + value, + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(index)) => { + negative_sentinel_scope.and_then(|scope| { + real_condition_player_target(scope).map(|target| { + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } + }) + }) + } RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => { negative_sentinel_scope.and_then(|scope| { real_condition_chairman_target(scope).map(|target| { @@ -3600,6 +3727,16 @@ fn decode_real_condition_row( }) }) } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable( + index, + )) => negative_sentinel_scope + .filter(|scope| scope.territory_scope_selector_is_0x63) + .map(|_| RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + index, + comparator, + value, + }), RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => { negative_sentinel_scope .filter(|scope| scope.territory_scope_selector_is_0x63) @@ -3727,6 +3864,22 @@ fn real_condition_chairman_target( } } +fn real_condition_player_target( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> Option { + match scope.player_test_scope { + RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimePlayerTarget::AllActive), + RuntimePlayerConditionTestScope::SelectedPlayerOnly => { + Some(RuntimePlayerTarget::SelectedPlayer) + } + RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimePlayerTarget::AiPlayers), + RuntimePlayerConditionTestScope::HumanPlayersOnly => { + Some(RuntimePlayerTarget::HumanPlayers) + } + RuntimePlayerConditionTestScope::Disabled => None, + } +} + fn real_grouped_effect_descriptor_metadata( descriptor_id: u32, ) -> Option { @@ -4981,9 +5134,13 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool { match condition { - RuntimeCondition::CompanyNumericThreshold { .. } + RuntimeCondition::WorldVariableThreshold { .. } + | RuntimeCondition::CompanyNumericThreshold { .. } + | RuntimeCondition::CompanyVariableThreshold { .. } + | RuntimeCondition::PlayerVariableThreshold { .. } | RuntimeCondition::ChairmanNumericThreshold { .. } | RuntimeCondition::TerritoryNumericThreshold { .. } + | RuntimeCondition::TerritoryVariableThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } @@ -11188,6 +11345,14 @@ mod tests { #[test] fn looks_up_checked_in_chairman_and_governance_condition_metadata() { + let world_variable = real_ordinary_condition_metadata(REAL_WORLD_VARIABLE_1_CONDITION_ID) + .expect("world-variable condition metadata should exist"); + assert_eq!(world_variable.label, "Game Variable 1"); + + let player_variable = real_ordinary_condition_metadata(REAL_PLAYER_VARIABLE_3_CONDITION_ID) + .expect("player-variable condition metadata should exist"); + assert_eq!(player_variable.label, "Player Variable 3"); + let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID) .expect("chairman cash condition metadata should exist"); assert_eq!(chairman_cash.label, "Player Cash"); @@ -11228,6 +11393,116 @@ mod tests { assert_eq!(book_value.label, "Book Value Per Share"); } + #[test] + fn decodes_world_variable_condition() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&111_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Game Variable 1".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Game Variable 1 == 111".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::WorldVariableThreshold { + index: 1, + comparator: RuntimeConditionComparator::Eq, + value: 111, + }) + ); + } + + #[test] + fn decodes_player_variable_condition_from_selected_player_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&333_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Variable 3".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Variable 3 == 333".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 3, + comparator: RuntimeConditionComparator::Eq, + value: 333, + }) + ); + } + + #[test] + fn decodes_territory_variable_condition_with_world_territory_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&444_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Territory Variable 4".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Territory Variable 4 == 444".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: true, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::AllTerritories, + index: 4, + comparator: RuntimeConditionComparator::Eq, + value: 444, + }) + ); + } + #[test] fn decodes_chairman_cash_condition_from_selected_player_scope() { let row = SmpLoadedPackedEventConditionRowSummary { diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 2e9f88b..42f9f86 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -782,10 +782,25 @@ fn evaluate_record_conditions( } let mut company_matches: Option> = None; + let mut player_matches: Option> = None; let mut chairman_matches: Option> = None; for condition in conditions { match condition { + RuntimeCondition::WorldVariableThreshold { + index, + comparator, + value, + } => { + let actual = state + .world_runtime_variables + .get(index) + .copied() + .unwrap_or(0); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } RuntimeCondition::CompanyNumericThreshold { target, metric, @@ -821,6 +836,37 @@ fn evaluate_record_conditions( return Ok(None); } } + RuntimeCondition::CompanyVariableThreshold { + target, + index, + comparator, + value, + } => { + let resolved = resolve_company_target_ids( + state, + target, + &ResolvedConditionContext::default(), + )?; + let matching = resolved + .into_iter() + .filter(|company_id| { + let actual = state + .company_runtime_variables + .get(company_id) + .and_then(|vars| vars.get(index)) + .copied() + .unwrap_or(0); + compare_condition_value(actual, *comparator, *value) + }) + .collect::>(); + if matching.is_empty() { + return Ok(None); + } + intersect_company_matches(&mut company_matches, matching); + if company_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } RuntimeCondition::TerritoryNumericThreshold { target, metric, @@ -833,6 +879,56 @@ fn evaluate_record_conditions( return Ok(None); } } + RuntimeCondition::TerritoryVariableThreshold { + target, + index, + comparator, + value, + } => { + let territory_ids = resolve_territory_target_ids(state, target)?; + let actual = territory_ids + .iter() + .map(|territory_id| { + state + .territory_runtime_variables + .get(territory_id) + .and_then(|vars| vars.get(index)) + .copied() + .unwrap_or(0) + }) + .sum::(); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::PlayerVariableThreshold { + target, + index, + comparator, + value, + } => { + let resolved = + resolve_player_target_ids(state, target, &ResolvedConditionContext::default())?; + let matching = resolved + .into_iter() + .filter(|player_id| { + let actual = state + .player_runtime_variables + .get(player_id) + .and_then(|vars| vars.get(index)) + .copied() + .unwrap_or(0); + compare_condition_value(actual, *comparator, *value) + }) + .collect::>(); + if matching.is_empty() { + return Ok(None); + } + intersect_player_matches(&mut player_matches, matching); + if player_matches.as_ref().is_some_and(BTreeSet::is_empty) { + return Ok(None); + } + } RuntimeCondition::ChairmanNumericThreshold { target, metric, @@ -1050,7 +1146,7 @@ fn evaluate_record_conditions( Ok(Some(ResolvedConditionContext { matching_company_ids: company_matches.unwrap_or_default(), - matching_player_ids: BTreeSet::new(), + matching_player_ids: player_matches.unwrap_or_default(), matching_chairman_profile_ids: chairman_matches.unwrap_or_default(), })) } @@ -1066,6 +1162,17 @@ fn intersect_company_matches(company_matches: &mut Option>, next: } } +fn intersect_player_matches(player_matches: &mut Option>, next: BTreeSet) { + match player_matches { + Some(existing) => { + existing.retain(|player_id| next.contains(player_id)); + } + None => { + *player_matches = Some(next); + } + } +} + fn intersect_chairman_matches(chairman_matches: &mut Option>, next: BTreeSet) { match chairman_matches { Some(existing) => { @@ -1586,8 +1693,9 @@ mod tests { RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, - RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, + RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, + RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, + RuntimeWorldRestoreState, }; fn state() -> RuntimeState { @@ -2965,6 +3073,122 @@ mod tests { assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); } + #[test] + fn evaluates_runtime_variable_conditions_before_effects_run() { + 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, + }], + selected_company_id: Some(1), + players: vec![RuntimePlayer { + player_id: 1, + current_cash: 50, + active: true, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_player_id: Some(1), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("North".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + world_runtime_variables: BTreeMap::from([(1, 111)]), + company_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(2, 222)]))]), + player_runtime_variables: BTreeMap::from([(1, BTreeMap::from([(3, 333)]))]), + territory_runtime_variables: BTreeMap::from([(7, BTreeMap::from([(4, 444)]))]), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 27, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![ + RuntimeCondition::WorldVariableThreshold { + index: 1, + comparator: RuntimeConditionComparator::Eq, + value: 111, + }, + RuntimeCondition::CompanyVariableThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + index: 2, + comparator: RuntimeConditionComparator::Eq, + value: 222, + }, + RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 3, + comparator: RuntimeConditionComparator::Eq, + value: 333, + }, + RuntimeCondition::TerritoryVariableThreshold { + target: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + index: 4, + comparator: RuntimeConditionComparator::Eq, + value: 444, + }, + ], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "runtime_variable_condition_passed".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 28, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::PlayerVariableThreshold { + target: RuntimePlayerTarget::SelectedPlayer, + index: 4, + comparator: RuntimeConditionComparator::Gt, + value: 0, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "runtime_variable_condition_failed".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("runtime-variable conditions should evaluate successfully"); + + assert_eq!(result.service_events[0].serviced_record_ids, vec![27]); + assert_eq!( + state.world_flags.get("runtime_variable_condition_passed"), + Some(&true) + ); + assert_eq!( + state.world_flags.get("runtime_variable_condition_failed"), + None + ); + } + #[test] fn one_shot_record_only_fires_once() { let mut state = RuntimeState { diff --git a/docs/README.md b/docs/README.md index aa79fbd..01faf19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -114,14 +114,17 @@ The highest-value next passes are now: 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 + world/company/player/territory state, and the matching ordinary-condition strip now gates + imported records through those same runtime-owned variable maps; these variables are still + 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 named cargo-production strip `180..229` now lands on named cargo production overrides too - the named cargo-price strip `106..176` remains explicit `blocked_evidence_blocked_descriptor` parity until descriptor ordering is pinned more strongly +- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity, + with tracked fixture coverage, instead of generic unresolved descriptor residue - widen real packed-event executable coverage descriptor by descriptor after identity, target mask, and normalized effect semantics are all grounded, not just after row framing is parsed - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 12c9707..fdac63f 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -76,14 +76,17 @@ Implemented today: 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 + world/company/player/territory variable maps, and the matching ordinary-condition strip now + gates runtime records through those same maps too, so those descriptors and conditions 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 named cargo-production strip `180..229` now imports through named cargo production overrides too - the named cargo-price strip `106..176` now sits on explicit `blocked_evidence_blocked_descriptor` parity instead of generic unmapped-descriptor frontier +- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity + with tracked fixture coverage, not generic unresolved descriptor residue - a minimal event-owned train surface and an opaque economic-status lane now exist in runtime state, and real descriptors `8` = `Economic Status`, `9` = `Confiscate All`, and `15` = `Retire Train` now import and execute through the ordinary runtime path when overlay context diff --git a/fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json b/fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json new file mode 100644 index 0000000..5a20468 --- /dev/null +++ b/fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json @@ -0,0 +1,37 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-add-building-shell-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture pinning the explicit shell-owned descriptor frontier for recovered add-building rows." + }, + "state_save_slice_path": "packed-event-add-building-shell-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "calendar_projection_source": "default-1830-placeholder", + "calendar_projection_is_placeholder": true, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_parity_only_record_count": 1, + "packed_event_blocked_shell_owned_descriptor_count": 1, + "event_runtime_record_count": 0, + "total_event_record_service_count": 0, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_shell_owned_descriptor" + } + ] + }, + "event_runtime_records": [] + } +} diff --git a/fixtures/runtime/packed-event-add-building-shell-save-slice.json b/fixtures/runtime/packed-event-add-building-shell-save-slice.json new file mode 100644 index 0000000..a0f729a --- /dev/null +++ b/fixtures/runtime/packed-event-add-building-shell-save-slice.json @@ -0,0 +1,110 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-add-building-shell-save-slice", + "source": { + "description": "Tracked save-slice document pinning the recovered add-building strip on the explicit shell-owned descriptor frontier.", + "original_save_filename": "captured-add-building-shell.gms", + "original_save_sha256": "add-building-shell-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "pins descriptor 503 as recovered shell-owned parity instead of unresolved residue" + ] + }, + "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": 28928, + "records_tag_offset": 29184, + "close_tag_offset": 29696, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 75, + "live_record_count": 1, + "live_entry_ids": [75], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 75, + "payload_offset": 29186, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "active": null, + "marks_collection_dirty": null, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 0, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 1, + "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": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 503, + "descriptor_label": "Add Building Slot 1", + "target_mask_bits": 8, + "parameter_family": "world_building_spawn", + "opcode": 3, + "raw_scalar_value": 1, + "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 Add Building Slot 1 to 1", + "locomotive_name": null, + "notes": [ + "descriptor recovered in the checked-in effect table as shell_owned parity" + ] + } + ], + "decoded_conditions": [], + "decoded_actions": [], + "executable_import_ready": false, + "notes": [ + "add-building descriptor is recovered but remains shell-owned parity" + ] + } + ] + }, + "notes": [ + "recovered add-building descriptor sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json b/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json new file mode 100644 index 0000000..425c960 --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json @@ -0,0 +1,134 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-runtime-variable-condition-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving runtime-variable ordinary conditions gate imported records through bounded world/company/player/territory variable surfaces." + }, + "state_import_path": "packed-event-runtime-variable-condition-overlay.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + }, + { + "kind": "service_trigger_kind", + "trigger_kind": 8 + } + ], + "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": 5, + "packed_event_decoded_record_count": 5, + "packed_event_imported_runtime_record_count": 5, + "event_runtime_record_count": 5, + "world_runtime_variable_count": 2, + "company_runtime_variable_owner_count": 1, + "player_runtime_variable_owner_count": 1, + "territory_runtime_variable_owner_count": 1, + "total_event_record_service_count": 5, + "total_trigger_dispatch_count": 2 + }, + "expected_state_fragment": { + "world_runtime_variables": { + "1": 111, + "2": 211 + }, + "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" + }, + { + "import_outcome": "imported", + "decoded_conditions": [ + { + "kind": "world_variable_threshold", + "index": 1, + "comparator": "eq", + "value": 111 + }, + { + "kind": "company_variable_threshold", + "target": { + "kind": "selected_company" + }, + "index": 2, + "comparator": "eq", + "value": 222 + }, + { + "kind": "player_variable_threshold", + "target": { + "kind": "selected_player" + }, + "index": 3, + "comparator": "eq", + "value": 333 + }, + { + "kind": "territory_variable_threshold", + "target": { + "kind": "all_territories" + }, + "index": 4, + "comparator": "eq", + "value": 444 + } + ] + } + ] + }, + "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 + }, + { + "record_id": 75, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-runtime-variable-condition-overlay.json b/fixtures/runtime/packed-event-runtime-variable-condition-overlay.json new file mode 100644 index 0000000..619cfd8 --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-condition-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-runtime-variable-condition-overlay", + "source": { + "description": "Overlay import combining company/player/territory runtime context with the runtime-variable condition sample." + }, + "base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json", + "save_slice_path": "packed-event-runtime-variable-condition-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-runtime-variable-condition-save-slice.json b/fixtures/runtime/packed-event-runtime-variable-condition-save-slice.json new file mode 100644 index 0000000..f7ae672 --- /dev/null +++ b/fixtures/runtime/packed-event-runtime-variable-condition-save-slice.json @@ -0,0 +1,479 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-runtime-variable-condition-save-slice", + "source": { + "description": "Tracked save-slice document proving runtime-variable ordinary conditions gate a packed world-variable effect.", + "original_save_filename": "captured-runtime-variable-condition.gms", + "original_save_sha256": "runtime-variable-condition-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "uses overlay-backed company/player/territory context while proving world/company/player/territory runtime-variable conditions together" + ] + }, + "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": 34944, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 75, + "live_record_count": 5, + "live_entry_ids": [71, 72, 73, 74, 75], + "decoded_record_count": 5, + "imported_runtime_record_count": 5, + "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" + ] + }, + { + "record_index": 4, + "live_entry_id": 75, + "payload_offset": 34208, + "payload_len": 224, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 8, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 2, + "modifier_flag_0x7fa": 2, + "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": 4, + "standalone_condition_rows": [ + { + "row_index": 0, + "raw_condition_id": 2241, + "subtype": 4, + "flag_bytes": [111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Game Variable 1", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Game Variable 1 == 111", + "requires_candidate_name_binding": false, + "notes": [] + }, + { + "row_index": 1, + "raw_condition_id": 2246, + "subtype": 4, + "flag_bytes": [222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Company Variable 2", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Company Variable 2 == 222", + "requires_candidate_name_binding": false, + "notes": [] + }, + { + "row_index": 2, + "raw_condition_id": 2251, + "subtype": 4, + "flag_bytes": [77, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Player Variable 3", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Player Variable 3 == 333", + "requires_candidate_name_binding": false, + "notes": [] + }, + { + "row_index": 3, + "raw_condition_id": 2256, + "subtype": 4, + "flag_bytes": [188, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Territory Variable 4", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Territory Variable 4 == 444", + "requires_candidate_name_binding": false, + "notes": [] + } + ], + "negative_sentinel_scope": { + "company_test_scope": "selected_company_only", + "player_test_scope": "selected_player_only", + "territory_scope_selector_is_0x63": true, + "source_row_indexes": [0] + }, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 40, + "descriptor_label": "Game Variable 2", + "target_mask_bits": 8, + "parameter_family": "runtime_variable_scalar", + "opcode": 3, + "raw_scalar_value": 211, + "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 2 to 211", + "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_conditions": [ + { + "kind": "world_variable_threshold", + "index": 1, + "comparator": "eq", + "value": 111 + }, + { + "kind": "company_variable_threshold", + "target": { + "kind": "condition_true_company" + }, + "index": 2, + "comparator": "eq", + "value": 222 + }, + { + "kind": "player_variable_threshold", + "target": { + "kind": "selected_player" + }, + "index": 3, + "comparator": "eq", + "value": 333 + }, + { + "kind": "territory_variable_threshold", + "target": { + "kind": "all_territories" + }, + "index": 4, + "comparator": "eq", + "value": 444 + } + ], + "decoded_actions": [ + { + "kind": "set_world_variable", + "index": 2, + "value": 211 + } + ], + "executable_import_ready": true, + "notes": [ + "runtime variable conditions gate a world-variable effect" + ] + } + ] + }, + "notes": [ + "runtime variable condition executable sample" + ] + } +} diff --git a/tools/py/build_event_effect_semantic_catalog.py b/tools/py/build_event_effect_semantic_catalog.py index e48744c..f9007bb 100644 --- a/tools/py/build_event_effect_semantic_catalog.py +++ b/tools/py/build_event_effect_semantic_catalog.py @@ -208,6 +208,7 @@ def classify( executable_in_runtime = True elif 503 <= descriptor_id <= 519: parameter_family = "world_building_spawn" + label = f"Add Building Slot {descriptor_id - 502}" runtime_status = "shell_owned" elif signature_byte_0x63 == 0 and signature_byte_0x64 == 0x8F: parameter_family = "runtime_variable_scalar"