diff --git a/README.md b/README.md index 042d99b..a600a11 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ company-territory track rows can import through overlay-backed runtime context. named-territory binding now executes, and the runtime now also carries the minimal event-owned train roster and opaque economic-status lane needed for real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` to execute through the same path. Descriptor `3` -`Territory - Allow All` remains the explicit parity-only descriptor frontier. Mixed -supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and -integration tooling, but it is no longer the main execution milestone. +`Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights +rather than a territory-owned policy bit. Shell purchase-flow and selected-profile parity remain +out of scope. Mixed supported/unsupported real rows still 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-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index ca0bf19..80b1bf5 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -179,6 +179,7 @@ mod tests { trains: 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(), @@ -347,6 +348,7 @@ mod tests { trains: 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(), diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index d309637..0e3e2eb 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -122,7 +122,9 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub packed_event_blocked_unmapped_real_descriptor_count: Option, #[serde(default)] - pub packed_event_blocked_territory_policy_descriptor_count: Option, + pub packed_event_blocked_territory_access_variant_count: Option, + #[serde(default)] + pub packed_event_blocked_territory_access_scope_count: Option, #[serde(default)] pub packed_event_blocked_missing_train_context_count: Option, #[serde(default)] @@ -615,11 +617,19 @@ impl ExpectedRuntimeSummary { )); } } - if let Some(count) = self.packed_event_blocked_territory_policy_descriptor_count { - if actual.packed_event_blocked_territory_policy_descriptor_count != count { + if let Some(count) = self.packed_event_blocked_territory_access_variant_count { + if actual.packed_event_blocked_territory_access_variant_count != count { mismatches.push(format!( - "packed_event_blocked_territory_policy_descriptor_count mismatch: expected {count}, got {}", - actual.packed_event_blocked_territory_policy_descriptor_count + "packed_event_blocked_territory_access_variant_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_territory_access_variant_count + )); + } + } + if let Some(count) = self.packed_event_blocked_territory_access_scope_count { + if actual.packed_event_blocked_territory_access_scope_count != count { + mismatches.push(format!( + "packed_event_blocked_territory_access_scope_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_territory_access_scope_count )); } } diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 9cbdfd6..a715292 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -233,6 +233,7 @@ pub fn project_save_slice_to_runtime_state_import( trains: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), packed_event_collection: projection.packed_event_collection, event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, @@ -289,6 +290,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import( company_territory_track_piece_counts: base_state .company_territory_track_piece_counts .clone(), + company_territory_access: base_state.company_territory_access.clone(), packed_event_collection: projection.packed_event_collection, event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, @@ -1044,6 +1046,18 @@ fn lower_condition_targets_in_effect( )?, value: *value, }, + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => RuntimeEffect::SetCompanyTerritoryAccess { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_company_target, + )?, + territory: territory.clone(), + value: *value, + }, RuntimeEffect::ConfiscateCompanyAssets { target } => { RuntimeEffect::ConfiscateCompanyAssets { target: lower_condition_true_company_target_in_company_target( @@ -1332,6 +1346,24 @@ fn smp_runtime_effect_to_runtime_effect( Err(player_target_import_error_message(target, company_context)) } } + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => { + if !company_target_allowed_for_import(target, company_context, allow_condition_true_company) + { + Err(company_target_import_error_message(target, company_context)) + } else if territory_target_import_blocker(territory, company_context).is_some() { + Err("packed effect requires territory runtime context".to_string()) + } else { + Ok(RuntimeEffect::SetCompanyTerritoryAccess { + target: target.clone(), + territory: territory.clone(), + value: *value, + }) + } + } RuntimeEffect::ConfiscateCompanyAssets { target } => { if company_target_allowed_for_import( target, @@ -1724,9 +1756,16 @@ fn determine_packed_event_import_outcome( if record .grouped_effect_rows .iter() - .any(|row| row.descriptor_id == 3) + .any(real_grouped_row_is_unsupported_territory_access_scope) { - return "blocked_territory_policy_descriptor".to_string(); + return "blocked_territory_access_scope".to_string(); + } + if record + .grouped_effect_rows + .iter() + .any(real_grouped_row_is_unsupported_territory_access_variant) + { + return "blocked_territory_access_variant".to_string(); } if record .grouped_effect_rows @@ -1867,6 +1906,24 @@ fn territory_ids_match_known_context(ids: &[u32], company_context: &ImportRuntim .all(|territory_id| company_context.known_territory_ids.contains(territory_id)) } +fn real_grouped_row_is_unsupported_territory_access_variant( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0) +} + +fn real_grouped_row_is_unsupported_territory_access_scope( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, +) -> bool { + row.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && row + .notes + .iter() + .any(|note| note == "territory access row is missing company or territory scope") +} + fn real_grouped_row_is_unsupported_confiscation_variant( row: &SmpLoadedPackedEventGroupedEffectRowSummary, ) -> bool { @@ -1894,6 +1951,7 @@ fn real_grouped_row_is_unsupported_retire_train_scope( fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } @@ -1939,6 +1997,7 @@ fn runtime_effect_company_target_import_blocker( ) -> Option { match effect { RuntimeEffect::SetCompanyCash { target, .. } + | RuntimeEffect::SetCompanyTerritoryAccess { target, .. } | RuntimeEffect::ConfiscateCompanyAssets { target } | RuntimeEffect::DeactivateCompany { target } | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } @@ -1948,6 +2007,9 @@ fn runtime_effect_company_target_import_blocker( && !company_context.has_train_context { Some(ImportBlocker::MissingTrainContext) + } else if let RuntimeEffect::SetCompanyTerritoryAccess { territory, .. } = effect { + company_target_import_blocker(target, company_context) + .or_else(|| territory_target_import_blocker(territory, company_context)) } else { company_target_import_blocker(target, company_context) } @@ -2315,6 +2377,7 @@ mod tests { trains: 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(), @@ -2517,6 +2580,36 @@ mod tests { } } + fn real_territory_access_row( + enabled: bool, + notes: Vec, + ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 3, + descriptor_label: Some("Territory - Allow All".to_string()), + target_mask_bits: Some(0x05), + parameter_family: Some("territory_access_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 Territory - Allow All to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + locomotive_name: None, + notes, + } + } + fn real_economic_status_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { crate::SmpLoadedPackedEventGroupedEffectRowSummary { group_index: 0, @@ -4139,6 +4232,7 @@ mod tests { trains: 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(), @@ -4265,6 +4359,298 @@ mod tests { assert_eq!(import.state.companies[0].current_cash, 250); } + #[test] + fn overlays_real_territory_access_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, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + }], + selected_company_id: Some(42), + territories: vec![crate::RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + ..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: 11, + live_record_count: 1, + live_entry_ids: vec![11], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 11, + payload_offset: Some(0x7202), + payload_len: Some(120), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row(true, vec![])], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![7] }, + value: true, + }], + 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-territory-access-overlay", + None, + ) + .expect("overlay import should project"); + + assert_eq!(import.state.event_runtime_records.len(), 1); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("imported") + ); + + execute_step_command( + &mut import.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("real territory-access descriptor should execute"); + + assert_eq!( + import.state.company_territory_access, + vec![crate::RuntimeCompanyTerritoryAccess { + company_id: 42, + territory_id: 7, + }] + ); + } + + #[test] + fn keeps_real_territory_access_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: 12, + live_record_count: 1, + live_entry_ids: vec![12], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 12, + payload_offset: Some(0x7202), + payload_len: Some(120), + 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(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![7, -1, -1, -1], + }), + text_bands: packed_text_bands(), + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row(false, vec![])], + decoded_conditions: Vec::new(), + 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-territory-access-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_territory_access_variant") + ); + } + + #[test] + fn keeps_real_territory_access_missing_scope_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: 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(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(false), + compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 12, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![9, 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![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_territory_access_row( + true, + vec!["territory access row is missing company or territory scope" + .to_string()], + )], + decoded_conditions: Vec::new(), + 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-territory-access-missing-scope", + 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_territory_access_scope") + ); + } + #[test] fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() { let base_state = RuntimeState { @@ -5567,6 +5953,7 @@ mod tests { trains: 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![RuntimeEventRecord { record_id: 1, @@ -5742,6 +6129,7 @@ mod tests { trains: 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(), diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 755714c..c86d463 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -36,9 +36,10 @@ pub use pk4::{ }; pub use runtime::{ RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, - RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyTerritoryTrackPieceCount, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, + RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, + RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, + RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, diff --git a/crates/rrt-runtime/src/persistence.rs b/crates/rrt-runtime/src/persistence.rs index 11c7afb..3baea3b 100644 --- a/crates/rrt-runtime/src/persistence.rs +++ b/crates/rrt-runtime/src/persistence.rs @@ -99,6 +99,7 @@ mod tests { trains: 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(), diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 0a6610a..2fc5165 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -69,6 +69,12 @@ pub struct RuntimeCompanyTerritoryTrackPieceCount { pub track_piece_counts: RuntimeTrackPieceCounts, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyTerritoryAccess { + pub company_id: u32, + pub territory_id: u32, +} + fn runtime_player_default_active() -> bool { true } @@ -242,6 +248,11 @@ pub enum RuntimeEffect { target: RuntimePlayerTarget, value: i64, }, + SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget, + territory: RuntimeTerritoryTarget, + value: bool, + }, ConfiscateCompanyAssets { target: RuntimeCompanyTarget, }, @@ -592,6 +603,8 @@ pub struct RuntimeState { #[serde(default)] pub company_territory_track_piece_counts: Vec, #[serde(default)] + pub company_territory_access: Vec, + #[serde(default)] pub packed_event_collection: Option, #[serde(default)] pub event_runtime_records: Vec, @@ -725,6 +738,27 @@ impl RuntimeState { )); } } + let mut seen_company_territory_access = BTreeSet::new(); + for entry in &self.company_territory_access { + if !seen_company_ids.contains(&entry.company_id) { + return Err(format!( + "company_territory_access references unknown company_id {}", + entry.company_id + )); + } + if !seen_territory_ids.contains(&entry.territory_id) { + return Err(format!( + "company_territory_access references unknown territory_id {}", + entry.territory_id + )); + } + if !seen_company_territory_access.insert((entry.company_id, entry.territory_id)) { + return Err(format!( + "duplicate company_territory_access pair ({}, {})", + entry.company_id, entry.territory_id + )); + } + } let mut seen_record_ids = BTreeSet::new(); for record in &self.event_runtime_records { @@ -1090,6 +1124,12 @@ fn validate_runtime_effect( | RuntimeEffect::AdjustCompanyDebt { target, .. } => { validate_company_target(target, valid_company_ids)?; } + RuntimeEffect::SetCompanyTerritoryAccess { + target, territory, .. + } => { + validate_company_target(target, valid_company_ids)?; + validate_territory_target(territory, valid_territory_ids)?; + } RuntimeEffect::SetPlayerCash { target, .. } => { validate_player_target(target, valid_player_ids)?; } @@ -1315,6 +1355,7 @@ mod tests { trains: 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(), @@ -1368,6 +1409,7 @@ mod tests { trains: 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(), @@ -1408,6 +1450,7 @@ mod tests { trains: 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![RuntimeEventRecord { record_id: 7, @@ -1461,6 +1504,7 @@ mod tests { trains: 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![RuntimeEventRecord { record_id: 7, @@ -1514,6 +1558,7 @@ mod tests { trains: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), packed_event_collection: Some(RuntimePackedEventCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), mechanism_family: "classic-save-rehydrate-v1".to_string(), @@ -1618,6 +1663,7 @@ mod tests { trains: 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(), @@ -1658,6 +1704,7 @@ mod tests { trains: 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(), @@ -1715,6 +1762,7 @@ mod tests { ], 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(), @@ -1762,6 +1810,7 @@ mod tests { }], 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(), @@ -1813,6 +1862,7 @@ mod tests { track_piece_counts: RuntimeTrackPieceCounts::default(), }], 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(), @@ -1860,6 +1910,157 @@ mod tests { }], 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(), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + assert!(state.validate().is_err()); + } + + #[test] + fn rejects_duplicate_company_territory_access_pairs() { + 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, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + trains: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![ + RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + ], + 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()); + } + + #[test] + fn rejects_company_territory_access_with_unknown_company() { + 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, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + trains: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![RuntimeCompanyTerritoryAccess { + company_id: 2, + territory_id: 7, + }], + 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()); + } + + #[test] + fn rejects_company_territory_access_with_unknown_territory() { + 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, + controller_kind: RuntimeCompanyControllerKind::Human, + }], + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + trains: Vec::new(), + territories: vec![RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }], + company_territory_track_piece_counts: Vec::new(), + company_territory_access: vec![RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 8, + }], packed_event_collection: None, event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index aca6caf..f1e6cb0 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -147,7 +147,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad label: "Territory - Allow All", target_mask_bits: 0x05, parameter_family: "territory_access_toggle", - executable_in_runtime: false, + executable_in_runtime: true, }, RealGroupedEffectDescriptorMetadata { descriptor_id: 8, @@ -2102,12 +2102,6 @@ fn parse_real_event_runtime_record_summary( } if let Some(control) = compact_control.as_ref() { for row in &mut grouped_effect_rows { - if row.descriptor_id != 15 - || row.row_shape != "bool_toggle" - || row.raw_scalar_value == 0 - { - continue; - } let company_target_present = control .grouped_target_scope_ordinals_0x7fb .get(row.group_index) @@ -2118,10 +2112,24 @@ fn parse_real_event_runtime_record_summary( .grouped_territory_selectors_0x80f .get(row.group_index) .is_some_and(|selector| *selector >= 0); - if !company_target_present && !territory_target_present { + if row.descriptor_id == 15 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && !company_target_present + && !territory_target_present + { row.notes .push("retire train row is missing company and territory scope".to_string()); } + if row.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + && (!company_target_present || !territory_target_present) + { + row.notes.push( + "territory access row is missing company or territory scope".to_string(), + ); + } } } @@ -2643,6 +2651,27 @@ fn decode_real_grouped_effect_action( }); } + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 3 + && row.row_shape == "bool_toggle" + && row.raw_scalar_value != 0 + { + let target = real_grouped_company_target(target_scope_ordinal)?; + let territory = compact_control + .grouped_territory_selectors_0x80f + .get(row.group_index) + .copied() + .filter(|selector| *selector >= 0) + .map(|selector| RuntimeTerritoryTarget::Ids { + ids: vec![selector as u32], + })?; + return Some(RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value: true, + }); + } + if descriptor_metadata.executable_in_runtime && descriptor_metadata.descriptor_id == 8 && row.row_shape == "scalar_assignment" @@ -2896,6 +2925,20 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimePlayerTarget::SelectedPlayer | RuntimePlayerTarget::ConditionTruePlayer ), + RuntimeEffect::SetCompanyTerritoryAccess { + target, territory, .. + } => matches!( + target, + RuntimeCompanyTarget::AllActive + | RuntimeCompanyTarget::Ids { .. } + | RuntimeCompanyTarget::HumanCompanies + | RuntimeCompanyTarget::AiCompanies + | RuntimeCompanyTarget::SelectedCompany + | RuntimeCompanyTarget::ConditionTrueCompany + ) && matches!( + territory, + RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. } + ), RuntimeEffect::SetCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 7c2b3aa..25b3a19 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -342,6 +342,21 @@ fn apply_runtime_effects( mutated_player_ids.insert(player_id); } } + RuntimeEffect::SetCompanyTerritoryAccess { + target, + territory, + value, + } => { + let company_ids = resolve_company_target_ids(state, target, condition_context)?; + let territory_ids = resolve_territory_target_ids(state, territory)?; + set_company_territory_access_pairs( + &mut state.company_territory_access, + &company_ids, + &territory_ids, + *value, + ); + mutated_company_ids.extend(company_ids); + } RuntimeEffect::ConfiscateCompanyAssets { target } => { let company_ids = resolve_company_target_ids(state, target, condition_context)?; for company_id in company_ids.iter().copied() { @@ -1003,6 +1018,32 @@ fn retire_matching_trains( } } +fn set_company_territory_access_pairs( + access_entries: &mut Vec, + company_ids: &[u32], + territory_ids: &[u32], + value: bool, +) { + if value { + for company_id in company_ids { + for territory_id in territory_ids { + if !access_entries.iter().any(|entry| { + entry.company_id == *company_id && entry.territory_id == *territory_id + }) { + access_entries.push(crate::RuntimeCompanyTerritoryAccess { + company_id: *company_id, + territory_id: *territory_id, + }); + } + } + } + } else { + access_entries.retain(|entry| { + !(company_ids.contains(&entry.company_id) && territory_ids.contains(&entry.territory_id)) + }); + } +} + #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -1044,6 +1085,7 @@ mod tests { trains: 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(), @@ -1603,6 +1645,117 @@ mod tests { assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); } + #[test] + fn sets_and_clears_company_territory_access_for_resolved_targets() { + let mut state = RuntimeState { + companies: vec![ + RuntimeCompany { + company_id: 1, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 10, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + }, + RuntimeCompany { + company_id: 2, + controller_kind: RuntimeCompanyControllerKind::Ai, + current_cash: 20, + debt: 0, + credit_rating_score: None, + prime_rate: None, + track_piece_counts: RuntimeTrackPieceCounts::default(), + active: true, + available_track_laying_capacity: None, + }, + ], + territories: vec![ + RuntimeTerritory { + territory_id: 7, + name: Some("Appalachia".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + RuntimeTerritory { + territory_id: 8, + name: Some("Great Plains".to_string()), + track_piece_counts: RuntimeTrackPieceCounts::default(), + }, + ], + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 21, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![7, 8] }, + value: true, + }], + }, + RuntimeEventRecord { + record_id: 22, + trigger_kind: 8, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: true, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetCompanyTerritoryAccess { + target: RuntimeCompanyTarget::SelectedCompany, + territory: RuntimeTerritoryTarget::Ids { ids: vec![8] }, + value: false, + }], + }, + ], + selected_company_id: Some(1), + ..state() + }; + + let first = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("territory access grant should succeed"); + + assert_eq!( + state.company_territory_access, + vec![ + crate::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }, + crate::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 8, + }, + ] + ); + assert_eq!(first.service_events[0].mutated_company_ids, vec![1]); + + execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 8 }, + ) + .expect("territory access clear should succeed"); + + assert_eq!( + state.company_territory_access, + vec![crate::RuntimeCompanyTerritoryAccess { + company_id: 1, + territory_id: 7, + }] + ); + } + #[test] fn rejects_condition_true_company_target_without_condition_context() { let mut state = RuntimeState { diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index f724804..a0a0a84 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -58,7 +58,8 @@ pub struct RuntimeSummary { pub packed_event_blocked_unmapped_ordinary_condition_count: usize, pub packed_event_blocked_missing_compact_control_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize, - pub packed_event_blocked_territory_policy_descriptor_count: usize, + pub packed_event_blocked_territory_access_variant_count: usize, + pub packed_event_blocked_territory_access_scope_count: usize, pub packed_event_blocked_missing_train_context_count: usize, pub packed_event_blocked_missing_train_territory_context_count: usize, pub packed_event_blocked_confiscation_variant_count: usize, @@ -420,7 +421,7 @@ impl RuntimeSummary { .count() }) .unwrap_or(0), - packed_event_blocked_territory_policy_descriptor_count: state + packed_event_blocked_territory_access_variant_count: state .packed_event_collection .as_ref() .map(|summary| { @@ -429,7 +430,21 @@ impl RuntimeSummary { .iter() .filter(|record| { record.import_outcome.as_deref() - == Some("blocked_territory_policy_descriptor") + == Some("blocked_territory_access_variant") + }) + .count() + }) + .unwrap_or(0), + packed_event_blocked_territory_access_scope_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_territory_access_scope") }) .count() }) @@ -587,6 +602,7 @@ mod tests { trains: Vec::new(), territories: Vec::new(), company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), packed_event_collection: Some(RuntimePackedEventCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), mechanism_family: "classic-save-rehydrate-v1".to_string(), @@ -815,6 +831,7 @@ mod tests { trains: 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(), diff --git a/docs/README.md b/docs/README.md index f22a3f7..2121126 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,8 +94,9 @@ The highest-value next passes are now: - real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` now join the executable batch through the same ordinary runtime path, backed by the opaque economic-status lane and the minimal event-owned train roster -- descriptor `3` `Territory - Allow All` remains the explicit parity-only descriptor frontier, and - mixed supported/unsupported real rows still stay parity-only +- descriptor `3` `Territory - Allow All` now executes as company-to-territory access rights through + the same ordinary runtime path; shell purchase-flow parity remains out of scope, and mixed + supported/unsupported real rows still stay 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 042239e..8d480c2 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -44,13 +44,14 @@ Implemented today: 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 supplies the required train ownership data -- descriptor `3` = `Territory - Allow All` remains the explicit parity-only descriptor frontier - instead of hiding behind the generic unmapped bucket +- descriptor `3` = `Territory - Allow All` now imports and executes too, reinterpreted as + company-to-territory access rights instead of a territory-owned policy bit; shell purchase-flow + and selected-profile parity still remain outside the runtime surface That means the next implementation work is breadth, not bootstrap. The recommended next slice is -broader real policy-descriptor coverage beyond `3/8/9/15`, wider ordinary condition-id coverage -beyond the current numeric-threshold batch, and richer train/runtime simulation only if later -descriptor families need more than the current event-owned roster. +broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, +world, train, player, and numeric-threshold batches, plus richer runtime ownership only where a +later descriptor family needs more than the current event-owned roster. ## Why This Boundary @@ -394,8 +395,9 @@ Checked-in fixture families already include: ## Next Slice -The recommended next implementation slice is broader ordinary-condition breadth on top of the -now-stable numeric-threshold, overlay-context, and current company-scoped real-descriptor batch. +The recommended next implementation slice is broader ordinary-condition and grouped-descriptor +breadth on top of the now-stable numeric-threshold, overlay-context, named-territory, player, +world/train, and company-territory-access batches. Target behavior: @@ -412,6 +414,8 @@ Target behavior: richer player metrics or profile/chairman ownership - continue widening real grouped-descriptor execution only when both descriptor identity and runtime effect semantics are grounded enough to map into the normalized runtime path honestly +- keep descriptor `3` on the now-executable company-territory-access interpretation; do not drift + back into territory-owned policy wording without new contrary evidence Public-model expectations for that slice: @@ -427,8 +431,8 @@ Fixture work for that slice: - preserve the new ordinary-condition tracked overlays for executable company finance, company track, aggregate territory track, and company-territory track thresholds - preserve the named-territory no-match tracked overlay as the explicit binding blocker frontier -- preserve the territory-policy tracked sample as the explicit descriptor frontier until mutation - semantics are grounded strongly enough to move beyond parity-only +- preserve the territory-access tracked overlays and parity samples so descriptor `3` access-rights + execution does not regress while other grouped families widen - keep the older negative-sentinel, mixed real-row, and company-scoped descriptor fixtures green so ordinary-condition breadth does not regress descriptor-side execution - keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens @@ -442,6 +446,6 @@ Current local constraint: Do not mix this slice with: - shell queue/modal behavior -- territory-access or selected-profile parity +- shell territory-access purchase or selected-profile parity - speculative condition execution without grounded runtime ownership - speculative executable import for real rows whose descriptor meaning is still weak diff --git a/fixtures/runtime/packed-event-territory-access-false-save-slice-fixture.json b/fixtures/runtime/packed-event-territory-access-false-save-slice-fixture.json new file mode 100644 index 0000000..3321cc3 --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-false-save-slice-fixture.json @@ -0,0 +1,32 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-territory-access-false-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture keeping the unsupported FALSE Territory - Allow All variant explicit." + }, + "state_save_slice_path": "packed-event-territory-access-false-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 6 + } + ], + "expected_summary": { + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 0, + "event_runtime_record_count": 0, + "packed_event_blocked_territory_access_variant_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_territory_access_variant" + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-territory-policy-save-slice.json b/fixtures/runtime/packed-event-territory-access-false-save-slice.json similarity index 80% rename from fixtures/runtime/packed-event-territory-policy-save-slice.json rename to fixtures/runtime/packed-event-territory-access-false-save-slice.json index 073b21d..e6b1335 100644 --- a/fixtures/runtime/packed-event-territory-policy-save-slice.json +++ b/fixtures/runtime/packed-event-territory-access-false-save-slice.json @@ -1,13 +1,13 @@ { "format_version": 1, - "save_slice_id": "packed-event-territory-policy-save-slice", + "save_slice_id": "packed-event-territory-access-false-save-slice", "source": { - "description": "Tracked save-slice document with a real Territory - Allow All row that stays parity-only.", - "original_save_filename": "captured-territory-policy.gms", - "original_save_sha256": "territory-policy-sample-sha256", + "description": "Tracked save-slice document with an unsupported FALSE Territory - Allow All row.", + "original_save_filename": "captured-territory-access-false.gms", + "original_save_sha256": "territory-access-false-sample-sha256", "notes": [ "tracked as JSON save-slice document rather than raw .smp", - "keeps descriptor 3 explicit without guessing territory policy mutation semantics" + "keeps the unsupported descriptor 3 FALSE variant explicit" ] }, "save_slice": { @@ -30,16 +30,16 @@ "close_tag_offset": 29696, "packed_state_version": 1001, "packed_state_version_hex": "0x000003e9", - "live_id_bound": 48, + "live_id_bound": 49, "live_record_count": 1, - "live_entry_ids": [48], + "live_entry_ids": [49], "decoded_record_count": 1, "imported_runtime_record_count": 0, "records": [ { "record_index": 0, - "live_entry_id": 48, - "payload_offset": 29320, + "live_entry_id": 49, + "payload_offset": 29360, "payload_len": 132, "decode_status": "parity_only", "payload_family": "real_packed_v1", @@ -71,7 +71,7 @@ "target_mask_bits": 5, "parameter_family": "territory_access_toggle", "opcode": 1, - "raw_scalar_value": 1, + "raw_scalar_value": 0, "value_byte_0x09": 0, "value_dword_0x0d": 0, "value_byte_0x11": 0, @@ -80,7 +80,7 @@ "value_word_0x16": 0, "row_shape": "bool_toggle", "semantic_family": "bool_toggle", - "semantic_preview": "Set Territory - Allow All to TRUE", + "semantic_preview": "Set Territory - Allow All to FALSE", "locomotive_name": null, "notes": [] } @@ -89,14 +89,13 @@ "decoded_actions": [], "executable_import_ready": false, "notes": [ - "decoded from grounded real 0x4e9a row framing", - "territory policy mutation remains parity-only in this slice" + "decoded from grounded real 0x4e9a row framing" ] } ] }, "notes": [ - "real territory policy descriptor sample" + "unsupported real territory-access FALSE variant sample" ] } } diff --git a/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice-fixture.json b/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice-fixture.json new file mode 100644 index 0000000..3e6cf23 --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice-fixture.json @@ -0,0 +1,32 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-territory-access-missing-scope-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture keeping the missing-scope Territory - Allow All variant explicit." + }, + "state_save_slice_path": "packed-event-territory-access-missing-scope-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 6 + } + ], + "expected_summary": { + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 0, + "event_runtime_record_count": 0, + "packed_event_blocked_territory_access_scope_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_territory_access_scope" + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice.json b/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice.json new file mode 100644 index 0000000..b3deab7 --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-missing-scope-save-slice.json @@ -0,0 +1,104 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-territory-access-missing-scope-save-slice", + "source": { + "description": "Tracked save-slice document with a TRUE Territory - Allow All row missing company or territory scope.", + "original_save_filename": "captured-territory-access-missing-scope.gms", + "original_save_sha256": "territory-access-missing-scope-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "keeps the descriptor 3 missing-scope boundary explicit" + ] + }, + "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": 50, + "live_record_count": 1, + "live_entry_ids": [50], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 50, + "payload_offset": 29400, + "payload_len": 132, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 6, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 12, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 0, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [9, 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": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 3, + "descriptor_label": "Territory - Allow All", + "target_mask_bits": 5, + "parameter_family": "territory_access_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 Territory - Allow All to TRUE", + "locomotive_name": null, + "notes": [ + "territory access row is missing company or territory scope" + ] + } + ], + "decoded_conditions": [], + "decoded_actions": [], + "executable_import_ready": false, + "notes": [ + "decoded from grounded real 0x4e9a row framing", + "descriptor 3 remains parity-only when company or territory scope is absent" + ] + } + ] + }, + "notes": [ + "unsupported real territory-access missing-scope sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-territory-access-overlay-fixture.json b/fixtures/runtime/packed-event-territory-access-overlay-fixture.json new file mode 100644 index 0000000..31310ee --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-overlay-fixture.json @@ -0,0 +1,64 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-territory-access-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving descriptor 3 Territory - Allow All imports as company territory-access rights and executes through the ordinary runtime path." + }, + "state_import_path": "packed-event-territory-access-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": 3, + "active_company_count": 3, + "player_count": 2, + "territory_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": { + "company_territory_access": [ + { + "company_id": 1, + "territory_id": 7 + } + ], + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported", + "decoded_actions": [ + { + "kind": "set_company_territory_access", + "target": { + "kind": "selected_company" + }, + "territory": { + "kind": "ids", + "ids": [7] + }, + "value": true + } + ] + } + ] + }, + "event_runtime_records": [ + { + "record_id": 48, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-territory-access-overlay.json b/fixtures/runtime/packed-event-territory-access-overlay.json new file mode 100644 index 0000000..27f69e3 --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-territory-access-overlay", + "source": { + "description": "Overlay import combining company and territory runtime context with the real Territory - Allow All descriptor sample." + }, + "base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json", + "save_slice_path": "packed-event-territory-access-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-territory-access-save-slice.json b/fixtures/runtime/packed-event-territory-access-save-slice.json new file mode 100644 index 0000000..f847606 --- /dev/null +++ b/fixtures/runtime/packed-event-territory-access-save-slice.json @@ -0,0 +1,114 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-territory-access-save-slice", + "source": { + "description": "Tracked save-slice document with a real Territory - Allow All row interpreted as company territory-access rights.", + "original_save_filename": "captured-territory-access.gms", + "original_save_sha256": "territory-access-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "locks descriptor 3 import as company-to-territory access rights instead of a territory-owned policy bit" + ] + }, + "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": 48, + "live_record_count": 1, + "live_entry_ids": [48], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 48, + "payload_offset": 29320, + "payload_len": 132, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 6, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 12, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 0, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [7, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 3, + "descriptor_label": "Territory - Allow All", + "target_mask_bits": 5, + "parameter_family": "territory_access_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 Territory - Allow All to TRUE", + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [], + "decoded_actions": [ + { + "kind": "set_company_territory_access", + "target": { + "kind": "selected_company" + }, + "territory": { + "kind": "ids", + "ids": [7] + }, + "value": true + } + ], + "executable_import_ready": true, + "notes": [ + "decoded from grounded real 0x4e9a row framing", + "descriptor 3 now lowers to company territory-access grants when company and territory scope are both explicit" + ] + } + ] + }, + "notes": [ + "real territory-access descriptor sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-territory-policy-save-slice-fixture.json b/fixtures/runtime/packed-event-territory-policy-save-slice-fixture.json deleted file mode 100644 index ccb26df..0000000 --- a/fixtures/runtime/packed-event-territory-policy-save-slice-fixture.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "format_version": 1, - "fixture_id": "packed-event-territory-policy-save-slice-fixture", - "source": { - "kind": "captured-runtime", - "description": "Fixture proving descriptor 3 Territory - Allow All stays parity-only with an explicit blocker." - }, - "state_save_slice_path": "packed-event-territory-policy-save-slice.json", - "commands": [ - { - "kind": "step_count", - "steps": 1 - } - ], - "expected_summary": { - "calendar_projection_is_placeholder": true, - "packed_event_collection_present": true, - "packed_event_record_count": 1, - "packed_event_decoded_record_count": 1, - "packed_event_imported_runtime_record_count": 0, - "packed_event_parity_only_record_count": 1, - "packed_event_blocked_territory_policy_descriptor_count": 1, - "event_runtime_record_count": 0 - }, - "expected_state_fragment": { - "packed_event_collection": { - "records": [ - { - "import_outcome": "blocked_territory_policy_descriptor", - "grouped_effect_rows": [ - { - "descriptor_label": "Territory - Allow All" - } - ] - } - ] - } - } -}