From a63de904fa36ecb2d5f340c0d10a4fdd89ec095a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 16 Apr 2026 18:03:17 -0700 Subject: [PATCH] Broaden chairman target scope event support --- README.md | 13 +- crates/rrt-cli/src/main.rs | 15 + crates/rrt-runtime/src/import.rs | 116 ++++++- crates/rrt-runtime/src/runtime.rs | 33 +- crates/rrt-runtime/src/smp.rs | 100 +++++- crates/rrt-runtime/src/step.rs | 97 +++++- docs/README.md | 8 +- docs/runtime-rehost-plan.md | 7 +- ...man-condition-true-save-slice-fixture.json | 49 +++ ...nt-chairman-condition-true-save-slice.json | 284 ++++++++++++++++++ ...hairman-human-cash-save-slice-fixture.json | 49 +++ ...-event-chairman-human-cash-save-slice.json | 222 ++++++++++++++ ...vent-chairman-scope-parity-save-slice.json | 11 +- ...tivate-chairman-ai-save-slice-fixture.json | 62 ++++ ...ent-deactivate-chairman-ai-save-slice.json | 221 ++++++++++++++ 15 files changed, 1257 insertions(+), 30 deletions(-) create mode 100644 fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json create mode 100644 fixtures/runtime/packed-event-chairman-condition-true-save-slice.json create mode 100644 fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json create mode 100644 fixtures/runtime/packed-event-chairman-human-cash-save-slice.json create mode 100644 fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json create mode 100644 fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice.json diff --git a/README.md b/README.md index cd9b21b..3b8ca6a 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,20 @@ 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 +chairman-profile runtime slice now exists too: save-slice or overlay-backed chairman/company +context plus the hidden grouped target-subject lane let those same real descriptors `1` and `14` +execute on the grounded chairman scope ordinals `0..3` (`condition_true`, `selected`, `human`, +`ai`), while wider chairman ordinals remain explicit parity. The first grounded chairman and governance condition batch is broader now: selected-chairman cash / holdings / net worth / purchasing-power thresholds and company book-value-per-share / investor-confidence / management-attitude thresholds now import through the normal event-service path, while wider -chairman target scopes remain explicit frontier. Checked-in save-slice +chairman ordinals remain explicit frontier. Checked-in save-slice documents can now also carry explicit company rosters and chairman-profile tables, so the current company-targeted and chairman-targeted descriptor and condition batches can execute from standalone save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection -still does not reconstruct those company/chairman collections automatically. The first grounded +still does not reconstruct those company/chairman collections automatically. A generic +company-governance scalar effect surface now exists in runtime too, but real governance descriptor +ids are still deferred until the checked-in effect-table evidence is stronger. The first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and the first ordinary nonnegative condition batch now executes too: numeric-threshold company finance, company track, aggregate territory track, and company-territory track rows can import diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index e93ed16..9a02d93 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4489,11 +4489,20 @@ mod tests { .join("../../fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json"); let chairman_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-chairman-cash-save-slice-fixture.json"); + let chairman_condition_true_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json", + ); + let chairman_human_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json", + ); let deactivate_chairman_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json"); let deactivate_chairman_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-deactivate-chairman-save-slice-fixture.json", ); + let deactivate_chairman_ai_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json", + ); let deactivate_company_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-deactivate-company-save-slice-fixture.json"); let track_capacity_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -4580,10 +4589,16 @@ mod tests { .expect("overlay-backed chairman-cash fixture should summarize"); run_runtime_summarize_fixture(&chairman_cash_save_fixture) .expect("save-slice-backed chairman-cash fixture should summarize"); + run_runtime_summarize_fixture(&chairman_condition_true_save_fixture) + .expect("save-slice-backed condition-true chairman fixture should summarize"); + run_runtime_summarize_fixture(&chairman_human_cash_save_fixture) + .expect("save-slice-backed human-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(&deactivate_chairman_save_fixture) .expect("save-slice-backed deactivate-chairman fixture should summarize"); + run_runtime_summarize_fixture(&deactivate_chairman_ai_save_fixture) + .expect("save-slice-backed AI-chairman deactivate fixture should summarize"); run_runtime_summarize_fixture(&deactivate_company_save_fixture) .expect("save-slice-backed deactivate-company fixture should summarize"); run_runtime_summarize_fixture(&track_capacity_save_fixture) diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index df70379..5c234c0 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -1161,6 +1161,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp( target_mask_bits: row.target_mask_bits, parameter_family: row.parameter_family.clone(), grouped_target_subject: row.grouped_target_subject.clone(), + grouped_target_scope: row.grouped_target_scope.clone(), opcode: row.opcode, raw_scalar_value: row.raw_scalar_value, value_byte_0x09: row.value_byte_0x09, @@ -1279,6 +1280,7 @@ fn lowered_record_decoded_actions( if let Some(blocker) = packed_record_condition_scope_import_blocker(record, company_context) { return Err(blocker); } + ensure_condition_true_chairman_context(record)?; let lowered_company_target = lowered_condition_true_company_target(record)?; let lowered_player_target = lowered_condition_true_player_target(record)?; @@ -1582,6 +1584,18 @@ fn lower_condition_targets_in_effect( )?, value: *value, }, + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => RuntimeEffect::SetCompanyGovernanceScalar { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + metric: *metric, + value: *value, + }, RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash { target: target.clone(), value: *value, @@ -1912,6 +1926,23 @@ fn lower_condition_true_player_target_in_player_target( } } +fn ensure_condition_true_chairman_context( + record: &SmpLoadedPackedEventRecordSummary, +) -> Result<(), ImportBlocker> { + if !record_uses_condition_true_chairman(record) { + return Ok(()); + } + if record + .decoded_conditions + .iter() + .any(|condition| matches!(condition, RuntimeCondition::ChairmanNumericThreshold { .. })) + { + Ok(()) + } else { + Err(ImportBlocker::MissingConditionContext) + } +} + fn lower_territory_target_in_condition( target: &RuntimeTerritoryTarget, row: &SmpLoadedPackedEventConditionRowSummary, @@ -1991,6 +2022,15 @@ fn chairman_target_import_blocker( None } } + RuntimeChairmanTarget::HumanChairmen | RuntimeChairmanTarget::AiChairmen => { + if company_context.known_chairman_profile_ids.is_empty() { + Some(ImportBlocker::MissingChairmanContext) + } else if !company_context.has_complete_company_controller_context { + Some(ImportBlocker::MissingCompanyRoleContext) + } else { + None + } + } RuntimeChairmanTarget::SelectedChairman => { if company_context.selected_chairman_profile_id.is_some() { None @@ -1998,6 +2038,13 @@ fn chairman_target_import_blocker( Some(ImportBlocker::MissingChairmanContext) } } + RuntimeChairmanTarget::ConditionTrueChairman => { + if company_context.known_chairman_profile_ids.is_empty() { + Some(ImportBlocker::MissingChairmanContext) + } else { + None + } + } RuntimeChairmanTarget::Ids { ids } => { if company_context.known_chairman_profile_ids.is_empty() { Some(ImportBlocker::MissingChairmanContext) @@ -2173,6 +2220,25 @@ fn smp_runtime_effect_to_runtime_effect( Err(company_target_import_error_message(target, company_context)) } } + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => { + if company_target_allowed_for_import( + target, + company_context, + allow_condition_true_company, + ) { + Ok(RuntimeEffect::SetCompanyGovernanceScalar { + target: target.clone(), + metric: *metric, + value: *value, + }) + } else { + Err(company_target_import_error_message(target, company_context)) + } + } RuntimeEffect::RetireTrains { company_target, territory_target, @@ -2838,10 +2904,9 @@ fn real_grouped_row_is_unsupported_chairman_target_scope( ) -> 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") + && row.notes.iter().any(|note| { + note.starts_with("chairman row uses unsupported grouped target scope ordinal ") + }) } fn real_grouped_row_is_unsupported_territory_access_scope( @@ -2883,6 +2948,7 @@ fn real_grouped_row_is_unsupported_retire_train_scope( fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } @@ -2936,12 +3002,34 @@ fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool { } } +fn record_uses_condition_true_chairman(record: &SmpLoadedPackedEventRecordSummary) -> bool { + record + .decoded_actions + .iter() + .any(runtime_effect_uses_condition_true_chairman) +} + +fn runtime_effect_uses_condition_true_chairman(effect: &RuntimeEffect) -> bool { + match effect { + RuntimeEffect::SetChairmanCash { target, .. } + | RuntimeEffect::DeactivateChairman { target } => { + matches!(target, RuntimeChairmanTarget::ConditionTrueChairman) + } + RuntimeEffect::AppendEventRecord { record } => record + .effects + .iter() + .any(runtime_effect_uses_condition_true_chairman), + _ => false, + } +} + fn runtime_effect_company_target_import_blocker( effect: &RuntimeEffect, company_context: &ImportRuntimeContext, ) -> Option { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyGovernanceScalar { target, .. } | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } @@ -3490,6 +3578,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_finance_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 8, raw_scalar_value: 7, value_byte_0x09: 1, @@ -3520,6 +3609,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_lifecycle_toggle".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -3551,6 +3641,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_build_limit_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3581,6 +3672,7 @@ mod tests { target_mask_bits: Some(0x02), parameter_family: Some("player_lifecycle_toggle".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -3615,6 +3707,7 @@ mod tests { target_mask_bits: Some(0x05), parameter_family: Some("territory_access_toggle".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -3646,6 +3739,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("whole_game_state_enum".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3676,6 +3770,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("world_track_build_limit_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3706,6 +3801,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("special_condition_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3736,6 +3832,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("candidate_availability_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3767,6 +3864,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("locomotive_availability_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3810,6 +3908,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("locomotive_cost_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -3998,6 +4097,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("cargo_production_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -4028,6 +4128,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("territory_access_cost_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: value, value_byte_0x09: 0, @@ -4060,6 +4161,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("world_flag_toggle".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 0, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -4093,6 +4195,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_confiscation_variant".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -4128,6 +4231,7 @@ mod tests { target_mask_bits: Some(0x0d), parameter_family: Some("company_or_territory_asset_toggle".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: if enabled { 1 } else { 0 }, value_byte_0x09: 0, @@ -4159,6 +4263,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_confiscation_variant".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 1, raw_scalar_value: 0, value_byte_0x09: 0, @@ -6005,6 +6110,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("locomotive_availability_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: 42, value_byte_0x09: 0, @@ -7155,6 +7261,7 @@ mod tests { target_mask_bits: Some(0x01), parameter_family: Some("company_finance_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 8, raw_scalar_value: 250, value_byte_0x09: 1, @@ -8785,6 +8892,7 @@ mod tests { target_mask_bits: Some(0x08), parameter_family: Some("candidate_availability_scalar".to_string()), grouped_target_subject: None, + grouped_target_scope: None, opcode: 3, raw_scalar_value: 1, value_byte_0x09: 0, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index ecd729c..e534707 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -196,7 +196,10 @@ pub enum RuntimePlayerTarget { #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeChairmanTarget { AllActive, + HumanChairmen, + AiChairmen, SelectedChairman, + ConditionTrueChairman, Ids { ids: Vec }, } @@ -402,6 +405,11 @@ pub enum RuntimeEffect { target: RuntimeChairmanTarget, value: i64, }, + SetCompanyGovernanceScalar { + target: RuntimeCompanyTarget, + metric: RuntimeCompanyMetric, + value: i64, + }, DeactivatePlayer { target: RuntimePlayerTarget, }, @@ -661,6 +669,8 @@ pub struct RuntimePackedEventGroupedEffectRowSummary { pub parameter_family: Option, #[serde(default)] pub grouped_target_subject: Option, + #[serde(default)] + pub grouped_target_scope: Option, pub opcode: u8, pub raw_scalar_value: i32, pub value_byte_0x09: u8, @@ -1512,6 +1522,10 @@ fn validate_runtime_effect( | RuntimeEffect::AdjustCompanyDebt { target, .. } => { validate_company_target(target, valid_company_ids)?; } + RuntimeEffect::SetCompanyGovernanceScalar { target, metric, .. } => { + validate_company_target(target, valid_company_ids)?; + validate_company_governance_scalar_metric(*metric)?; + } RuntimeEffect::SetCompanyTerritoryAccess { target, territory, .. } => { @@ -1761,7 +1775,11 @@ fn validate_chairman_target( valid_chairman_profile_ids: &BTreeSet, ) -> Result<(), String> { match target { - RuntimeChairmanTarget::AllActive | RuntimeChairmanTarget::SelectedChairman => Ok(()), + RuntimeChairmanTarget::AllActive + | RuntimeChairmanTarget::HumanChairmen + | RuntimeChairmanTarget::AiChairmen + | RuntimeChairmanTarget::SelectedChairman + | RuntimeChairmanTarget::ConditionTrueChairman => Ok(()), RuntimeChairmanTarget::Ids { ids } => { if ids.is_empty() { return Err("target ids must not be empty".to_string()); @@ -1778,6 +1796,19 @@ fn validate_chairman_target( } } +fn validate_company_governance_scalar_metric(metric: RuntimeCompanyMetric) -> Result<(), String> { + match metric { + RuntimeCompanyMetric::CreditRating + | RuntimeCompanyMetric::PrimeRate + | RuntimeCompanyMetric::BookValuePerShare + | RuntimeCompanyMetric::InvestorConfidence + | RuntimeCompanyMetric::ManagementAttitude => Ok(()), + _ => Err( + "governance scalar effect requires a writable company governance metric".to_string(), + ), + } +} + fn validate_territory_target( target: &RuntimeTerritoryTarget, valid_territory_ids: &BTreeSet, diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 1313608..77c39da 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -2007,6 +2007,8 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary { pub parameter_family: Option, #[serde(default)] pub grouped_target_subject: Option, + #[serde(default)] + pub grouped_target_scope: Option, pub opcode: u8, pub raw_scalar_value: i32, pub value_byte_0x09: u8, @@ -2736,9 +2738,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) + let target_subject = derive_real_grouped_target_subject(row, control); + let target_scope_ordinal = control + .grouped_target_scope_ordinals_0x7fb + .get(row.group_index) + .copied(); + row.grouped_target_subject = target_subject .map(real_grouped_target_subject_name) .map(str::to_string); + row.grouped_target_scope = derive_real_grouped_target_scope_name( + row, + control, + target_subject, + target_scope_ordinal, + ); let company_target_present = control .grouped_target_scope_ordinals_0x7fb .get(row.group_index) @@ -2771,13 +2784,13 @@ 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 + if matches!(target_subject, Some(RealGroupedTargetSubject::Chairman)) + && !chairman_target_present { - row.notes - .push("chairman row requires selected-chairman scope".to_string()); + let ordinal = target_scope_ordinal.unwrap_or(u8::MAX); + row.notes.push(format!( + "chairman row uses unsupported grouped target scope ordinal {ordinal}" + )); } } } @@ -3337,6 +3350,7 @@ fn parse_real_grouped_effect_row_summary( 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, + grouped_target_scope: None, opcode, raw_scalar_value, value_byte_0x09, @@ -3524,9 +3538,11 @@ fn real_condition_chairman_target( RuntimePlayerConditionTestScope::SelectedPlayerOnly => { Some(RuntimeChairmanTarget::SelectedChairman) } - RuntimePlayerConditionTestScope::Disabled - | RuntimePlayerConditionTestScope::AiPlayersOnly - | RuntimePlayerConditionTestScope::HumanPlayersOnly => None, + RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimeChairmanTarget::AiChairmen), + RuntimePlayerConditionTestScope::HumanPlayersOnly => { + Some(RuntimeChairmanTarget::HumanChairmen) + } + RuntimePlayerConditionTestScope::Disabled => None, } } @@ -3892,6 +3908,63 @@ fn real_grouped_target_subject_name(subject: RealGroupedTargetSubject) -> &'stat } } +fn derive_real_grouped_target_scope_name( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + compact_control: &SmpLoadedPackedEventCompactControlSummary, + target_subject: Option, + target_scope_ordinal: Option, +) -> Option { + match target_subject { + Some(RealGroupedTargetSubject::Company) => target_scope_ordinal + .map(real_grouped_company_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Player) => target_scope_ordinal + .map(real_grouped_player_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Chairman) => target_scope_ordinal + .map(real_grouped_chairman_scope_name) + .map(str::to_string), + Some(RealGroupedTargetSubject::Territory) => compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|_| "specified_territories".to_string()), + Some(RealGroupedTargetSubject::WholeGame) => Some("whole_game".to_string()), + None => None, + } +} + +fn real_grouped_company_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_company", + 1 => "selected_company", + 2 => "human_companies", + 3 => "ai_companies", + _ => "unsupported_company_scope", + } +} + +fn real_grouped_player_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_player", + 1 => "selected_player", + 2 => "human_players", + 3 => "ai_players", + _ => "unsupported_player_scope", + } +} + +fn real_grouped_chairman_scope_name(ordinal: u8) -> &'static str { + match ordinal { + 0 => "condition_true_chairman", + 1 => "selected_chairman", + 2 => "human_chairmen", + 3 => "ai_chairmen", + _ => "unsupported_chairman_scope", + } +} + fn decode_real_grouped_effect_actions( grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], compact_control: &SmpLoadedPackedEventCompactControlSummary, @@ -4127,7 +4200,10 @@ fn real_grouped_player_target(ordinal: u8) -> Option { fn real_grouped_chairman_target(ordinal: u8) -> Option { match ordinal { + 0 => Some(RuntimeChairmanTarget::ConditionTrueChairman), 1 => Some(RuntimeChairmanTarget::SelectedChairman), + 2 => Some(RuntimeChairmanTarget::HumanChairmen), + 3 => Some(RuntimeChairmanTarget::AiChairmen), _ => None, } } @@ -4287,12 +4363,16 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimeEffect::DeactivateChairman { target } => matches!( target, RuntimeChairmanTarget::AllActive + | RuntimeChairmanTarget::HumanChairmen + | RuntimeChairmanTarget::AiChairmen | RuntimeChairmanTarget::SelectedChairman + | RuntimeChairmanTarget::ConditionTrueChairman | RuntimeChairmanTarget::Ids { .. } ), RuntimeEffect::SetWorldFlag { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetEconomicStatusCode { .. } + | RuntimeEffect::SetCompanyGovernanceScalar { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. } diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 1b8362a..8abda78 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -87,7 +87,6 @@ struct AppliedEffectsSummary { struct ResolvedConditionContext { matching_company_ids: BTreeSet, matching_player_ids: BTreeSet, - #[allow(dead_code)] matching_chairman_profile_ids: BTreeSet, } @@ -363,6 +362,48 @@ fn apply_runtime_effects( chairman.current_cash = *value; } } + RuntimeEffect::SetCompanyGovernanceScalar { + target, + metric, + value, + } => { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + for company_id in company_ids { + let company = state + .companies + .iter_mut() + .find(|company| company.company_id == company_id) + .ok_or_else(|| { + format!( + "missing company_id {company_id} while applying governance effect" + ) + })?; + match metric { + RuntimeCompanyMetric::CreditRating => { + company.credit_rating_score = Some(*value); + } + RuntimeCompanyMetric::PrimeRate => { + company.prime_rate = Some(*value); + } + RuntimeCompanyMetric::BookValuePerShare => { + company.book_value_per_share = *value; + } + RuntimeCompanyMetric::InvestorConfidence => { + company.investor_confidence = *value; + } + RuntimeCompanyMetric::ManagementAttitude => { + company.management_attitude = *value; + } + _ => { + return Err(format!( + "unsupported governance metric {:?} in company governance effect", + metric + )); + } + } + mutated_company_ids.insert(company_id); + } + } RuntimeEffect::DeactivatePlayer { target } => { let player_ids = resolve_player_target_ids(state, target, condition_context)?; for player_id in player_ids { @@ -1152,7 +1193,7 @@ fn resolve_player_target_ids( fn resolve_chairman_target_ids( state: &RuntimeState, target: &RuntimeChairmanTarget, - _condition_context: &ResolvedConditionContext, + condition_context: &ResolvedConditionContext, ) -> Result, String> { match target { RuntimeChairmanTarget::AllActive => Ok(state @@ -1161,6 +1202,30 @@ fn resolve_chairman_target_ids( .filter(|profile| profile.active) .map(|profile| profile.profile_id) .collect()), + RuntimeChairmanTarget::HumanChairmen => Ok(state + .chairman_profiles + .iter() + .filter(|profile| { + chairman_profile_matches_company_controller_kind( + state, + profile, + RuntimeCompanyControllerKind::Human, + ) + }) + .map(|profile| profile.profile_id) + .collect()), + RuntimeChairmanTarget::AiChairmen => Ok(state + .chairman_profiles + .iter() + .filter(|profile| { + chairman_profile_matches_company_controller_kind( + state, + profile, + RuntimeCompanyControllerKind::Ai, + ) + }) + .map(|profile| profile.profile_id) + .collect()), RuntimeChairmanTarget::Ids { ids } => { let known_ids = state .chairman_profiles @@ -1193,9 +1258,37 @@ fn resolve_chairman_target_ids( ) } } + RuntimeChairmanTarget::ConditionTrueChairman => { + if condition_context.matching_chairman_profile_ids.is_empty() { + Err("target requires chairman condition-evaluation context".to_string()) + } else { + Ok(condition_context + .matching_chairman_profile_ids + .iter() + .copied() + .collect()) + } + } } } +fn chairman_profile_matches_company_controller_kind( + state: &RuntimeState, + profile: &crate::RuntimeChairmanProfile, + controller_kind: RuntimeCompanyControllerKind, +) -> bool { + profile.active + && profile + .linked_company_id + .and_then(|company_id| { + state + .companies + .iter() + .find(|company| company.company_id == company_id) + }) + .is_some_and(|company| company.controller_kind == controller_kind) +} + fn resolve_territory_target_ids( state: &RuntimeState, target: &RuntimeTerritoryTarget, diff --git a/docs/README.md b/docs/README.md index f734b39..1335cac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -85,8 +85,9 @@ The highest-value next passes are now: 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` + hidden grouped target-subject lane resolves to grounded chairman scope ordinals `0..3`: + `condition_true_chairman`, `selected_chairman`, `human_chairmen`, and `ai_chairmen`; wider + chairman ordinals stay parity-only under `blocked_chairman_target_scope` - chairman runtime ownership is broader now too: selected-chairman condition rows for chairman cash, holdings value, net worth, and purchasing power import through the same service path, and the first grounded company governance issue batch now executes too via book-value-per-share, @@ -96,6 +97,9 @@ The highest-value next passes are now: tables too, so the current company-targeted and chairman-targeted descriptor/condition batches can execute from standalone save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection/export still does not reconstruct those company/chairman surfaces +- a generic company-governance scalar effect surface now exists in runtime too, but real + governance descriptor ids remain deferred until the checked-in `EventEffects.win` evidence is + strong enough to recover them honestly - widen real packed-event executable coverage descriptor by descriptor after identity, target mask, and normalized effect semantics are all grounded, not just after row framing is parsed - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index db2ca6c..02e3d82 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -51,13 +51,18 @@ Implemented today: lanes, and the first grounded chairman/control-transfer ordinary-condition batch now imports and executes through the same path for selected-chairman cash / holdings / net-worth / purchasing-power thresholds plus company book-value-per-share / investor-confidence / - management-attitude thresholds; wider chairman target ordinals remain parity frontier + management-attitude thresholds; real chairman grouped-effect scope ordinals `0..3` now execute + too as `condition_true`, `selected`, `human`, and `ai`, while wider ordinals remain parity + frontier - checked-in save-slice documents can now carry explicit company rosters and chairman profile tables too, and runtime projection/import will seed or replace company/chairman context from those save-owned surfaces; that lets the currently supported company-targeted and chairman-targeted descriptor/condition batches execute from standalone save-slice fixtures without overlay snapshots when the checked-in documents include that context, while raw `.gms` inspection/export still leaves those company/chairman surfaces absent +- a generic company-governance scalar effect surface now exists in runtime, but real governance + descriptor recovery is still deferred until the checked-in effect-table evidence can ground the + ids honestly - a minimal event-owned train surface and an opaque economic-status lane now exist in runtime state, and real descriptors `8` = `Economic Status`, `9` = `Confiscate All`, and `15` = `Retire Train` now import and execute through the ordinary runtime path when overlay context diff --git a/fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json b/fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json new file mode 100644 index 0000000..858166a --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json @@ -0,0 +1,49 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-chairman-condition-true-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grouped target-scope ordinal 0 executes as condition-true chairman scope from save-slice-backed context." + }, + "state_save_slice_path": "packed-event-chairman-condition-true-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 6 + } + ], + "expected_summary": { + "calendar_projection_source": "default-1830-placeholder", + "calendar_projection_is_placeholder": true, + "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": 777 + }, + { + "profile_id": 2, + "current_cash": 250 + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-chairman-condition-true-save-slice.json b/fixtures/runtime/packed-event-chairman-condition-true-save-slice.json new file mode 100644 index 0000000..3f1fe56 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-condition-true-save-slice.json @@ -0,0 +1,284 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-chairman-condition-true-save-slice", + "source": { + "description": "Tracked save-slice document proving condition-true chairman scope executes through matched chairman conditions.", + "original_save_filename": "captured-chairman-condition-true.gms", + "original_save_sha256": "chairman-condition-true-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "proves grouped target-scope ordinal 0 resolves to condition-true chairman scope" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "named_locomotive_availability_table": null, + "cargo_catalog": 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": 72, + "live_record_count": 1, + "live_entry_ids": [ + 72 + ], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 72, + "payload_offset": 29186, + "payload_len": 136, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 6, + "active": null, + "marks_collection_dirty": null, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 1, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [ + 0, + 1, + 2, + 3 + ], + "grouped_scope_checkboxes_0x7ff": [ + 1, + 0, + 1, + 0 + ], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [ + -1, + 10, + -1, + 22 + ] + }, + "text_bands": [], + "standalone_condition_row_count": 1, + "standalone_condition_rows": [ + { + "row_index": 0, + "raw_condition_id": 2218, + "subtype": 4, + "flag_bytes": [ + 244, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "candidate_name": null, + "comparator": "eq", + "metric": "Player Cash", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Player Cash == 500", + "requires_candidate_name_binding": false, + "notes": [] + } + ], + "negative_sentinel_scope": { + "company_test_scope": "disabled", + "player_test_scope": "selected_player_only", + "territory_scope_selector_is_0x63": false, + "source_row_indexes": [ + 0 + ] + }, + "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", + "grouped_target_scope": "condition_true_chairman", + "opcode": 8, + "raw_scalar_value": 777, + "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 777 with aux [0, 0, 0, 0]", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [ + { + "kind": "chairman_numeric_threshold", + "target": { + "kind": "selected_chairman" + }, + "metric": "current_cash", + "comparator": "eq", + "value": 500 + } + ], + "decoded_actions": [ + { + "kind": "set_chairman_cash", + "target": { + "kind": "condition_true_chairman" + }, + "value": 777 + } + ], + "executable_import_ready": true, + "notes": [ + "condition-true chairman scope resolves through matched chairman conditions" + ] + } + ] + }, + "notes": [ + "real chairman condition-true grouped-effect sample" + ], + "company_roster": { + "source_kind": "tracked-save-slice-company-roster", + "semantic_family": "save-slice-runtime-company-context", + "observed_entry_count": 2, + "selected_company_id": 1, + "entries": [ + { + "company_id": 1, + "active": true, + "controller_kind": "human", + "current_cash": 150, + "debt": 80, + "credit_rating_score": 650, + "prime_rate": 5, + "available_track_laying_capacity": 6, + "track_piece_counts": { + "total": 20, + "single": 5, + "double": 8, + "transition": 1, + "electric": 3, + "non_electric": 17 + }, + "linked_chairman_profile_id": 1, + "book_value_per_share": 2620, + "investor_confidence": 37, + "management_attitude": 58, + "takeover_cooldown_year": 1839, + "merger_cooldown_year": 1838 + }, + { + "company_id": 2, + "active": true, + "controller_kind": "ai", + "current_cash": 90, + "debt": 40, + "credit_rating_score": 480, + "prime_rate": 6, + "available_track_laying_capacity": 2, + "track_piece_counts": { + "total": 8, + "single": 2, + "double": 2, + "transition": 0, + "electric": 1, + "non_electric": 7 + }, + "linked_chairman_profile_id": 2, + "book_value_per_share": 1400, + "investor_confidence": 22, + "management_attitude": 31, + "takeover_cooldown_year": null, + "merger_cooldown_year": null + } + ] + }, + "chairman_profile_table": { + "source_kind": "tracked-save-slice-chairman-profile-table", + "semantic_family": "save-slice-runtime-chairman-context", + "observed_entry_count": 2, + "selected_chairman_profile_id": 1, + "entries": [ + { + "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 + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json b/fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json new file mode 100644 index 0000000..3be1c01 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json @@ -0,0 +1,49 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-chairman-human-cash-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grouped target-scope ordinal 2 executes as human-chairmen scope from save-slice-backed context." + }, + "state_save_slice_path": "packed-event-chairman-human-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, + "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": 777 + }, + { + "profile_id": 2, + "current_cash": 250 + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-chairman-human-cash-save-slice.json b/fixtures/runtime/packed-event-chairman-human-cash-save-slice.json new file mode 100644 index 0000000..7066ce4 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-human-cash-save-slice.json @@ -0,0 +1,222 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-chairman-human-cash-save-slice", + "source": { + "description": "Tracked save-slice document proving grouped target-scope ordinal 2 executes as human-chairmen scope.", + "original_save_filename": "captured-chairman-human-cash.gms", + "original_save_sha256": "chairman-human-cash-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "proves human-chairmen grouped target scope through save-native company/chairman context" + ] + }, + "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": 73, + "live_record_count": 1, + "live_entry_ids": [ + 73 + ], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 73, + "payload_offset": 29296, + "payload_len": 140, + "decode_status": "executable", + "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": [ + 2, + 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", + "grouped_target_scope": "human_chairmen", + "opcode": 8, + "raw_scalar_value": 777, + "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 777 with aux [0, 0, 0, 0]", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [], + "decoded_actions": [ + { + "kind": "set_chairman_cash", + "target": { + "kind": "human_chairmen" + }, + "value": 777 + } + ], + "executable_import_ready": true, + "notes": [ + "hidden grouped target-subject lane resolves descriptor 1 to human-chairmen scope" + ] + } + ] + }, + "notes": [ + "real chairman-targeted human-scope cash descriptor sample" + ], + "company_roster": { + "source_kind": "tracked-save-slice-company-roster", + "semantic_family": "save-slice-runtime-company-context", + "observed_entry_count": 2, + "selected_company_id": 1, + "entries": [ + { + "company_id": 1, + "active": true, + "controller_kind": "human", + "current_cash": 150, + "debt": 80, + "credit_rating_score": 650, + "prime_rate": 5, + "available_track_laying_capacity": 6, + "track_piece_counts": { + "total": 20, + "single": 5, + "double": 8, + "transition": 1, + "electric": 3, + "non_electric": 17 + }, + "linked_chairman_profile_id": 1, + "book_value_per_share": 2620, + "investor_confidence": 37, + "management_attitude": 58, + "takeover_cooldown_year": 1839, + "merger_cooldown_year": 1838 + }, + { + "company_id": 2, + "active": true, + "controller_kind": "ai", + "current_cash": 90, + "debt": 40, + "credit_rating_score": 480, + "prime_rate": 6, + "available_track_laying_capacity": 2, + "track_piece_counts": { + "total": 8, + "single": 2, + "double": 2, + "transition": 0, + "electric": 1, + "non_electric": 7 + }, + "linked_chairman_profile_id": 2, + "book_value_per_share": 1400, + "investor_confidence": 22, + "management_attitude": 31, + "takeover_cooldown_year": null, + "merger_cooldown_year": null + } + ] + }, + "chairman_profile_table": { + "source_kind": "tracked-save-slice-chairman-profile-table", + "semantic_family": "save-slice-runtime-chairman-context", + "observed_entry_count": 2, + "selected_chairman_profile_id": 1, + "entries": [ + { + "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 + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-chairman-scope-parity-save-slice.json b/fixtures/runtime/packed-event-chairman-scope-parity-save-slice.json index 2c644b7..82d666f 100644 --- a/fixtures/runtime/packed-event-chairman-scope-parity-save-slice.json +++ b/fixtures/runtime/packed-event-chairman-scope-parity-save-slice.json @@ -2,12 +2,12 @@ "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.", + "description": "Tracked save-slice document with a chairman-targeted row on an unsupported chairman scope ordinal outside the grounded 0..3 strip.", "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" + "pins the remaining unsupported chairman-scope ordinal frontier" ] }, "save_slice": { @@ -52,7 +52,7 @@ "one_shot_header_0x7f5": 0, "modifier_flag_0x7f9": 0, "modifier_flag_0x7fa": 0, - "grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1], + "grouped_target_scope_ordinals_0x7fb": [8, 1, 1, 1], "grouped_scope_checkboxes_0x7ff": [2, 0, 0, 0], "summary_toggle_0x800": 1, "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] @@ -70,6 +70,7 @@ "target_mask_bits": 2, "parameter_family": "player_cash_scalar", "grouped_target_subject": "chairman", + "grouped_target_scope": "unsupported_chairman_scope", "opcode": 8, "raw_scalar_value": 700, "value_byte_0x09": 0, @@ -83,7 +84,7 @@ "semantic_preview": "Set Player Cash to 700 with aux [0, 0, 0, 0]", "locomotive_name": null, "notes": [ - "chairman row requires selected-chairman scope" + "chairman row uses unsupported grouped target scope ordinal 8" ] } ], @@ -92,7 +93,7 @@ "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" + "only chairman target-scope ordinals 0..3 are grounded in this slice" ] } ] diff --git a/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json b/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json new file mode 100644 index 0000000..3d9cf3c --- /dev/null +++ b/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json @@ -0,0 +1,62 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-deactivate-chairman-ai-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grouped target-scope ordinal 3 executes as AI-chairmen scope from save-slice-backed context." + }, + "state_save_slice_path": "packed-event-deactivate-chairman-ai-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 6 + } + ], + "expected_summary": { + "calendar_projection_source": "default-1830-placeholder", + "calendar_projection_is_placeholder": true, + "company_count": 2, + "chairman_profile_count": 2, + "active_chairman_profile_count": 1, + "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": { + "selected_chairman_profile_id": 1, + "companies": [ + { + "company_id": 1, + "linked_chairman_profile_id": 1 + }, + { + "company_id": 2, + "linked_chairman_profile_id": null + } + ], + "chairman_profiles": [ + { + "profile_id": 1, + "active": true, + "linked_company_id": 1 + }, + { + "profile_id": 2, + "active": false, + "linked_company_id": null + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice.json b/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice.json new file mode 100644 index 0000000..63d4fe9 --- /dev/null +++ b/fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice.json @@ -0,0 +1,221 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-deactivate-chairman-ai-save-slice", + "source": { + "description": "Tracked save-slice document proving grouped target-scope ordinal 3 executes as AI-chairmen scope.", + "original_save_filename": "captured-deactivate-chairman-ai.gms", + "original_save_sha256": "deactivate-chairman-ai-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "proves AI-chairmen lifecycle import through save-native company/chairman context" + ] + }, + "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": 74, + "live_record_count": 1, + "live_entry_ids": [ + 74 + ], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 74, + "payload_offset": 29296, + "payload_len": 140, + "decode_status": "executable", + "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": [ + 3, + 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", + "grouped_target_scope": "ai_chairmen", + "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": "ai_chairmen" + } + } + ], + "executable_import_ready": true, + "notes": [ + "hidden grouped target-subject lane resolves descriptor 14 to AI-chairmen scope" + ] + } + ] + }, + "notes": [ + "real chairman-targeted AI-scope lifecycle descriptor sample" + ], + "company_roster": { + "source_kind": "tracked-save-slice-company-roster", + "semantic_family": "save-slice-runtime-company-context", + "observed_entry_count": 2, + "selected_company_id": 1, + "entries": [ + { + "company_id": 1, + "active": true, + "controller_kind": "human", + "current_cash": 150, + "debt": 80, + "credit_rating_score": 650, + "prime_rate": 5, + "available_track_laying_capacity": 6, + "track_piece_counts": { + "total": 20, + "single": 5, + "double": 8, + "transition": 1, + "electric": 3, + "non_electric": 17 + }, + "linked_chairman_profile_id": 1, + "book_value_per_share": 2620, + "investor_confidence": 37, + "management_attitude": 58, + "takeover_cooldown_year": 1839, + "merger_cooldown_year": 1838 + }, + { + "company_id": 2, + "active": true, + "controller_kind": "ai", + "current_cash": 90, + "debt": 40, + "credit_rating_score": 480, + "prime_rate": 6, + "available_track_laying_capacity": 2, + "track_piece_counts": { + "total": 8, + "single": 2, + "double": 2, + "transition": 0, + "electric": 1, + "non_electric": 7 + }, + "linked_chairman_profile_id": 2, + "book_value_per_share": 1400, + "investor_confidence": 22, + "management_attitude": 31, + "takeover_cooldown_year": null, + "merger_cooldown_year": null + } + ] + }, + "chairman_profile_table": { + "source_kind": "tracked-save-slice-chairman-profile-table", + "semantic_family": "save-slice-runtime-chairman-context", + "observed_entry_count": 2, + "selected_chairman_profile_id": 1, + "entries": [ + { + "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 + } + ] + } + } +}