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`
`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
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
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

View file

@ -4485,6 +4485,16 @@ mod tests {
);
let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.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)
.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");
run_runtime_summarize_fixture(&cargo_catalog_fixture)
.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]

View file

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

View file

@ -72,6 +72,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub player_count: Option<usize>,
#[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>,
#[serde(default)]
pub active_train_count: Option<usize>,
@ -110,6 +116,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
#[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>,
#[serde(default)]
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 actual.train_count != count {
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 actual.packed_event_blocked_missing_condition_context_count != count {
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::{
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget,
RuntimeWorldRestoreState, SmpLoadedPackedEventConditionRowSummary,
SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeChairmanTarget,
RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeCondition, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget, RuntimeWorldRestoreState,
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
};
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
@ -116,6 +117,8 @@ struct ImportRuntimeContext {
known_player_ids: BTreeSet<u32>,
selected_player_id: Option<u32>,
has_complete_player_controller_context: bool,
known_chairman_profile_ids: BTreeSet<u32>,
selected_chairman_profile_id: Option<u32>,
known_territory_ids: BTreeSet<u32>,
has_territory_context: bool,
territory_name_to_id: BTreeMap<String, u32>,
@ -132,6 +135,8 @@ enum ImportBlocker {
MissingPlayerContext,
MissingPlayerSelectionContext,
MissingPlayerRoleContext,
MissingChairmanContext,
ChairmanTargetScope,
MissingConditionContext,
MissingPlayerConditionContext,
CompanyConditionScopeDisabled,
@ -153,6 +158,8 @@ impl ImportRuntimeContext {
known_player_ids: BTreeSet::new(),
selected_player_id: None,
has_complete_player_controller_context: false,
known_chairman_profile_ids: BTreeSet::new(),
selected_chairman_profile_id: None,
known_territory_ids: BTreeSet::new(),
has_territory_context: false,
territory_name_to_id: BTreeMap::new(),
@ -185,6 +192,12 @@ impl ImportRuntimeContext {
.players
.iter()
.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
.territories
.iter()
@ -244,6 +257,8 @@ pub fn project_save_slice_to_runtime_state_import(
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: projection.locomotive_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,
players: base_state.players.clone(),
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(),
locomotive_catalog: projection
.locomotive_catalog
@ -1007,6 +1024,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
descriptor_label: row.descriptor_label.clone(),
target_mask_bits: row.target_mask_bits,
parameter_family: row.parameter_family.clone(),
grouped_target_subject: row.grouped_target_subject.clone(),
opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value,
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());
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)? {
effects.push(effect);
continue;
@ -1425,12 +1446,19 @@ fn lower_condition_targets_in_effect(
)?,
value: *value,
},
RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash {
target: target.clone(),
value: *value,
},
RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer {
target: lower_condition_true_player_target_in_player_target(
target,
lowered_player_target,
)?,
},
RuntimeEffect::DeactivateChairman { target } => RuntimeEffect::DeactivateChairman {
target: target.clone(),
},
RuntimeEffect::SetCompanyTerritoryAccess {
target,
territory,
@ -1590,6 +1618,17 @@ fn lower_condition_targets_in_condition(
comparator: *comparator,
value: *value,
},
RuntimeCondition::ChairmanNumericThreshold {
target,
metric,
comparator,
value,
} => RuntimeCondition::ChairmanNumericThreshold {
target: target.clone(),
metric: *metric,
comparator: *comparator,
value: *value,
},
RuntimeCondition::TerritoryNumericThreshold {
target,
metric,
@ -1786,6 +1825,7 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
}
RuntimeCondition::ChairmanNumericThreshold { .. } => false,
RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. }
| 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(
effects: &[RuntimeEffect],
company_context: &ImportRuntimeContext,
@ -1867,6 +1941,16 @@ fn smp_runtime_effect_to_runtime_effect(
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 } => {
if player_target_allowed_for_import(
target,
@ -1880,6 +1964,15 @@ fn smp_runtime_effect_to_runtime_effect(
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 {
target,
territory,
@ -2236,6 +2329,8 @@ fn company_target_import_error_message(
Some(ImportBlocker::MissingPlayerContext)
| Some(ImportBlocker::MissingPlayerSelectionContext)
| Some(ImportBlocker::MissingPlayerRoleContext)
| Some(ImportBlocker::MissingChairmanContext)
| Some(ImportBlocker::ChairmanTargetScope)
| Some(ImportBlocker::MissingPlayerConditionContext) => {
"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 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();
}
}
if record
.grouped_effect_rows
.iter()
.any(real_grouped_row_is_unsupported_chairman_target_scope)
{
return "blocked_chairman_target_scope".to_string();
}
if record
.grouped_effect_rows
.iter()
@ -2507,6 +2614,9 @@ fn runtime_condition_company_target_import_blocker(
RuntimeCondition::CompanyNumericThreshold { target, .. } => {
company_target_import_blocker(target, company_context)
}
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
chairman_target_import_blocker(target, company_context)
}
RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
territory_target_import_blocker(target, company_context)
}
@ -2559,6 +2669,8 @@ fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str {
ImportBlocker::MissingPlayerContext => "blocked_missing_player_context",
ImportBlocker::MissingPlayerSelectionContext => "blocked_missing_player_selection_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::MissingPlayerConditionContext => "blocked_missing_player_condition_context",
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)
}
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(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
) -> bool {
@ -2644,7 +2767,9 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetPlayerCash { .. }
| RuntimeEffect::SetChairmanCash { .. }
| RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::DeactivateChairman { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
@ -2666,6 +2791,7 @@ fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool {
RuntimeEffect::DeactivatePlayer { target } => {
matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
}
RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivateChairman { .. } => false,
RuntimeEffect::AppendEventRecord { record } => record
.effects
.iter()
@ -2701,6 +2827,10 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::DeactivatePlayer { target } => {
player_target_import_blocker(target, company_context)
}
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => {
chairman_target_import_blocker(target, company_context)
}
RuntimeEffect::RetireTrains {
company_target,
territory_target,
@ -3067,6 +3197,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -3211,6 +3343,7 @@ mod tests {
descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()),
grouped_target_subject: None,
opcode: 8,
raw_scalar_value: 7,
value_byte_0x09: 1,
@ -3240,6 +3373,7 @@ mod tests {
descriptor_label: Some("Deactivate Company".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_lifecycle_toggle".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3270,6 +3404,7 @@ mod tests {
descriptor_label: Some("Company Track Pieces Buildable".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_build_limit_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3299,6 +3434,7 @@ mod tests {
descriptor_label: Some("Deactivate Player".to_string()),
target_mask_bits: Some(0x02),
parameter_family: Some("player_lifecycle_toggle".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3332,6 +3468,7 @@ mod tests {
descriptor_label: Some("Territory - Allow All".to_string()),
target_mask_bits: Some(0x05),
parameter_family: Some("territory_access_toggle".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3362,6 +3499,7 @@ mod tests {
descriptor_label: Some("Economic Status".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("whole_game_state_enum".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3391,6 +3529,7 @@ mod tests {
descriptor_label: Some("Limited Track Building Amount".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("world_track_build_limit_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3420,6 +3559,7 @@ mod tests {
descriptor_label: Some("Use Wartime Cargos".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("special_condition_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3449,6 +3589,7 @@ mod tests {
descriptor_label: Some("Turbo Diesel Availability".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("candidate_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3479,6 +3620,7 @@ mod tests {
descriptor_label: Some("Unknown Loco Available".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3521,6 +3663,7 @@ mod tests {
descriptor_label: Some(descriptor_label.clone()),
target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_cost_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3616,6 +3759,7 @@ mod tests {
descriptor_label: Some(descriptor_label.clone()),
target_mask_bits: Some(0x08),
parameter_family: Some("cargo_production_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3645,6 +3789,7 @@ mod tests {
descriptor_label: Some("Territory Access Cost".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("territory_access_cost_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
@ -3676,6 +3821,7 @@ mod tests {
descriptor_label: Some(label.to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("world_flag_toggle".to_string()),
grouped_target_subject: None,
opcode: 0,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3708,6 +3854,7 @@ mod tests {
descriptor_label: Some("Confiscate All".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_confiscation_variant".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3742,6 +3889,7 @@ mod tests {
descriptor_label: Some("Retire Train".to_string()),
target_mask_bits: Some(0x0d),
parameter_family: Some("company_or_territory_asset_toggle".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
@ -3772,6 +3920,7 @@ mod tests {
descriptor_label: Some("Confiscate All".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_confiscation_variant".to_string()),
grouped_target_subject: None,
opcode: 1,
raw_scalar_value: 0,
value_byte_0x09: 0,
@ -5451,6 +5600,7 @@ mod tests {
descriptor_label: Some("Unknown Loco Available".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: 42,
value_byte_0x09: 0,
@ -5697,6 +5847,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
@ -6092,6 +6244,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
@ -6488,6 +6642,8 @@ mod tests {
selected_company_id: Some(42),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -6566,6 +6722,7 @@ mod tests {
descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()),
grouped_target_subject: None,
opcode: 8,
raw_scalar_value: 250,
value_byte_0x09: 1,
@ -8145,6 +8302,7 @@ mod tests {
descriptor_label: Some("Turbo Diesel Availability".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("candidate_availability_scalar".to_string()),
grouped_target_subject: None,
opcode: 3,
raw_scalar_value: 1,
value_byte_0x09: 0,
@ -9850,6 +10008,8 @@ mod tests {
selected_company_id: Some(42),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -10034,6 +10194,8 @@ mod tests {
selected_company_id: Some(42),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_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,
};
pub use runtime::{
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,

View file

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

View file

@ -89,6 +89,30 @@ pub struct RuntimePlayer {
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 {
true
}
@ -156,6 +180,14 @@ pub enum RuntimePlayerTarget {
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)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeTerritoryTarget {
@ -211,6 +243,15 @@ pub enum RuntimeCompanyMetric {
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)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTerritoryMetric {
@ -242,6 +283,12 @@ pub enum RuntimeCondition {
comparator: RuntimeConditionComparator,
value: i64,
},
ChairmanNumericThreshold {
target: RuntimeChairmanTarget,
metric: RuntimeChairmanMetric,
comparator: RuntimeConditionComparator,
value: i64,
},
TerritoryNumericThreshold {
target: RuntimeTerritoryTarget,
metric: RuntimeTerritoryMetric,
@ -336,9 +383,16 @@ pub enum RuntimeEffect {
target: RuntimePlayerTarget,
value: i64,
},
SetChairmanCash {
target: RuntimeChairmanTarget,
value: i64,
},
DeactivatePlayer {
target: RuntimePlayerTarget,
},
DeactivateChairman {
target: RuntimeChairmanTarget,
},
SetCompanyTerritoryAccess {
target: RuntimeCompanyTarget,
territory: RuntimeTerritoryTarget,
@ -590,6 +644,8 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub target_mask_bits: Option<u8>,
#[serde(default)]
pub parameter_family: Option<String>,
#[serde(default)]
pub grouped_target_subject: Option<String>,
pub opcode: u8,
pub raw_scalar_value: i32,
pub value_byte_0x09: u8,
@ -733,6 +789,10 @@ pub struct RuntimeState {
#[serde(default)]
pub selected_player_id: Option<u32>,
#[serde(default)]
pub chairman_profiles: Vec<RuntimeChairmanProfile>,
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub trains: Vec<RuntimeTrain>,
#[serde(default)]
pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>,
@ -801,6 +861,63 @@ impl RuntimeState {
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 !seen_player_ids.contains(&selected_player_id) {
return Err(format!(
@ -956,7 +1073,12 @@ impl RuntimeState {
return Err(format!("duplicate record_id {}", record.record_id));
}
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| {
format!(
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
@ -969,6 +1091,7 @@ impl RuntimeState {
effect,
&seen_company_ids,
&seen_player_ids,
&seen_chairman_profile_ids,
&seen_territory_ids,
)
.map_err(|err| {
@ -1315,6 +1438,7 @@ fn validate_runtime_effect(
effect: &RuntimeEffect,
valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match effect {
@ -1343,6 +1467,10 @@ fn validate_runtime_effect(
| RuntimeEffect::DeactivatePlayer { target } => {
validate_player_target(target, valid_player_ids)?;
}
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => {
validate_chairman_target(target, valid_chairman_profile_ids)?;
}
RuntimeEffect::RetireTrains {
company_target,
territory_target,
@ -1403,6 +1531,7 @@ fn validate_runtime_effect(
record,
valid_company_ids,
valid_player_ids,
valid_chairman_profile_ids,
valid_territory_ids,
)?;
}
@ -1418,23 +1547,29 @@ fn validate_event_record_template(
record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> {
for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err(
|err| {
validate_runtime_condition(
condition,
valid_company_ids,
valid_chairman_profile_ids,
valid_territory_ids,
)
.map_err(|err| {
format!(
"template record_id={}.conditions[{condition_index}] {err}",
record.record_id
)
},
)?;
})?;
}
for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(
effect,
valid_company_ids,
valid_player_ids,
valid_chairman_profile_ids,
valid_territory_ids,
)
.map_err(|err| {
@ -1451,12 +1586,16 @@ fn validate_event_record_template(
fn validate_runtime_condition(
condition: &RuntimeCondition,
valid_company_ids: &BTreeSet<u32>,
valid_chairman_profile_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. } => {
validate_company_target(target, valid_company_ids)
}
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
validate_chairman_target(target, valid_chairman_profile_ids)
}
RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
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(
target: &RuntimeTerritoryTarget,
valid_territory_ids: &BTreeSet<u32>,
@ -1628,6 +1789,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1689,6 +1852,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1735,6 +1900,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1794,6 +1961,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1853,6 +2022,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1963,6 +2134,8 @@ mod tests {
selected_company_id: Some(2),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -2009,6 +2182,8 @@ mod tests {
selected_company_id: Some(1),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -2055,6 +2230,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![
RuntimeTrain {
train_id: 7,
@ -2118,6 +2295,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 2,
@ -2171,6 +2350,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 1,
@ -2228,6 +2409,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 1,
@ -2281,6 +2464,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -2340,6 +2525,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -2393,6 +2580,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),

View file

@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
RuntimeCargoClass, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
};
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>,
#[serde(default)]
pub parameter_family: Option<String>,
#[serde(default)]
pub grouped_target_subject: Option<String>,
pub opcode: u8,
pub raw_scalar_value: i32,
pub value_byte_0x09: u8,
@ -1901,6 +1903,15 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub notes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealGroupedTargetSubject {
Company,
Player,
Chairman,
Territory,
WholeGame,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSaveSlice {
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() {
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
.grouped_target_scope_ordinals_0x7fb
.get(row.group_index)
.copied()
.and_then(real_grouped_company_target)
.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
.grouped_territory_selectors_0x80f
.get(row.group_index)
@ -2617,6 +2636,14 @@ fn parse_real_event_runtime_record_summary(
row.notes
.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()),
target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits),
parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()),
grouped_target_subject: None,
opcode,
raw_scalar_value,
value_byte_0x09,
@ -3665,6 +3693,44 @@ fn runtime_world_flag_key_from_label(label: &str) -> String {
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(
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
compact_control: &SmpLoadedPackedEventCompactControlSummary,
@ -3684,17 +3750,23 @@ fn decode_real_grouped_effect_action(
.grouped_target_scope_ordinals_0x7fb
.get(row.group_index)
.copied()?;
let target_subject = derive_real_grouped_target_subject(row, compact_control);
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 1
&& row.opcode == 8
&& row.row_shape == "multivalue_scalar"
{
let target = real_grouped_player_target(target_scope_ordinal)?;
return Some(RuntimeEffect::SetPlayerCash {
target,
return match target_subject {
Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash {
target: real_grouped_chairman_target(target_scope_ordinal)?,
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
@ -3823,8 +3895,14 @@ fn decode_real_grouped_effect_action(
&& row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0
{
let target = real_grouped_player_target(target_scope_ordinal)?;
return Some(RuntimeEffect::DeactivatePlayer { target });
return match target_subject {
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
@ -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> {
let opcode = read_u8_at(bytes, *cursor)?;
*cursor += 1;
@ -4033,6 +4122,13 @@ fn parse_optional_u16_len_prefixed_string(
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
match effect {
RuntimeEffect::SetChairmanCash { target, .. }
| RuntimeEffect::DeactivateChairman { target } => matches!(
target,
RuntimeChairmanTarget::AllActive
| RuntimeChairmanTarget::SelectedChairman
| RuntimeChairmanTarget::Ids { .. }
),
RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| 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 {
match condition {
RuntimeCondition::CompanyNumericThreshold { .. }
| RuntimeCondition::ChairmanNumericThreshold { .. }
| RuntimeCondition::TerritoryNumericThreshold { .. }
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
| RuntimeCondition::SpecialConditionThreshold { .. }

View file

@ -3,10 +3,10 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{
RuntimeCargoClass, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyControllerKind,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeState, RuntimeSummary,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind,
};
@ -87,6 +87,8 @@ struct AppliedEffectsSummary {
struct ResolvedConditionContext {
matching_company_ids: BTreeSet<u32>,
matching_player_ids: BTreeSet<u32>,
#[allow(dead_code)]
matching_chairman_profile_ids: BTreeSet<u32>,
}
pub fn execute_step_command(
@ -346,6 +348,21 @@ fn apply_runtime_effects(
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 } => {
let player_ids = resolve_player_target_ids(state, target, condition_context)?;
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 {
target,
territory,
@ -607,6 +657,7 @@ fn evaluate_record_conditions(
}
let mut company_matches: Option<BTreeSet<u32>> = None;
let mut chairman_matches: Option<BTreeSet<u32>> = None;
for condition in conditions {
match condition {
@ -657,6 +708,41 @@ fn evaluate_record_conditions(
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 {
target,
territory,
@ -840,6 +926,7 @@ fn evaluate_record_conditions(
Ok(Some(ResolvedConditionContext {
matching_company_ids: company_matches.unwrap_or_default(),
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(
state: &RuntimeState,
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(
state: &RuntimeState,
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(
state: &RuntimeState,
territory_ids: &[u32],
@ -1277,6 +1434,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),

View file

@ -33,6 +33,9 @@ pub struct RuntimeSummary {
pub company_count: usize,
pub active_company_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 active_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_selection_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_player_condition_context_count: usize,
pub packed_event_blocked_company_condition_scope_disabled_count: usize,
@ -169,6 +174,13 @@ impl RuntimeSummary {
.filter(|company| company.active)
.count(),
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(),
active_train_count: state.trains.iter().filter(|train| train.active).count(),
retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
@ -298,6 +310,34 @@ impl RuntimeSummary {
.count()
})
.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_collection
.as_ref()
@ -666,6 +706,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
@ -909,6 +951,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
@ -956,6 +1000,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
@ -1009,6 +1055,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1053,6 +1101,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1092,6 +1142,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
@ -1200,6 +1252,8 @@ mod tests {
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_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
through the same ordinary runtime path, backed by the minimal player runtime and overlay-import
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,
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`

View file

@ -41,6 +41,11 @@ Implemented today:
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
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
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
@ -111,8 +116,8 @@ Implemented today:
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,
whole-game toggle, train, player, numeric-threshold, named locomotive availability, named
locomotive cost, world scalar override, and world-scalar condition batches.
whole-game toggle, train, player, chairman selected-scope, numeric-threshold, named locomotive
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
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"
]
}
}