Implement runtime variable event conditions

This commit is contained in:
Jan Petykiewicz 2026-04-17 08:50:35 -07:00
commit bd9e1421a1
15 changed files with 1442 additions and 29 deletions

View file

@ -46,7 +46,8 @@ bounded runtime landing surface too: representative descriptors import into
`RuntimeState.world_scalar_overrides` through stable normalized keys such as `RuntimeState.world_scalar_overrides` through stable normalized keys such as
`world.build_stations_cost`, `world.track_maintenance_cost`, `world.all_engine_speeds`, and `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 `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 reconstruction or adding a second packed executor. The grounded aggregate cargo-economics
descriptors now have bounded descriptors now have bounded
runtime landing surfaces too: descriptor `105` `All Cargo Prices` plus descriptors `177..179` 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 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` 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 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 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 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 finance, company track, aggregate territory track, and company-territory track rows can import

View file

@ -4532,7 +4532,7 @@
}, },
{ {
"descriptor_id": 503, "descriptor_id": 503,
"label": "Unknown Add Building", "label": "Add Building Slot 1",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4541,7 +4541,7 @@
}, },
{ {
"descriptor_id": 504, "descriptor_id": 504,
"label": "Unknown Add Building", "label": "Add Building Slot 2",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4550,7 +4550,7 @@
}, },
{ {
"descriptor_id": 505, "descriptor_id": 505,
"label": "Unknown Add Building", "label": "Add Building Slot 3",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4559,7 +4559,7 @@
}, },
{ {
"descriptor_id": 506, "descriptor_id": 506,
"label": "Unknown Add Building", "label": "Add Building Slot 4",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4568,7 +4568,7 @@
}, },
{ {
"descriptor_id": 507, "descriptor_id": 507,
"label": "Unknown Add Building", "label": "Add Building Slot 5",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4577,7 +4577,7 @@
}, },
{ {
"descriptor_id": 508, "descriptor_id": 508,
"label": "Unknown Add Building", "label": "Add Building Slot 6",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4586,7 +4586,7 @@
}, },
{ {
"descriptor_id": 509, "descriptor_id": 509,
"label": "Unknown Add Building", "label": "Add Building Slot 7",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4595,7 +4595,7 @@
}, },
{ {
"descriptor_id": 510, "descriptor_id": 510,
"label": "Unknown Add Building", "label": "Add Building Slot 8",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4604,7 +4604,7 @@
}, },
{ {
"descriptor_id": 511, "descriptor_id": 511,
"label": "Unknown Add Building", "label": "Add Building Slot 9",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4613,7 +4613,7 @@
}, },
{ {
"descriptor_id": 512, "descriptor_id": 512,
"label": "Unknown Add Building", "label": "Add Building Slot 10",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4622,7 +4622,7 @@
}, },
{ {
"descriptor_id": 513, "descriptor_id": 513,
"label": "Unknown Add Building", "label": "Add Building Slot 11",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4631,7 +4631,7 @@
}, },
{ {
"descriptor_id": 514, "descriptor_id": 514,
"label": "Unknown Add Building", "label": "Add Building Slot 12",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4640,7 +4640,7 @@
}, },
{ {
"descriptor_id": 515, "descriptor_id": 515,
"label": "Unknown Add Building", "label": "Add Building Slot 13",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4649,7 +4649,7 @@
}, },
{ {
"descriptor_id": 516, "descriptor_id": 516,
"label": "Unknown Add Building", "label": "Add Building Slot 14",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4658,7 +4658,7 @@
}, },
{ {
"descriptor_id": 517, "descriptor_id": 517,
"label": "Unknown Add Building", "label": "Add Building Slot 15",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4667,7 +4667,7 @@
}, },
{ {
"descriptor_id": 518, "descriptor_id": 518,
"label": "Unknown Add Building", "label": "Add Building Slot 16",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,
@ -4676,7 +4676,7 @@
}, },
{ {
"descriptor_id": 519, "descriptor_id": 519,
"label": "Unknown Add Building", "label": "Add Building Slot 17",
"target_mask_bits": 8, "target_mask_bits": 8,
"parameter_family": "world_building_spawn", "parameter_family": "world_building_spawn",
"runtime_key": null, "runtime_key": null,

View file

@ -4481,11 +4481,17 @@ mod tests {
); );
let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json"); .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")) let cargo_economics_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-economics-save-slice-fixture.json"); .join("../../fixtures/runtime/packed-event-cargo-economics-save-slice-fixture.json");
let cargo_economics_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( let cargo_economics_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-cargo-economics-parity-save-slice-fixture.json", "../../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( let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json", "../../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"); .expect("save-slice-backed world-scalar override fixture should summarize");
run_runtime_summarize_fixture(&runtime_variable_overlay_fixture) run_runtime_summarize_fixture(&runtime_variable_overlay_fixture)
.expect("overlay-backed runtime-variable fixture should summarize"); .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) run_runtime_summarize_fixture(&cargo_economics_fixture)
.expect("save-slice-backed cargo-economics fixture should summarize"); .expect("save-slice-backed cargo-economics fixture should summarize");
run_runtime_summarize_fixture(&cargo_economics_parity_fixture) run_runtime_summarize_fixture(&cargo_economics_parity_fixture)
.expect("save-slice-backed cargo-economics parity fixture should summarize"); .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) run_runtime_summarize_fixture(&world_scalar_condition_fixture)
.expect("save-slice-backed executable world-scalar condition fixture should summarize"); .expect("save-slice-backed executable world-scalar condition fixture should summarize");
run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture) run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture)

View file

@ -1302,6 +1302,7 @@ fn lowered_record_decoded_conditions(
} }
let lowered_company_target = lowered_condition_true_company_target(record)?; let lowered_company_target = lowered_condition_true_company_target(record)?;
let lowered_player_target = lowered_condition_true_player_target(record)?;
let ordinary_rows = record let ordinary_rows = record
.standalone_condition_rows .standalone_condition_rows
.iter() .iter()
@ -1313,6 +1314,7 @@ fn lowered_record_decoded_conditions(
condition, condition,
row, row,
lowered_company_target.as_ref(), lowered_company_target.as_ref(),
lowered_player_target.as_ref(),
company_context, company_context,
) )
}) })
@ -1949,9 +1951,19 @@ fn lower_condition_targets_in_condition(
condition: &RuntimeCondition, condition: &RuntimeCondition,
row: &SmpLoadedPackedEventConditionRowSummary, row: &SmpLoadedPackedEventConditionRowSummary,
lowered_company_target: Option<&RuntimeCompanyTarget>, lowered_company_target: Option<&RuntimeCompanyTarget>,
lowered_player_target: Option<&RuntimePlayerTarget>,
company_context: &ImportRuntimeContext, company_context: &ImportRuntimeContext,
) -> Result<RuntimeCondition, ImportBlocker> { ) -> Result<RuntimeCondition, ImportBlocker> {
Ok(match condition { Ok(match condition {
RuntimeCondition::WorldVariableThreshold {
index,
comparator,
value,
} => RuntimeCondition::WorldVariableThreshold {
index: *index,
comparator: *comparator,
value: *value,
},
RuntimeCondition::CompanyNumericThreshold { RuntimeCondition::CompanyNumericThreshold {
target, target,
metric, metric,
@ -1966,6 +1978,20 @@ fn lower_condition_targets_in_condition(
comparator: *comparator, comparator: *comparator,
value: *value, 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 { RuntimeCondition::ChairmanNumericThreshold {
target, target,
metric, metric,
@ -1977,6 +2003,20 @@ fn lower_condition_targets_in_condition(
comparator: *comparator, comparator: *comparator,
value: *value, 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 { RuntimeCondition::TerritoryNumericThreshold {
target, target,
metric, metric,
@ -1988,6 +2028,17 @@ fn lower_condition_targets_in_condition(
comparator: *comparator, comparator: *comparator,
value: *value, 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 { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, target,
territory, territory,
@ -2187,11 +2238,17 @@ fn record_uses_condition_true_player(record: &SmpLoadedPackedEventRecordSummary)
fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
match condition { match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. } RuntimeCondition::CompanyNumericThreshold { target, .. }
| RuntimeCondition::CompanyVariableThreshold { target, .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => { | RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
} }
RuntimeCondition::PlayerVariableThreshold { target, .. } => {
matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
}
RuntimeCondition::ChairmanNumericThreshold { .. } => false, RuntimeCondition::ChairmanNumericThreshold { .. } => false,
RuntimeCondition::TerritoryNumericThreshold { .. } RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::TerritoryVariableThreshold { .. }
| RuntimeCondition::WorldVariableThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
@ -2719,6 +2776,7 @@ fn conditions_provide_company_context(conditions: &[RuntimeCondition]) -> bool {
matches!( matches!(
condition, condition,
RuntimeCondition::CompanyNumericThreshold { .. } RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::CompanyVariableThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
) )
}) })
@ -3026,7 +3084,8 @@ fn record_has_world_state_condition_rows(record: &SmpLoadedPackedEventRecordSumm
fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool {
matches!( matches!(
condition, condition,
RuntimeCondition::SpecialConditionThreshold { .. } RuntimeCondition::WorldVariableThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
| RuntimeCondition::NamedLocomotiveCostThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. }
@ -3180,15 +3239,25 @@ fn runtime_condition_company_target_import_blocker(
company_context: &ImportRuntimeContext, company_context: &ImportRuntimeContext,
) -> Option<ImportBlocker> { ) -> Option<ImportBlocker> {
match condition { match condition {
RuntimeCondition::WorldVariableThreshold { .. } => None,
RuntimeCondition::CompanyNumericThreshold { target, .. } => { RuntimeCondition::CompanyNumericThreshold { target, .. } => {
company_target_import_blocker(target, company_context) 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, .. } => { RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
chairman_target_import_blocker(target, company_context) chairman_target_import_blocker(target, company_context)
} }
RuntimeCondition::TerritoryNumericThreshold { target, .. } => { RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
territory_target_import_blocker(target, company_context) territory_target_import_blocker(target, company_context)
} }
RuntimeCondition::TerritoryVariableThreshold { target, .. } => {
territory_target_import_blocker(target, company_context)
}
RuntimeCondition::CompanyTerritoryNumericThreshold { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, territory, .. target, territory, ..
} => company_target_import_blocker(target, company_context) } => company_target_import_blocker(target, company_context)

View file

@ -311,24 +311,47 @@ pub enum RuntimeTrackMetric {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCondition { pub enum RuntimeCondition {
WorldVariableThreshold {
index: u32,
comparator: RuntimeConditionComparator,
value: i64,
},
CompanyNumericThreshold { CompanyNumericThreshold {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
metric: RuntimeCompanyMetric, metric: RuntimeCompanyMetric,
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
}, },
CompanyVariableThreshold {
target: RuntimeCompanyTarget,
index: u32,
comparator: RuntimeConditionComparator,
value: i64,
},
ChairmanNumericThreshold { ChairmanNumericThreshold {
target: RuntimeChairmanTarget, target: RuntimeChairmanTarget,
metric: RuntimeChairmanMetric, metric: RuntimeChairmanMetric,
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
}, },
PlayerVariableThreshold {
target: RuntimePlayerTarget,
index: u32,
comparator: RuntimeConditionComparator,
value: i64,
},
TerritoryNumericThreshold { TerritoryNumericThreshold {
target: RuntimeTerritoryTarget, target: RuntimeTerritoryTarget,
metric: RuntimeTerritoryMetric, metric: RuntimeTerritoryMetric,
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
}, },
TerritoryVariableThreshold {
target: RuntimeTerritoryTarget,
index: u32,
comparator: RuntimeConditionComparator,
value: i64,
},
CompanyTerritoryNumericThreshold { CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
territory: RuntimeTerritoryTarget, territory: RuntimeTerritoryTarget,
@ -1209,6 +1232,7 @@ impl RuntimeState {
validate_runtime_condition( validate_runtime_condition(
condition, condition,
&seen_company_ids, &seen_company_ids,
&seen_player_ids,
&seen_chairman_profile_ids, &seen_chairman_profile_ids,
&seen_territory_ids, &seen_territory_ids,
) )
@ -1810,6 +1834,7 @@ fn validate_event_record_template(
validate_runtime_condition( validate_runtime_condition(
condition, condition,
valid_company_ids, valid_company_ids,
valid_player_ids,
valid_chairman_profile_ids, valid_chairman_profile_ids,
valid_territory_ids, valid_territory_ids,
) )
@ -1842,19 +1867,51 @@ fn validate_event_record_template(
fn validate_runtime_condition( fn validate_runtime_condition(
condition: &RuntimeCondition, condition: &RuntimeCondition,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>, valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>, valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match condition { match condition {
RuntimeCondition::WorldVariableThreshold { index, .. } => {
if !(1..=4).contains(index) {
Err("index must be in 1..=4".to_string())
} else {
Ok(())
}
}
RuntimeCondition::CompanyNumericThreshold { target, .. } => { RuntimeCondition::CompanyNumericThreshold { target, .. } => {
validate_company_target(target, valid_company_ids) 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, .. } => { RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
validate_chairman_target(target, valid_chairman_profile_ids) 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, .. } => { RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
validate_territory_target(target, valid_territory_ids) 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 { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, territory, .. target, territory, ..
} => { } => {

View file

@ -333,9 +333,13 @@ pub(crate) fn grouped_effect_descriptor_runtime_status_name(
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealOrdinaryConditionMetric { enum RealOrdinaryConditionMetric {
WorldVariable(u32),
Company(RuntimeCompanyMetric), Company(RuntimeCompanyMetric),
CompanyVariable(u32),
PlayerVariable(u32),
Chairman(RuntimeChairmanMetric), Chairman(RuntimeChairmanMetric),
Territory(RuntimeTerritoryMetric), Territory(RuntimeTerritoryMetric),
TerritoryVariable(u32),
CompanyTerritory(RuntimeTrackMetric), CompanyTerritory(RuntimeTrackMetric),
} }
@ -511,6 +515,22 @@ const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [
]; ];
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; 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_CASH_CONDITION_ID: i32 = 2218;
const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239; const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239;
const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240; 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_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547;
const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; 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 { RealOrdinaryConditionMetadata {
raw_condition_id: 1802, raw_condition_id: 1802,
label: "Current Cash", label: "Current Cash",
@ -3580,6 +3680,13 @@ fn decode_real_condition_row(
let comparator = decode_real_condition_comparator(row.subtype)?; let comparator = decode_real_condition_comparator(row.subtype)?;
let value = decode_real_condition_threshold(&row.flag_bytes)?; let value = decode_real_condition_threshold(&row.flag_bytes)?;
match metadata.kind { match metadata.kind {
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(index)) => {
Some(RuntimeCondition::WorldVariableThreshold {
index,
comparator,
value,
})
}
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => { RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => {
Some(RuntimeCondition::CompanyNumericThreshold { Some(RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany, target: RuntimeCompanyTarget::ConditionTrueCompany,
@ -3588,6 +3695,26 @@ fn decode_real_condition_row(
value, 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)) => { RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => {
negative_sentinel_scope.and_then(|scope| { negative_sentinel_scope.and_then(|scope| {
real_condition_chairman_target(scope).map(|target| { 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)) => { RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
negative_sentinel_scope negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63) .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<RuntimePlayerTarget> {
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( fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32, descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> { ) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -4981,9 +5134,13 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool { fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool {
match condition { match condition {
RuntimeCondition::CompanyNumericThreshold { .. } RuntimeCondition::WorldVariableThreshold { .. }
| RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::CompanyVariableThreshold { .. }
| RuntimeCondition::PlayerVariableThreshold { .. }
| RuntimeCondition::ChairmanNumericThreshold { .. } | RuntimeCondition::ChairmanNumericThreshold { .. }
| RuntimeCondition::TerritoryNumericThreshold { .. } | RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::TerritoryVariableThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. }
@ -11188,6 +11345,14 @@ mod tests {
#[test] #[test]
fn looks_up_checked_in_chairman_and_governance_condition_metadata() { 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) let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID)
.expect("chairman cash condition metadata should exist"); .expect("chairman cash condition metadata should exist");
assert_eq!(chairman_cash.label, "Player Cash"); assert_eq!(chairman_cash.label, "Player Cash");
@ -11228,6 +11393,116 @@ mod tests {
assert_eq!(book_value.label, "Book Value Per Share"); 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] #[test]
fn decodes_chairman_cash_condition_from_selected_player_scope() { fn decodes_chairman_cash_condition_from_selected_player_scope() {
let row = SmpLoadedPackedEventConditionRowSummary { let row = SmpLoadedPackedEventConditionRowSummary {

View file

@ -782,10 +782,25 @@ fn evaluate_record_conditions(
} }
let mut company_matches: Option<BTreeSet<u32>> = None; let mut company_matches: Option<BTreeSet<u32>> = None;
let mut player_matches: Option<BTreeSet<u32>> = None;
let mut chairman_matches: Option<BTreeSet<u32>> = None; let mut chairman_matches: Option<BTreeSet<u32>> = None;
for condition in conditions { for condition in conditions {
match condition { 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 { RuntimeCondition::CompanyNumericThreshold {
target, target,
metric, metric,
@ -821,6 +836,37 @@ fn evaluate_record_conditions(
return Ok(None); 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::<BTreeSet<_>>();
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 { RuntimeCondition::TerritoryNumericThreshold {
target, target,
metric, metric,
@ -833,6 +879,56 @@ fn evaluate_record_conditions(
return Ok(None); 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::<i64>();
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::<BTreeSet<_>>();
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 { RuntimeCondition::ChairmanNumericThreshold {
target, target,
metric, metric,
@ -1050,7 +1146,7 @@ fn evaluate_record_conditions(
Ok(Some(ResolvedConditionContext { Ok(Some(ResolvedConditionContext {
matching_company_ids: company_matches.unwrap_or_default(), 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(), matching_chairman_profile_ids: chairman_matches.unwrap_or_default(),
})) }))
} }
@ -1066,6 +1162,17 @@ fn intersect_company_matches(company_matches: &mut Option<BTreeSet<u32>>, next:
} }
} }
fn intersect_player_matches(player_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
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<BTreeSet<u32>>, next: BTreeSet<u32>) { fn intersect_chairman_matches(chairman_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
match chairman_matches { match chairman_matches {
Some(existing) => { Some(existing) => {
@ -1586,8 +1693,9 @@ mod tests {
RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState,
RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain,
RuntimeWorldRestoreState,
}; };
fn state() -> RuntimeState { fn state() -> RuntimeState {
@ -2965,6 +3073,122 @@ mod tests {
assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); 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] #[test]
fn one_shot_record_only_fires_once() { fn one_shot_record_only_fires_once() {
let mut state = RuntimeState { let mut state = RuntimeState {

View file

@ -114,14 +114,17 @@ The highest-value next passes are now:
through stable normalized keys such as `world.build_stations_cost` and through stable normalized keys such as `world.build_stations_cost` and
`world.track_maintenance_cost` `world.track_maintenance_cost`
- the runtime-variable strip `39..54` now executes too through bounded event-owned scalar maps on - 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 world/company/player/territory state, and the matching ordinary-condition strip now gates
model and are not yet reconstructed from raw saves 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` - 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 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 / `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 named cargo-production strip `180..229` now lands on named cargo production overrides too
- the named cargo-price strip `106..176` remains explicit - the named cargo-price strip `106..176` remains explicit
`blocked_evidence_blocked_descriptor` parity until descriptor ordering is pinned more strongly `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, - 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 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` - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`

