Unlock negative-sentinel company condition scopes

This commit is contained in:
Jan Petykiewicz 2026-04-15 14:21:12 -07:00
commit 087ebf1097
18 changed files with 1315 additions and 79 deletions

View file

@ -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` 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 `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 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 engine without a parallel packed executor. The first grounded condition-side unlock now exists for
blocked until condition evaluation is grounded, and mixed supported/unsupported real rows stay 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 parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer
the main execution milestone. the main execution milestone.

View file

@ -4440,14 +4440,20 @@ mod tests {
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json"); .join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json"); .join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json");
let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
.join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json"); "../../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")) let deactivate_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json"); .join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json");
let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json"); .join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json");
let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
.join("../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json"); "../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture) run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize"); .expect("save-slice-backed parity fixture should summarize");
@ -4457,6 +4463,8 @@ mod tests {
.expect("overlay-backed selective-import fixture should summarize"); .expect("overlay-backed selective-import fixture should summarize");
run_runtime_summarize_fixture(&symbolic_overlay_fixture) run_runtime_summarize_fixture(&symbolic_overlay_fixture)
.expect("overlay-backed symbolic-target fixture should summarize"); .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) run_runtime_summarize_fixture(&deactivate_overlay_fixture)
.expect("overlay-backed deactivate-company fixture should summarize"); .expect("overlay-backed deactivate-company fixture should summarize");
run_runtime_summarize_fixture(&track_capacity_overlay_fixture) run_runtime_summarize_fixture(&track_capacity_overlay_fixture)

View file

