diff --git a/README.md b/README.md index c3454e2..c36aadc 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,13 @@ runtime rehost layer that can execute deterministic world work, compare normaliz subsystem breadth without depending on the shell or presentation path. The current packed-event frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot, overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries -selected-company and controller-role context through overlay imports, real descriptor `2` -`Company Cash` now parses and executes through the ordinary runtime path, and synthetic packed -records still exercise the same service engine without a parallel packed executor. Condition- -relative company scopes remain explicitly blocked until condition evaluation is grounded. The PE32 -hook remains useful as capture and integration tooling, but it is no longer the main execution -milestone. +selected-company and controller-role context through overlay imports, and real descriptors `2` +`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and +execute through the ordinary runtime path. Synthetic packed records still exercise the same service +engine without a parallel packed executor. Condition-relative company scopes remain explicitly +blocked until condition evaluation is grounded, and mixed supported/unsupported real rows stay +parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer +the main execution milestone. ## Project Docs diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index dd7af6e..4f42109 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4442,6 +4442,12 @@ mod tests { .join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json"); let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json"); + let deactivate_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json"); + let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json"); + let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json"); run_runtime_summarize_fixture(&parity_fixture) .expect("save-slice-backed parity fixture should summarize"); @@ -4451,6 +4457,12 @@ mod tests { .expect("overlay-backed selective-import fixture should summarize"); run_runtime_summarize_fixture(&symbolic_overlay_fixture) .expect("overlay-backed symbolic-target fixture should summarize"); + run_runtime_summarize_fixture(&deactivate_overlay_fixture) + .expect("overlay-backed deactivate-company fixture should summarize"); + run_runtime_summarize_fixture(&track_capacity_overlay_fixture) + .expect("overlay-backed track-capacity fixture should summarize"); + run_runtime_summarize_fixture(&mixed_overlay_fixture) + .expect("overlay-backed mixed real-row fixture should summarize"); } #[test] diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 8f61f41..916a645 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -330,6 +330,8 @@ mod tests { controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, }], selected_company_id: Some(42), packed_event_collection: None, diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index a6a1a34..4396095 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -62,6 +62,8 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub company_count: Option, #[serde(default)] + pub active_company_count: Option, + #[serde(default)] pub packed_event_collection_present: Option, #[serde(default)] pub packed_event_record_count: Option, @@ -327,6 +329,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.active_company_count { + if actual.active_company_count != count { + mismatches.push(format!( + "active_company_count mismatch: expected {count}, got {}", + actual.active_company_count + )); + } + } if let Some(present) = self.packed_event_collection_present { if actual.packed_event_collection_present != present { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 196d787..fadc9b9 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -710,7 +710,7 @@ fn smp_packed_record_to_runtime_event_record( if record.decode_status == "unsupported_framing" { return None; } - if record.payload_family == "real_packed_v1" && record.decoded_actions.is_empty() { + if record.payload_family == "real_packed_v1" && !record.executable_import_ready { return None; } @@ -771,6 +771,25 @@ fn smp_runtime_effect_to_runtime_effect( Err(company_target_import_error_message(target, company_context)) } } + RuntimeEffect::DeactivateCompany { target } => { + if company_target_import_blocker(target, company_context).is_none() { + Ok(RuntimeEffect::DeactivateCompany { + target: target.clone(), + }) + } else { + Err(company_target_import_error_message(target, company_context)) + } + } + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + if company_target_import_blocker(target, company_context).is_none() { + Ok(RuntimeEffect::SetCompanyTrackLayingCapacity { + target: target.clone(), + value: *value, + }) + } else { + Err(company_target_import_error_message(target, company_context)) + } + } RuntimeEffect::AdjustCompanyCash { target, delta } => { if company_target_import_blocker(target, company_context).is_none() { Ok(RuntimeEffect::AdjustCompanyCash { @@ -912,17 +931,14 @@ fn determine_packed_event_import_outcome( if record.compact_control.is_none() { return "blocked_missing_compact_control".to_string(); } + if !record.executable_import_ready { + return "blocked_unmapped_real_descriptor".to_string(); + } if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { return company_target_import_outcome(blocker).to_string(); } - if !record.decoded_actions.is_empty() { - return "blocked_unsupported_decode".to_string(); - } - if let Some(blocker) = real_record_company_target_import_blocker(record, company_context) { - return company_target_import_outcome(blocker).to_string(); - } - return "blocked_unmapped_real_descriptor".to_string(); + return "blocked_unsupported_decode".to_string(); } if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { return company_target_import_outcome(blocker).to_string(); @@ -946,6 +962,8 @@ fn runtime_effect_company_target_import_blocker( ) -> Option { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::DeactivateCompany { target } + | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyDebt { target, .. } => { company_target_import_blocker(target, company_context) @@ -996,16 +1014,6 @@ fn classify_real_grouped_company_target(ordinal: u8) -> Option Option { - classify_real_grouped_company_targets(record) - .into_iter() - .flatten() - .find_map(|target| company_target_import_blocker(&target, company_context)) -} - fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str { match blocker { CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context", @@ -1417,6 +1425,83 @@ mod tests { }] } + fn real_deactivate_company_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 13, + descriptor_label: Some("Deactivate Company".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_lifecycle_toggle".to_string()), + opcode: 1, + raw_scalar_value: if enabled { 1 } else { 0 }, + 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".to_string(), + semantic_family: Some("bool_toggle".to_string()), + semantic_preview: Some(format!( + "Set Deactivate Company to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + locomotive_name: None, + notes: vec![], + } + } + + fn real_track_capacity_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 16, + descriptor_label: Some("Company Track Pieces Buildable".to_string()), + target_mask_bits: Some(0x01), + parameter_family: Some("company_build_limit_scalar".to_string()), + opcode: 3, + raw_scalar_value: value, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some(format!( + "Set Company Track Pieces Buildable to {value}" + )), + locomotive_name: None, + notes: vec![], + } + } + + fn unsupported_real_grouped_row() -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 1, + row_index: 0, + descriptor_id: 8, + descriptor_label: Some("Economic Status".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("whole_game_state_enum".to_string()), + opcode: 3, + raw_scalar_value: 2, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + row_shape: "scalar_assignment".to_string(), + semantic_family: Some("scalar_assignment".to_string()), + semantic_preview: Some("Set Economic Status to 2".to_string()), + locomotive_name: None, + notes: vec![], + } + } + fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary { crate::SmpLoadedPackedEventCompactControlSummary { mode_byte_0x7ef: 6, @@ -2173,12 +2258,16 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 100, debt: 10, + active: true, + available_track_laying_capacity: None, }, crate::RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 50, debt: 20, + active: true, + available_track_laying_capacity: None, }, ], selected_company_id: Some(1), @@ -2312,7 +2401,7 @@ mod tests { target: RuntimeCompanyTarget::ConditionTrueCompany, value: 7, }], - executable_import_ready: false, + executable_import_ready: true, notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], }], }), @@ -2398,8 +2487,11 @@ mod tests { standalone_condition_rows: real_condition_rows(), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), - decoded_actions: vec![], - executable_import_ready: false, + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 7, + }], + executable_import_ready: true, notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], }], }), @@ -2521,6 +2613,8 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 500, debt: 20, + active: true, + available_track_laying_capacity: None, }], selected_company_id: Some(42), packed_event_collection: None, @@ -2610,7 +2704,7 @@ mod tests { target: RuntimeCompanyTarget::SelectedCompany, value: 250, }], - executable_import_ready: false, + executable_import_ready: true, notes: vec![ "decoded from grounded real 0x4e9a row framing".to_string(), "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), @@ -2647,6 +2741,381 @@ mod tests { assert_eq!(import.state.companies[0].current_cash, 250); } + #[test] + fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![crate::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + active: true, + available_track_laying_capacity: None, + }], + selected_company_id: Some(42), + ..state() + }; + 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, + 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: 13, + live_record_count: 1, + live_entry_ids: vec![13], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 13, + payload_offset: Some(0x7202), + payload_len: Some(120), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_company_row(true)], + decoded_actions: vec![RuntimeEffect::DeactivateCompany { + target: RuntimeCompanyTarget::SelectedCompany, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "real-deactivate-company-overlay", + None, + ) + .expect("overlay import should project"); + + assert_eq!(import.state.event_runtime_records.len(), 1); + execute_step_command( + &mut import.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real deactivate-company descriptor should execute"); + + assert!(!import.state.companies[0].active); + assert_eq!(import.state.selected_company_id, None); + } + + #[test] + fn keeps_real_deactivate_company_false_row_parity_only() { + 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, + 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: 14, + live_record_count: 1, + live_entry_ids: vec![14], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 14, + payload_offset: Some(0x7202), + payload_len: Some(120), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_deactivate_company_row(false)], + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "real-deactivate-company-false", + None, + ) + .expect("save slice should project"); + + assert!(import.state.event_runtime_records.is_empty()); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_unmapped_real_descriptor") + ); + } + + #[test] + fn overlays_real_track_capacity_descriptor_into_executable_runtime_record() { + let base_state = RuntimeState { + companies: vec![crate::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + active: true, + available_track_laying_capacity: None, + }], + selected_company_id: Some(42), + ..state() + }; + 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, + 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: 16, + live_record_count: 1, + live_entry_ids: vec![16], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 16, + payload_offset: Some(0x7202), + payload_len: Some(120), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_track_capacity_row(18)], + decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::SelectedCompany, + value: Some(18), + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "real-track-capacity-overlay", + None, + ) + .expect("overlay import should project"); + + execute_step_command( + &mut import.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real track-capacity descriptor should execute"); + + assert_eq!( + import.state.companies[0].available_track_laying_capacity, + Some(18) + ); + } + + #[test] + fn keeps_mixed_real_records_out_of_event_runtime_records() { + let base_state = RuntimeState { + companies: vec![crate::RuntimeCompany { + company_id: 42, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 500, + debt: 20, + active: true, + available_track_laying_capacity: None, + }], + selected_company_id: Some(42), + ..state() + }; + 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, + 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: 17, + live_record_count: 1, + live_entry_ids: vec![17], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 17, + payload_offset: Some(0x7202), + payload_len: Some(160), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 1, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + grouped_effect_row_counts: vec![1, 1, 0, 0], + grouped_effect_rows: vec![ + real_track_capacity_row(18), + unsupported_real_grouped_row(), + ], + decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::SelectedCompany, + value: Some(18), + }], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "mixed-real-record-overlay", + None, + ) + .expect("overlay import should project"); + + assert!(import.state.event_runtime_records.is_empty()); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_unmapped_real_descriptor") + ); + } + #[test] fn overlays_save_slice_events_onto_base_company_context() { let base_state = RuntimeState { @@ -2665,6 +3134,8 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 500, debt: 20, + active: true, + available_track_laying_capacity: None, }], selected_company_id: Some(42), packed_event_collection: None, @@ -2827,6 +3298,8 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, }], selected_company_id: Some(42), packed_event_collection: None, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 3589f2c..a0987b0 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -13,11 +13,19 @@ pub enum RuntimeCompanyControllerKind { Ai, } +fn runtime_company_default_active() -> bool { + true +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeCompany { pub company_id: u32, pub current_cash: i64, pub debt: u64, + #[serde(default = "runtime_company_default_active")] + pub active: bool, + #[serde(default)] + pub available_track_laying_capacity: Option, #[serde(default)] pub controller_kind: RuntimeCompanyControllerKind, } @@ -44,6 +52,13 @@ pub enum RuntimeEffect { target: RuntimeCompanyTarget, value: i64, }, + DeactivateCompany { + target: RuntimeCompanyTarget, + }, + SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget, + value: Option, + }, AdjustCompanyCash { target: RuntimeCompanyTarget, delta: i64, @@ -352,10 +367,14 @@ impl RuntimeState { self.calendar.validate()?; let mut seen_company_ids = BTreeSet::new(); + let mut active_company_ids = BTreeSet::new(); for company in &self.companies { if !seen_company_ids.insert(company.company_id) { return Err(format!("duplicate company_id {}", company.company_id)); } + if company.active { + active_company_ids.insert(company.company_id); + } } if let Some(selected_company_id) = self.selected_company_id { if !seen_company_ids.contains(&selected_company_id) { @@ -364,6 +383,12 @@ impl RuntimeState { selected_company_id )); } + if !active_company_ids.contains(&selected_company_id) { + return Err(format!( + "selected_company_id {} must reference an active company", + selected_company_id + )); + } } let mut seen_record_ids = BTreeSet::new(); @@ -669,6 +694,8 @@ fn validate_runtime_effect( } } RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::DeactivateCompany { target } + | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyDebt { target, .. } => { validate_company_target(target, valid_company_ids)?; @@ -756,12 +783,16 @@ mod tests { company_id: 1, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, RuntimeCompany { company_id: 1, current_cash: 200, debt: 0, + active: true, + available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }, ], @@ -840,6 +871,8 @@ mod tests { company_id: 1, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, @@ -882,6 +915,8 @@ mod tests { company_id: 1, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Unknown, }], selected_company_id: None, @@ -1018,6 +1053,8 @@ mod tests { company_id: 1, current_cash: 100, debt: 0, + active: true, + available_track_laying_capacity: None, controller_kind: RuntimeCompanyControllerKind::Human, }], selected_company_id: Some(2), @@ -1030,4 +1067,36 @@ mod tests { assert!(state.validate().is_err()); } + + #[test] + fn rejects_selected_company_id_that_is_inactive() { + 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, + active: false, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: Some(1), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: 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 4286a4b..aa3a254 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -163,7 +163,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad label: "Deactivate Company", target_mask_bits: 0x01, parameter_family: "company_lifecycle_toggle", - executable_in_runtime: false, + executable_in_runtime: true, }, RealGroupedEffectDescriptorMetadata { descriptor_id: 15, @@ -177,7 +177,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad label: "Company Track Pieces Buildable", target_mask_bits: 0x01, parameter_family: "company_build_limit_scalar", - executable_in_runtime: false, + executable_in_runtime: true, }, ]; @@ -1944,7 +1944,8 @@ fn parse_real_event_runtime_record_summary( .as_ref() .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) .unwrap_or_default(); - let executable_import_ready = !decoded_actions.is_empty() + let executable_import_ready = !grouped_effect_rows.is_empty() + && decoded_actions.len() == grouped_effect_rows.len() && decoded_actions .iter() .all(runtime_effect_supported_for_save_import); @@ -2265,6 +2266,25 @@ fn decode_real_grouped_effect_action( }); } + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 13 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + return Some(RuntimeEffect::DeactivateCompany { target }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 16 + && row.row_shape == "scalar_assignment" + && row.raw_scalar_value >= 0 + { + return Some(RuntimeEffect::SetCompanyTrackLayingCapacity { + target, + value: Some(row.raw_scalar_value as u32), + }); + } + None } @@ -2417,14 +2437,22 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { RuntimeEffect::SetWorldFlag { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetSpecialCondition { .. } + | RuntimeEffect::DeactivateCompany { .. } + | RuntimeEffect::SetCompanyTrackLayingCapacity { .. } | RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::RemoveEventRecord { .. } => true, RuntimeEffect::SetCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } - | RuntimeEffect::AdjustCompanyDebt { target, .. } => { - matches!(target, RuntimeCompanyTarget::AllActive) - } + | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( + target, + RuntimeCompanyTarget::AllActive + | RuntimeCompanyTarget::Ids { .. } + | RuntimeCompanyTarget::HumanCompanies + | RuntimeCompanyTarget::AiCompanies + | RuntimeCompanyTarget::SelectedCompany + | RuntimeCompanyTarget::ConditionTrueCompany + ), RuntimeEffect::AppendEventRecord { record } => record .effects .iter() @@ -7513,10 +7541,10 @@ mod tests { .expect("event runtime collection summary should parse"); assert_eq!(summary.decoded_record_count, 1); - assert_eq!(summary.imported_runtime_record_count, 0); - assert_eq!(summary.records[0].decode_status, "parity_only"); + assert_eq!(summary.imported_runtime_record_count, 1); + assert_eq!(summary.records[0].decode_status, "executable"); assert_eq!(summary.records[0].payload_family, "synthetic_harness"); - assert!(!summary.records[0].executable_import_ready); + assert!(summary.records[0].executable_import_ready); } #[test] diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 752b312..b03a55f 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -299,6 +299,41 @@ fn apply_runtime_effects( mutated_company_ids.insert(company_id); } } + RuntimeEffect::DeactivateCompany { target } => { + let company_ids = resolve_company_target_ids(state, target)?; + 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 deactivate effect" + ) + })?; + company.active = false; + mutated_company_ids.insert(company_id); + if state.selected_company_id == Some(company_id) { + state.selected_company_id = None; + } + } + } + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + let company_ids = resolve_company_target_ids(state, target)?; + 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 track capacity effect" + ) + })?; + company.available_track_laying_capacity = *value; + mutated_company_ids.insert(company_id); + } + } RuntimeEffect::AdjustCompanyCash { target, delta } => { let company_ids = resolve_company_target_ids(state, target)?; for company_id in company_ids { @@ -429,6 +464,7 @@ fn resolve_company_target_ids( RuntimeCompanyTarget::AllActive => Ok(state .companies .iter() + .filter(|company| company.active) .map(|company| company.company_id) .collect()), RuntimeCompanyTarget::Ids { ids } => { @@ -458,7 +494,10 @@ fn resolve_company_target_ids( Ok(state .companies .iter() - .filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human) + .filter(|company| { + company.active + && company.controller_kind == RuntimeCompanyControllerKind::Human + }) .map(|company| company.company_id) .collect()) } @@ -476,14 +515,27 @@ fn resolve_company_target_ids( Ok(state .companies .iter() - .filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai) + .filter(|company| { + company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai + }) .map(|company| company.company_id) .collect()) } - RuntimeCompanyTarget::SelectedCompany => state - .selected_company_id - .map(|company_id| vec![company_id]) - .ok_or_else(|| "target requires selected_company_id context".to_string()), + RuntimeCompanyTarget::SelectedCompany => { + let selected_company_id = state + .selected_company_id + .ok_or_else(|| "target requires selected_company_id context".to_string())?; + if state + .companies + .iter() + .any(|company| company.company_id == selected_company_id && company.active) + { + Ok(vec![selected_company_id]) + } else { + Err("target requires selected_company_id to reference an active company" + .to_string()) + } + } RuntimeCompanyTarget::ConditionTrueCompany => { Err("target requires condition-evaluation context".to_string()) } @@ -530,6 +582,8 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, debt: 0, + active: true, + available_track_laying_capacity: None, }], selected_company_id: None, packed_event_collection: None, @@ -692,12 +746,16 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, debt: 5, + active: true, + available_track_laying_capacity: None, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 20, debt: 8, + active: true, + available_track_laying_capacity: None, }, ], event_runtime_records: vec![RuntimeEventRecord { @@ -744,12 +802,16 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 10, debt: 0, + active: true, + available_track_laying_capacity: None, }, RuntimeCompany { company_id: 2, controller_kind: RuntimeCompanyControllerKind::Ai, current_cash: 20, debt: 2, + active: true, + available_track_laying_capacity: None, }, ], selected_company_id: Some(1), @@ -866,6 +928,179 @@ mod tests { assert!(error.contains("controller_kind")); } + #[test] + fn all_active_and_role_targets_exclude_inactive_companies() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 1, + active: true, + available_track_laying_capacity: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 20, + debt: 2, + active: false, + available_track_laying_capacity: None, + }, + RuntimeCompany { + company_id: 3, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 30, + debt: 3, + active: true, + available_track_laying_capacity: None, + }, + ], + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 16, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + effects: vec![RuntimeEffect::AdjustCompanyCash { + target: RuntimeCompanyTarget::AllActive, + delta: 5, + }], + }, + RuntimeEventRecord { + record_id: 17, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::HumanCompanies, + delta: 4, + }], + }, + RuntimeEventRecord { + record_id: 18, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + effects: vec![RuntimeEffect::AdjustCompanyDebt { + target: RuntimeCompanyTarget::AiCompanies, + delta: 6, + }], + }, + ], + ..state() + }; + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("active-company filtering should succeed"); + + assert_eq!(state.companies[0].current_cash, 15); + assert_eq!(state.companies[1].current_cash, 20); + assert_eq!(state.companies[2].current_cash, 35); + assert_eq!(state.companies[0].debt, 5); + assert_eq!(state.companies[1].debt, 2); + assert_eq!(state.companies[2].debt, 9); + } + + #[test] + fn deactivating_selected_company_clears_selection() { + let mut state = RuntimeState { + companies: vec![RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + active: true, + available_track_laying_capacity: Some(8), + }], + selected_company_id: Some(1), + event_runtime_records: vec![RuntimeEventRecord { + record_id: 19, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + effects: vec![RuntimeEffect::DeactivateCompany { + target: RuntimeCompanyTarget::SelectedCompany, + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("deactivate company effect should succeed"); + + assert!(!state.companies[0].active); + assert_eq!(state.selected_company_id, None); + assert_eq!(result.service_events[0].mutated_company_ids, vec![1]); + } + + #[test] + fn sets_track_laying_capacity_for_resolved_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + active: true, + available_track_laying_capacity: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + active: true, + available_track_laying_capacity: None, + }, + ], + event_runtime_records: vec![RuntimeEventRecord { + record_id: 20, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { + target: RuntimeCompanyTarget::Ids { ids: vec![2] }, + value: Some(14), + }], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("track capacity effect should succeed"); + + assert_eq!(state.companies[0].available_track_laying_capacity, None); + assert_eq!(state.companies[1].available_track_laying_capacity, Some(14)); + assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); + } + #[test] fn rejects_condition_true_company_target_without_condition_context() { let mut state = RuntimeState { @@ -941,6 +1176,8 @@ mod tests { controller_kind: RuntimeCompanyControllerKind::Unknown, current_cash: 10, debt: 2, + active: true, + available_track_laying_capacity: None, }], event_runtime_records: vec![RuntimeEventRecord { record_id: 30, diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index dcabebd..6bf4d7a 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -28,6 +28,7 @@ pub struct RuntimeSummary { pub world_restore_absolute_counter_adjustment_context: Option, pub metadata_count: usize, pub company_count: usize, + pub active_company_count: usize, pub packed_event_collection_present: bool, pub packed_event_record_count: usize, pub packed_event_decoded_record_count: usize, @@ -122,6 +123,7 @@ impl RuntimeSummary { .clone(), metadata_count: state.metadata.len(), company_count: state.companies.len(), + active_company_count: state.companies.iter().filter(|company| company.active).count(), packed_event_collection_present: state.packed_event_collection.is_some(), packed_event_record_count: state .packed_event_collection @@ -302,7 +304,8 @@ mod tests { use std::collections::BTreeMap; use crate::{ - CalendarPoint, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, + CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, + RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, }; @@ -399,4 +402,48 @@ mod tests { assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0); assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0); } + + #[test] + fn counts_active_companies_separately_from_total_companies() { + 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: 10, + debt: 0, + active: true, + available_track_laying_capacity: None, + controller_kind: RuntimeCompanyControllerKind::Human, + }, + RuntimeCompany { + company_id: 2, + current_cash: 20, + debt: 0, + active: false, + available_track_laying_capacity: Some(7), + controller_kind: RuntimeCompanyControllerKind::Ai, + }, + ], + selected_company_id: None, + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!(summary.company_count, 2); + assert_eq!(summary.active_company_count, 1); + } } diff --git a/docs/README.md b/docs/README.md index 9bd78ff..9cb84b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,13 +77,14 @@ The highest-value next passes are now: avoid shell-first implementation bets - keep using overlay imports as the context bridge when selectively executable packed rows still need live company state that save slices do not persist -- treat broader real grouped-descriptor recovery as the active packed-event frontier now that - descriptor `2` `Company Cash` already parses, summarizes, and executes through the ordinary - runtime path when overlay context resolves its symbolic company scope +- treat broader real grouped-descriptor recovery as the active packed-event frontier now that the + first company-scoped batch already parses, summarizes, and executes through the ordinary runtime + path when overlay context resolves its symbolic company scope: descriptor `2` `Company Cash`, + descriptor `13` `Deactivate Company`, and descriptor `16` `Company Track Pieces Buildable` - 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 - leave condition-relative company scopes explicit and blocked until condition evaluation has - grounded runtime semantics + grounded runtime semantics, and keep mixed supported/unsupported real rows parity-only - keep in mind that the current local `.gms` corpus still exports with no packed event collection, so real descriptor mapping needs to stay plumbing-first until better captures exist - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 52d1ec8..21399d5 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -29,11 +29,13 @@ Implemented today: symbolic scopes through the ordinary runtime service path while keeping condition-relative company scopes explicitly blocked - real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview - summaries, and one recovered executable family: descriptor `2` = `Company Cash` + summaries, and three recovered executable company-scoped families: descriptor `2` = `Company Cash`, + descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable` That means the next implementation work is breadth, not bootstrap. The recommended next slice is -broader real grouped-descriptor coverage beyond `Company Cash`, plus condition-relative execution -for the still-blocked symbolic scopes, not another persistence scaffold pass. +broader real grouped-descriptor coverage beyond the current company-scoped batch, plus +condition-relative execution for the still-blocked symbolic scopes, not another persistence +scaffold pass. ## Why This Boundary @@ -375,11 +377,12 @@ Checked-in fixture families already include: ## Next Slice The recommended next implementation slice is broader real grouped-descriptor coverage on top of the -now-stable compact-control, symbolic-target, and first recovered real-family path. +now-stable compact-control, symbolic-target, and current company-scoped real-family batch. Target behavior: -- keep descriptor `2` `Company Cash` as the proof that real grouped rows can cross the whole path: +- keep descriptors `2` `Company Cash`, `13` `Deactivate Company`, and `16` + `Company Track Pieces Buildable` as the proof that real grouped rows can cross the whole path: parse, semantic summary, overlay-backed import, and ordinary trigger execution - recover more real descriptor identities from the checked-in effect table and expose their target masks and conservative semantic previews without guessing unsupported behavior @@ -399,8 +402,11 @@ Public-model expectations for that slice: Fixture work for that slice: - preserve the parity-heavy tracked sample as the condition-relative blocked frontier while it now - carries recovered `Company Cash` semantics -- add overlay-backed captured fixtures whenever a new real descriptor family becomes executable + carries recovered `Company Cash` semantics with executable import readiness +- keep overlay-backed captured fixtures for the executable company-scoped real families: + `Company Cash`, `Deactivate Company`, and `Company Track Pieces Buildable` +- keep a mixed real-row overlay fixture to lock the all-or-nothing parity rule for partially + supported real records - keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens Current local constraint: diff --git a/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json b/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json new file mode 100644 index 0000000..3c923a8 --- /dev/null +++ b/fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json @@ -0,0 +1,58 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-deactivate-company-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture backed by an overlay import document so the real Deactivate Company row executes against selected-company context." + }, + "state_import_path": "packed-event-deactivate-company-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": 3, + "active_company_count": 2, + "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_company_id": null, + "companies": [ + { + "company_id": 1, + "active": true + }, + { + "company_id": 2, + "active": true + }, + { + "company_id": 3, + "active": false + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + } + ] + }, + "event_runtime_records": [ + { + "record_id": 31, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-deactivate-company-overlay.json b/fixtures/runtime/packed-event-deactivate-company-overlay.json new file mode 100644 index 0000000..291b53a --- /dev/null +++ b/fixtures/runtime/packed-event-deactivate-company-overlay.json @@ -0,0 +1,10 @@ +{ + "format_version": 1, + "import_id": "packed-event-deactivate-company-overlay", + "source": { + "description": "Overlay import document for the real Deactivate Company descriptor sample.", + "notes": [] + }, + "base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json", + "save_slice_path": "packed-event-deactivate-company-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-deactivate-company-save-slice.json b/fixtures/runtime/packed-event-deactivate-company-save-slice.json new file mode 100644 index 0000000..818aa5d --- /dev/null +++ b/fixtures/runtime/packed-event-deactivate-company-save-slice.json @@ -0,0 +1,106 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-deactivate-company-save-slice", + "source": { + "description": "Tracked save-slice document with a real packed-event row for Deactivate Company.", + "original_save_filename": "captured-deactivate-company.gms", + "original_save_sha256": "deactivate-company-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "locks executable import for real descriptor 13" + ] + }, + "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": 31, + "live_record_count": 1, + "live_entry_ids": [31], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 31, + "payload_offset": 29186, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 13, + "descriptor_label": "Deactivate Company", + "target_mask_bits": 1, + "parameter_family": "company_lifecycle_toggle", + "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 Company to TRUE", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_actions": [ + { + "kind": "deactivate_company", + "target": { + "kind": "selected_company" + } + } + ], + "executable_import_ready": true, + "notes": [ + "decoded from grounded real 0x4e9a row framing" + ] + } + ] + }, + "notes": [ + "real descriptor 13 sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json new file mode 100644 index 0000000..c41b1ef --- /dev/null +++ b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json @@ -0,0 +1,54 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-mixed-company-descriptor-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture backed by an overlay import document proving mixed real grouped-row records stay parity-only." + }, + "state_import_path": "packed-event-mixed-company-descriptor-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": 3, + "active_company_count": 3, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 0, + "packed_event_blocked_unmapped_real_descriptor_count": 1, + "event_runtime_record_count": 0, + "total_event_record_service_count": 0, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "selected_company_id": 3, + "companies": [ + { + "company_id": 1, + "available_track_laying_capacity": null + }, + { + "company_id": 2, + "available_track_laying_capacity": null + }, + { + "company_id": 3, + "available_track_laying_capacity": null + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_unmapped_real_descriptor" + } + ] + }, + "event_runtime_records": [] + } +} diff --git a/fixtures/runtime/packed-event-mixed-company-descriptor-overlay.json b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay.json new file mode 100644 index 0000000..500a824 --- /dev/null +++ b/fixtures/runtime/packed-event-mixed-company-descriptor-overlay.json @@ -0,0 +1,10 @@ +{ + "format_version": 1, + "import_id": "packed-event-mixed-company-descriptor-overlay", + "source": { + "description": "Overlay import document for a mixed real grouped-row record that should remain parity-only.", + "notes": [] + }, + "base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json", + "save_slice_path": "packed-event-mixed-company-descriptor-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-mixed-company-descriptor-save-slice.json b/fixtures/runtime/packed-event-mixed-company-descriptor-save-slice.json new file mode 100644 index 0000000..76e4c9a --- /dev/null +++ b/fixtures/runtime/packed-event-mixed-company-descriptor-save-slice.json @@ -0,0 +1,128 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-mixed-company-descriptor-save-slice", + "source": { + "description": "Tracked save-slice document with one supported and one unsupported real company-scoped grouped row.", + "original_save_filename": "captured-mixed-company-descriptor.gms", + "original_save_sha256": "mixed-company-descriptor-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "locks all-or-nothing import for mixed real grouped-row records" + ] + }, + "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": 33, + "live_record_count": 1, + "live_entry_ids": [33], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 33, + "payload_offset": 29186, + "payload_len": 160, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 1, 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, 1, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 16, + "descriptor_label": "Company Track Pieces Buildable", + "target_mask_bits": 1, + "parameter_family": "company_build_limit_scalar", + "opcode": 3, + "raw_scalar_value": 18, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Company Track Pieces Buildable to 18", + "locomotive_name": null, + "notes": [] + }, + { + "group_index": 1, + "row_index": 0, + "descriptor_id": 8, + "descriptor_label": "Economic Status", + "target_mask_bits": 8, + "parameter_family": "whole_game_state_enum", + "opcode": 3, + "raw_scalar_value": 2, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Economic Status to 2", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_actions": [ + { + "kind": "set_company_track_laying_capacity", + "target": { + "kind": "selected_company" + }, + "value": 18 + } + ], + "executable_import_ready": false, + "notes": [ + "decoded from grounded real 0x4e9a row framing" + ] + } + ] + }, + "notes": [ + "mixed real descriptor sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-parity-save-slice.json b/fixtures/runtime/packed-event-parity-save-slice.json index 054efdd..9f144f4 100644 --- a/fixtures/runtime/packed-event-parity-save-slice.json +++ b/fixtures/runtime/packed-event-parity-save-slice.json @@ -162,7 +162,7 @@ "value": 7 } ], - "executable_import_ready": false, + "executable_import_ready": true, "notes": [ "decoded from grounded real 0x4e9a row framing", "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0" diff --git a/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json b/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json new file mode 100644 index 0000000..ff840de --- /dev/null +++ b/fixtures/runtime/packed-event-track-capacity-overlay-fixture.json @@ -0,0 +1,58 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-track-capacity-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture backed by an overlay import document so the real Company Track Pieces Buildable row executes against selected-company context." + }, + "state_import_path": "packed-event-track-capacity-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": 3, + "active_company_count": 3, + "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_company_id": 3, + "companies": [ + { + "company_id": 1, + "available_track_laying_capacity": null + }, + { + "company_id": 2, + "available_track_laying_capacity": null + }, + { + "company_id": 3, + "available_track_laying_capacity": 18 + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + } + ] + }, + "event_runtime_records": [ + { + "record_id": 32, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-track-capacity-overlay.json b/fixtures/runtime/packed-event-track-capacity-overlay.json new file mode 100644 index 0000000..c6ad4a5 --- /dev/null +++ b/fixtures/runtime/packed-event-track-capacity-overlay.json @@ -0,0 +1,10 @@ +{ + "format_version": 1, + "import_id": "packed-event-track-capacity-overlay", + "source": { + "description": "Overlay import document for the real Company Track Pieces Buildable descriptor sample.", + "notes": [] + }, + "base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json", + "save_slice_path": "packed-event-track-capacity-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-track-capacity-save-slice.json b/fixtures/runtime/packed-event-track-capacity-save-slice.json new file mode 100644 index 0000000..f2347d5 --- /dev/null +++ b/fixtures/runtime/packed-event-track-capacity-save-slice.json @@ -0,0 +1,107 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-track-capacity-save-slice", + "source": { + "description": "Tracked save-slice document with a real packed-event row for Company Track Pieces Buildable.", + "original_save_filename": "captured-track-capacity.gms", + "original_save_sha256": "track-capacity-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "locks executable import for real descriptor 16" + ] + }, + "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": 32, + "live_record_count": 1, + "live_entry_ids": [32], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 32, + "payload_offset": 29186, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 7, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 16, + "descriptor_label": "Company Track Pieces Buildable", + "target_mask_bits": 1, + "parameter_family": "company_build_limit_scalar", + "opcode": 3, + "raw_scalar_value": 18, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Company Track Pieces Buildable to 18", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_actions": [ + { + "kind": "set_company_track_laying_capacity", + "target": { + "kind": "selected_company" + }, + "value": 18 + } + ], + "executable_import_ready": true, + "notes": [ + "decoded from grounded real 0x4e9a row framing" + ] + } + ] + }, + "notes": [ + "real descriptor 16 sample" + ] + } +}