View file

@ -76,14 +76,17 @@ Implemented today:
landing surface too: representative descriptors import as `SetWorldScalarOverride` and land in landing surface too: representative descriptors import as `SetWorldScalarOverride` and land in
`RuntimeState.world_scalar_overrides` `RuntimeState.world_scalar_overrides`
- the runtime-variable strip `39..54` now imports and executes too through bounded event-owned - 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 world/company/player/territory variable maps, and the matching ordinary-condition strip now
evidence parity or ad hoc fixture-only state 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` - 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 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 / `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 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 - the named cargo-price strip `106..176` now sits on explicit
`blocked_evidence_blocked_descriptor` parity instead of generic unmapped-descriptor frontier `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 - 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` = 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 `Retire Train` now import and execute through the ordinary runtime path when overlay context

View file

@ -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": []
}
}

View file

@ -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"
]
}
}

View file

@ -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
}
]
}
}

View file

@ -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"
}

View file

@ -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"
]
}
}

View file

@ -208,6 +208,7 @@ def classify(
executable_in_runtime = True executable_in_runtime = True
elif 503 <= descriptor_id <= 519: elif 503 <= descriptor_id <= 519:
parameter_family = "world_building_spawn" parameter_family = "world_building_spawn"
label = f"Add Building Slot {descriptor_id - 502}"
runtime_status = "shell_owned" runtime_status = "shell_owned"
elif signature_byte_0x63 == 0 and signature_byte_0x64 == 0x8F: elif signature_byte_0x63 == 0 and signature_byte_0x64 == 0x8F:
parameter_family = "runtime_variable_scalar" parameter_family = "runtime_variable_scalar"