@ -388,6 +388,7 @@ mod tests {
text_bands: vec![], text_bands: vec![],
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {

View file

@ -84,6 +84,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>, pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_player_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_compact_control_count: Option<usize>, pub packed_event_blocked_missing_compact_control_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>, pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
@ -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 let Some(count) = self.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count { if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -247,14 +247,12 @@ impl CompanyFinanceState {
self.current_dividend_per_share = new_rate.clamp(0.0, self.board_dividend_ceiling); self.current_dividend_per_share = new_rate.clamp(0.0, self.board_dividend_ceiling);
} }
pub fn read_recent_metric( pub fn read_recent_metric(&self, metric: AnnualReportMetric, years_ago: usize) -> Option<f64> {
&self,
metric: AnnualReportMetric,
years_ago: usize,
) -> Option<f64> {
match metric { match metric {
AnnualReportMetric::FuelCost if years_ago == 0 => Some(self.current_fuel_cost as f64), 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 AnnualReportMetric::NetProfits => self
.recent_net_profits .recent_net_profits
.get(years_ago) .get(years_ago)
@ -278,11 +276,7 @@ impl CompanyFinanceState {
} }
} }
pub fn read_recent_metric_window( pub fn read_recent_metric_window(&self, metric: AnnualReportMetric, years: usize) -> Vec<f64> {
&self,
metric: AnnualReportMetric,
years: usize,
) -> Vec<f64> {
(0..years) (0..years)
.filter_map(|years_ago| self.read_recent_metric(metric, years_ago)) .filter_map(|years_ago| self.read_recent_metric(metric, years_ago))
.collect() .collect()
@ -457,7 +451,10 @@ fn should_bankrupt_deep_distress(
&& company.current_cash < -300_000 && company.current_cash < -300_000
&& company.years_since_founding >= 3 && company.years_since_founding >= 3
&& company.years_since_last_bankruptcy >= 5 && 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( fn issue_bond_evaluation(
@ -552,9 +549,8 @@ fn issue_stock_evaluation(
return None; return None;
} }
let mut tranche = let mut tranche = ((company.outstanding_share_count / 10) / CompanyFinanceState::SHARE_LOT)
((company.outstanding_share_count / 10) / CompanyFinanceState::SHARE_LOT) * CompanyFinanceState::SHARE_LOT;
* CompanyFinanceState::SHARE_LOT;
tranche = tranche.max(2_000); tranche = tranche.max(2_000);
while tranche >= CompanyFinanceState::SHARE_LOT while tranche >= CompanyFinanceState::SHARE_LOT
&& company.support_adjusted_share_price * tranche as f64 > 55_000.0 && company.support_adjusted_share_price * tranche as f64 > 55_000.0

View file

@ -5,12 +5,14 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{ use crate::{
CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, CalendarPoint, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
}; };
@ -112,6 +114,9 @@ enum CompanyTargetImportBlocker {
MissingSelectionContext, MissingSelectionContext,
MissingCompanyRoleContext, MissingCompanyRoleContext,
MissingConditionContext, MissingConditionContext,
CompanyConditionScopeDisabled,
PlayerConditionScope,
TerritoryConditionScope,
} }
impl ImportCompanyContext { impl ImportCompanyContext {
@ -592,6 +597,8 @@ fn runtime_packed_event_record_summary_from_smp(
company_context: &ImportCompanyContext, company_context: &ImportCompanyContext,
imported: bool, imported: bool,
) -> RuntimePackedEventRecordSummary { ) -> RuntimePackedEventRecordSummary {
let lowered_decoded_actions =
lowered_record_decoded_actions(record).unwrap_or_else(|_| record.decoded_actions.clone());
RuntimePackedEventRecordSummary { RuntimePackedEventRecordSummary {
record_index: record.record_index, record_index: record.record_index,
live_entry_id: record.live_entry_id, live_entry_id: record.live_entry_id,
@ -618,6 +625,10 @@ fn runtime_packed_event_record_summary_from_smp(
.iter() .iter()
.map(runtime_packed_event_condition_row_summary_from_smp) .map(runtime_packed_event_condition_row_summary_from_smp)
.collect(), .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_row_counts: record.grouped_effect_row_counts.clone(),
grouped_effect_rows: record grouped_effect_rows: record
.grouped_effect_rows .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) .map(runtime_packed_event_grouped_effect_row_summary_from_smp)
.collect(), .collect(),
grouped_company_targets: classify_real_grouped_company_targets(record), 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, executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome( import_outcome: Some(determine_packed_event_import_outcome(
record, 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( fn runtime_packed_event_compact_control_summary_from_smp(
control: &crate::SmpLoadedPackedEventCompactControlSummary, control: &crate::SmpLoadedPackedEventCompactControlSummary,
) -> RuntimePackedEventCompactControlSummary { ) -> RuntimePackedEventCompactControlSummary {
@ -710,15 +732,20 @@ fn smp_packed_record_to_runtime_event_record(
if record.decode_status == "unsupported_framing" { if record.decode_status == "unsupported_framing" {
return None; return None;
} }
if record.payload_family == "real_packed_v1" && !record.executable_import_ready { if record.payload_family == "real_packed_v1" {
return None; if record.compact_control.is_none() || !record.executable_import_ready {
return None;
}
} }
let effects = let lowered_effects = match lowered_record_decoded_actions(record) {
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) { Ok(effects) => effects,
Ok(effects) => effects, Err(_) => return None,
Err(_) => return None, };
}; let effects = match smp_runtime_effects_to_runtime_effects(&lowered_effects, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
Some((|| { Some((|| {
let trigger_kind = record.trigger_kind.ok_or_else(|| { 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<Vec<RuntimeEffect>, 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<CompanyTargetImportBlocker> {
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<RuntimeCompanyTarget> {
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( fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect], effects: &[RuntimeEffect],
company_context: &ImportCompanyContext, company_context: &ImportCompanyContext,
@ -912,6 +1093,18 @@ fn company_target_import_error_message(
Some(CompanyTargetImportBlocker::MissingConditionContext) => { Some(CompanyTargetImportBlocker::MissingConditionContext) => {
"packed company effect requires condition-relative context".to_string() "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(), None => "packed company effect is importable".to_string(),
} }
} }
@ -934,6 +1127,9 @@ fn determine_packed_event_import_outcome(
if !record.executable_import_ready { if !record.executable_import_ready {
return "blocked_unmapped_real_descriptor".to_string(); 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) if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context)
{ {
return company_target_import_outcome(blocker).to_string(); return company_target_import_outcome(blocker).to_string();
@ -950,8 +1146,11 @@ fn packed_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext, company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> { ) -> Option<CompanyTargetImportBlocker> {
record let lowered_effects = match lowered_record_decoded_actions(record) {
.decoded_actions Ok(effects) => effects,
Err(blocker) => return Some(blocker),
};
lowered_effects
.iter() .iter()
.find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context)) .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" "blocked_missing_company_role_context"
} }
CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_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(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![effect], 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<crate::SmpLoadedPackedEventGroupedEffectRowSummary> { fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0, 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 { crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0, group_index: 0,
row_index: 0, row_index: 0,
@ -1470,9 +1707,7 @@ mod tests {
value_word_0x16: 0, value_word_0x16: 0,
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!( semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")),
"Set Company Track Pieces Buildable to {value}"
)),
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -1758,6 +1993,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
@ -1779,6 +2015,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
@ -1800,6 +2037,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
@ -2011,6 +2249,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 1, 0, 0], grouped_effect_row_counts: vec![0, 1, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![ decoded_actions: vec![
@ -2121,6 +2360,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
@ -2395,6 +2635,9 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(), 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_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(), grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash { decoded_actions: vec![RuntimeEffect::SetCompanyCash {
@ -2443,8 +2686,272 @@ mod tests {
} }
#[test] #[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::<Vec<_>>();
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::<Vec<_>>()
}),
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 { let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()), file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -2485,6 +2992,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(), 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_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(), grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![RuntimeEffect::SetCompanyCash { decoded_actions: vec![RuntimeEffect::SetCompanyCash {
@ -2500,7 +3008,7 @@ mod tests {
let import = project_save_slice_to_runtime_state_import( let import = project_save_slice_to_runtime_state_import(
&save_slice, &save_slice,
"packed-events-real-descriptor-frontier", "negative-sentinel-player-scope",
None, None,
) )
.expect("save slice should project"); .expect("save slice should project");
@ -2511,17 +3019,82 @@ mod tests {
.state .state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
.and_then(|summary| summary.records[0].compact_control.as_ref()) .and_then(|summary| summary.records[0].import_outcome.as_deref()),
.map(|control| control.mode_byte_0x7ef), Some("blocked_player_condition_scope")
Some(6)
); );
}
#[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!( assert_eq!(
import import
.state .state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()), .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(), text_bands: packed_text_bands(),
standalone_condition_row_count: 1, standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(), standalone_condition_rows: real_condition_rows(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(), grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![], decoded_actions: vec![],
@ -2674,6 +3248,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0, group_index: 0,
@ -2806,6 +3381,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(true)], grouped_effect_rows: vec![real_deactivate_company_row(true)],
decoded_actions: vec![RuntimeEffect::DeactivateCompany { decoded_actions: vec![RuntimeEffect::DeactivateCompany {
@ -2890,6 +3466,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(false)], grouped_effect_rows: vec![real_deactivate_company_row(false)],
decoded_actions: vec![], decoded_actions: vec![],
@ -2983,6 +3560,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_track_capacity_row(18)], grouped_effect_rows: vec![real_track_capacity_row(18)],
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity { decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
@ -3081,6 +3659,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 1, 0, 0], grouped_effect_row_counts: vec![1, 1, 0, 0],
grouped_effect_rows: vec![ grouped_effect_rows: vec![
real_track_capacity_row(18), real_track_capacity_row(18),
@ -3198,6 +3777,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
@ -3356,6 +3936,7 @@ mod tests {
text_bands: packed_text_bands(), text_bands: packed_text_bands(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: vec![], standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![], grouped_effect_rows: vec![],
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {

View file

@ -35,11 +35,12 @@ pub use pk4::{
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
}; };
pub use runtime::{ pub use runtime::{
RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCompactControlSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
}; };
pub use smp::{ pub use smp::{
@ -48,8 +49,8 @@ pub use smp::{
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit, SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary,
SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventConditionRowSummary,
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile,
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe, SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,

View file

@ -41,6 +41,28 @@ pub enum RuntimeCompanyTarget {
Ids { ids: Vec<u32> }, Ids { ids: Vec<u32> },
} }
#[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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEffect { pub enum RuntimeEffect {
@ -167,6 +189,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>, pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
#[serde(default)] #[serde(default)]
pub negative_sentinel_scope: Option<RuntimePackedEventNegativeSentinelScopeSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>, pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>, pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
@ -182,6 +206,15 @@ pub struct RuntimePackedEventRecordSummary {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
#[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<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePackedEventCompactControlSummary { pub struct RuntimePackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8, pub mode_byte_0x7ef: u8,
@ -994,6 +1027,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
@ -1017,6 +1051,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),

View file

@ -4,7 +4,10 @@ use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; 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; pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
const PREAMBLE_U32_WORD_COUNT: usize = 16; const PREAMBLE_U32_WORD_COUNT: usize = 16;
@ -1312,6 +1315,8 @@ pub struct SmpLoadedPackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>, pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
#[serde(default)] #[serde(default)]
pub negative_sentinel_scope: Option<SmpLoadedPackedEventNegativeSentinelScopeSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>, pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>, pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
@ -1323,6 +1328,15 @@ pub struct SmpLoadedPackedEventRecordSummary {
pub notes: Vec<String>, pub notes: Vec<String>,
} }
#[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<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedPackedEventCompactControlSummary { pub struct SmpLoadedPackedEventCompactControlSummary {
pub mode_byte_0x7ef: u8, pub mode_byte_0x7ef: u8,
@ -1836,6 +1850,7 @@ fn parse_synthetic_event_runtime_record_summary(
text_bands, text_bands,
standalone_condition_row_count, standalone_condition_row_count,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions, 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 let decoded_actions = compact_control
.as_ref() .as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
@ -1968,6 +1986,7 @@ fn parse_real_event_runtime_record_summary(
text_bands, text_bands,
standalone_condition_row_count, standalone_condition_row_count,
standalone_condition_rows, standalone_condition_rows,
negative_sentinel_scope,
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows, grouped_effect_rows,
decoded_actions, decoded_actions,
@ -2074,6 +2093,49 @@ fn parse_real_condition_row_summary(
}) })
} }
fn derive_negative_sentinel_scope_summary(
rows: &[SmpLoadedPackedEventConditionRowSummary],
control: &SmpLoadedPackedEventCompactControlSummary,
) -> Option<SmpLoadedPackedEventNegativeSentinelScopeSummary> {
let source_row_indexes = rows
.iter()
.filter(|row| row.raw_condition_id == -1)
.map(|row| row.row_index)
.collect::<Vec<_>>();
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<RuntimeCompanyConditionTestScope> {
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<RuntimePlayerConditionTestScope> {
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( fn parse_real_grouped_effect_row_summary(
row_bytes: &[u8], row_bytes: &[u8],
group_index: usize, group_index: usize,
@ -2484,6 +2546,7 @@ fn build_unsupported_event_runtime_record_summaries(
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
decoded_actions: 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].text_bands[0].preview, "Alpha");
assert_eq!(summary.records[0].standalone_condition_row_count, 0); assert_eq!(summary.records[0].standalone_condition_row_count, 0);
assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); assert_eq!(summary.records[0].standalone_condition_rows.len(), 0);
assert!(summary.records[0].negative_sentinel_scope.is_none());
assert_eq!( assert_eq!(
summary.records[0].grouped_effect_row_counts, summary.records[0].grouped_effect_row_counts,
vec![0, 0, 0, 0] vec![0, 0, 0, 0]
@ -7682,6 +7746,20 @@ mod tests {
.as_deref(), .as_deref(),
Some("AutoPlant") 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.len(), 1);
assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8); assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8);
assert_eq!( 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] #[test]
fn classifies_real_grouped_row_semantic_families() { fn classifies_real_grouped_row_semantic_families() {
let grouped_rows = vec![ let grouped_rows = vec![

View file

@ -495,8 +495,7 @@ fn resolve_company_target_ids(
.companies .companies
.iter() .iter()
.filter(|company| { .filter(|company| {
company.active company.active && company.controller_kind == RuntimeCompanyControllerKind::Human
&& company.controller_kind == RuntimeCompanyControllerKind::Human
}) })
.map(|company| company.company_id) .map(|company| company.company_id)
.collect()) .collect())
@ -532,8 +531,10 @@ fn resolve_company_target_ids(
{ {
Ok(vec![selected_company_id]) Ok(vec![selected_company_id])
} else { } else {
Err("target requires selected_company_id to reference an active company" Err(
.to_string()) "target requires selected_company_id to reference an active company"
.to_string(),
)
} }
} }
RuntimeCompanyTarget::ConditionTrueCompany => { RuntimeCompanyTarget::ConditionTrueCompany => {

View file

@ -39,6 +39,9 @@ pub struct RuntimeSummary {
pub packed_event_blocked_missing_selection_context_count: usize, pub packed_event_blocked_missing_selection_context_count: usize,
pub packed_event_blocked_missing_company_role_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_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_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_structural_only_count: usize, pub packed_event_blocked_structural_only_count: usize,
@ -123,7 +126,11 @@ impl RuntimeSummary {
.clone(), .clone(),
metadata_count: state.metadata.len(), metadata_count: state.metadata.len(),
company_count: state.companies.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_collection_present: state.packed_event_collection.is_some(),
packed_event_record_count: state packed_event_record_count: state
.packed_event_collection .packed_event_collection
@ -218,6 +225,48 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .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_blocked_missing_compact_control_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -333,10 +382,10 @@ mod tests {
container_profile_family: Some("rt3-classic-save-container-v1".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
packed_state_version: 0x3e9, packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(), packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7, live_id_bound: 11,
live_record_count: 2, live_record_count: 5,
live_entry_ids: vec![3, 7], live_entry_ids: vec![3, 7, 9, 10, 11],
decoded_record_count: 2, decoded_record_count: 5,
imported_runtime_record_count: 0, imported_runtime_record_count: 0,
records: vec![ records: vec![
RuntimePackedEventRecordSummary { RuntimePackedEventRecordSummary {
@ -354,6 +403,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
@ -377,6 +427,7 @@ mod tests {
text_bands: Vec::new(), text_bands: Vec::new(),
standalone_condition_row_count: 0, standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(), grouped_company_targets: Vec::new(),
@ -385,6 +436,80 @@ mod tests {
import_outcome: Some("blocked_missing_company_context".to_string()), import_outcome: Some("blocked_missing_company_context".to_string()),
notes: Vec::new(), 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(), event_runtime_records: Vec::new(),
@ -394,13 +519,40 @@ mod tests {
}; };
let summary = RuntimeSummary::from_state(&state); let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.packed_event_blocked_missing_compact_control_count, 1); assert_eq!(
assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0); 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_structural_only_count, 0);
assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1); assert_eq!(
assert_eq!(summary.packed_event_blocked_missing_selection_context_count, 0); summary.packed_event_blocked_missing_company_context_count,
assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0); 1
assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0); );
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] #[test]

View file

@ -83,8 +83,9 @@ The highest-value next passes are now:
descriptor `13` `Deactivate Company`, and descriptor `16` `Company Track Pieces Buildable` descriptor `13` `Deactivate Company`, and descriptor `16` `Company Track Pieces Buildable`
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask, - 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 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 - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`
grounded runtime semantics, and keep mixed supported/unsupported real rows parity-only 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, - 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 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 - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution

View file

@ -31,11 +31,15 @@ Implemented today:
- real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview - 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`, summaries, and three recovered executable company-scoped families: descriptor `2` = `Company Cash`,
descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable` 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 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 ordinary nonnegative condition-id semantics plus runtime ownership for the still-blocked player and
condition-relative execution for the still-blocked symbolic scopes, not another persistence territory scope families, alongside broader real grouped-descriptor coverage beyond the current
scaffold pass. company-scoped batch.
## Why This Boundary ## Why This Boundary
@ -232,8 +236,8 @@ Current status:
raw `.smp` binaries raw `.smp` binaries
- overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic - 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 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, - the remaining gap is wider real grouped-descriptor semantic coverage plus ordinary condition-id
not first-pass captured-runtime plumbing evaluation and player/territory runtime ownership, not first-pass captured-runtime plumbing
### Milestone 4: Domain Expansion ### Milestone 4: Domain Expansion

View file

@ -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
}
]
}
]
}
}

View file

@ -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"
}

View file

@ -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"
]
}
}

View file

@ -26,7 +26,8 @@
"packed_event_imported_runtime_record_count": 0, "packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1, "packed_event_parity_only_record_count": 1,
"packed_event_unsupported_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_missing_compact_control_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 0, "packed_event_blocked_unmapped_real_descriptor_count": 0,
"packed_event_blocked_structural_only_count": 0, "packed_event_blocked_structural_only_count": 0,
@ -52,11 +53,17 @@
"payload_family": "real_packed_v1", "payload_family": "real_packed_v1",
"trigger_kind": 6, "trigger_kind": 6,
"one_shot": true, "one_shot": true,
"import_outcome": "blocked_missing_condition_context", "import_outcome": "blocked_territory_condition_scope",
"compact_control": { "compact_control": {
"primary_selector_0x7f0": 99, "primary_selector_0x7f0": 99,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3] "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": [ "grouped_company_targets": [
{ {
"kind": "condition_true_company" "kind": "condition_true_company"

View file

@ -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_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [ "grouped_effect_rows": [
{ {