diff --git a/README.md b/README.md index 71ddab5..32f5afb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ still exercise the same runtime without a parallel packed executor. The first gr 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 and governance condition batch is grounded too: selected-chairman cash / holdings / net +worth / purchasing-power thresholds and company book-value-per-share thresholds now import through +the normal event-service path, while wider chairman target scopes and the still-unpinned investor- +confidence / management-attitude condition ids remain explicit frontier. 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 3c8303f..0c62b94 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4495,6 +4495,14 @@ mod tests { let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json", ); + let chairman_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json"); + let company_governance_condition_overlay_fixture = PathBuf::from(env!( + "CARGO_MANIFEST_DIR" + )) + .join( + "../../fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json", + ); run_runtime_summarize_fixture(&parity_fixture) .expect("save-slice-backed parity fixture should summarize"); @@ -4544,6 +4552,10 @@ mod tests { .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"); + run_runtime_summarize_fixture(&chairman_condition_overlay_fixture) + .expect("overlay-backed chairman condition fixture should summarize"); + run_runtime_summarize_fixture(&company_governance_condition_overlay_fixture) + .expect("overlay-backed company governance condition fixture should summarize"); } #[test] diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index dcdca71..c7bd9f6 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -351,6 +351,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), players: Vec::new(), diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 8665b8f..dbe39a0 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -78,6 +78,12 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_chairman_profile_id: Option, #[serde(default)] + pub linked_chairman_company_count: Option, + #[serde(default)] + pub company_takeover_cooldown_count: Option, + #[serde(default)] + pub company_merger_cooldown_count: Option, + #[serde(default)] pub train_count: Option, #[serde(default)] pub active_train_count: Option, @@ -473,6 +479,30 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.linked_chairman_company_count { + if actual.linked_chairman_company_count != count { + mismatches.push(format!( + "linked_chairman_company_count mismatch: expected {count}, got {}", + actual.linked_chairman_company_count + )); + } + } + if let Some(count) = self.company_takeover_cooldown_count { + if actual.company_takeover_cooldown_count != count { + mismatches.push(format!( + "company_takeover_cooldown_count mismatch: expected {count}, got {}", + actual.company_takeover_cooldown_count + )); + } + } + if let Some(count) = self.company_merger_cooldown_count { + if actual.company_merger_cooldown_count != count { + mismatches.push(format!( + "company_merger_cooldown_count mismatch: expected {count}, got {}", + actual.company_merger_cooldown_count + )); + } + } if let Some(count) = self.train_count { if actual.train_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index dfad6e8..92b1eac 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -3335,6 +3335,16 @@ mod tests { } } + fn selected_chairman_negative_sentinel_scope() + -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } + } + fn real_grouped_rows() -> Vec { vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { group_index: 0, @@ -4814,6 +4824,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, crate::RuntimeCompany { company_id: 2, @@ -4825,6 +4841,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(1), @@ -5023,6 +5045,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, crate::RuntimeCompany { company_id: 2, @@ -5034,6 +5062,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, crate::RuntimeCompany { company_id: 3, @@ -5045,6 +5079,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(3), @@ -6638,6 +6678,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), players: Vec::new(), @@ -6799,6 +6845,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), territories: vec![crate::RuntimeTerritory { @@ -7102,6 +7154,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), ..state() @@ -7500,6 +7558,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), ..state() @@ -8804,6 +8868,375 @@ mod tests { ); } + #[test] + fn overlays_selected_chairman_conditions_into_imported_runtime_records() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1840, + month_slot: 1, + phase_slot: 2, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1839), + merger_cooldown_year: Some(1838), + }], + selected_company_id: Some(1), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![ + crate::RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 500, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 700, + net_worth_total: 1200, + purchasing_power_total: 1500, + }, + crate::RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 200, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 400, + net_worth_total: 600, + purchasing_power_total: 800, + }, + ], + selected_chairman_profile_id: Some(1), + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + trailer_family: None, + bridge_family: None, + profile: None, + candidate_availability_table: None, + named_locomotive_availability_table: None, + locomotive_catalog: None, + cargo_catalog: None, + special_conditions_table: None, + event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 71, + live_record_count: 1, + live_entry_ids: vec![71], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 71, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![ + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2218, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Cash".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Cash == 500".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + ], + negative_sentinel_scope: Some(selected_chairman_negative_sentinel_scope()), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: crate::RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.chairman_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["chairman metric condition gates a world-side effect".to_string()], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "chairman-condition-overlay", + None, + ) + .expect("overlay import should project"); + + assert_eq!(import.state.event_runtime_records.len(), 1); + assert_eq!( + import.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: crate::RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }] + ); + + crate::execute_step_command( + &mut import.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("chairman-gated trigger should execute"); + + assert_eq!( + import + .state + .world_flags + .get("world.chairman_condition_imported"), + Some(&true) + ); + } + + #[test] + fn overlays_book_value_conditions_into_imported_runtime_records() { + let base_state = RuntimeState { + calendar: CalendarPoint { + year: 1840, + month_slot: 1, + phase_slot: 2, + tick_slot: 3, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![crate::RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1839), + merger_cooldown_year: Some(1838), + }], + 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(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + let save_slice = SmpLoadedSaveSlice { + file_extension_hint: Some("gms".to_string()), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + trailer_family: None, + bridge_family: None, + profile: None, + candidate_availability_table: None, + named_locomotive_availability_table: None, + locomotive_catalog: None, + cargo_catalog: None, + special_conditions_table: None, + event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 72, + live_record_count: 1, + live_entry_ids: vec![72], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 72, + payload_offset: Some(0x7202), + payload_len: Some(136), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(7), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(real_compact_control()), + text_bands: vec![], + standalone_condition_row_count: 1, + standalone_condition_rows: vec![ + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2620, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Book Value Per Share".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + ], + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: crate::RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.book_value_condition_imported".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec![ + "book value per share condition gates a world-side effect".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "company-book-value-condition-overlay", + None, + ) + .expect("overlay import should project"); + + assert_eq!(import.state.event_runtime_records.len(), 1); + assert_eq!( + import.state.event_runtime_records[0].conditions, + vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }] + ); + + crate::execute_step_command( + &mut import.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("company-governance trigger should execute"); + + assert_eq!( + import + .state + .world_flags + .get("world.book_value_condition_imported"), + Some(&true) + ); + } + #[test] fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() { let base_state = state(); @@ -9058,6 +9491,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, crate::RuntimeCompany { company_id: 7, @@ -9069,6 +9508,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(42), @@ -9272,6 +9717,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), ..state() @@ -9377,6 +9828,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, crate::RuntimeCompany { company_id: 7, @@ -9388,6 +9845,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(42), @@ -9533,6 +9996,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), trains: vec![ @@ -9767,6 +10236,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), trains: vec![RuntimeTrain { @@ -9886,6 +10361,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), ..state() @@ -10004,6 +10485,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), players: Vec::new(), @@ -10190,6 +10677,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(42), players: Vec::new(), diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index b735c5c..ecd729c 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -33,6 +33,18 @@ pub struct RuntimeCompany { #[serde(default)] pub controller_kind: RuntimeCompanyControllerKind, #[serde(default)] + pub linked_chairman_profile_id: Option, + #[serde(default)] + pub book_value_per_share: i64, + #[serde(default)] + pub investor_confidence: i64, + #[serde(default)] + pub management_attitude: i64, + #[serde(default)] + pub takeover_cooldown_year: Option, + #[serde(default)] + pub merger_cooldown_year: Option, + #[serde(default)] pub track_piece_counts: RuntimeTrackPieceCounts, } @@ -235,6 +247,9 @@ pub enum RuntimeCompanyMetric { TotalDebt, CreditRating, PrimeRate, + BookValuePerShare, + InvestorConfidence, + ManagementAttitude, TrackPiecesTotal, TrackPiecesSingle, TrackPiecesDouble, @@ -918,6 +933,46 @@ impl RuntimeState { )); } } + for company in &self.companies { + if let Some(linked_chairman_profile_id) = company.linked_chairman_profile_id { + let linked_profile = self + .chairman_profiles + .iter() + .find(|profile| profile.profile_id == linked_chairman_profile_id) + .ok_or_else(|| { + format!( + "company {} references unknown linked_chairman_profile_id {}", + company.company_id, linked_chairman_profile_id + ) + })?; + if linked_profile.linked_company_id != Some(company.company_id) { + return Err(format!( + "company {} linked_chairman_profile_id {} must point back through linked_company_id", + company.company_id, linked_chairman_profile_id + )); + } + } + } + for chairman in &self.chairman_profiles { + if let Some(linked_company_id) = chairman.linked_company_id { + let linked_company = self + .companies + .iter() + .find(|company| company.company_id == linked_company_id) + .ok_or_else(|| { + format!( + "chairman_profile {} references unknown linked_company_id {}", + chairman.profile_id, linked_company_id + ) + })?; + if linked_company.linked_chairman_profile_id != Some(chairman.profile_id) { + return Err(format!( + "chairman_profile {} linked_company_id {} must point back through linked_chairman_profile_id", + chairman.profile_id, linked_company_id + )); + } + } + } if let Some(selected_player_id) = self.selected_player_id { if !seen_player_ids.contains(&selected_player_id) { return Err(format!( @@ -1772,6 +1827,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, RuntimeCompany { @@ -1783,6 +1844,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, ], @@ -1895,6 +1962,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, @@ -1956,6 +2029,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, @@ -2129,6 +2208,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: Some(2), @@ -2177,6 +2262,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: false, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: Some(1), @@ -2225,6 +2316,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2290,6 +2387,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2345,6 +2448,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2404,6 +2513,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2459,6 +2574,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2520,6 +2641,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2575,6 +2702,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: None, @@ -2607,4 +2740,122 @@ mod tests { assert!(state.validate().is_err()); } + + #[test] + fn rejects_company_with_unknown_linked_chairman_profile() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(9), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + 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(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); + } + + #[test] + fn rejects_mismatched_company_chairman_back_links() { + let state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + world_flags: BTreeMap::new(), + save_profile: RuntimeSaveProfileState::default(), + world_restore: RuntimeWorldRestoreState::default(), + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 1, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 0, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + selected_chairman_profile_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + cargo_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), + cargo_production_overrides: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); + } } diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 1556978..07f703d 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -7,10 +7,11 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::{ - RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope, - RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, - RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, - RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, + RuntimeCargoClass, RuntimeChairmanMetric, 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; @@ -232,6 +233,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RealOrdinaryConditionMetric { Company(RuntimeCompanyMetric), + Chairman(RuntimeChairmanMetric), Territory(RuntimeTerritoryMetric), CompanyTerritory(RuntimeTrackMetric), } @@ -344,6 +346,11 @@ const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [ ]; const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; +const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218; +const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239; +const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240; +const REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID: i32 = 1247; +const REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID: i32 = 2620; const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200; const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422; const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423; @@ -354,7 +361,7 @@ const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421; const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547; const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; -const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [ +const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 38] = [ RealOrdinaryConditionMetadata { raw_condition_id: 1802, label: "Current Cash", @@ -362,6 +369,34 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [ RuntimeCompanyMetric::CurrentCash, )), }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, + label: "Player Cash", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::CurrentCash, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID, + label: "Player Stock Value", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::HoldingsValueTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_NET_WORTH_CONDITION_ID, + label: "Player Net Worth", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::NetWorthTotal, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID, + label: "Purchasing Power", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman( + RuntimeChairmanMetric::PurchasingPowerTotal, + )), + }, RealOrdinaryConditionMetadata { raw_condition_id: 951, label: "Total Debt", @@ -383,6 +418,13 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [ RuntimeCompanyMetric::PrimeRate, )), }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, + label: "Book Value Per Share", + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::BookValuePerShare, + )), + }, RealOrdinaryConditionMetadata { raw_condition_id: 2293, label: "Company Track Pieces", @@ -3258,6 +3300,18 @@ fn decode_real_condition_row( value, }) } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => { + negative_sentinel_scope.and_then(|scope| { + real_condition_chairman_target(scope).map(|target| { + RuntimeCondition::ChairmanNumericThreshold { + target, + metric, + comparator, + value, + } + }) + }) + } RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => { negative_sentinel_scope .filter(|scope| scope.territory_scope_selector_is_0x63) @@ -3369,6 +3423,20 @@ fn decode_world_flag_condition( }) } +fn real_condition_chairman_target( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> Option { + match scope.player_test_scope { + RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimeChairmanTarget::AllActive), + RuntimePlayerConditionTestScope::SelectedPlayerOnly => { + Some(RuntimeChairmanTarget::SelectedChairman) + } + RuntimePlayerConditionTestScope::Disabled + | RuntimePlayerConditionTestScope::AiPlayersOnly + | RuntimePlayerConditionTestScope::HumanPlayersOnly => None, + } +} + fn real_grouped_effect_descriptor_metadata( descriptor_id: u32, ) -> Option { @@ -10397,6 +10465,102 @@ mod tests { assert_eq!(access_cost.label, "Access Rights Cost:"); } + #[test] + fn looks_up_checked_in_chairman_and_governance_condition_metadata() { + let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID) + .expect("chairman cash condition metadata should exist"); + assert_eq!(chairman_cash.label, "Player Cash"); + + let holdings = real_ordinary_condition_metadata(REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID) + .expect("chairman holdings condition metadata should exist"); + assert_eq!(holdings.label, "Player Stock Value"); + + let net_worth = real_ordinary_condition_metadata(REAL_CHAIRMAN_NET_WORTH_CONDITION_ID) + .expect("chairman net worth condition metadata should exist"); + assert_eq!(net_worth.label, "Player Net Worth"); + + let purchasing_power = + real_ordinary_condition_metadata(REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID) + .expect("chairman purchasing-power condition metadata should exist"); + assert_eq!(purchasing_power.label, "Purchasing Power"); + + let book_value = real_ordinary_condition_metadata(REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID) + .expect("book value condition metadata should exist"); + assert_eq!(book_value.label, "Book Value Per Share"); + } + + #[test] + fn decodes_chairman_cash_condition_from_selected_player_scope() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&500_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Player Cash".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Player Cash == 500".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::Disabled, + player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + }; + + assert_eq!( + decode_real_condition_row(&row, Some(&negative_scope)), + Some(RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::SelectedChairman, + metric: RuntimeChairmanMetric::CurrentCash, + comparator: RuntimeConditionComparator::Eq, + value: 500, + }) + ); + } + + #[test] + fn decodes_book_value_per_share_condition_to_company_metric() { + let row = SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Book Value Per Share".to_string()), + semantic_family: Some("numeric_threshold".to_string()), + semantic_preview: Some("Test Book Value Per Share == 2620".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }; + + assert_eq!( + decode_real_condition_row(&row, None), + Some(RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::ConditionTrueCompany, + metric: RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }) + ); + } + #[test] fn looks_up_checked_in_world_flag_descriptor_metadata() { let metadata = diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 369f1bf..1b8362a 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -405,6 +405,14 @@ fn apply_runtime_effects( state.selected_chairman_profile_id = None; } if let Some(linked_company_id) = linked_company_id { + if let Some(company) = state + .companies + .iter_mut() + .find(|company| company.company_id == linked_company_id) + { + company.linked_chairman_profile_id = None; + mutated_company_ids.insert(linked_company_id); + } for other in &mut state.chairman_profiles { if other.profile_id != profile_id && other.linked_company_id == Some(linked_company_id) @@ -1222,6 +1230,9 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM RuntimeCompanyMetric::TotalDebt => company.debt as i64, RuntimeCompanyMetric::CreditRating => company.credit_rating_score.unwrap_or(0), RuntimeCompanyMetric::PrimeRate => company.prime_rate.unwrap_or(0), + RuntimeCompanyMetric::BookValuePerShare => company.book_value_per_share, + RuntimeCompanyMetric::InvestorConfidence => company.investor_confidence, + RuntimeCompanyMetric::ManagementAttitude => company.management_attitude, RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total), RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single), RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double), @@ -1402,10 +1413,11 @@ mod tests { use super::*; use crate::{ - CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, - RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePlayer, - RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, - RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, + CalendarPoint, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, + RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, + RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, + RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, }; fn state() -> RuntimeState { @@ -1430,6 +1442,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: None, players: Vec::new(), @@ -1613,6 +1631,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -1624,6 +1648,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], event_runtime_records: vec![RuntimeEventRecord { @@ -1824,6 +1854,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -1835,6 +1871,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(1), @@ -1970,6 +2012,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -1981,6 +2029,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: false, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 3, @@ -1992,6 +2046,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], event_runtime_records: vec![ @@ -2068,6 +2128,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: Some(8), + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], selected_company_id: Some(1), event_runtime_records: vec![RuntimeEventRecord { @@ -2148,6 +2214,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -2159,6 +2231,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], event_runtime_records: vec![RuntimeEventRecord { @@ -2203,6 +2281,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -2214,6 +2298,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], territories: vec![ @@ -2588,6 +2678,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }], event_runtime_records: vec![RuntimeEventRecord { record_id: 30, @@ -2982,6 +3078,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, RuntimeCompany { company_id: 2, @@ -2993,6 +3095,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, }, ], selected_company_id: Some(1), @@ -3114,4 +3222,280 @@ mod tests { assert!(!state.trains[1].retired); assert!(!state.trains[2].retired); } + + #[test] + fn set_chairman_cash_supports_all_active_target() { + let mut state = RuntimeState { + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 10, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 20, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 3, + name: "Chairman Three".to_string(), + active: false, + current_cash: 30, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 93, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetChairmanCash { + target: RuntimeChairmanTarget::AllActive, + value: 77, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("all-active chairman cash effect should succeed"); + + assert_eq!(state.chairman_profiles[0].current_cash, 77); + assert_eq!(state.chairman_profiles[1].current_cash, 77); + assert_eq!(state.chairman_profiles[2].current_cash, 30); + } + + #[test] + fn deactivate_chairman_clears_selected_and_company_links_for_ids_target() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(1), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 80, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: Some(2), + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, + }, + ], + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 10, + linked_company_id: Some(1), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 20, + linked_company_id: Some(2), + company_holdings: BTreeMap::new(), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }, + ], + selected_chairman_profile_id: Some(2), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 94, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::DeactivateChairman { + target: RuntimeChairmanTarget::Ids { ids: vec![2] }, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("ids-target chairman deactivation should succeed"); + + assert!(state.chairman_profiles[0].active); + assert!(!state.chairman_profiles[1].active); + assert_eq!(state.chairman_profiles[1].linked_company_id, None); + assert_eq!(state.selected_chairman_profile_id, None); + assert_eq!(state.companies[0].linked_chairman_profile_id, Some(1)); + assert_eq!(state.companies[1].linked_chairman_profile_id, None); + } + + #[test] + fn company_governance_metric_conditions_gate_execution() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 100, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 2620, + investor_confidence: 37, + management_attitude: 58, + takeover_cooldown_year: Some(1844), + merger_cooldown_year: Some(1845), + }], + selected_company_id: Some(1), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 95, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::CompanyNumericThreshold { + target: RuntimeCompanyTarget::SelectedCompany, + metric: crate::RuntimeCompanyMetric::BookValuePerShare, + comparator: RuntimeConditionComparator::Eq, + value: 2620, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.book_value_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("book-value company condition should gate execution"); + + assert_eq!( + state.world_flags.get("world.book_value_gate_passed"), + Some(&true) + ); + } + + #[test] + fn chairman_metric_conditions_support_all_active_target() { + let mut state = RuntimeState { + chairman_profiles: vec![ + RuntimeChairmanProfile { + profile_id: 1, + name: "Chairman One".to_string(), + active: true, + current_cash: 20, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 500, + net_worth_total: 700, + purchasing_power_total: 900, + }, + RuntimeChairmanProfile { + profile_id: 2, + name: "Chairman Two".to_string(), + active: true, + current_cash: 30, + linked_company_id: None, + company_holdings: BTreeMap::new(), + holdings_value_total: 600, + net_worth_total: 800, + purchasing_power_total: 1000, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 96, + trigger_kind: 6, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::ChairmanNumericThreshold { + target: RuntimeChairmanTarget::AllActive, + metric: RuntimeChairmanMetric::PurchasingPowerTotal, + comparator: RuntimeConditionComparator::Ge, + value: 900, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.chairman_gate_passed".to_string(), + value: true, + }], + }], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("all-active chairman condition should gate execution"); + + assert_eq!( + state.world_flags.get("world.chairman_gate_passed"), + Some(&true) + ); + } } diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 9ab33de..b19dcc8 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -36,6 +36,9 @@ pub struct RuntimeSummary { pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, pub selected_chairman_profile_id: Option, + pub linked_chairman_company_count: usize, + pub company_takeover_cooldown_count: usize, + pub company_merger_cooldown_count: usize, pub train_count: usize, pub active_train_count: usize, pub retired_train_count: usize, @@ -181,6 +184,21 @@ impl RuntimeSummary { .filter(|profile| profile.active) .count(), selected_chairman_profile_id: state.selected_chairman_profile_id, + linked_chairman_company_count: state + .companies + .iter() + .filter(|company| company.linked_chairman_profile_id.is_some()) + .count(), + company_takeover_cooldown_count: state + .companies + .iter() + .filter(|company| company.takeover_cooldown_year.is_some()) + .count(), + company_merger_cooldown_count: state + .companies + .iter() + .filter(|company| company.merger_cooldown_year.is_some()) + .count(), 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(), @@ -934,6 +952,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: true, available_track_laying_capacity: None, + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Human, }, RuntimeCompany { @@ -945,6 +969,12 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), active: false, available_track_laying_capacity: Some(7), + linked_chairman_profile_id: None, + book_value_per_share: 0, + investor_confidence: 0, + management_attitude: 0, + takeover_cooldown_year: None, + merger_cooldown_year: None, controller_kind: RuntimeCompanyControllerKind::Ai, }, ], diff --git a/docs/README.md b/docs/README.md index 06d5d5e..bb2858d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,6 +87,11 @@ The highest-value next passes are now: - 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` +- 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 condition family now executes too via book-value-per-share + thresholds; wider chairman target ordinals and the still-unpinned investor-confidence / + management-attitude condition ids remain frontier - 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 016e01d..9a6607b 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -46,6 +46,13 @@ Implemented today: `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` +- chairman governance state is broader now too: companies carry explicit chairman links plus + book-value-per-share, investor-confidence, management-attitude, and takeover/merger cooldown + 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 thresholds; wider chairman target + ordinals and the still-unpinned investor-confidence / management-attitude condition ids remain + parity frontier - 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 @@ -116,10 +123,11 @@ 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, 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. +whole-game toggle, train, player, chairman selected-scope grouped effects, grounded +chairman/governance conditions, 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. ## Why This Boundary diff --git a/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json b/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json new file mode 100644 index 0000000..d3eb095 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json @@ -0,0 +1,57 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-chairman-condition-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grounded chairman metric conditions import and execute through selected-chairman context." + }, + "state_import_path": "packed-event-chairman-condition-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, + "linked_chairman_company_count": 2, + "company_takeover_cooldown_count": 1, + "company_merger_cooldown_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, + "world_flag_count": 10, + "total_event_record_service_count": 1, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "world_flags": { + "world.chairman_condition_imported": true + }, + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported", + "decoded_conditions": [ + { + "kind": "chairman_numeric_threshold", + "target": { + "kind": "selected_chairman" + }, + "metric": "current_cash", + "comparator": "eq", + "value": 500 + } + ] + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-chairman-condition-overlay.json b/fixtures/runtime/packed-event-chairman-condition-overlay.json new file mode 100644 index 0000000..93075a6 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-condition-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-chairman-condition-overlay", + "source": { + "description": "Overlay import combining chairman runtime context with a grounded chairman-metric condition sample." + }, + "base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json", + "save_slice_path": "packed-event-chairman-condition-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-chairman-condition-save-slice.json b/fixtures/runtime/packed-event-chairman-condition-save-slice.json new file mode 100644 index 0000000..344be20 --- /dev/null +++ b/fixtures/runtime/packed-event-chairman-condition-save-slice.json @@ -0,0 +1,190 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-chairman-condition-save-slice", + "source": { + "description": "Tracked save-slice document proving grounded chairman metric conditions import and execute through selected-chairman context.", + "original_save_filename": "captured-chairman-condition.gms", + "original_save_sha256": "chairman-condition-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "pins the first grounded chairman ordinary-condition family" + ] + }, + "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": 71, + "live_record_count": 1, + "live_entry_ids": [ + 71 + ], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 71, + "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": 110, + "descriptor_label": "Disable Stock Buying and Selling", + "target_mask_bits": 8, + "parameter_family": "world_flag_toggle", + "opcode": 0, + "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 Disable Stock Buying and Selling to TRUE", + "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_world_flag", + "key": "world.chairman_condition_imported", + "value": true + } + ], + "executable_import_ready": true, + "notes": [ + "chairman metric condition gates a world-side effect" + ] + } + ] + }, + "notes": [ + "real chairman ordinary-condition sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-chairman-overlay-base-snapshot.json b/fixtures/runtime/packed-event-chairman-overlay-base-snapshot.json index 9bbefe9..a781a57 100644 --- a/fixtures/runtime/packed-event-chairman-overlay-base-snapshot.json +++ b/fixtures/runtime/packed-event-chairman-overlay-base-snapshot.json @@ -25,6 +25,12 @@ "credit_rating_score": 650, "prime_rate": 5, "controller_kind": "human", + "linked_chairman_profile_id": 1, + "book_value_per_share": 2620, + "investor_confidence": 37, + "management_attitude": 58, + "takeover_cooldown_year": 1839, + "merger_cooldown_year": 1838, "track_piece_counts": { "total": 20, "single": 5, @@ -41,6 +47,12 @@ "credit_rating_score": 480, "prime_rate": 6, "controller_kind": "ai", + "linked_chairman_profile_id": 2, + "book_value_per_share": 1400, + "investor_confidence": 22, + "management_attitude": 31, + "takeover_cooldown_year": null, + "merger_cooldown_year": null, "track_piece_counts": { "total": 8, "single": 2, diff --git a/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json b/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json new file mode 100644 index 0000000..90823a8 --- /dev/null +++ b/fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json @@ -0,0 +1,72 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-company-governance-condition-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grounded company governance conditions import and execute." + }, + "state_import_path": "packed-event-company-governance-condition-overlay.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "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, + "linked_chairman_company_count": 2, + "company_takeover_cooldown_count": 1, + "company_merger_cooldown_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, + "world_flag_count": 10, + "total_event_record_service_count": 1, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "world_flags": { + "world.book_value_condition_imported": true + }, + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported", + "decoded_conditions": [ + { + "kind": "company_numeric_threshold", + "target": { + "kind": "selected_company" + }, + "metric": "book_value_per_share", + "comparator": "eq", + "value": 2620 + } + ] + } + ] + }, + "event_runtime_records": [ + { + "conditions": [ + { + "kind": "company_numeric_threshold", + "target": { + "kind": "selected_company" + }, + "metric": "book_value_per_share", + "comparator": "eq", + "value": 2620 + } + ] + } + ] + } +} diff --git a/fixtures/runtime/packed-event-company-governance-condition-overlay.json b/fixtures/runtime/packed-event-company-governance-condition-overlay.json new file mode 100644 index 0000000..3d38e1a --- /dev/null +++ b/fixtures/runtime/packed-event-company-governance-condition-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-company-governance-condition-overlay", + "source": { + "description": "Overlay import combining company governance runtime context with a grounded book-value-per-share condition sample." + }, + "base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json", + "save_slice_path": "packed-event-company-governance-condition-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-company-governance-condition-save-slice.json b/fixtures/runtime/packed-event-company-governance-condition-save-slice.json new file mode 100644 index 0000000..5c68dd4 --- /dev/null +++ b/fixtures/runtime/packed-event-company-governance-condition-save-slice.json @@ -0,0 +1,190 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-company-governance-condition-save-slice", + "source": { + "description": "Tracked save-slice document proving grounded company governance conditions import and execute.", + "original_save_filename": "captured-company-governance-condition.gms", + "original_save_sha256": "company-governance-condition-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "pins the first grounded company control-transfer condition family" + ] + }, + "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": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 72, + "payload_offset": 29186, + "payload_len": 136, + "decode_status": "executable", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "active": null, + "marks_collection_dirty": null, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 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": 2620, + "subtype": 4, + "flag_bytes": [ + 60, + 10, + 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": "Book Value Per Share", + "semantic_family": "numeric_threshold", + "semantic_preview": "Test Book Value Per Share == 2620", + "requires_candidate_name_binding": false, + "notes": [] + } + ], + "negative_sentinel_scope": { + "company_test_scope": "selected_company_only", + "player_test_scope": "disabled", + "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": 110, + "descriptor_label": "Disable Stock Buying and Selling", + "target_mask_bits": 8, + "parameter_family": "world_flag_toggle", + "opcode": 0, + "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 Disable Stock Buying and Selling to TRUE", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [ + { + "kind": "company_numeric_threshold", + "target": { + "kind": "condition_true_company" + }, + "metric": "book_value_per_share", + "comparator": "eq", + "value": 2620 + } + ], + "decoded_actions": [ + { + "kind": "set_world_flag", + "key": "world.book_value_condition_imported", + "value": true + } + ], + "executable_import_ready": true, + "notes": [ + "book value per share condition gates a world-side effect" + ] + } + ] + }, + "notes": [ + "real company-governance ordinary-condition sample" + ] + } +}