From 087ebf1097fa754d9ff725253d3d3b269175f175 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 Apr 2026 14:21:12 -0700 Subject: [PATCH] Unlock negative-sentinel company condition scopes --- README.md | 5 +- crates/rrt-cli/src/main.rs | 16 +- crates/rrt-fixtures/src/load.rs | 1 + crates/rrt-fixtures/src/schema.rs | 30 + crates/rrt-model/src/finance.rs | 26 +- crates/rrt-runtime/src/import.rs | 635 +++++++++++++++++- crates/rrt-runtime/src/lib.rs | 13 +- crates/rrt-runtime/src/runtime.rs | 35 + crates/rrt-runtime/src/smp.rs | 137 +++- crates/rrt-runtime/src/step.rs | 9 +- crates/rrt-runtime/src/summary.rs | 174 ++++- docs/README.md | 5 +- docs/runtime-rehost-plan.md | 14 +- ...egative-company-scope-overlay-fixture.json | 96 +++ ...-event-negative-company-scope-overlay.json | 12 + ...ent-negative-company-scope-save-slice.json | 169 +++++ ...acked-event-parity-save-slice-fixture.json | 11 +- .../packed-event-parity-save-slice.json | 6 + 18 files changed, 1315 insertions(+), 79 deletions(-) create mode 100644 fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json create mode 100644 fixtures/runtime/packed-event-negative-company-scope-overlay.json create mode 100644 fixtures/runtime/packed-event-negative-company-scope-save-slice.json diff --git a/README.md b/README.md index c36aadc..7791e53 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ overlay-import, compact-control, and symbolic company-target workflows. The runt 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 +engine without a parallel packed executor. The first grounded condition-side unlock now exists for +negative-sentinel `raw_condition_id = -1` company scopes, while ordinary condition-id semantics and +player/territory runtime ownership remain blocked. 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. diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 4f42109..dfdd47a 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4440,14 +4440,20 @@ mod tests { .join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json"); let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .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 symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json", + ); + let negative_company_scope_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join( + "../../fixtures/runtime/packed-event-negative-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"); + 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"); @@ -4457,6 +4463,8 @@ 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(&negative_company_scope_overlay_fixture) + .expect("overlay-backed negative-sentinel company-scope 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) diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 916a645..43e5f67 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -388,6 +388,7 @@ mod tests { text_bands: vec![], standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash { diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 4396095..9dc718e 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -84,6 +84,12 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub packed_event_blocked_missing_condition_context_count: Option, #[serde(default)] + pub packed_event_blocked_company_condition_scope_disabled_count: Option, + #[serde(default)] + pub packed_event_blocked_player_condition_scope_count: Option, + #[serde(default)] + pub packed_event_blocked_territory_condition_scope_count: Option, + #[serde(default)] pub packed_event_blocked_missing_compact_control_count: Option, #[serde(default)] pub packed_event_blocked_unmapped_real_descriptor_count: Option, @@ -417,6 +423,30 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.packed_event_blocked_company_condition_scope_disabled_count { + if actual.packed_event_blocked_company_condition_scope_disabled_count != count { + mismatches.push(format!( + "packed_event_blocked_company_condition_scope_disabled_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_company_condition_scope_disabled_count + )); + } + } + if let Some(count) = self.packed_event_blocked_player_condition_scope_count { + if actual.packed_event_blocked_player_condition_scope_count != count { + mismatches.push(format!( + "packed_event_blocked_player_condition_scope_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_player_condition_scope_count + )); + } + } + if let Some(count) = self.packed_event_blocked_territory_condition_scope_count { + if actual.packed_event_blocked_territory_condition_scope_count != count { + mismatches.push(format!( + "packed_event_blocked_territory_condition_scope_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_territory_condition_scope_count + )); + } + } if let Some(count) = self.packed_event_blocked_missing_compact_control_count { if actual.packed_event_blocked_missing_compact_control_count != count { mismatches.push(format!( diff --git a/crates/rrt-model/src/finance.rs b/crates/rrt-model/src/finance.rs index 31468d9..637fdb1 100644 --- a/crates/rrt-model/src/finance.rs +++ b/crates/rrt-model/src/finance.rs @@ -247,14 +247,12 @@ impl CompanyFinanceState { self.current_dividend_per_share = new_rate.clamp(0.0, self.board_dividend_ceiling); } - pub fn read_recent_metric( - &self, - metric: AnnualReportMetric, - years_ago: usize, - ) -> Option { + pub fn read_recent_metric(&self, metric: AnnualReportMetric, years_ago: usize) -> Option { match metric { AnnualReportMetric::FuelCost if years_ago == 0 => Some(self.current_fuel_cost as f64), - AnnualReportMetric::BookValuePerShare if years_ago == 0 => Some(self.book_value_per_share), + AnnualReportMetric::BookValuePerShare if years_ago == 0 => { + Some(self.book_value_per_share) + } AnnualReportMetric::NetProfits => self .recent_net_profits .get(years_ago) @@ -278,11 +276,7 @@ impl CompanyFinanceState { } } - pub fn read_recent_metric_window( - &self, - metric: AnnualReportMetric, - years: usize, - ) -> Vec { + pub fn read_recent_metric_window(&self, metric: AnnualReportMetric, years: usize) -> Vec { (0..years) .filter_map(|years_ago| self.read_recent_metric(metric, years_ago)) .collect() @@ -457,7 +451,10 @@ fn should_bankrupt_deep_distress( && company.current_cash < -300_000 && company.years_since_founding >= 3 && company.years_since_last_bankruptcy >= 5 - && company.recent_net_profits.iter().all(|profit| *profit <= -20_000) + && company + .recent_net_profits + .iter() + .all(|profit| *profit <= -20_000) } fn issue_bond_evaluation( @@ -552,9 +549,8 @@ fn issue_stock_evaluation( return None; } - let mut tranche = - ((company.outstanding_share_count / 10) / CompanyFinanceState::SHARE_LOT) - * CompanyFinanceState::SHARE_LOT; + let mut tranche = ((company.outstanding_share_count / 10) / CompanyFinanceState::SHARE_LOT) + * CompanyFinanceState::SHARE_LOT; tranche = tranche.max(2_000); while tranche >= CompanyFinanceState::SHARE_LOT && company.support_adjusted_share_price * tranche as f64 > 55_000.0 diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index fadc9b9..716cd3a 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -5,12 +5,14 @@ use serde::{Deserialize, Serialize}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::{ - CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, - RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, - RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, - RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary, - RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, - RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary, + CalendarPoint, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, + RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, + RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, + RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, + RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState, + RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, + SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, }; @@ -112,6 +114,9 @@ enum CompanyTargetImportBlocker { MissingSelectionContext, MissingCompanyRoleContext, MissingConditionContext, + CompanyConditionScopeDisabled, + PlayerConditionScope, + TerritoryConditionScope, } impl ImportCompanyContext { @@ -592,6 +597,8 @@ fn runtime_packed_event_record_summary_from_smp( company_context: &ImportCompanyContext, imported: bool, ) -> RuntimePackedEventRecordSummary { + let lowered_decoded_actions = + lowered_record_decoded_actions(record).unwrap_or_else(|_| record.decoded_actions.clone()); RuntimePackedEventRecordSummary { record_index: record.record_index, live_entry_id: record.live_entry_id, @@ -618,6 +625,10 @@ fn runtime_packed_event_record_summary_from_smp( .iter() .map(runtime_packed_event_condition_row_summary_from_smp) .collect(), + negative_sentinel_scope: record + .negative_sentinel_scope + .as_ref() + .map(runtime_packed_event_negative_sentinel_scope_summary_from_smp), grouped_effect_row_counts: record.grouped_effect_row_counts.clone(), grouped_effect_rows: record .grouped_effect_rows @@ -625,7 +636,7 @@ fn runtime_packed_event_record_summary_from_smp( .map(runtime_packed_event_grouped_effect_row_summary_from_smp) .collect(), grouped_company_targets: classify_real_grouped_company_targets(record), - decoded_actions: record.decoded_actions.clone(), + decoded_actions: lowered_decoded_actions, executable_import_ready: record.executable_import_ready, import_outcome: Some(determine_packed_event_import_outcome( record, @@ -636,6 +647,17 @@ fn runtime_packed_event_record_summary_from_smp( } } +fn runtime_packed_event_negative_sentinel_scope_summary_from_smp( + scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, +) -> RuntimePackedEventNegativeSentinelScopeSummary { + RuntimePackedEventNegativeSentinelScopeSummary { + company_test_scope: scope.company_test_scope, + player_test_scope: scope.player_test_scope, + territory_scope_selector_is_0x63: scope.territory_scope_selector_is_0x63, + source_row_indexes: scope.source_row_indexes.clone(), + } +} + fn runtime_packed_event_compact_control_summary_from_smp( control: &crate::SmpLoadedPackedEventCompactControlSummary, ) -> RuntimePackedEventCompactControlSummary { @@ -710,15 +732,20 @@ fn smp_packed_record_to_runtime_event_record( if record.decode_status == "unsupported_framing" { return None; } - if record.payload_family == "real_packed_v1" && !record.executable_import_ready { - return None; + if record.payload_family == "real_packed_v1" { + if record.compact_control.is_none() || !record.executable_import_ready { + return None; + } } - let effects = - match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) { - Ok(effects) => effects, - Err(_) => return None, - }; + let lowered_effects = match lowered_record_decoded_actions(record) { + Ok(effects) => effects, + Err(_) => return None, + }; + let effects = match smp_runtime_effects_to_runtime_effects(&lowered_effects, company_context) { + Ok(effects) => effects, + Err(_) => return None, + }; Some((|| { let trigger_kind = record.trigger_kind.ok_or_else(|| { @@ -742,6 +769,160 @@ fn smp_packed_record_to_runtime_event_record( })()) } +fn lowered_record_decoded_actions( + record: &SmpLoadedPackedEventRecordSummary, +) -> Result, CompanyTargetImportBlocker> { + if let Some(blocker) = packed_record_condition_scope_import_blocker(record) { + return Err(blocker); + } + + let Some(lowered_target) = lowered_condition_true_company_target(record) else { + return Ok(record.decoded_actions.clone()); + }; + Ok(record + .decoded_actions + .iter() + .map(|effect| lower_condition_true_company_target_in_effect(effect, &lowered_target)) + .collect()) +} + +fn packed_record_condition_scope_import_blocker( + record: &SmpLoadedPackedEventRecordSummary, +) -> Option { + if record.standalone_condition_rows.is_empty() { + return None; + } + + let negative_sentinel_row_count = record + .standalone_condition_rows + .iter() + .filter(|row| row.raw_condition_id == -1) + .count(); + if negative_sentinel_row_count == 0 { + return Some(CompanyTargetImportBlocker::MissingConditionContext); + } + if negative_sentinel_row_count != record.standalone_condition_rows.len() { + return Some(CompanyTargetImportBlocker::MissingConditionContext); + } + + let Some(scope) = record.negative_sentinel_scope.as_ref() else { + return Some(CompanyTargetImportBlocker::MissingConditionContext); + }; + if scope.player_test_scope != RuntimePlayerConditionTestScope::Disabled { + return Some(CompanyTargetImportBlocker::PlayerConditionScope); + } + if scope.territory_scope_selector_is_0x63 { + return Some(CompanyTargetImportBlocker::TerritoryConditionScope); + } + if scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled { + return Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled); + } + + None +} + +fn lowered_condition_true_company_target( + record: &SmpLoadedPackedEventRecordSummary, +) -> Option { + let scope = record.negative_sentinel_scope.as_ref()?; + match scope.company_test_scope { + RuntimeCompanyConditionTestScope::Disabled => None, + RuntimeCompanyConditionTestScope::AllCompanies => Some(RuntimeCompanyTarget::AllActive), + RuntimeCompanyConditionTestScope::SelectedCompanyOnly => { + Some(RuntimeCompanyTarget::SelectedCompany) + } + RuntimeCompanyConditionTestScope::AiCompaniesOnly => { + Some(RuntimeCompanyTarget::AiCompanies) + } + RuntimeCompanyConditionTestScope::HumanCompaniesOnly => { + Some(RuntimeCompanyTarget::HumanCompanies) + } + } +} + +fn lower_condition_true_company_target_in_effect( + effect: &RuntimeEffect, + lowered_target: &RuntimeCompanyTarget, +) -> RuntimeEffect { + match effect { + RuntimeEffect::SetWorldFlag { key, value } => RuntimeEffect::SetWorldFlag { + key: key.clone(), + value: *value, + }, + RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash { + target: lower_condition_true_company_target_in_company_target(target, lowered_target), + value: *value, + }, + RuntimeEffect::DeactivateCompany { target } => RuntimeEffect::DeactivateCompany { + target: lower_condition_true_company_target_in_company_target(target, lowered_target), + }, + RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { + RuntimeEffect::SetCompanyTrackLayingCapacity { + target: lower_condition_true_company_target_in_company_target( + target, + lowered_target, + ), + value: *value, + } + } + RuntimeEffect::AdjustCompanyCash { target, delta } => RuntimeEffect::AdjustCompanyCash { + target: lower_condition_true_company_target_in_company_target(target, lowered_target), + delta: *delta, + }, + RuntimeEffect::AdjustCompanyDebt { target, delta } => RuntimeEffect::AdjustCompanyDebt { + target: lower_condition_true_company_target_in_company_target(target, lowered_target), + delta: *delta, + }, + RuntimeEffect::SetCandidateAvailability { name, value } => { + RuntimeEffect::SetCandidateAvailability { + name: name.clone(), + value: *value, + } + } + RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { + label: label.clone(), + value: *value, + }, + RuntimeEffect::AppendEventRecord { record } => RuntimeEffect::AppendEventRecord { + record: Box::new(RuntimeEventRecordTemplate { + record_id: record.record_id, + trigger_kind: record.trigger_kind, + active: record.active, + marks_collection_dirty: record.marks_collection_dirty, + one_shot: record.one_shot, + effects: record + .effects + .iter() + .map(|nested| { + lower_condition_true_company_target_in_effect(nested, lowered_target) + }) + .collect(), + }), + }, + RuntimeEffect::ActivateEventRecord { record_id } => RuntimeEffect::ActivateEventRecord { + record_id: *record_id, + }, + RuntimeEffect::DeactivateEventRecord { record_id } => { + RuntimeEffect::DeactivateEventRecord { + record_id: *record_id, + } + } + RuntimeEffect::RemoveEventRecord { record_id } => RuntimeEffect::RemoveEventRecord { + record_id: *record_id, + }, + } +} + +fn lower_condition_true_company_target_in_company_target( + target: &RuntimeCompanyTarget, + lowered_target: &RuntimeCompanyTarget, +) -> RuntimeCompanyTarget { + match target { + RuntimeCompanyTarget::ConditionTrueCompany => lowered_target.clone(), + _ => target.clone(), + } +} + fn smp_runtime_effects_to_runtime_effects( effects: &[RuntimeEffect], company_context: &ImportCompanyContext, @@ -912,6 +1093,18 @@ fn company_target_import_error_message( Some(CompanyTargetImportBlocker::MissingConditionContext) => { "packed company effect requires condition-relative context".to_string() } + Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled) => { + "packed company effect disables company-side negative-sentinel condition scope" + .to_string() + } + Some(CompanyTargetImportBlocker::PlayerConditionScope) => { + "packed company effect requires player runtime ownership for negative-sentinel scope" + .to_string() + } + Some(CompanyTargetImportBlocker::TerritoryConditionScope) => { + "packed company effect requires territory runtime ownership for negative-sentinel scope" + .to_string() + } None => "packed company effect is importable".to_string(), } } @@ -934,6 +1127,9 @@ fn determine_packed_event_import_outcome( if !record.executable_import_ready { return "blocked_unmapped_real_descriptor".to_string(); } + if let Some(blocker) = packed_record_condition_scope_import_blocker(record) { + return company_target_import_outcome(blocker).to_string(); + } if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { return company_target_import_outcome(blocker).to_string(); @@ -950,8 +1146,11 @@ fn packed_record_company_target_import_blocker( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportCompanyContext, ) -> Option { - record - .decoded_actions + let lowered_effects = match lowered_record_decoded_actions(record) { + Ok(effects) => effects, + Err(blocker) => return Some(blocker), + }; + lowered_effects .iter() .find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context)) } @@ -1022,6 +1221,11 @@ fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'stati "blocked_missing_company_role_context" } CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context", + CompanyTargetImportBlocker::CompanyConditionScopeDisabled => { + "blocked_company_condition_scope_disabled" + } + CompanyTargetImportBlocker::PlayerConditionScope => "blocked_player_condition_scope", + CompanyTargetImportBlocker::TerritoryConditionScope => "blocked_territory_condition_scope", } } @@ -1393,6 +1597,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![effect], @@ -1401,6 +1606,36 @@ mod tests { } } + fn company_negative_sentinel_scope( + company_test_scope: RuntimeCompanyConditionTestScope, + ) -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } + } + + fn territory_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary + { + crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, + player_test_scope: RuntimePlayerConditionTestScope::Disabled, + territory_scope_selector_is_0x63: true, + source_row_indexes: vec![0], + } + } + + fn player_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, + player_test_scope: RuntimePlayerConditionTestScope::AllPlayers, + territory_scope_selector_is_0x63: false, + source_row_indexes: vec![0], + } + } + fn real_grouped_rows() -> Vec { vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { group_index: 0, @@ -1425,7 +1660,9 @@ mod tests { }] } - fn real_deactivate_company_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + fn real_deactivate_company_row( + enabled: bool, + ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { crate::SmpLoadedPackedEventGroupedEffectRowSummary { group_index: 0, row_index: 0, @@ -1470,9 +1707,7 @@ mod tests { 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}" - )), + semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")), locomotive_name: None, notes: vec![], } @@ -1758,6 +1993,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), @@ -1779,6 +2015,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), @@ -1800,6 +2037,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), @@ -2011,6 +2249,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 1, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![ @@ -2121,6 +2360,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { @@ -2395,6 +2635,9 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AllCompanies, + )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { @@ -2443,8 +2686,272 @@ mod tests { } #[test] - fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context() - { + fn lowers_negative_sentinel_company_scopes_into_runtime_company_targets() { + let base_state = RuntimeState { + companies: vec![ + crate::RuntimeCompany { + company_id: 1, + 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, + }, + crate::RuntimeCompany { + company_id: 3, + controller_kind: RuntimeCompanyControllerKind::Human, + current_cash: 70, + debt: 30, + active: true, + available_track_laying_capacity: None, + }, + ], + selected_company_id: Some(3), + ..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: 5, + live_entry_ids: vec![7, 8, 9, 10, 11], + decoded_record_count: 5, + imported_runtime_record_count: 0, + records: vec![ + crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AllCompanies, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + 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()], + }, + crate::SmpLoadedPackedEventRecordSummary { + record_index: 1, + live_entry_id: 8, + payload_offset: Some(0x7282), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::SelectedCompanyOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 8, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + crate::SmpLoadedPackedEventRecordSummary { + record_index: 2, + live_entry_id: 9, + payload_offset: Some(0x7302), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::AiCompaniesOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 9, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + crate::SmpLoadedPackedEventRecordSummary { + record_index: 3, + live_entry_id: 10, + payload_offset: Some(0x7382), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::HumanCompaniesOnly, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 10, + }], + executable_import_ready: true, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }, + crate::SmpLoadedPackedEventRecordSummary { + record_index: 4, + live_entry_id: 11, + payload_offset: Some(0x7402), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(company_negative_sentinel_scope( + RuntimeCompanyConditionTestScope::Disabled, + )), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_actions: vec![RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::ConditionTrueCompany, + value: 11, + }], + executable_import_ready: true, + 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, + "packed-events-real-descriptor-frontier", + None, + ) + .expect("save slice should project"); + + assert_eq!(import.state.event_runtime_records.len(), 4); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].compact_control.as_ref()) + .map(|control| control.mode_byte_0x7ef), + Some(6) + ); + let effects = import + .state + .event_runtime_records + .iter() + .map(|record| record.effects[0].clone()) + .collect::>(); + assert_eq!( + effects, + vec![ + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::AllActive, + value: 7, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::SelectedCompany, + value: 8, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::AiCompanies, + value: 9, + }, + RuntimeEffect::SetCompanyCash { + target: RuntimeCompanyTarget::HumanCompanies, + value: 10, + }, + ] + ); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .map(|record| record.import_outcome.clone()) + .collect::>() + }), + Some(vec![ + Some("imported".to_string()), + Some("imported".to_string()), + Some("imported".to_string()), + Some("imported".to_string()), + Some("blocked_company_condition_scope_disabled".to_string()), + ]) + ); + } + + #[test] + fn blocks_negative_sentinel_player_scope_until_player_runtime_exists() { let save_slice = SmpLoadedSaveSlice { file_extension_hint: Some("gms".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()), @@ -2485,6 +2992,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(player_negative_sentinel_scope()), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { @@ -2500,7 +3008,7 @@ mod tests { let import = project_save_slice_to_runtime_state_import( &save_slice, - "packed-events-real-descriptor-frontier", + "negative-sentinel-player-scope", None, ) .expect("save slice should project"); @@ -2511,17 +3019,82 @@ mod tests { .state .packed_event_collection .as_ref() - .and_then(|summary| summary.records[0].compact_control.as_ref()) - .map(|control| control.mode_byte_0x7ef), - Some(6) + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_player_condition_scope") ); + } + + #[test] + fn blocks_negative_sentinel_territory_scope_until_territory_runtime_exists() { + 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: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + 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(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: Some(territory_negative_sentinel_scope()), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + 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()], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "negative-sentinel-territory-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_missing_condition_context") + Some("blocked_territory_condition_scope") ); } @@ -2567,6 +3140,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![], @@ -2674,6 +3248,7 @@ mod tests { 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![crate::SmpLoadedPackedEventGroupedEffectRowSummary { group_index: 0, @@ -2806,6 +3381,7 @@ mod tests { 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_deactivate_company_row(true)], decoded_actions: vec![RuntimeEffect::DeactivateCompany { @@ -2890,6 +3466,7 @@ mod tests { 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_deactivate_company_row(false)], decoded_actions: vec![], @@ -2983,6 +3560,7 @@ mod tests { 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_track_capacity_row(18)], decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { @@ -3081,6 +3659,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![1, 1, 0, 0], grouped_effect_rows: vec![ real_track_capacity_row(18), @@ -3198,6 +3777,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { @@ -3356,6 +3936,7 @@ mod tests { text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 90d8299..ffbff9c 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -35,11 +35,12 @@ pub use pk4::{ extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, }; pub use runtime::{ - RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, - RuntimePackedEventCompactControlSummary, + RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, + RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, - RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, + RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, + RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, }; pub use smp::{ @@ -48,8 +49,8 @@ pub use smp::{ SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit, SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, - SmpLoadedPackedEventCompactControlSummary, - SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, + SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventConditionRowSummary, + SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation, SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index a0987b0..59946e7 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -41,6 +41,28 @@ pub enum RuntimeCompanyTarget { Ids { ids: Vec }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeCompanyConditionTestScope { + #[default] + Disabled, + AllCompanies, + SelectedCompanyOnly, + AiCompaniesOnly, + HumanCompaniesOnly, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum RuntimePlayerConditionTestScope { + #[default] + Disabled, + AllPlayers, + SelectedPlayerOnly, + AiPlayersOnly, + HumanPlayersOnly, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum RuntimeEffect { @@ -167,6 +189,8 @@ pub struct RuntimePackedEventRecordSummary { #[serde(default)] pub standalone_condition_rows: Vec, #[serde(default)] + pub negative_sentinel_scope: Option, + #[serde(default)] pub grouped_effect_row_counts: Vec, #[serde(default)] pub grouped_effect_rows: Vec, @@ -182,6 +206,15 @@ pub struct RuntimePackedEventRecordSummary { pub notes: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventNegativeSentinelScopeSummary { + pub company_test_scope: RuntimeCompanyConditionTestScope, + pub player_test_scope: RuntimePlayerConditionTestScope, + pub territory_scope_selector_is_0x63: bool, + #[serde(default)] + pub source_row_indexes: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimePackedEventCompactControlSummary { pub mode_byte_0x7ef: u8, @@ -994,6 +1027,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), @@ -1017,6 +1051,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index aa3a254..f762794 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -4,7 +4,10 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::{RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate}; +use crate::{ + RuntimeCompanyConditionTestScope, RuntimeCompanyTarget, RuntimeEffect, + RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, +}; pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; const PREAMBLE_U32_WORD_COUNT: usize = 16; @@ -1312,6 +1315,8 @@ pub struct SmpLoadedPackedEventRecordSummary { #[serde(default)] pub standalone_condition_rows: Vec, #[serde(default)] + pub negative_sentinel_scope: Option, + #[serde(default)] pub grouped_effect_row_counts: Vec, #[serde(default)] pub grouped_effect_rows: Vec, @@ -1323,6 +1328,15 @@ pub struct SmpLoadedPackedEventRecordSummary { pub notes: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventNegativeSentinelScopeSummary { + pub company_test_scope: RuntimeCompanyConditionTestScope, + pub player_test_scope: RuntimePlayerConditionTestScope, + pub territory_scope_selector_is_0x63: bool, + #[serde(default)] + pub source_row_indexes: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SmpLoadedPackedEventCompactControlSummary { pub mode_byte_0x7ef: u8, @@ -1836,6 +1850,7 @@ fn parse_synthetic_event_runtime_record_summary( text_bands, standalone_condition_row_count, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts, grouped_effect_rows: Vec::new(), decoded_actions, @@ -1940,6 +1955,9 @@ fn parse_real_event_runtime_record_summary( } } + let negative_sentinel_scope = compact_control.as_ref().and_then(|control| { + derive_negative_sentinel_scope_summary(&standalone_condition_rows, control) + }); let decoded_actions = compact_control .as_ref() .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) @@ -1968,6 +1986,7 @@ fn parse_real_event_runtime_record_summary( text_bands, standalone_condition_row_count, standalone_condition_rows, + negative_sentinel_scope, grouped_effect_row_counts, grouped_effect_rows, decoded_actions, @@ -2074,6 +2093,49 @@ fn parse_real_condition_row_summary( }) } +fn derive_negative_sentinel_scope_summary( + rows: &[SmpLoadedPackedEventConditionRowSummary], + control: &SmpLoadedPackedEventCompactControlSummary, +) -> Option { + let source_row_indexes = rows + .iter() + .filter(|row| row.raw_condition_id == -1) + .map(|row| row.row_index) + .collect::>(); + if source_row_indexes.is_empty() { + return None; + } + + Some(SmpLoadedPackedEventNegativeSentinelScopeSummary { + company_test_scope: decode_company_condition_test_scope(control.modifier_flag_0x7f9)?, + player_test_scope: decode_player_condition_test_scope(control.modifier_flag_0x7fa)?, + territory_scope_selector_is_0x63: control.primary_selector_0x7f0 == 0x63, + source_row_indexes, + }) +} + +fn decode_company_condition_test_scope(value: u8) -> Option { + match value { + 0 => Some(RuntimeCompanyConditionTestScope::Disabled), + 1 => Some(RuntimeCompanyConditionTestScope::AllCompanies), + 2 => Some(RuntimeCompanyConditionTestScope::SelectedCompanyOnly), + 3 => Some(RuntimeCompanyConditionTestScope::AiCompaniesOnly), + 4 => Some(RuntimeCompanyConditionTestScope::HumanCompaniesOnly), + _ => None, + } +} + +fn decode_player_condition_test_scope(value: u8) -> Option { + match value { + 0 => Some(RuntimePlayerConditionTestScope::Disabled), + 1 => Some(RuntimePlayerConditionTestScope::AllPlayers), + 2 => Some(RuntimePlayerConditionTestScope::SelectedPlayerOnly), + 3 => Some(RuntimePlayerConditionTestScope::AiPlayersOnly), + 4 => Some(RuntimePlayerConditionTestScope::HumanPlayersOnly), + _ => None, + } +} + fn parse_real_grouped_effect_row_summary( row_bytes: &[u8], group_index: usize, @@ -2484,6 +2546,7 @@ fn build_unsupported_event_runtime_record_summaries( text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), @@ -7603,6 +7666,7 @@ mod tests { assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); assert_eq!(summary.records[0].standalone_condition_row_count, 0); assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); + assert!(summary.records[0].negative_sentinel_scope.is_none()); assert_eq!( summary.records[0].grouped_effect_row_counts, vec![0, 0, 0, 0] @@ -7682,6 +7746,20 @@ mod tests { .as_deref(), Some("AutoPlant") ); + let negative_sentinel_scope = summary.records[0] + .negative_sentinel_scope + .as_ref() + .expect("negative-sentinel scope summary should decode"); + assert_eq!( + negative_sentinel_scope.company_test_scope, + RuntimeCompanyConditionTestScope::SelectedCompanyOnly + ); + assert_eq!( + negative_sentinel_scope.player_test_scope, + RuntimePlayerConditionTestScope::AiPlayersOnly + ); + assert!(!negative_sentinel_scope.territory_scope_selector_is_0x63); + assert_eq!(negative_sentinel_scope.source_row_indexes, vec![0]); assert_eq!(summary.records[0].grouped_effect_rows.len(), 1); assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8); assert_eq!( @@ -7725,6 +7803,63 @@ mod tests { ); } + #[test] + fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() { + for (value, expected) in [ + (0, RuntimeCompanyConditionTestScope::Disabled), + (1, RuntimeCompanyConditionTestScope::AllCompanies), + (2, RuntimeCompanyConditionTestScope::SelectedCompanyOnly), + (3, RuntimeCompanyConditionTestScope::AiCompaniesOnly), + (4, RuntimeCompanyConditionTestScope::HumanCompaniesOnly), + ] { + assert_eq!(decode_company_condition_test_scope(value), Some(expected)); + } + for (value, expected) in [ + (0, RuntimePlayerConditionTestScope::Disabled), + (1, RuntimePlayerConditionTestScope::AllPlayers), + (2, RuntimePlayerConditionTestScope::SelectedPlayerOnly), + (3, RuntimePlayerConditionTestScope::AiPlayersOnly), + (4, RuntimePlayerConditionTestScope::HumanPlayersOnly), + ] { + assert_eq!(decode_player_condition_test_scope(value), Some(expected)); + } + + let rows = vec![SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: -1, + subtype: 4, + flag_bytes: vec![0x30; 25], + candidate_name: Some("AutoPlant".to_string()), + notes: vec![], + }]; + let summary = derive_negative_sentinel_scope_summary( + &rows, + &SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 4, + modifier_flag_0x7fa: 2, + grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1], + }, + ) + .expect("negative sentinel summary should derive"); + assert_eq!( + summary.company_test_scope, + RuntimeCompanyConditionTestScope::HumanCompaniesOnly + ); + assert_eq!( + summary.player_test_scope, + RuntimePlayerConditionTestScope::SelectedPlayerOnly + ); + assert!(summary.territory_scope_selector_is_0x63); + assert_eq!(summary.source_row_indexes, vec![0]); + } + #[test] fn classifies_real_grouped_row_semantic_families() { let grouped_rows = vec![ diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index b03a55f..db2334a 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -495,8 +495,7 @@ fn resolve_company_target_ids( .companies .iter() .filter(|company| { - company.active - && company.controller_kind == RuntimeCompanyControllerKind::Human + company.active && company.controller_kind == RuntimeCompanyControllerKind::Human }) .map(|company| company.company_id) .collect()) @@ -532,8 +531,10 @@ fn resolve_company_target_ids( { Ok(vec![selected_company_id]) } else { - Err("target requires selected_company_id to reference an active company" - .to_string()) + Err( + "target requires selected_company_id to reference an active company" + .to_string(), + ) } } RuntimeCompanyTarget::ConditionTrueCompany => { diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 6bf4d7a..f638fa1 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -39,6 +39,9 @@ pub struct RuntimeSummary { pub packed_event_blocked_missing_selection_context_count: usize, pub packed_event_blocked_missing_company_role_context_count: usize, pub packed_event_blocked_missing_condition_context_count: usize, + pub packed_event_blocked_company_condition_scope_disabled_count: usize, + pub packed_event_blocked_player_condition_scope_count: usize, + pub packed_event_blocked_territory_condition_scope_count: usize, pub packed_event_blocked_missing_compact_control_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_structural_only_count: usize, @@ -123,7 +126,11 @@ impl RuntimeSummary { .clone(), metadata_count: state.metadata.len(), company_count: state.companies.len(), - active_company_count: state.companies.iter().filter(|company| company.active).count(), + 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 @@ -218,6 +225,48 @@ impl RuntimeSummary { .count() }) .unwrap_or(0), + packed_event_blocked_company_condition_scope_disabled_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_company_condition_scope_disabled") + }) + .count() + }) + .unwrap_or(0), + packed_event_blocked_player_condition_scope_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_player_condition_scope") + }) + .count() + }) + .unwrap_or(0), + packed_event_blocked_territory_condition_scope_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_territory_condition_scope") + }) + .count() + }) + .unwrap_or(0), packed_event_blocked_missing_compact_control_count: state .packed_event_collection .as_ref() @@ -333,10 +382,10 @@ mod tests { container_profile_family: Some("rt3-classic-save-container-v1".to_string()), packed_state_version: 0x3e9, packed_state_version_hex: "0x000003e9".to_string(), - live_id_bound: 7, - live_record_count: 2, - live_entry_ids: vec![3, 7], - decoded_record_count: 2, + live_id_bound: 11, + live_record_count: 5, + live_entry_ids: vec![3, 7, 9, 10, 11], + decoded_record_count: 5, imported_runtime_record_count: 0, records: vec![ RuntimePackedEventRecordSummary { @@ -354,6 +403,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), @@ -377,6 +427,7 @@ mod tests { text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), grouped_company_targets: Vec::new(), @@ -385,6 +436,80 @@ mod tests { import_outcome: Some("blocked_missing_company_context".to_string()), notes: Vec::new(), }, + RuntimePackedEventRecordSummary { + record_index: 2, + live_entry_id: 9, + payload_offset: Some(0x7292), + payload_len: Some(48), + 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: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some( + "blocked_company_condition_scope_disabled".to_string(), + ), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 3, + live_entry_id: 10, + payload_offset: Some(0x72c2), + payload_len: Some(48), + 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: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_player_condition_scope".to_string()), + notes: Vec::new(), + }, + RuntimePackedEventRecordSummary { + record_index: 4, + live_entry_id: 11, + payload_offset: Some(0x72f2), + payload_len: Some(48), + 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: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + grouped_company_targets: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + import_outcome: Some("blocked_territory_condition_scope".to_string()), + notes: Vec::new(), + }, ], }), event_runtime_records: Vec::new(), @@ -394,13 +519,40 @@ mod tests { }; let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.packed_event_blocked_missing_compact_control_count, 1); - assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0); + assert_eq!( + summary.packed_event_blocked_missing_compact_control_count, + 1 + ); + assert_eq!( + summary.packed_event_blocked_unmapped_real_descriptor_count, + 0 + ); assert_eq!(summary.packed_event_blocked_structural_only_count, 0); - assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1); - assert_eq!(summary.packed_event_blocked_missing_selection_context_count, 0); - assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0); - assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0); + assert_eq!( + summary.packed_event_blocked_missing_company_context_count, + 1 + ); + assert_eq!( + summary.packed_event_blocked_missing_selection_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_missing_company_role_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_missing_condition_context_count, + 0 + ); + assert_eq!( + summary.packed_event_blocked_company_condition_scope_disabled_count, + 1 + ); + assert_eq!(summary.packed_event_blocked_player_condition_scope_count, 1); + assert_eq!( + summary.packed_event_blocked_territory_condition_scope_count, + 1 + ); } #[test] diff --git a/docs/README.md b/docs/README.md index 9cb84b5..90eaac6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -83,8 +83,9 @@ The highest-value next passes are now: 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, and keep mixed supported/unsupported real rows parity-only +- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` + company scopes; broader ordinary condition-id evaluation and player/territory runtime ownership + are the remaining condition frontier, and mixed supported/unsupported real rows 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 21399d5..9639ab3 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -31,11 +31,15 @@ Implemented today: - real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview summaries, and three recovered executable company-scoped families: descriptor `2` = `Company Cash`, descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable` +- the first grounded condition-side unlock now exists for real packed rows: negative-sentinel + `raw_condition_id = -1` company scope lowers `condition_true_company` into normalized company + targets during import, while player and territory scope variants remain parity-visible and + explicitly blocked That means the next implementation work is breadth, not bootstrap. The recommended next slice is -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. +ordinary nonnegative condition-id semantics plus runtime ownership for the still-blocked player and +territory scope families, alongside broader real grouped-descriptor coverage beyond the current +company-scoped batch. ## Why This Boundary @@ -232,8 +236,8 @@ Current status: raw `.smp` binaries - overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic selected-company and controller-role scopes without inventing company state from save bytes alone -- the remaining gap is wider real grouped-descriptor semantic coverage plus condition evaluation, - not first-pass captured-runtime plumbing +- the remaining gap is wider real grouped-descriptor semantic coverage plus ordinary condition-id + evaluation and player/territory runtime ownership, not first-pass captured-runtime plumbing ### Milestone 4: Domain Expansion diff --git a/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json b/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json new file mode 100644 index 0000000..41f2f8c --- /dev/null +++ b/fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json @@ -0,0 +1,96 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-negative-company-scope-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture backed by an overlay import document so the first real negative-sentinel company-scope row executes against selected-company context." + }, + "state_import_path": "packed-event-negative-company-scope-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, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 1, + "packed_event_parity_only_record_count": 1, + "packed_event_unsupported_record_count": 0, + "packed_event_blocked_missing_condition_context_count": 0, + "packed_event_blocked_company_condition_scope_disabled_count": 0, + "packed_event_blocked_player_condition_scope_count": 0, + "packed_event_blocked_territory_condition_scope_count": 0, + "event_runtime_record_count": 1, + "total_event_record_service_count": 1, + "total_trigger_dispatch_count": 1, + "dirty_rerun_count": 0, + "total_company_cash": 400 + }, + "expected_state_fragment": { + "selected_company_id": 3, + "companies": [ + { + "company_id": 1, + "controller_kind": "human", + "current_cash": 100, + "debt": 10 + }, + { + "company_id": 2, + "controller_kind": "ai", + "current_cash": 50, + "debt": 20 + }, + { + "company_id": 3, + "controller_kind": "human", + "current_cash": 250, + "debt": 30 + } + ], + "packed_event_collection": { + "live_entry_ids": [9], + "records": [ + { + "import_outcome": "imported", + "negative_sentinel_scope": { + "company_test_scope": "selected_company_only", + "player_test_scope": "disabled", + "territory_scope_selector_is_0x63": false, + "source_row_indexes": [0] + }, + "decoded_actions": [ + { + "kind": "set_company_cash", + "target": { + "kind": "selected_company" + }, + "value": 250 + } + ] + } + ] + }, + "event_runtime_records": [ + { + "record_id": 9, + "service_count": 1, + "effects": [ + { + "kind": "set_company_cash", + "target": { + "kind": "selected_company" + }, + "value": 250 + } + ] + } + ] + } +} diff --git a/fixtures/runtime/packed-event-negative-company-scope-overlay.json b/fixtures/runtime/packed-event-negative-company-scope-overlay.json new file mode 100644 index 0000000..251b5f3 --- /dev/null +++ b/fixtures/runtime/packed-event-negative-company-scope-overlay.json @@ -0,0 +1,12 @@ +{ + "format_version": 1, + "import_id": "packed-event-negative-company-scope-overlay", + "source": { + "description": "Overlay import that combines selected-company snapshot context with a real negative-sentinel company-scope packed row.", + "notes": [ + "used to prove that the first real negative-sentinel company scope imports through the ordinary runtime path" + ] + }, + "base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json", + "save_slice_path": "packed-event-negative-company-scope-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-negative-company-scope-save-slice.json b/fixtures/runtime/packed-event-negative-company-scope-save-slice.json new file mode 100644 index 0000000..63fcc78 --- /dev/null +++ b/fixtures/runtime/packed-event-negative-company-scope-save-slice.json @@ -0,0 +1,169 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-negative-company-scope-save-slice", + "source": { + "description": "Tracked save-slice document with a real packed Company Cash row unlocked by negative-sentinel company scope.", + "original_save_filename": "captured-negative-company-scope.gms", + "original_save_sha256": "negative-company-scope-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "proves the first executable real negative-sentinel company-scope path" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "special_conditions_table": null, + "event_runtime_collection": { + "source_kind": "packed-event-runtime-collection", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "container_profile_family": "rt3-classic-save-container-v1", + "metadata_tag_offset": 28928, + "records_tag_offset": 29184, + "close_tag_offset": 29696, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 9, + "live_record_count": 1, + "live_entry_ids": [9], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 9, + "payload_offset": 29290, + "payload_len": 109, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 6, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 42, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 0, + "modifier_flag_0x7f9": 2, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3], + "grouped_scope_checkboxes_0x7ff": [1, 0, 1, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, 10, -1, 22] + }, + "text_bands": [ + { + "label": "primary_text_band", + "packed_len": 8, + "present": true, + "preview": "Resolve!" + }, + { + "label": "secondary_text_band_0", + "packed_len": 0, + "present": false, + "preview": "" + }, + { + "label": "secondary_text_band_1", + "packed_len": 0, + "present": false, + "preview": "" + }, + { + "label": "secondary_text_band_2", + "packed_len": 0, + "present": false, + "preview": "" + }, + { + "label": "secondary_text_band_3", + "packed_len": 0, + "present": false, + "preview": "" + }, + { + "label": "secondary_text_band_4", + "packed_len": 0, + "present": false, + "preview": "" + } + ], + "standalone_condition_row_count": 1, + "standalone_condition_rows": [ + { + "row_index": 0, + "raw_condition_id": -1, + "subtype": 4, + "flag_bytes": [ + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, + 68, 69, 70, 71, 72 + ], + "candidate_name": "AutoPlant", + "notes": [ + "negative sentinel-style condition row id", + "condition row carries candidate-name side string" + ] + } + ], + "negative_sentinel_scope": { + "company_test_scope": "selected_company_only", + "player_test_scope": "disabled", + "territory_scope_selector_is_0x63": false, + "source_row_indexes": [0] + }, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 2, + "descriptor_label": "Company Cash", + "target_mask_bits": 1, + "parameter_family": "company_finance_scalar", + "opcode": 8, + "raw_scalar_value": 250, + "value_byte_0x09": 1, + "value_dword_0x0d": 12, + "value_byte_0x11": 2, + "value_byte_0x12": 3, + "value_word_0x14": 24, + "value_word_0x16": 36, + "row_shape": "multivalue_scalar", + "semantic_family": "multivalue_scalar", + "semantic_preview": "Set Company Cash to 250 with aux [2, 3, 24, 36]", + "locomotive_name": "Mikado", + "notes": [ + "grouped effect row carries locomotive-name side string" + ] + } + ], + "decoded_actions": [ + { + "kind": "set_company_cash", + "target": { + "kind": "condition_true_company" + }, + "value": 250 + } + ], + "executable_import_ready": true, + "notes": [ + "decoded from grounded real 0x4e9a row framing", + "negative-sentinel company scope lowers the condition-relative target at import time" + ] + } + ] + }, + "notes": [ + "real negative-sentinel company-scope sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-parity-save-slice-fixture.json index be43779..9a707df 100644 --- a/fixtures/runtime/packed-event-parity-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-parity-save-slice-fixture.json @@ -26,7 +26,8 @@ "packed_event_imported_runtime_record_count": 0, "packed_event_parity_only_record_count": 1, "packed_event_unsupported_record_count": 1, - "packed_event_blocked_missing_condition_context_count": 1, + "packed_event_blocked_missing_condition_context_count": 0, + "packed_event_blocked_territory_condition_scope_count": 1, "packed_event_blocked_missing_compact_control_count": 0, "packed_event_blocked_unmapped_real_descriptor_count": 0, "packed_event_blocked_structural_only_count": 0, @@ -52,11 +53,17 @@ "payload_family": "real_packed_v1", "trigger_kind": 6, "one_shot": true, - "import_outcome": "blocked_missing_condition_context", + "import_outcome": "blocked_territory_condition_scope", "compact_control": { "primary_selector_0x7f0": 99, "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3] }, + "negative_sentinel_scope": { + "company_test_scope": "all_companies", + "player_test_scope": "disabled", + "territory_scope_selector_is_0x63": true, + "source_row_indexes": [0] + }, "grouped_company_targets": [ { "kind": "condition_true_company" diff --git a/fixtures/runtime/packed-event-parity-save-slice.json b/fixtures/runtime/packed-event-parity-save-slice.json index 9f144f4..0d71f32 100644 --- a/fixtures/runtime/packed-event-parity-save-slice.json +++ b/fixtures/runtime/packed-event-parity-save-slice.json @@ -127,6 +127,12 @@ ] } ], + "negative_sentinel_scope": { + "company_test_scope": "all_companies", + "player_test_scope": "disabled", + "territory_scope_selector_is_0x63": true, + "source_row_indexes": [0] + }, "grouped_effect_row_counts": [1, 0, 0, 0], "grouped_effect_rows": [ {