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`
`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.

View file

@ -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)

View file

@ -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 {

View file

@ -84,6 +84,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[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>,
#[serde(default)]
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 actual.packed_event_blocked_missing_compact_control_count != count {
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);
}
pub fn read_recent_metric(
&self,
metric: AnnualReportMetric,
years_ago: usize,
) -> Option<f64> {
pub fn read_recent_metric(&self, metric: AnnualReportMetric, years_ago: usize) -> Option<f64> {
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<f64> {
pub fn read_recent_metric_window(&self, metric: AnnualReportMetric, years: usize) -> Vec<f64> {
(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

View file

@ -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<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(
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<CompanyTargetImportBlocker> {
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<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
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::<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 {
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 {

View file

@ -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,

View file

@ -41,6 +41,28 @@ pub enum RuntimeCompanyTarget {
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)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEffect {
@ -167,6 +189,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)]
pub standalone_condition_rows: Vec<RuntimePackedEventConditionRowSummary>,
#[serde(default)]
pub negative_sentinel_scope: Option<RuntimePackedEventNegativeSentinelScopeSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
@ -182,6 +206,15 @@ pub struct RuntimePackedEventRecordSummary {
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)]
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(),

View file

@ -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<SmpLoadedPackedEventConditionRowSummary>,
#[serde(default)]
pub negative_sentinel_scope: Option<SmpLoadedPackedEventNegativeSentinelScopeSummary>,
#[serde(default)]
pub grouped_effect_row_counts: Vec<usize>,
#[serde(default)]
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
@ -1323,6 +1328,15 @@ pub struct SmpLoadedPackedEventRecordSummary {
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)]
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<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(
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![

View file

@ -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 => {

View file

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

View file

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

View file

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

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_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"

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_rows": [
{