Add chairman packed event runtime support

This commit is contained in:
Jan Petykiewicz 2026-04-16 16:07:10 -07:00
commit 86cf89b26c
23 changed files with 1431 additions and 41 deletions

View file

@ -18,6 +18,9 @@ selected-company and controller-role context through overlay imports, and real d
execute through the ordinary runtime path, and descriptors `1` `Player Cash` and `14` execute through the ordinary runtime path, and descriptors `1` `Player Cash` and `14`
`Deactivate Player` now join that batch through the same service engine. Synthetic packed records `Deactivate Player` now join that batch through the same service engine. Synthetic packed records
still exercise the same runtime without a parallel packed executor. The first grounded still exercise the same runtime without a parallel packed executor. The first grounded
chairman-profile runtime slice now exists too: overlay-backed selected-chairman context plus the
hidden grouped target-subject lane let those same real descriptors `1` and `14` execute on
selected-chairman scope, while wider chairman target scopes remain explicit parity. 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

@ -4485,6 +4485,16 @@ mod tests {
); );
let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-catalog-save-slice-fixture.json"); .join("../../fixtures/runtime/packed-event-cargo-catalog-save-slice-fixture.json");
let chairman_cash_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json");
let deactivate_chairman_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json");
let missing_chairman_context_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json",
);
let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture) run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize"); .expect("save-slice-backed parity fixture should summarize");
@ -4526,6 +4536,14 @@ mod tests {
.expect("save-slice-backed parity world-scalar condition fixture should summarize"); .expect("save-slice-backed parity world-scalar condition fixture should summarize");
run_runtime_summarize_fixture(&cargo_catalog_fixture) run_runtime_summarize_fixture(&cargo_catalog_fixture)
.expect("save-slice-backed cargo catalog fixture should summarize"); .expect("save-slice-backed cargo catalog fixture should summarize");
run_runtime_summarize_fixture(&chairman_cash_overlay_fixture)
.expect("overlay-backed chairman-cash fixture should summarize");
run_runtime_summarize_fixture(&deactivate_chairman_overlay_fixture)
.expect("overlay-backed deactivate-chairman fixture should summarize");
run_runtime_summarize_fixture(&missing_chairman_context_fixture)
.expect("save-slice-backed chairman missing-context fixture should summarize");
run_runtime_summarize_fixture(&chairman_scope_parity_fixture)
.expect("save-slice-backed chairman scope parity fixture should summarize");
} }
#[test] #[test]

View file

@ -176,6 +176,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -353,6 +355,8 @@ mod tests {
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -72,6 +72,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub player_count: Option<usize>, pub player_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub chairman_profile_count: Option<usize>,
#[serde(default)]
pub active_chairman_profile_count: Option<usize>,
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub train_count: Option<usize>, pub train_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub active_train_count: Option<usize>, pub active_train_count: Option<usize>,
@ -110,6 +116,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_player_role_context_count: Option<usize>, pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_chairman_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_chairman_target_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>, pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_player_condition_context_count: Option<usize>, pub packed_event_blocked_missing_player_condition_context_count: Option<usize>,
@ -439,6 +449,30 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.chairman_profile_count {
if actual.chairman_profile_count != count {
mismatches.push(format!(
"chairman_profile_count mismatch: expected {count}, got {}",
actual.chairman_profile_count
));
}
}
if let Some(count) = self.active_chairman_profile_count {
if actual.active_chairman_profile_count != count {
mismatches.push(format!(
"active_chairman_profile_count mismatch: expected {count}, got {}",
actual.active_chairman_profile_count
));
}
}
if let Some(selected_id) = self.selected_chairman_profile_id {
if actual.selected_chairman_profile_id != Some(selected_id) {
mismatches.push(format!(
"selected_chairman_profile_id mismatch: expected {selected_id:?}, got {:?}",
actual.selected_chairman_profile_id
));
}
}
if let Some(count) = self.train_count { if let Some(count) = self.train_count {
if actual.train_count != count { if actual.train_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -591,6 +625,22 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_chairman_context_count {
if actual.packed_event_blocked_missing_chairman_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_chairman_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_chairman_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_chairman_target_scope_count {
if actual.packed_event_blocked_chairman_target_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_chairman_target_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_chairman_target_scope_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_condition_context_count { if let Some(count) = self.packed_event_blocked_missing_condition_context_count {
if actual.packed_event_blocked_missing_condition_context_count != count { if actual.packed_event_blocked_missing_condition_context_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -5,17 +5,18 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{ use crate::{
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCompanyConditionTestScope, CalendarPoint, RuntimeCargoCatalogEntry, RuntimeChairmanTarget,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimeCondition, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeWorldRestoreState, SmpLoadedPackedEventConditionRowSummary, RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget, RuntimeWorldRestoreState,
SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
}; };
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1; pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
@ -116,6 +117,8 @@ struct ImportRuntimeContext {
known_player_ids: BTreeSet<u32>, known_player_ids: BTreeSet<u32>,
selected_player_id: Option<u32>, selected_player_id: Option<u32>,
has_complete_player_controller_context: bool, has_complete_player_controller_context: bool,
known_chairman_profile_ids: BTreeSet<u32>,
selected_chairman_profile_id: Option<u32>,
known_territory_ids: BTreeSet<u32>, known_territory_ids: BTreeSet<u32>,
has_territory_context: bool, has_territory_context: bool,
territory_name_to_id: BTreeMap<String, u32>, territory_name_to_id: BTreeMap<String, u32>,
@ -132,6 +135,8 @@ enum ImportBlocker {
MissingPlayerContext, MissingPlayerContext,
MissingPlayerSelectionContext, MissingPlayerSelectionContext,
MissingPlayerRoleContext, MissingPlayerRoleContext,
MissingChairmanContext,
ChairmanTargetScope,
MissingConditionContext, MissingConditionContext,
MissingPlayerConditionContext, MissingPlayerConditionContext,
CompanyConditionScopeDisabled, CompanyConditionScopeDisabled,
@ -153,6 +158,8 @@ impl ImportRuntimeContext {
known_player_ids: BTreeSet::new(), known_player_ids: BTreeSet::new(),
selected_player_id: None, selected_player_id: None,
has_complete_player_controller_context: false, has_complete_player_controller_context: false,
known_chairman_profile_ids: BTreeSet::new(),
selected_chairman_profile_id: None,
known_territory_ids: BTreeSet::new(), known_territory_ids: BTreeSet::new(),
has_territory_context: false, has_territory_context: false,
territory_name_to_id: BTreeMap::new(), territory_name_to_id: BTreeMap::new(),
@ -185,6 +192,12 @@ impl ImportRuntimeContext {
.players .players
.iter() .iter()
.all(|player| player.controller_kind != RuntimeCompanyControllerKind::Unknown), .all(|player| player.controller_kind != RuntimeCompanyControllerKind::Unknown),
known_chairman_profile_ids: state
.chairman_profiles
.iter()
.map(|profile| profile.profile_id)
.collect(),
selected_chairman_profile_id: state.selected_chairman_profile_id,
known_territory_ids: state known_territory_ids: state
.territories .territories
.iter() .iter()
@ -244,6 +257,8 @@ pub fn project_save_slice_to_runtime_state_import(
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(), locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(),
cargo_catalog: projection.cargo_catalog.unwrap_or_default(), cargo_catalog: projection.cargo_catalog.unwrap_or_default(),
@ -307,6 +322,8 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
selected_company_id: base_state.selected_company_id, selected_company_id: base_state.selected_company_id,
players: base_state.players.clone(), players: base_state.players.clone(),
selected_player_id: base_state.selected_player_id, selected_player_id: base_state.selected_player_id,
chairman_profiles: base_state.chairman_profiles.clone(),
selected_chairman_profile_id: base_state.selected_chairman_profile_id,
trains: base_state.trains.clone(), trains: base_state.trains.clone(),
locomotive_catalog: projection locomotive_catalog: projection
.locomotive_catalog .locomotive_catalog
@ -1007,6 +1024,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
descriptor_label: row.descriptor_label.clone(), descriptor_label: row.descriptor_label.clone(),
target_mask_bits: row.target_mask_bits, target_mask_bits: row.target_mask_bits,
parameter_family: row.parameter_family.clone(), parameter_family: row.parameter_family.clone(),
grouped_target_subject: row.grouped_target_subject.clone(),
opcode: row.opcode, opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value, raw_scalar_value: row.raw_scalar_value,
value_byte_0x09: row.value_byte_0x09, value_byte_0x09: row.value_byte_0x09,
@ -1157,6 +1175,9 @@ fn lower_contextual_real_grouped_effects(
let mut effects = Vec::with_capacity(record.grouped_effect_rows.len()); let mut effects = Vec::with_capacity(record.grouped_effect_rows.len());
for row in &record.grouped_effect_rows { for row in &record.grouped_effect_rows {
if real_grouped_row_is_unsupported_chairman_target_scope(row) {
return Err(ImportBlocker::ChairmanTargetScope);
}
if let Some(effect) = lower_contextual_cargo_production_effect(row)? { if let Some(effect) = lower_contextual_cargo_production_effect(row)? {
effects.push(effect); effects.push(effect);
continue; continue;
@ -1425,12 +1446,19 @@ fn lower_condition_targets_in_effect(
)?, )?,
value: *value, value: *value,
}, },
RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash {
target: target.clone(),
value: *value,
},
RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer { RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer {
target: lower_condition_true_player_target_in_player_target( target: lower_condition_true_player_target_in_player_target(
target, target,
lowered_player_target, lowered_player_target,
)?, )?,
}, },
RuntimeEffect::DeactivateChairman { target } => RuntimeEffect::DeactivateChairman {
target: target.clone(),
},
RuntimeEffect::SetCompanyTerritoryAccess { RuntimeEffect::SetCompanyTerritoryAccess {
target, target,
territory, territory,
@ -1590,6 +1618,17 @@ fn lower_condition_targets_in_condition(
comparator: *comparator, comparator: *comparator,
value: *value, value: *value,
}, },
RuntimeCondition::ChairmanNumericThreshold {
target,
metric,
comparator,
value,
} => RuntimeCondition::ChairmanNumericThreshold {
target: target.clone(),
metric: *metric,
comparator: *comparator,
value: *value,
},
RuntimeCondition::TerritoryNumericThreshold { RuntimeCondition::TerritoryNumericThreshold {
target, target,
metric, metric,
@ -1786,6 +1825,7 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => { | RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
matches!(target, RuntimeCompanyTarget::ConditionTrueCompany) matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
} }
RuntimeCondition::ChairmanNumericThreshold { .. } => false,
RuntimeCondition::TerritoryNumericThreshold { .. } RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. }
| RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. }
@ -1803,6 +1843,40 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
} }
} }
fn chairman_target_import_blocker(
target: &RuntimeChairmanTarget,
company_context: &ImportRuntimeContext,
) -> Option<ImportBlocker> {
match target {
RuntimeChairmanTarget::AllActive => {
if company_context.known_chairman_profile_ids.is_empty() {
Some(ImportBlocker::MissingChairmanContext)
} else {
None
}
}
RuntimeChairmanTarget::SelectedChairman => {
if company_context.selected_chairman_profile_id.is_some() {
None
} else {
Some(ImportBlocker::MissingChairmanContext)
}
}
RuntimeChairmanTarget::Ids { ids } => {
if company_context.known_chairman_profile_ids.is_empty() {
Some(ImportBlocker::MissingChairmanContext)
} else if ids
.iter()
.all(|id| company_context.known_chairman_profile_ids.contains(id))
{
None
} else {
Some(ImportBlocker::MissingChairmanContext)
}
}
}
}
fn smp_runtime_effects_to_runtime_effects( fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect], effects: &[RuntimeEffect],
company_context: &ImportRuntimeContext, company_context: &ImportRuntimeContext,
@ -1867,6 +1941,16 @@ fn smp_runtime_effect_to_runtime_effect(
Err(player_target_import_error_message(target, company_context)) Err(player_target_import_error_message(target, company_context))
} }
} }
RuntimeEffect::SetChairmanCash { target, value } => {
if chairman_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::SetChairmanCash {
target: target.clone(),
value: *value,
})
} else {
Err("packed effect requires chairman runtime context".to_string())
}
}
RuntimeEffect::DeactivatePlayer { target } => { RuntimeEffect::DeactivatePlayer { target } => {
if player_target_allowed_for_import( if player_target_allowed_for_import(
target, target,
@ -1880,6 +1964,15 @@ fn smp_runtime_effect_to_runtime_effect(
Err(player_target_import_error_message(target, company_context)) Err(player_target_import_error_message(target, company_context))
} }
} }
RuntimeEffect::DeactivateChairman { target } => {
if chairman_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::DeactivateChairman {
target: target.clone(),
})
} else {
Err("packed effect requires chairman runtime context".to_string())
}
}
RuntimeEffect::SetCompanyTerritoryAccess { RuntimeEffect::SetCompanyTerritoryAccess {
target, target,
territory, territory,
@ -2236,6 +2329,8 @@ fn company_target_import_error_message(
Some(ImportBlocker::MissingPlayerContext) Some(ImportBlocker::MissingPlayerContext)
| Some(ImportBlocker::MissingPlayerSelectionContext) | Some(ImportBlocker::MissingPlayerSelectionContext)
| Some(ImportBlocker::MissingPlayerRoleContext) | Some(ImportBlocker::MissingPlayerRoleContext)
| Some(ImportBlocker::MissingChairmanContext)
| Some(ImportBlocker::ChairmanTargetScope)
| Some(ImportBlocker::MissingPlayerConditionContext) => { | Some(ImportBlocker::MissingPlayerConditionContext) => {
"packed company effect is blocked by non-company import context".to_string() "packed company effect is blocked by non-company import context".to_string()
} }
@ -2324,10 +2419,22 @@ fn determine_packed_event_import_outcome(
} }
if !record.executable_import_ready { if !record.executable_import_ready {
if let Err(blocker) = lowered_record_decoded_actions(record, company_context) { if let Err(blocker) = lowered_record_decoded_actions(record, company_context) {
if blocker == ImportBlocker::MissingLocomotiveCatalogContext { if matches!(
blocker,
ImportBlocker::MissingLocomotiveCatalogContext
| ImportBlocker::MissingChairmanContext
| ImportBlocker::ChairmanTargetScope
) {
return company_target_import_outcome(blocker).to_string(); return company_target_import_outcome(blocker).to_string();
} }
} }
if record
.grouped_effect_rows
.iter()
.any(real_grouped_row_is_unsupported_chairman_target_scope)
{
return "blocked_chairman_target_scope".to_string();
}
if record if record
.grouped_effect_rows .grouped_effect_rows
.iter() .iter()
@ -2507,6 +2614,9 @@ fn runtime_condition_company_target_import_blocker(
RuntimeCondition::CompanyNumericThreshold { target, .. } => { RuntimeCondition::CompanyNumericThreshold { target, .. } => {
company_target_import_blocker(target, company_context) company_target_import_blocker(target, company_context)
} }
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
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)
} }
@ -2559,6 +2669,8 @@ fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str {
ImportBlocker::MissingPlayerContext => "blocked_missing_player_context", ImportBlocker::MissingPlayerContext => "blocked_missing_player_context",
ImportBlocker::MissingPlayerSelectionContext => "blocked_missing_player_selection_context", ImportBlocker::MissingPlayerSelectionContext => "blocked_missing_player_selection_context",
ImportBlocker::MissingPlayerRoleContext => "blocked_missing_player_role_context", ImportBlocker::MissingPlayerRoleContext => "blocked_missing_player_role_context",
ImportBlocker::MissingChairmanContext => "blocked_missing_chairman_context",
ImportBlocker::ChairmanTargetScope => "blocked_chairman_target_scope",
ImportBlocker::MissingConditionContext => "blocked_missing_condition_context", ImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
ImportBlocker::MissingPlayerConditionContext => "blocked_missing_player_condition_context", ImportBlocker::MissingPlayerConditionContext => "blocked_missing_player_condition_context",
ImportBlocker::CompanyConditionScopeDisabled => "blocked_company_condition_scope_disabled", ImportBlocker::CompanyConditionScopeDisabled => "blocked_company_condition_scope_disabled",
@ -2585,6 +2697,17 @@ fn real_grouped_row_is_unsupported_territory_access_variant(
row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0)
} }
fn real_grouped_row_is_unsupported_chairman_target_scope(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
) -> bool {
matches!(row.grouped_target_subject.as_deref(), Some("chairman"))
&& matches!(row.descriptor_id, 1 | 14)
&& row
.notes
.iter()
.any(|note| note == "chairman row requires selected-chairman scope")
}
fn real_grouped_row_is_unsupported_territory_access_scope( fn real_grouped_row_is_unsupported_territory_access_scope(
row: &SmpLoadedPackedEventGroupedEffectRowSummary, row: &SmpLoadedPackedEventGroupedEffectRowSummary,
) -> bool { ) -> bool {
@ -2644,7 +2767,9 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetPlayerCash { .. } | RuntimeEffect::SetPlayerCash { .. }
| RuntimeEffect::SetChairmanCash { .. }
| RuntimeEffect::DeactivatePlayer { .. } | RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::DeactivateChairman { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
@ -2666,6 +2791,7 @@ fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool {
RuntimeEffect::DeactivatePlayer { target } => { RuntimeEffect::DeactivatePlayer { target } => {
matches!(target, RuntimePlayerTarget::ConditionTruePlayer) matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
} }
RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivateChairman { .. } => false,
RuntimeEffect::AppendEventRecord { record } => record RuntimeEffect::AppendEventRecord { record } => record
.effects .effects
.iter() .iter()
@ -2701,6 +2827,10 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::DeactivatePlayer { target } => { | RuntimeEffect::DeactivatePlayer { target } => {
player_target_import_blocker(target, company_context) player_target_import_blocker(target, company_context)
} }
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => {
chairman_target_import_blocker(target, company_context)
}
RuntimeEffect::RetireTrains { RuntimeEffect::RetireTrains {
company_target, company_target,
territory_target, territory_target,
@ -3067,6 +3197,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -3211,6 +3343,7 @@ mod tests {
descriptor_label: Some("Company Cash".to_string()), descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()), parameter_family: Some("company_finance_scalar".to_string()),
grouped_target_subject: None,
opcode: 8, opcode: 8,
raw_scalar_value: 7, raw_scalar_value: 7,
value_byte_0x09: 1, value_byte_0x09: 1,
@ -3240,6 +3373,7 @@ mod tests {
descriptor_label: Some("Deactivate Company".to_string()), descriptor_label: Some("Deactivate Company".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_lifecycle_toggle".to_string()), parameter_family: Some("company_lifecycle_toggle".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3270,6 +3404,7 @@ mod tests {
descriptor_label: Some("Company Track Pieces Buildable".to_string()), descriptor_label: Some("Company Track Pieces Buildable".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_build_limit_scalar".to_string()), parameter_family: Some("company_build_limit_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3299,6 +3434,7 @@ mod tests {
descriptor_label: Some("Deactivate Player".to_string()), descriptor_label: Some("Deactivate Player".to_string()),
target_mask_bits: Some(0x02), target_mask_bits: Some(0x02),
parameter_family: Some("player_lifecycle_toggle".to_string()), parameter_family: Some("player_lifecycle_toggle".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3332,6 +3468,7 @@ mod tests {
descriptor_label: Some("Territory - Allow All".to_string()), descriptor_label: Some("Territory - Allow All".to_string()),
target_mask_bits: Some(0x05), target_mask_bits: Some(0x05),
parameter_family: Some("territory_access_toggle".to_string()), parameter_family: Some("territory_access_toggle".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3362,6 +3499,7 @@ mod tests {
descriptor_label: Some("Economic Status".to_string()), descriptor_label: Some("Economic Status".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("whole_game_state_enum".to_string()), parameter_family: Some("whole_game_state_enum".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3391,6 +3529,7 @@ mod tests {
descriptor_label: Some("Limited Track Building Amount".to_string()), descriptor_label: Some("Limited Track Building Amount".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("world_track_build_limit_scalar".to_string()), parameter_family: Some("world_track_build_limit_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3420,6 +3559,7 @@ mod tests {
descriptor_label: Some("Use Wartime Cargos".to_string()), descriptor_label: Some("Use Wartime Cargos".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("special_condition_scalar".to_string()), parameter_family: Some("special_condition_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3449,6 +3589,7 @@ mod tests {
descriptor_label: Some("Turbo Diesel Availability".to_string()), descriptor_label: Some("Turbo Diesel Availability".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("candidate_availability_scalar".to_string()), parameter_family: Some("candidate_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3479,6 +3620,7 @@ mod tests {
descriptor_label: Some("Unknown Loco Available".to_string()), descriptor_label: Some("Unknown Loco Available".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_availability_scalar".to_string()), parameter_family: Some("locomotive_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3521,6 +3663,7 @@ mod tests {
descriptor_label: Some(descriptor_label.clone()), descriptor_label: Some(descriptor_label.clone()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_cost_scalar".to_string()), parameter_family: Some("locomotive_cost_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3616,6 +3759,7 @@ mod tests {
descriptor_label: Some(descriptor_label.clone()), descriptor_label: Some(descriptor_label.clone()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("cargo_production_scalar".to_string()), parameter_family: Some("cargo_production_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3645,6 +3789,7 @@ mod tests {
descriptor_label: Some("Territory Access Cost".to_string()), descriptor_label: Some("Territory Access Cost".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("territory_access_cost_scalar".to_string()), parameter_family: Some("territory_access_cost_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: value, raw_scalar_value: value,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3676,6 +3821,7 @@ mod tests {
descriptor_label: Some(label.to_string()), descriptor_label: Some(label.to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("world_flag_toggle".to_string()), parameter_family: Some("world_flag_toggle".to_string()),
grouped_target_subject: None,
opcode: 0, opcode: 0,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3708,6 +3854,7 @@ mod tests {
descriptor_label: Some("Confiscate All".to_string()), descriptor_label: Some("Confiscate All".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_confiscation_variant".to_string()), parameter_family: Some("company_confiscation_variant".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3742,6 +3889,7 @@ mod tests {
descriptor_label: Some("Retire Train".to_string()), descriptor_label: Some("Retire Train".to_string()),
target_mask_bits: Some(0x0d), target_mask_bits: Some(0x0d),
parameter_family: Some("company_or_territory_asset_toggle".to_string()), parameter_family: Some("company_or_territory_asset_toggle".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 }, raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0, value_byte_0x09: 0,
@ -3772,6 +3920,7 @@ mod tests {
descriptor_label: Some("Confiscate All".to_string()), descriptor_label: Some("Confiscate All".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_confiscation_variant".to_string()), parameter_family: Some("company_confiscation_variant".to_string()),
grouped_target_subject: None,
opcode: 1, opcode: 1,
raw_scalar_value: 0, raw_scalar_value: 0,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -5451,6 +5600,7 @@ mod tests {
descriptor_label: Some("Unknown Loco Available".to_string()), descriptor_label: Some("Unknown Loco Available".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_availability_scalar".to_string()), parameter_family: Some("locomotive_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: 42, raw_scalar_value: 42,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -5697,6 +5847,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![ locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry { crate::RuntimeLocomotiveCatalogEntry {
@ -6092,6 +6244,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![ locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry { crate::RuntimeLocomotiveCatalogEntry {
@ -6488,6 +6642,8 @@ mod tests {
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -6566,6 +6722,7 @@ mod tests {
descriptor_label: Some("Company Cash".to_string()), descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01), target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()), parameter_family: Some("company_finance_scalar".to_string()),
grouped_target_subject: None,
opcode: 8, opcode: 8,
raw_scalar_value: 250, raw_scalar_value: 250,
value_byte_0x09: 1, value_byte_0x09: 1,
@ -8145,6 +8302,7 @@ mod tests {
descriptor_label: Some("Turbo Diesel Availability".to_string()), descriptor_label: Some("Turbo Diesel Availability".to_string()),
target_mask_bits: Some(0x08), target_mask_bits: Some(0x08),
parameter_family: Some("candidate_availability_scalar".to_string()), parameter_family: Some("candidate_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3, opcode: 3,
raw_scalar_value: 1, raw_scalar_value: 1,
value_byte_0x09: 0, value_byte_0x09: 0,
@ -9850,6 +10008,8 @@ mod tests {
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -10034,6 +10194,8 @@ mod tests {
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -35,7 +35,8 @@ pub use pk4::{
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
}; };
pub use runtime::{ pub use runtime::{
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,

View file

@ -96,6 +96,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -89,6 +89,30 @@ pub struct RuntimePlayer {
pub controller_kind: RuntimeCompanyControllerKind, pub controller_kind: RuntimeCompanyControllerKind,
} }
fn runtime_chairman_profile_default_active() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeChairmanProfile {
pub profile_id: u32,
pub name: String,
#[serde(default = "runtime_chairman_profile_default_active")]
pub active: bool,
#[serde(default)]
pub current_cash: i64,
#[serde(default)]
pub linked_company_id: Option<u32>,
#[serde(default)]
pub company_holdings: BTreeMap<u32, u32>,
#[serde(default)]
pub holdings_value_total: i64,
#[serde(default)]
pub net_worth_total: i64,
#[serde(default)]
pub purchasing_power_total: i64,
}
fn runtime_train_default_active() -> bool { fn runtime_train_default_active() -> bool {
true true
} }
@ -156,6 +180,14 @@ pub enum RuntimePlayerTarget {
Ids { ids: Vec<u32> }, Ids { ids: Vec<u32> },
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeChairmanTarget {
AllActive,
SelectedChairman,
Ids { ids: Vec<u32> },
}
#[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 RuntimeTerritoryTarget { pub enum RuntimeTerritoryTarget {
@ -211,6 +243,15 @@ pub enum RuntimeCompanyMetric {
TrackPiecesNonElectric, TrackPiecesNonElectric,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeChairmanMetric {
CurrentCash,
HoldingsValueTotal,
NetWorthTotal,
PurchasingPowerTotal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RuntimeTerritoryMetric { pub enum RuntimeTerritoryMetric {
@ -242,6 +283,12 @@ pub enum RuntimeCondition {
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
}, },
ChairmanNumericThreshold {
target: RuntimeChairmanTarget,
metric: RuntimeChairmanMetric,
comparator: RuntimeConditionComparator,
value: i64,
},
TerritoryNumericThreshold { TerritoryNumericThreshold {
target: RuntimeTerritoryTarget, target: RuntimeTerritoryTarget,
metric: RuntimeTerritoryMetric, metric: RuntimeTerritoryMetric,
@ -336,9 +383,16 @@ pub enum RuntimeEffect {
target: RuntimePlayerTarget, target: RuntimePlayerTarget,
value: i64, value: i64,
}, },
SetChairmanCash {
target: RuntimeChairmanTarget,
value: i64,
},
DeactivatePlayer { DeactivatePlayer {
target: RuntimePlayerTarget, target: RuntimePlayerTarget,
}, },
DeactivateChairman {
target: RuntimeChairmanTarget,
},
SetCompanyTerritoryAccess { SetCompanyTerritoryAccess {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
territory: RuntimeTerritoryTarget, territory: RuntimeTerritoryTarget,
@ -590,6 +644,8 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub target_mask_bits: Option<u8>, pub target_mask_bits: Option<u8>,
#[serde(default)] #[serde(default)]
pub parameter_family: Option<String>, pub parameter_family: Option<String>,
#[serde(default)]
pub grouped_target_subject: Option<String>,
pub opcode: u8, pub opcode: u8,
pub raw_scalar_value: i32, pub raw_scalar_value: i32,
pub value_byte_0x09: u8, pub value_byte_0x09: u8,
@ -733,6 +789,10 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub selected_player_id: Option<u32>, pub selected_player_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub chairman_profiles: Vec<RuntimeChairmanProfile>,
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub trains: Vec<RuntimeTrain>, pub trains: Vec<RuntimeTrain>,
#[serde(default)] #[serde(default)]
pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>, pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>,
@ -801,6 +861,63 @@ impl RuntimeState {
active_player_ids.insert(player.player_id); active_player_ids.insert(player.player_id);
} }
} }
let mut seen_chairman_profile_ids = BTreeSet::new();
let mut seen_chairman_names = BTreeSet::new();
let mut active_chairman_profile_ids = BTreeSet::new();
for chairman in &self.chairman_profiles {
if !seen_chairman_profile_ids.insert(chairman.profile_id) {
return Err(format!(
"duplicate chairman_profile.profile_id {}",
chairman.profile_id
));
}
if chairman.name.trim().is_empty() {
return Err(format!(
"chairman_profile {} has an empty name",
chairman.profile_id
));
}
if !seen_chairman_names.insert(chairman.name.clone()) {
return Err(format!(
"duplicate chairman_profile.name {:?}",
chairman.name
));
}
if chairman.active {
active_chairman_profile_ids.insert(chairman.profile_id);
}
if let Some(linked_company_id) = chairman.linked_company_id {
if !seen_company_ids.contains(&linked_company_id) {
return Err(format!(
"chairman_profile {} references unknown linked_company_id {}",
chairman.profile_id, linked_company_id
));
}
}
for company_id in chairman.company_holdings.keys() {
if !seen_company_ids.contains(company_id) {
return Err(format!(
"chairman_profile {} references unknown holdings company_id {}",
chairman.profile_id, company_id
));
}
}
}
if let Some(selected_chairman_profile_id) = self.selected_chairman_profile_id {
if !seen_chairman_profile_ids.contains(&selected_chairman_profile_id) {
return Err(format!(
"selected_chairman_profile_id {} does not reference a live chairman profile",
selected_chairman_profile_id
));
}
if !active_chairman_profile_ids.contains(&selected_chairman_profile_id) {
return Err(format!(
"selected_chairman_profile_id {} must reference an active chairman profile",
selected_chairman_profile_id
));
}
}
if let Some(selected_player_id) = self.selected_player_id { if let Some(selected_player_id) = self.selected_player_id {
if !seen_player_ids.contains(&selected_player_id) { if !seen_player_ids.contains(&selected_player_id) {
return Err(format!( return Err(format!(
@ -956,7 +1073,12 @@ impl RuntimeState {
return Err(format!("duplicate record_id {}", record.record_id)); return Err(format!("duplicate record_id {}", record.record_id));
} }
for (condition_index, condition) in record.conditions.iter().enumerate() { for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, &seen_company_ids, &seen_territory_ids) validate_runtime_condition(
condition,
&seen_company_ids,
&seen_chairman_profile_ids,
&seen_territory_ids,
)
.map_err(|err| { .map_err(|err| {
format!( format!(
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}", "event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
@ -969,6 +1091,7 @@ impl RuntimeState {
effect, effect,
&seen_company_ids, &seen_company_ids,
&seen_player_ids, &seen_player_ids,
&seen_chairman_profile_ids,
&seen_territory_ids, &seen_territory_ids,
) )
.map_err(|err| { .map_err(|err| {
@ -1315,6 +1438,7 @@ fn validate_runtime_effect(
effect: &RuntimeEffect, effect: &RuntimeEffect,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>, valid_player_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>, valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match effect { match effect {
@ -1343,6 +1467,10 @@ fn validate_runtime_effect(
| RuntimeEffect::DeactivatePlayer { target } => { | RuntimeEffect::DeactivatePlayer { target } => {
validate_player_target(target, valid_player_ids)?; validate_player_target(target, valid_player_ids)?;
} }
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => {
validate_chairman_target(target, valid_chairman_profile_ids)?;
}
RuntimeEffect::RetireTrains { RuntimeEffect::RetireTrains {
company_target, company_target,
territory_target, territory_target,
@ -1403,6 +1531,7 @@ fn validate_runtime_effect(
record, record,
valid_company_ids, valid_company_ids,
valid_player_ids, valid_player_ids,
valid_chairman_profile_ids,
valid_territory_ids, valid_territory_ids,
)?; )?;
} }
@ -1418,23 +1547,29 @@ fn validate_event_record_template(
record: &RuntimeEventRecordTemplate, record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>, valid_player_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>, valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
for (condition_index, condition) in record.conditions.iter().enumerate() { for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err( validate_runtime_condition(
|err| { condition,
valid_company_ids,
valid_chairman_profile_ids,
valid_territory_ids,
)
.map_err(|err| {
format!( format!(
"template record_id={}.conditions[{condition_index}] {err}", "template record_id={}.conditions[{condition_index}] {err}",
record.record_id record.record_id
) )
}, })?;
)?;
} }
for (effect_index, effect) in record.effects.iter().enumerate() { for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect( validate_runtime_effect(
effect, effect,
valid_company_ids, valid_company_ids,
valid_player_ids, valid_player_ids,
valid_chairman_profile_ids,
valid_territory_ids, valid_territory_ids,
) )
.map_err(|err| { .map_err(|err| {
@ -1451,12 +1586,16 @@ 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_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>, valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match condition { match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. } => { RuntimeCondition::CompanyNumericThreshold { target, .. } => {
validate_company_target(target, valid_company_ids) validate_company_target(target, valid_company_ids)
} }
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
validate_chairman_target(target, valid_chairman_profile_ids)
}
RuntimeCondition::TerritoryNumericThreshold { target, .. } => { RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
validate_territory_target(target, valid_territory_ids) validate_territory_target(target, valid_territory_ids)
} }
@ -1562,6 +1701,28 @@ fn validate_player_target(
} }
} }
fn validate_chairman_target(
target: &RuntimeChairmanTarget,
valid_chairman_profile_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match target {
RuntimeChairmanTarget::AllActive | RuntimeChairmanTarget::SelectedChairman => Ok(()),
RuntimeChairmanTarget::Ids { ids } => {
if ids.is_empty() {
return Err("target ids must not be empty".to_string());
}
for profile_id in ids {
if !valid_chairman_profile_ids.contains(profile_id) {
return Err(format!(
"target references unknown chairman profile_id {profile_id}"
));
}
}
Ok(())
}
}
}
fn validate_territory_target( fn validate_territory_target(
target: &RuntimeTerritoryTarget, target: &RuntimeTerritoryTarget,
valid_territory_ids: &BTreeSet<u32>, valid_territory_ids: &BTreeSet<u32>,
@ -1628,6 +1789,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1689,6 +1852,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1735,6 +1900,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1794,6 +1961,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1853,6 +2022,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1963,6 +2134,8 @@ mod tests {
selected_company_id: Some(2), selected_company_id: Some(2),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -2009,6 +2182,8 @@ mod tests {
selected_company_id: Some(1), selected_company_id: Some(1),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -2055,6 +2230,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![ trains: vec![
RuntimeTrain { RuntimeTrain {
train_id: 7, train_id: 7,
@ -2118,6 +2295,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain { trains: vec![RuntimeTrain {
train_id: 7, train_id: 7,
owner_company_id: 2, owner_company_id: 2,
@ -2171,6 +2350,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain { trains: vec![RuntimeTrain {
train_id: 7, train_id: 7,
owner_company_id: 1, owner_company_id: 1,
@ -2228,6 +2409,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain { trains: vec![RuntimeTrain {
train_id: 7, train_id: 7,
owner_company_id: 1, owner_company_id: 1,
@ -2281,6 +2464,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -2340,6 +2525,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -2393,6 +2580,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::{ use crate::{
RuntimeCargoClass, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
}; };
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -1876,6 +1876,8 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub target_mask_bits: Option<u8>, pub target_mask_bits: Option<u8>,
#[serde(default)] #[serde(default)]
pub parameter_family: Option<String>, pub parameter_family: Option<String>,
#[serde(default)]
pub grouped_target_subject: Option<String>,
pub opcode: u8, pub opcode: u8,
pub raw_scalar_value: i32, pub raw_scalar_value: i32,
pub value_byte_0x09: u8, pub value_byte_0x09: u8,
@ -1901,6 +1903,15 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealGroupedTargetSubject {
Company,
Player,
Chairman,
Territory,
WholeGame,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSaveSlice { pub struct SmpLoadedSaveSlice {
pub file_extension_hint: Option<String>, pub file_extension_hint: Option<String>,
@ -2590,12 +2601,20 @@ fn parse_real_event_runtime_record_summary(
} }
if let Some(control) = compact_control.as_ref() { if let Some(control) = compact_control.as_ref() {
for row in &mut grouped_effect_rows { for row in &mut grouped_effect_rows {
row.grouped_target_subject = derive_real_grouped_target_subject(row, control)
.map(real_grouped_target_subject_name)
.map(str::to_string);
let company_target_present = control let company_target_present = control
.grouped_target_scope_ordinals_0x7fb .grouped_target_scope_ordinals_0x7fb
.get(row.group_index) .get(row.group_index)
.copied() .copied()
.and_then(real_grouped_company_target) .and_then(real_grouped_company_target)
.is_some(); .is_some();
let chairman_target_present = control
.grouped_target_scope_ordinals_0x7fb
.get(row.group_index)
.copied()
.is_some_and(real_grouped_chairman_target_supported_in_runtime);
let territory_target_present = control let territory_target_present = control
.grouped_territory_selectors_0x80f .grouped_territory_selectors_0x80f
.get(row.group_index) .get(row.group_index)
@ -2617,6 +2636,14 @@ fn parse_real_event_runtime_record_summary(
row.notes row.notes
.push("territory access row is missing company or territory scope".to_string()); .push("territory access row is missing company or territory scope".to_string());
} }
if matches!(
derive_real_grouped_target_subject(row, control),
Some(RealGroupedTargetSubject::Chairman)
) && !chairman_target_present
{
row.notes
.push("chairman row requires selected-chairman scope".to_string());
}
} }
} }
@ -3174,6 +3201,7 @@ fn parse_real_grouped_effect_row_summary(
descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()), descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()),
target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits), target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits),
parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()), parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()),
grouped_target_subject: None,
opcode, opcode,
raw_scalar_value, raw_scalar_value,
value_byte_0x09, value_byte_0x09,
@ -3665,6 +3693,44 @@ fn runtime_world_flag_key_from_label(label: &str) -> String {
key key
} }
fn derive_real_grouped_target_subject(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
compact_control: &SmpLoadedPackedEventCompactControlSummary,
) -> Option<RealGroupedTargetSubject> {
match row.target_mask_bits {
Some(0x08) => Some(RealGroupedTargetSubject::WholeGame),
Some(0x01) => Some(RealGroupedTargetSubject::Company),
Some(0x02) => match compact_control
.grouped_scope_checkboxes_0x7ff
.get(row.group_index)
.copied()
{
Some(2) => Some(RealGroupedTargetSubject::Chairman),
_ => Some(RealGroupedTargetSubject::Player),
},
_ if row.descriptor_id == 3 => Some(RealGroupedTargetSubject::Territory),
_ if row.descriptor_id == 15
&& compact_control
.grouped_territory_selectors_0x80f
.get(row.group_index)
.is_some_and(|selector| *selector >= 0) =>
{
Some(RealGroupedTargetSubject::Territory)
}
_ => None,
}
}
fn real_grouped_target_subject_name(subject: RealGroupedTargetSubject) -> &'static str {
match subject {
RealGroupedTargetSubject::Company => "company",
RealGroupedTargetSubject::Player => "player",
RealGroupedTargetSubject::Chairman => "chairman",
RealGroupedTargetSubject::Territory => "territory",
RealGroupedTargetSubject::WholeGame => "whole_game",
}
}
fn decode_real_grouped_effect_actions( fn decode_real_grouped_effect_actions(
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
compact_control: &SmpLoadedPackedEventCompactControlSummary, compact_control: &SmpLoadedPackedEventCompactControlSummary,
@ -3684,17 +3750,23 @@ fn decode_real_grouped_effect_action(
.grouped_target_scope_ordinals_0x7fb .grouped_target_scope_ordinals_0x7fb
.get(row.group_index) .get(row.group_index)
.copied()?; .copied()?;
let target_subject = derive_real_grouped_target_subject(row, compact_control);
if descriptor_metadata.executable_in_runtime if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 1 && descriptor_metadata.descriptor_id == 1
&& row.opcode == 8 && row.opcode == 8
&& row.row_shape == "multivalue_scalar" && row.row_shape == "multivalue_scalar"
{ {
let target = real_grouped_player_target(target_scope_ordinal)?; return match target_subject {
return Some(RuntimeEffect::SetPlayerCash { Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash {
target, target: real_grouped_chairman_target(target_scope_ordinal)?,
value: i64::from(row.raw_scalar_value), value: i64::from(row.raw_scalar_value),
}); }),
_ => Some(RuntimeEffect::SetPlayerCash {
target: real_grouped_player_target(target_scope_ordinal)?,
value: i64::from(row.raw_scalar_value),
}),
};
} }
if descriptor_metadata.executable_in_runtime if descriptor_metadata.executable_in_runtime
@ -3823,8 +3895,14 @@ fn decode_real_grouped_effect_action(
&& row.row_shape == "bool_toggle" && row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0 && row.raw_scalar_value != 0
{ {
let target = real_grouped_player_target(target_scope_ordinal)?; return match target_subject {
return Some(RuntimeEffect::DeactivatePlayer { target }); Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::DeactivateChairman {
target: real_grouped_chairman_target(target_scope_ordinal)?,
}),
_ => Some(RuntimeEffect::DeactivatePlayer {
target: real_grouped_player_target(target_scope_ordinal)?,
}),
};
} }
if descriptor_metadata.executable_in_runtime if descriptor_metadata.executable_in_runtime
@ -3886,6 +3964,17 @@ fn real_grouped_player_target(ordinal: u8) -> Option<RuntimePlayerTarget> {
} }
} }
fn real_grouped_chairman_target(ordinal: u8) -> Option<RuntimeChairmanTarget> {
match ordinal {
1 => Some(RuntimeChairmanTarget::SelectedChairman),
_ => None,
}
}
fn real_grouped_chairman_target_supported_in_runtime(ordinal: u8) -> bool {
real_grouped_chairman_target(ordinal).is_some()
}
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> { fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
let opcode = read_u8_at(bytes, *cursor)?; let opcode = read_u8_at(bytes, *cursor)?;
*cursor += 1; *cursor += 1;
@ -4033,6 +4122,13 @@ fn parse_optional_u16_len_prefixed_string(
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
match effect { match effect {
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => matches!(
target,
RuntimeChairmanTarget::AllActive
| RuntimeChairmanTarget::SelectedChairman
| RuntimeChairmanTarget::Ids { .. }
),
RuntimeEffect::SetWorldFlag { .. } RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetEconomicStatusCode { .. }
@ -4097,6 +4193,7 @@ 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::CompanyNumericThreshold { .. }
| RuntimeCondition::ChairmanNumericThreshold { .. }
| RuntimeCondition::TerritoryNumericThreshold { .. } | RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. } | RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. }

View file

@ -3,10 +3,10 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
RuntimeCargoClass, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyControllerKind,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimePlayerTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeState, RuntimeSummary,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind, calendar::BoundaryEventKind,
}; };
@ -87,6 +87,8 @@ struct AppliedEffectsSummary {
struct ResolvedConditionContext { struct ResolvedConditionContext {
matching_company_ids: BTreeSet<u32>, matching_company_ids: BTreeSet<u32>,
matching_player_ids: BTreeSet<u32>, matching_player_ids: BTreeSet<u32>,
#[allow(dead_code)]
matching_chairman_profile_ids: BTreeSet<u32>,
} }
pub fn execute_step_command( pub fn execute_step_command(
@ -346,6 +348,21 @@ fn apply_runtime_effects(
mutated_player_ids.insert(player_id); mutated_player_ids.insert(player_id);
} }
} }
RuntimeEffect::SetChairmanCash { target, value } => {
let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?;
for profile_id in profile_ids {
let chairman = state
.chairman_profiles
.iter_mut()
.find(|profile| profile.profile_id == profile_id)
.ok_or_else(|| {
format!(
"missing chairman profile_id {profile_id} while applying cash effect"
)
})?;
chairman.current_cash = *value;
}
}
RuntimeEffect::DeactivatePlayer { target } => { RuntimeEffect::DeactivatePlayer { target } => {
let player_ids = resolve_player_target_ids(state, target, condition_context)?; let player_ids = resolve_player_target_ids(state, target, condition_context)?;
for player_id in player_ids { for player_id in player_ids {
@ -365,6 +382,39 @@ fn apply_runtime_effects(
} }
} }
} }
RuntimeEffect::DeactivateChairman { target } => {
let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?;
for profile_id in profile_ids.iter().copied() {
let linked_company_id = state
.chairman_profiles
.iter()
.find(|profile| profile.profile_id == profile_id)
.and_then(|profile| profile.linked_company_id);
let chairman = state
.chairman_profiles
.iter_mut()
.find(|profile| profile.profile_id == profile_id)
.ok_or_else(|| {
format!(
"missing chairman profile_id {profile_id} while applying deactivate effect"
)
})?;
chairman.active = false;
chairman.linked_company_id = None;
if state.selected_chairman_profile_id == Some(profile_id) {
state.selected_chairman_profile_id = None;
}
if let Some(linked_company_id) = linked_company_id {
for other in &mut state.chairman_profiles {
if other.profile_id != profile_id
&& other.linked_company_id == Some(linked_company_id)
{
other.linked_company_id = None;
}
}
}
}
}
RuntimeEffect::SetCompanyTerritoryAccess { RuntimeEffect::SetCompanyTerritoryAccess {
target, target,
territory, territory,
@ -607,6 +657,7 @@ fn evaluate_record_conditions(
} }
let mut company_matches: Option<BTreeSet<u32>> = None; let mut company_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 {
@ -657,6 +708,41 @@ fn evaluate_record_conditions(
return Ok(None); return Ok(None);
} }
} }
RuntimeCondition::ChairmanNumericThreshold {
target,
metric,
comparator,
value,
} => {
let resolved = resolve_chairman_target_ids(
state,
target,
&ResolvedConditionContext::default(),
)?;
let matching = resolved
.into_iter()
.filter(|profile_id| {
state
.chairman_profiles
.iter()
.find(|profile| profile.profile_id == *profile_id)
.is_some_and(|profile| {
compare_condition_value(
chairman_metric_value(profile, *metric),
*comparator,
*value,
)
})
})
.collect::<BTreeSet<_>>();
if matching.is_empty() {
return Ok(None);
}
intersect_chairman_matches(&mut chairman_matches, matching);
if chairman_matches.as_ref().is_some_and(BTreeSet::is_empty) {
return Ok(None);
}
}
RuntimeCondition::CompanyTerritoryNumericThreshold { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, target,
territory, territory,
@ -840,6 +926,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: BTreeSet::new(),
matching_chairman_profile_ids: chairman_matches.unwrap_or_default(),
})) }))
} }
@ -854,6 +941,17 @@ fn intersect_company_matches(company_matches: &mut Option<BTreeSet<u32>>, next:
} }
} }
fn intersect_chairman_matches(chairman_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
match chairman_matches {
Some(existing) => {
existing.retain(|profile_id| next.contains(profile_id));
}
None => {
*chairman_matches = Some(next);
}
}
}
fn resolve_company_target_ids( fn resolve_company_target_ids(
state: &RuntimeState, state: &RuntimeState,
target: &RuntimeCompanyTarget, target: &RuntimeCompanyTarget,
@ -1043,6 +1141,53 @@ fn resolve_player_target_ids(
} }
} }
fn resolve_chairman_target_ids(
state: &RuntimeState,
target: &RuntimeChairmanTarget,
_condition_context: &ResolvedConditionContext,
) -> Result<Vec<u32>, String> {
match target {
RuntimeChairmanTarget::AllActive => Ok(state
.chairman_profiles
.iter()
.filter(|profile| profile.active)
.map(|profile| profile.profile_id)
.collect()),
RuntimeChairmanTarget::Ids { ids } => {
let known_ids = state
.chairman_profiles
.iter()
.map(|profile| profile.profile_id)
.collect::<BTreeSet<_>>();
for profile_id in ids {
if !known_ids.contains(profile_id) {
return Err(format!(
"target references unknown chairman profile_id {profile_id}"
));
}
}
Ok(ids.clone())
}
RuntimeChairmanTarget::SelectedChairman => {
let selected_profile_id = state.selected_chairman_profile_id.ok_or_else(|| {
"target requires selected_chairman_profile_id context".to_string()
})?;
if state
.chairman_profiles
.iter()
.any(|profile| profile.profile_id == selected_profile_id && profile.active)
{
Ok(vec![selected_profile_id])
} else {
Err(
"target requires selected_chairman_profile_id to reference an active chairman profile"
.to_string(),
)
}
}
}
}
fn resolve_territory_target_ids( fn resolve_territory_target_ids(
state: &RuntimeState, state: &RuntimeState,
target: &RuntimeTerritoryTarget, target: &RuntimeTerritoryTarget,
@ -1090,6 +1235,18 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
} }
} }
fn chairman_metric_value(
profile: &crate::RuntimeChairmanProfile,
metric: RuntimeChairmanMetric,
) -> i64 {
match metric {
RuntimeChairmanMetric::CurrentCash => profile.current_cash,
RuntimeChairmanMetric::HoldingsValueTotal => profile.holdings_value_total,
RuntimeChairmanMetric::NetWorthTotal => profile.net_worth_total,
RuntimeChairmanMetric::PurchasingPowerTotal => profile.purchasing_power_total,
}
}
fn territory_metric_value( fn territory_metric_value(
state: &RuntimeState, state: &RuntimeState,
territory_ids: &[u32], territory_ids: &[u32],
@ -1277,6 +1434,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -33,6 +33,9 @@ pub struct RuntimeSummary {
pub company_count: usize, pub company_count: usize,
pub active_company_count: usize, pub active_company_count: usize,
pub player_count: usize, pub player_count: usize,
pub chairman_profile_count: usize,
pub active_chairman_profile_count: usize,
pub selected_chairman_profile_id: Option<u32>,
pub train_count: usize, pub train_count: usize,
pub active_train_count: usize, pub active_train_count: usize,
pub retired_train_count: usize, pub retired_train_count: usize,
@ -52,6 +55,8 @@ pub struct RuntimeSummary {
pub packed_event_blocked_missing_player_context_count: usize, pub packed_event_blocked_missing_player_context_count: usize,
pub packed_event_blocked_missing_player_selection_context_count: usize, pub packed_event_blocked_missing_player_selection_context_count: usize,
pub packed_event_blocked_missing_player_role_context_count: usize, pub packed_event_blocked_missing_player_role_context_count: usize,
pub packed_event_blocked_missing_chairman_context_count: usize,
pub packed_event_blocked_chairman_target_scope_count: usize,
pub packed_event_blocked_missing_condition_context_count: usize, pub packed_event_blocked_missing_condition_context_count: usize,
pub packed_event_blocked_missing_player_condition_context_count: usize, pub packed_event_blocked_missing_player_condition_context_count: usize,
pub packed_event_blocked_company_condition_scope_disabled_count: usize, pub packed_event_blocked_company_condition_scope_disabled_count: usize,
@ -169,6 +174,13 @@ impl RuntimeSummary {
.filter(|company| company.active) .filter(|company| company.active)
.count(), .count(),
player_count: state.players.len(), player_count: state.players.len(),
chairman_profile_count: state.chairman_profiles.len(),
active_chairman_profile_count: state
.chairman_profiles
.iter()
.filter(|profile| profile.active)
.count(),
selected_chairman_profile_id: state.selected_chairman_profile_id,
train_count: state.trains.len(), train_count: state.trains.len(),
active_train_count: state.trains.iter().filter(|train| train.active).count(), active_train_count: state.trains.iter().filter(|train| train.active).count(),
retired_train_count: state.trains.iter().filter(|train| train.retired).count(), retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
@ -298,6 +310,34 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_chairman_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_chairman_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_chairman_target_scope_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_chairman_target_scope")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_condition_context_count: state packed_event_blocked_missing_condition_context_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -666,6 +706,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![ locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry { crate::RuntimeLocomotiveCatalogEntry {
@ -909,6 +951,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![ locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry { crate::RuntimeLocomotiveCatalogEntry {
@ -956,6 +1000,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![ locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry { crate::RuntimeLocomotiveCatalogEntry {
@ -1009,6 +1055,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1053,6 +1101,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1092,6 +1142,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),
@ -1200,6 +1252,8 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(), locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(), cargo_catalog: Vec::new(),

View file

@ -84,6 +84,9 @@ The highest-value next passes are now:
- descriptors `1` `Player Cash` and `14` `Deactivate Player` now join that executable real batch - descriptors `1` `Player Cash` and `14` `Deactivate Player` now join that executable real batch
through the same ordinary runtime path, backed by the minimal player runtime and overlay-import through the same ordinary runtime path, backed by the minimal player runtime and overlay-import
context context
- the first chairman-targeted real grouped rows now execute too through that same path when the
hidden grouped target-subject lane resolves to selected-chairman scope; broader chairman target
scopes stay parity-only under `blocked_chairman_target_scope`
- 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

@ -41,6 +41,11 @@ Implemented today:
names, a minimal player runtime now carries selected-player and role context, and real descriptor names, a minimal player runtime now carries selected-player and role context, and real descriptor
`1` = `Player Cash` and descriptor `14` = `Deactivate Player` now import and execute through the `1` = `Player Cash` and descriptor `14` = `Deactivate Player` now import and execute through the
ordinary runtime path ordinary runtime path
- a first-class chairman-profile runtime now exists too, with overlay-backed selected-chairman
context and the first chairman-targeted grouped-effect subset: the same real descriptors
`1` = `Player Cash` and `14` = `Deactivate Player` now also import and execute through the
hidden grouped target-subject lane when it resolves to selected-chairman scope, while broader
chairman target scopes remain explicit parity on `blocked_chairman_target_scope`
- 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
@ -111,8 +116,8 @@ Implemented today:
That means the next implementation work is breadth, not bootstrap. The recommended next slice is That means the next implementation work is breadth, not bootstrap. The recommended next slice is
broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, broader real grouped-descriptor and ordinary condition-id coverage beyond the current access,
whole-game toggle, train, player, numeric-threshold, named locomotive availability, named whole-game toggle, train, player, chairman selected-scope, numeric-threshold, named locomotive
locomotive cost, world scalar override, and world-scalar condition batches. availability, named locomotive cost, world scalar override, and world-scalar condition batches.
Richer runtime ownership should still be added only where a later descriptor or condition family Richer runtime ownership should still be added only where a later descriptor or condition family
needs more than the current event-owned roster. needs more than the current event-owned roster.

View file

@ -0,0 +1,63 @@
{
"format_version": 1,
"fixture_id": "packed-event-chairman-cash-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 1 imports and executes on selected-chairman scope."
},
"state_import_path": "packed-event-chairman-cash-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"chairman_profile_count": 2,
"active_chairman_profile_count": 2,
"selected_chairman_profile_id": 1,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"chairman_profiles": [
{
"profile_id": 1,
"current_cash": 999
},
{
"profile_id": 2,
"current_cash": 250
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "set_chairman_cash",
"target": {
"kind": "selected_chairman"
},
"value": 999
}
],
"grouped_effect_rows": [
{
"grouped_target_subject": "chairman"
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-chairman-cash-overlay",
"source": {
"description": "Overlay import combining chairman runtime context with the real chairman-targeted cash descriptor sample."
},
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
"save_slice_path": "packed-event-chairman-cash-save-slice.json"
}

View file

@ -0,0 +1,110 @@
{
"format_version": 1,
"save_slice_id": "packed-event-chairman-cash-save-slice",
"source": {
"description": "Tracked save-slice document with a chairman-targeted Player Cash row using the hidden grouped target-subject lane.",
"original_save_filename": "captured-chairman-cash.gms",
"original_save_sha256": "chairman-cash-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves selected-chairman descriptor import through the normal runtime path"
]
},
"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,
"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": 61,
"live_record_count": 1,
"live_entry_ids": [61],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 61,
"payload_offset": 29296,
"payload_len": 140,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"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": [2, 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": 1,
"descriptor_label": "Player Cash",
"target_mask_bits": 2,
"parameter_family": "player_cash_scalar",
"grouped_target_subject": "chairman",
"opcode": 8,
"raw_scalar_value": 999,
"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": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Player Cash to 999 with aux [0, 0, 0, 0]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "set_chairman_cash",
"target": {
"kind": "selected_chairman"
},
"value": 999
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"hidden grouped target-subject lane resolves descriptor 1 to selected chairman scope"
]
}
]
},
"notes": [
"real chairman-targeted cash descriptor sample"
]
}
}

View file

@ -0,0 +1,40 @@
{
"format_version": 1,
"fixture_id": "packed-event-chairman-missing-context-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving chairman-targeted rows stay parity-only without runtime chairman context."
},
"state_import_path": "packed-event-chairman-cash-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"expected_summary": {
"calendar_projection_source": "default-1830-placeholder",
"calendar_projection_is_placeholder": true,
"chairman_profile_count": 0,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_blocked_missing_chairman_context_count": 1,
"event_runtime_record_count": 0
},
"expected_state_fragment": {
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_missing_chairman_context",
"grouped_effect_rows": [
{
"grouped_target_subject": "chairman"
}
]
}
]
}
}
}

View file

@ -0,0 +1,96 @@
{
"format_version": 1,
"snapshot_id": "packed-event-chairman-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying selected-chairman context for chairman-targeted packed-event overlays."
},
"state": {
"calendar": {
"year": 1840,
"month_slot": 1,
"phase_slot": 2,
"tick_slot": 3
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "chairman overlay context"
},
"companies": [
{
"company_id": 1,
"current_cash": 150,
"debt": 80,
"credit_rating_score": 650,
"prime_rate": 5,
"controller_kind": "human",
"track_piece_counts": {
"total": 20,
"single": 5,
"double": 8,
"transition": 1,
"electric": 3,
"non_electric": 17
}
},
{
"company_id": 2,
"current_cash": 90,
"debt": 40,
"credit_rating_score": 480,
"prime_rate": 6,
"controller_kind": "ai",
"track_piece_counts": {
"total": 8,
"single": 2,
"double": 2,
"transition": 0,
"electric": 1,
"non_electric": 7
}
}
],
"selected_company_id": 1,
"players": [],
"selected_player_id": null,
"chairman_profiles": [
{
"profile_id": 1,
"name": "Chairman One",
"active": true,
"current_cash": 500,
"linked_company_id": 1,
"company_holdings": {
"1": 1000
},
"holdings_value_total": 700,
"net_worth_total": 1200,
"purchasing_power_total": 1500
},
{
"profile_id": 2,
"name": "Chairman Two",
"active": true,
"current_cash": 250,
"linked_company_id": 2,
"company_holdings": {
"2": 900
},
"holdings_value_total": 600,
"net_worth_total": 900,
"purchasing_power_total": 1100
}
],
"selected_chairman_profile_id": 1,
"event_runtime_records": [],
"candidate_availability": {},
"special_conditions": {},
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
}
}

View file

@ -0,0 +1,39 @@
{
"format_version": 1,
"fixture_id": "packed-event-chairman-scope-parity-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving unsupported chairman scopes stay parity-only under an explicit blocker."
},
"state_import_path": "packed-event-chairman-scope-parity-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"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_imported_runtime_record_count": 0,
"packed_event_blocked_chairman_target_scope_count": 1,
"event_runtime_record_count": 0
},
"expected_state_fragment": {
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_chairman_target_scope",
"grouped_effect_rows": [
{
"grouped_target_subject": "chairman"
}
]
}
]
}
}
}

View file

@ -0,0 +1,104 @@
{
"format_version": 1,
"save_slice_id": "packed-event-chairman-scope-parity-save-slice",
"source": {
"description": "Tracked save-slice document with a chairman-targeted row on an unsupported non-selected scope.",
"original_save_filename": "captured-chairman-scope-parity.gms",
"original_save_sha256": "chairman-scope-parity-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"pins the selected-chairman-only execution boundary"
]
},
"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,
"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": 63,
"live_record_count": 1,
"live_entry_ids": [63],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 63,
"payload_offset": 29296,
"payload_len": 140,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"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": [0, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [2, 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": 1,
"descriptor_label": "Player Cash",
"target_mask_bits": 2,
"parameter_family": "player_cash_scalar",
"grouped_target_subject": "chairman",
"opcode": 8,
"raw_scalar_value": 700,
"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": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Player Cash to 700 with aux [0, 0, 0, 0]",
"locomotive_name": null,
"notes": [
"chairman row requires selected-chairman scope"
]
}
],
"decoded_conditions": [],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"selected-chairman scope is the only grounded chairman-target subset in this slice"
]
}
]
},
"notes": [
"real chairman-targeted scope parity sample"
]
}
}

View file

@ -0,0 +1,64 @@
{
"format_version": 1,
"fixture_id": "packed-event-deactivate-chairman-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 14 imports and executes on selected-chairman scope."
},
"state_import_path": "packed-event-deactivate-chairman-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"chairman_profile_count": 2,
"active_chairman_profile_count": 1,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"selected_chairman_profile_id": null,
"chairman_profiles": [
{
"profile_id": 1,
"active": false,
"linked_company_id": null
},
{
"profile_id": 2,
"active": true,
"linked_company_id": 2
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "deactivate_chairman",
"target": {
"kind": "selected_chairman"
}
}
],
"grouped_effect_rows": [
{
"grouped_target_subject": "chairman"
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-deactivate-chairman-overlay",
"source": {
"description": "Overlay import combining chairman runtime context with the real chairman-targeted deactivation descriptor sample."
},
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
"save_slice_path": "packed-event-deactivate-chairman-save-slice.json"
}

View file

@ -0,0 +1,109 @@
{
"format_version": 1,
"save_slice_id": "packed-event-deactivate-chairman-save-slice",
"source": {
"description": "Tracked save-slice document with a chairman-targeted Deactivate Player row using the hidden grouped target-subject lane.",
"original_save_filename": "captured-deactivate-chairman.gms",
"original_save_sha256": "deactivate-chairman-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves selected-chairman lifecycle import through the normal runtime path"
]
},
"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,
"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": 62,
"live_record_count": 1,
"live_entry_ids": [62],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 62,
"payload_offset": 29296,
"payload_len": 140,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"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": [2, 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": 14,
"descriptor_label": "Deactivate Player",
"target_mask_bits": 2,
"parameter_family": "player_lifecycle_toggle",
"grouped_target_subject": "chairman",
"opcode": 1,
"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": "bool_toggle",
"semantic_family": "bool_toggle",
"semantic_preview": "Set Deactivate Player to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "deactivate_chairman",
"target": {
"kind": "selected_chairman"
}
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"hidden grouped target-subject lane resolves descriptor 14 to selected chairman scope"
]
}
]
},
"notes": [
"real chairman-targeted lifecycle descriptor sample"
]
}
}