Add chairman packed event runtime support
This commit is contained in:
parent
377de45631
commit
86cf89b26c
23 changed files with 1431 additions and 41 deletions
|
|
@ -18,6 +18,9 @@ selected-company and controller-role context through overlay imports, and real d
|
|||
execute through the ordinary runtime path, and descriptors `1` `Player Cash` and `14`
|
||||
`Deactivate Player` now join that batch through the same service engine. Synthetic packed records
|
||||
still exercise the same runtime without a parallel packed executor. The first grounded
|
||||
chairman-profile runtime slice now exists too: overlay-backed selected-chairman context plus the
|
||||
hidden grouped target-subject lane let those same real descriptors `1` and `14` execute on
|
||||
selected-chairman scope, while wider chairman target scopes remain explicit parity. The first grounded
|
||||
condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and
|
||||
the first ordinary nonnegative condition batch now executes too: numeric-threshold company
|
||||
finance, company track, aggregate territory track, and company-territory track rows can import
|
||||
|
|
|
|||
|
|
@ -4485,6 +4485,16 @@ mod tests {
|
|||
);
|
||||
let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../fixtures/runtime/packed-event-cargo-catalog-save-slice-fixture.json");
|
||||
let chairman_cash_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json");
|
||||
let deactivate_chairman_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json");
|
||||
let missing_chairman_context_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"../../fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json",
|
||||
);
|
||||
let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json",
|
||||
);
|
||||
|
||||
run_runtime_summarize_fixture(&parity_fixture)
|
||||
.expect("save-slice-backed parity fixture should summarize");
|
||||
|
|
@ -4526,6 +4536,14 @@ mod tests {
|
|||
.expect("save-slice-backed parity world-scalar condition fixture should summarize");
|
||||
run_runtime_summarize_fixture(&cargo_catalog_fixture)
|
||||
.expect("save-slice-backed cargo catalog fixture should summarize");
|
||||
run_runtime_summarize_fixture(&chairman_cash_overlay_fixture)
|
||||
.expect("overlay-backed chairman-cash fixture should summarize");
|
||||
run_runtime_summarize_fixture(&deactivate_chairman_overlay_fixture)
|
||||
.expect("overlay-backed deactivate-chairman fixture should summarize");
|
||||
run_runtime_summarize_fixture(&missing_chairman_context_fixture)
|
||||
.expect("save-slice-backed chairman missing-context fixture should summarize");
|
||||
run_runtime_summarize_fixture(&chairman_scope_parity_fixture)
|
||||
.expect("save-slice-backed chairman scope parity fixture should summarize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -353,6 +355,8 @@ mod tests {
|
|||
selected_company_id: Some(42),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ pub struct ExpectedRuntimeSummary {
|
|||
#[serde(default)]
|
||||
pub player_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub chairman_profile_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub active_chairman_profile_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub selected_chairman_profile_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub train_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub active_train_count: Option<usize>,
|
||||
|
|
@ -110,6 +116,10 @@ pub struct ExpectedRuntimeSummary {
|
|||
#[serde(default)]
|
||||
pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub packed_event_blocked_missing_chairman_context_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub packed_event_blocked_chairman_target_scope_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub packed_event_blocked_missing_player_condition_context_count: Option<usize>,
|
||||
|
|
@ -439,6 +449,30 @@ impl ExpectedRuntimeSummary {
|
|||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.chairman_profile_count {
|
||||
if actual.chairman_profile_count != count {
|
||||
mismatches.push(format!(
|
||||
"chairman_profile_count mismatch: expected {count}, got {}",
|
||||
actual.chairman_profile_count
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.active_chairman_profile_count {
|
||||
if actual.active_chairman_profile_count != count {
|
||||
mismatches.push(format!(
|
||||
"active_chairman_profile_count mismatch: expected {count}, got {}",
|
||||
actual.active_chairman_profile_count
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(selected_id) = self.selected_chairman_profile_id {
|
||||
if actual.selected_chairman_profile_id != Some(selected_id) {
|
||||
mismatches.push(format!(
|
||||
"selected_chairman_profile_id mismatch: expected {selected_id:?}, got {:?}",
|
||||
actual.selected_chairman_profile_id
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.train_count {
|
||||
if actual.train_count != count {
|
||||
mismatches.push(format!(
|
||||
|
|
@ -591,6 +625,22 @@ impl ExpectedRuntimeSummary {
|
|||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.packed_event_blocked_missing_chairman_context_count {
|
||||
if actual.packed_event_blocked_missing_chairman_context_count != count {
|
||||
mismatches.push(format!(
|
||||
"packed_event_blocked_missing_chairman_context_count mismatch: expected {count}, got {}",
|
||||
actual.packed_event_blocked_missing_chairman_context_count
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.packed_event_blocked_chairman_target_scope_count {
|
||||
if actual.packed_event_blocked_chairman_target_scope_count != count {
|
||||
mismatches.push(format!(
|
||||
"packed_event_blocked_chairman_target_scope_count mismatch: expected {count}, got {}",
|
||||
actual.packed_event_blocked_chairman_target_scope_count
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(count) = self.packed_event_blocked_missing_condition_context_count {
|
||||
if actual.packed_event_blocked_missing_condition_context_count != count {
|
||||
mismatches.push(format!(
|
||||
|
|
|
|||
|
|
@ -5,17 +5,18 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCompanyConditionTestScope,
|
||||
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect,
|
||||
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry,
|
||||
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
|
||||
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
|
||||
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
|
||||
RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
|
||||
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget,
|
||||
RuntimeWorldRestoreState, SmpLoadedPackedEventConditionRowSummary,
|
||||
SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary,
|
||||
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
|
||||
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeChairmanTarget,
|
||||
RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
|
||||
RuntimeCondition, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
|
||||
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
|
||||
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
|
||||
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
|
||||
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
|
||||
RuntimeServiceState, RuntimeState, RuntimeTerritoryTarget, RuntimeWorldRestoreState,
|
||||
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
|
||||
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
|
||||
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
|
||||
};
|
||||
|
||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||
|
|
@ -116,6 +117,8 @@ struct ImportRuntimeContext {
|
|||
known_player_ids: BTreeSet<u32>,
|
||||
selected_player_id: Option<u32>,
|
||||
has_complete_player_controller_context: bool,
|
||||
known_chairman_profile_ids: BTreeSet<u32>,
|
||||
selected_chairman_profile_id: Option<u32>,
|
||||
known_territory_ids: BTreeSet<u32>,
|
||||
has_territory_context: bool,
|
||||
territory_name_to_id: BTreeMap<String, u32>,
|
||||
|
|
@ -132,6 +135,8 @@ enum ImportBlocker {
|
|||
MissingPlayerContext,
|
||||
MissingPlayerSelectionContext,
|
||||
MissingPlayerRoleContext,
|
||||
MissingChairmanContext,
|
||||
ChairmanTargetScope,
|
||||
MissingConditionContext,
|
||||
MissingPlayerConditionContext,
|
||||
CompanyConditionScopeDisabled,
|
||||
|
|
@ -153,6 +158,8 @@ impl ImportRuntimeContext {
|
|||
known_player_ids: BTreeSet::new(),
|
||||
selected_player_id: None,
|
||||
has_complete_player_controller_context: false,
|
||||
known_chairman_profile_ids: BTreeSet::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
known_territory_ids: BTreeSet::new(),
|
||||
has_territory_context: false,
|
||||
territory_name_to_id: BTreeMap::new(),
|
||||
|
|
@ -185,6 +192,12 @@ impl ImportRuntimeContext {
|
|||
.players
|
||||
.iter()
|
||||
.all(|player| player.controller_kind != RuntimeCompanyControllerKind::Unknown),
|
||||
known_chairman_profile_ids: state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.map(|profile| profile.profile_id)
|
||||
.collect(),
|
||||
selected_chairman_profile_id: state.selected_chairman_profile_id,
|
||||
known_territory_ids: state
|
||||
.territories
|
||||
.iter()
|
||||
|
|
@ -244,6 +257,8 @@ pub fn project_save_slice_to_runtime_state_import(
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(),
|
||||
cargo_catalog: projection.cargo_catalog.unwrap_or_default(),
|
||||
|
|
@ -307,6 +322,8 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
|
|||
selected_company_id: base_state.selected_company_id,
|
||||
players: base_state.players.clone(),
|
||||
selected_player_id: base_state.selected_player_id,
|
||||
chairman_profiles: base_state.chairman_profiles.clone(),
|
||||
selected_chairman_profile_id: base_state.selected_chairman_profile_id,
|
||||
trains: base_state.trains.clone(),
|
||||
locomotive_catalog: projection
|
||||
.locomotive_catalog
|
||||
|
|
@ -1007,6 +1024,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
|
|||
descriptor_label: row.descriptor_label.clone(),
|
||||
target_mask_bits: row.target_mask_bits,
|
||||
parameter_family: row.parameter_family.clone(),
|
||||
grouped_target_subject: row.grouped_target_subject.clone(),
|
||||
opcode: row.opcode,
|
||||
raw_scalar_value: row.raw_scalar_value,
|
||||
value_byte_0x09: row.value_byte_0x09,
|
||||
|
|
@ -1157,6 +1175,9 @@ fn lower_contextual_real_grouped_effects(
|
|||
|
||||
let mut effects = Vec::with_capacity(record.grouped_effect_rows.len());
|
||||
for row in &record.grouped_effect_rows {
|
||||
if real_grouped_row_is_unsupported_chairman_target_scope(row) {
|
||||
return Err(ImportBlocker::ChairmanTargetScope);
|
||||
}
|
||||
if let Some(effect) = lower_contextual_cargo_production_effect(row)? {
|
||||
effects.push(effect);
|
||||
continue;
|
||||
|
|
@ -1425,12 +1446,19 @@ fn lower_condition_targets_in_effect(
|
|||
)?,
|
||||
value: *value,
|
||||
},
|
||||
RuntimeEffect::SetChairmanCash { target, value } => RuntimeEffect::SetChairmanCash {
|
||||
target: target.clone(),
|
||||
value: *value,
|
||||
},
|
||||
RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer {
|
||||
target: lower_condition_true_player_target_in_player_target(
|
||||
target,
|
||||
lowered_player_target,
|
||||
)?,
|
||||
},
|
||||
RuntimeEffect::DeactivateChairman { target } => RuntimeEffect::DeactivateChairman {
|
||||
target: target.clone(),
|
||||
},
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -1590,6 +1618,17 @@ fn lower_condition_targets_in_condition(
|
|||
comparator: *comparator,
|
||||
value: *value,
|
||||
},
|
||||
RuntimeCondition::ChairmanNumericThreshold {
|
||||
target,
|
||||
metric,
|
||||
comparator,
|
||||
value,
|
||||
} => RuntimeCondition::ChairmanNumericThreshold {
|
||||
target: target.clone(),
|
||||
metric: *metric,
|
||||
comparator: *comparator,
|
||||
value: *value,
|
||||
},
|
||||
RuntimeCondition::TerritoryNumericThreshold {
|
||||
target,
|
||||
metric,
|
||||
|
|
@ -1786,6 +1825,7 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
|
|||
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
|
||||
matches!(target, RuntimeCompanyTarget::ConditionTrueCompany)
|
||||
}
|
||||
RuntimeCondition::ChairmanNumericThreshold { .. } => false,
|
||||
RuntimeCondition::TerritoryNumericThreshold { .. }
|
||||
| RuntimeCondition::SpecialConditionThreshold { .. }
|
||||
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
|
||||
|
|
@ -1803,6 +1843,40 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
fn chairman_target_import_blocker(
|
||||
target: &RuntimeChairmanTarget,
|
||||
company_context: &ImportRuntimeContext,
|
||||
) -> Option<ImportBlocker> {
|
||||
match target {
|
||||
RuntimeChairmanTarget::AllActive => {
|
||||
if company_context.known_chairman_profile_ids.is_empty() {
|
||||
Some(ImportBlocker::MissingChairmanContext)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
RuntimeChairmanTarget::SelectedChairman => {
|
||||
if company_context.selected_chairman_profile_id.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(ImportBlocker::MissingChairmanContext)
|
||||
}
|
||||
}
|
||||
RuntimeChairmanTarget::Ids { ids } => {
|
||||
if company_context.known_chairman_profile_ids.is_empty() {
|
||||
Some(ImportBlocker::MissingChairmanContext)
|
||||
} else if ids
|
||||
.iter()
|
||||
.all(|id| company_context.known_chairman_profile_ids.contains(id))
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(ImportBlocker::MissingChairmanContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn smp_runtime_effects_to_runtime_effects(
|
||||
effects: &[RuntimeEffect],
|
||||
company_context: &ImportRuntimeContext,
|
||||
|
|
@ -1867,6 +1941,16 @@ fn smp_runtime_effect_to_runtime_effect(
|
|||
Err(player_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetChairmanCash { target, value } => {
|
||||
if chairman_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::SetChairmanCash {
|
||||
target: target.clone(),
|
||||
value: *value,
|
||||
})
|
||||
} else {
|
||||
Err("packed effect requires chairman runtime context".to_string())
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
if player_target_allowed_for_import(
|
||||
target,
|
||||
|
|
@ -1880,6 +1964,15 @@ fn smp_runtime_effect_to_runtime_effect(
|
|||
Err(player_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivateChairman { target } => {
|
||||
if chairman_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::DeactivateChairman {
|
||||
target: target.clone(),
|
||||
})
|
||||
} else {
|
||||
Err("packed effect requires chairman runtime context".to_string())
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -2236,6 +2329,8 @@ fn company_target_import_error_message(
|
|||
Some(ImportBlocker::MissingPlayerContext)
|
||||
| Some(ImportBlocker::MissingPlayerSelectionContext)
|
||||
| Some(ImportBlocker::MissingPlayerRoleContext)
|
||||
| Some(ImportBlocker::MissingChairmanContext)
|
||||
| Some(ImportBlocker::ChairmanTargetScope)
|
||||
| Some(ImportBlocker::MissingPlayerConditionContext) => {
|
||||
"packed company effect is blocked by non-company import context".to_string()
|
||||
}
|
||||
|
|
@ -2324,10 +2419,22 @@ fn determine_packed_event_import_outcome(
|
|||
}
|
||||
if !record.executable_import_ready {
|
||||
if let Err(blocker) = lowered_record_decoded_actions(record, company_context) {
|
||||
if blocker == ImportBlocker::MissingLocomotiveCatalogContext {
|
||||
if matches!(
|
||||
blocker,
|
||||
ImportBlocker::MissingLocomotiveCatalogContext
|
||||
| ImportBlocker::MissingChairmanContext
|
||||
| ImportBlocker::ChairmanTargetScope
|
||||
) {
|
||||
return company_target_import_outcome(blocker).to_string();
|
||||
}
|
||||
}
|
||||
if record
|
||||
.grouped_effect_rows
|
||||
.iter()
|
||||
.any(real_grouped_row_is_unsupported_chairman_target_scope)
|
||||
{
|
||||
return "blocked_chairman_target_scope".to_string();
|
||||
}
|
||||
if record
|
||||
.grouped_effect_rows
|
||||
.iter()
|
||||
|
|
@ -2507,6 +2614,9 @@ fn runtime_condition_company_target_import_blocker(
|
|||
RuntimeCondition::CompanyNumericThreshold { target, .. } => {
|
||||
company_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
|
||||
chairman_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
|
||||
territory_target_import_blocker(target, company_context)
|
||||
}
|
||||
|
|
@ -2559,6 +2669,8 @@ fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str {
|
|||
ImportBlocker::MissingPlayerContext => "blocked_missing_player_context",
|
||||
ImportBlocker::MissingPlayerSelectionContext => "blocked_missing_player_selection_context",
|
||||
ImportBlocker::MissingPlayerRoleContext => "blocked_missing_player_role_context",
|
||||
ImportBlocker::MissingChairmanContext => "blocked_missing_chairman_context",
|
||||
ImportBlocker::ChairmanTargetScope => "blocked_chairman_target_scope",
|
||||
ImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
|
||||
ImportBlocker::MissingPlayerConditionContext => "blocked_missing_player_condition_context",
|
||||
ImportBlocker::CompanyConditionScopeDisabled => "blocked_company_condition_scope_disabled",
|
||||
|
|
@ -2585,6 +2697,17 @@ fn real_grouped_row_is_unsupported_territory_access_variant(
|
|||
row.descriptor_id == 3 && !(row.row_shape == "bool_toggle" && row.raw_scalar_value != 0)
|
||||
}
|
||||
|
||||
fn real_grouped_row_is_unsupported_chairman_target_scope(
|
||||
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
||||
) -> bool {
|
||||
matches!(row.grouped_target_subject.as_deref(), Some("chairman"))
|
||||
&& matches!(row.descriptor_id, 1 | 14)
|
||||
&& row
|
||||
.notes
|
||||
.iter()
|
||||
.any(|note| note == "chairman row requires selected-chairman scope")
|
||||
}
|
||||
|
||||
fn real_grouped_row_is_unsupported_territory_access_scope(
|
||||
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
||||
) -> bool {
|
||||
|
|
@ -2644,7 +2767,9 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
|
|||
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
|
||||
| RuntimeEffect::SetEconomicStatusCode { .. }
|
||||
| RuntimeEffect::SetPlayerCash { .. }
|
||||
| RuntimeEffect::SetChairmanCash { .. }
|
||||
| RuntimeEffect::DeactivatePlayer { .. }
|
||||
| RuntimeEffect::DeactivateChairman { .. }
|
||||
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
|
||||
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
|
||||
|
|
@ -2666,6 +2791,7 @@ fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool {
|
|||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
|
||||
}
|
||||
RuntimeEffect::SetChairmanCash { .. } | RuntimeEffect::DeactivateChairman { .. } => false,
|
||||
RuntimeEffect::AppendEventRecord { record } => record
|
||||
.effects
|
||||
.iter()
|
||||
|
|
@ -2701,6 +2827,10 @@ fn runtime_effect_company_target_import_blocker(
|
|||
| RuntimeEffect::DeactivatePlayer { target } => {
|
||||
player_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeEffect::SetChairmanCash { target, .. }
|
||||
| RuntimeEffect::DeactivateChairman { target } => {
|
||||
chairman_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeEffect::RetireTrains {
|
||||
company_target,
|
||||
territory_target,
|
||||
|
|
@ -3067,6 +3197,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -3211,6 +3343,7 @@ mod tests {
|
|||
descriptor_label: Some("Company Cash".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_finance_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 8,
|
||||
raw_scalar_value: 7,
|
||||
value_byte_0x09: 1,
|
||||
|
|
@ -3240,6 +3373,7 @@ mod tests {
|
|||
descriptor_label: Some("Deactivate Company".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_lifecycle_toggle".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3270,6 +3404,7 @@ mod tests {
|
|||
descriptor_label: Some("Company Track Pieces Buildable".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_build_limit_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3299,6 +3434,7 @@ mod tests {
|
|||
descriptor_label: Some("Deactivate Player".to_string()),
|
||||
target_mask_bits: Some(0x02),
|
||||
parameter_family: Some("player_lifecycle_toggle".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3332,6 +3468,7 @@ mod tests {
|
|||
descriptor_label: Some("Territory - Allow All".to_string()),
|
||||
target_mask_bits: Some(0x05),
|
||||
parameter_family: Some("territory_access_toggle".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3362,6 +3499,7 @@ mod tests {
|
|||
descriptor_label: Some("Economic Status".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("whole_game_state_enum".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3391,6 +3529,7 @@ mod tests {
|
|||
descriptor_label: Some("Limited Track Building Amount".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("world_track_build_limit_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3420,6 +3559,7 @@ mod tests {
|
|||
descriptor_label: Some("Use Wartime Cargos".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("special_condition_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3449,6 +3589,7 @@ mod tests {
|
|||
descriptor_label: Some("Turbo Diesel Availability".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("candidate_availability_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3479,6 +3620,7 @@ mod tests {
|
|||
descriptor_label: Some("Unknown Loco Available".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("locomotive_availability_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3521,6 +3663,7 @@ mod tests {
|
|||
descriptor_label: Some(descriptor_label.clone()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("locomotive_cost_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3616,6 +3759,7 @@ mod tests {
|
|||
descriptor_label: Some(descriptor_label.clone()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("cargo_production_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3645,6 +3789,7 @@ mod tests {
|
|||
descriptor_label: Some("Territory Access Cost".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("territory_access_cost_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3676,6 +3821,7 @@ mod tests {
|
|||
descriptor_label: Some(label.to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("world_flag_toggle".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 0,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3708,6 +3854,7 @@ mod tests {
|
|||
descriptor_label: Some("Confiscate All".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_confiscation_variant".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3742,6 +3889,7 @@ mod tests {
|
|||
descriptor_label: Some("Retire Train".to_string()),
|
||||
target_mask_bits: Some(0x0d),
|
||||
parameter_family: Some("company_or_territory_asset_toggle".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -3772,6 +3920,7 @@ mod tests {
|
|||
descriptor_label: Some("Confiscate All".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_confiscation_variant".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 1,
|
||||
raw_scalar_value: 0,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -5451,6 +5600,7 @@ mod tests {
|
|||
descriptor_label: Some("Unknown Loco Available".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("locomotive_availability_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: 42,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -5697,6 +5847,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: vec![
|
||||
crate::RuntimeLocomotiveCatalogEntry {
|
||||
|
|
@ -6092,6 +6244,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: vec![
|
||||
crate::RuntimeLocomotiveCatalogEntry {
|
||||
|
|
@ -6488,6 +6642,8 @@ mod tests {
|
|||
selected_company_id: Some(42),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -6566,6 +6722,7 @@ mod tests {
|
|||
descriptor_label: Some("Company Cash".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_finance_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 8,
|
||||
raw_scalar_value: 250,
|
||||
value_byte_0x09: 1,
|
||||
|
|
@ -8145,6 +8302,7 @@ mod tests {
|
|||
descriptor_label: Some("Turbo Diesel Availability".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("candidate_availability_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode: 3,
|
||||
raw_scalar_value: 1,
|
||||
value_byte_0x09: 0,
|
||||
|
|
@ -9850,6 +10008,8 @@ mod tests {
|
|||
selected_company_id: Some(42),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -10034,6 +10194,8 @@ mod tests {
|
|||
selected_company_id: Some(42),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ pub use pk4::{
|
|||
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
|
||||
};
|
||||
pub use runtime::{
|
||||
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCompany, RuntimeCompanyConditionTestScope,
|
||||
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanProfile,
|
||||
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope,
|
||||
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
|
||||
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
|
||||
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -89,6 +89,30 @@ pub struct RuntimePlayer {
|
|||
pub controller_kind: RuntimeCompanyControllerKind,
|
||||
}
|
||||
|
||||
fn runtime_chairman_profile_default_active() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeChairmanProfile {
|
||||
pub profile_id: u32,
|
||||
pub name: String,
|
||||
#[serde(default = "runtime_chairman_profile_default_active")]
|
||||
pub active: bool,
|
||||
#[serde(default)]
|
||||
pub current_cash: i64,
|
||||
#[serde(default)]
|
||||
pub linked_company_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub company_holdings: BTreeMap<u32, u32>,
|
||||
#[serde(default)]
|
||||
pub holdings_value_total: i64,
|
||||
#[serde(default)]
|
||||
pub net_worth_total: i64,
|
||||
#[serde(default)]
|
||||
pub purchasing_power_total: i64,
|
||||
}
|
||||
|
||||
fn runtime_train_default_active() -> bool {
|
||||
true
|
||||
}
|
||||
|
|
@ -156,6 +180,14 @@ pub enum RuntimePlayerTarget {
|
|||
Ids { ids: Vec<u32> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum RuntimeChairmanTarget {
|
||||
AllActive,
|
||||
SelectedChairman,
|
||||
Ids { ids: Vec<u32> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum RuntimeTerritoryTarget {
|
||||
|
|
@ -211,6 +243,15 @@ pub enum RuntimeCompanyMetric {
|
|||
TrackPiecesNonElectric,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuntimeChairmanMetric {
|
||||
CurrentCash,
|
||||
HoldingsValueTotal,
|
||||
NetWorthTotal,
|
||||
PurchasingPowerTotal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuntimeTerritoryMetric {
|
||||
|
|
@ -242,6 +283,12 @@ pub enum RuntimeCondition {
|
|||
comparator: RuntimeConditionComparator,
|
||||
value: i64,
|
||||
},
|
||||
ChairmanNumericThreshold {
|
||||
target: RuntimeChairmanTarget,
|
||||
metric: RuntimeChairmanMetric,
|
||||
comparator: RuntimeConditionComparator,
|
||||
value: i64,
|
||||
},
|
||||
TerritoryNumericThreshold {
|
||||
target: RuntimeTerritoryTarget,
|
||||
metric: RuntimeTerritoryMetric,
|
||||
|
|
@ -336,9 +383,16 @@ pub enum RuntimeEffect {
|
|||
target: RuntimePlayerTarget,
|
||||
value: i64,
|
||||
},
|
||||
SetChairmanCash {
|
||||
target: RuntimeChairmanTarget,
|
||||
value: i64,
|
||||
},
|
||||
DeactivatePlayer {
|
||||
target: RuntimePlayerTarget,
|
||||
},
|
||||
DeactivateChairman {
|
||||
target: RuntimeChairmanTarget,
|
||||
},
|
||||
SetCompanyTerritoryAccess {
|
||||
target: RuntimeCompanyTarget,
|
||||
territory: RuntimeTerritoryTarget,
|
||||
|
|
@ -590,6 +644,8 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
|
|||
pub target_mask_bits: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub parameter_family: Option<String>,
|
||||
#[serde(default)]
|
||||
pub grouped_target_subject: Option<String>,
|
||||
pub opcode: u8,
|
||||
pub raw_scalar_value: i32,
|
||||
pub value_byte_0x09: u8,
|
||||
|
|
@ -733,6 +789,10 @@ pub struct RuntimeState {
|
|||
#[serde(default)]
|
||||
pub selected_player_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub chairman_profiles: Vec<RuntimeChairmanProfile>,
|
||||
#[serde(default)]
|
||||
pub selected_chairman_profile_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub trains: Vec<RuntimeTrain>,
|
||||
#[serde(default)]
|
||||
pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>,
|
||||
|
|
@ -801,6 +861,63 @@ impl RuntimeState {
|
|||
active_player_ids.insert(player.player_id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_chairman_profile_ids = BTreeSet::new();
|
||||
let mut seen_chairman_names = BTreeSet::new();
|
||||
let mut active_chairman_profile_ids = BTreeSet::new();
|
||||
for chairman in &self.chairman_profiles {
|
||||
if !seen_chairman_profile_ids.insert(chairman.profile_id) {
|
||||
return Err(format!(
|
||||
"duplicate chairman_profile.profile_id {}",
|
||||
chairman.profile_id
|
||||
));
|
||||
}
|
||||
if chairman.name.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"chairman_profile {} has an empty name",
|
||||
chairman.profile_id
|
||||
));
|
||||
}
|
||||
if !seen_chairman_names.insert(chairman.name.clone()) {
|
||||
return Err(format!(
|
||||
"duplicate chairman_profile.name {:?}",
|
||||
chairman.name
|
||||
));
|
||||
}
|
||||
if chairman.active {
|
||||
active_chairman_profile_ids.insert(chairman.profile_id);
|
||||
}
|
||||
if let Some(linked_company_id) = chairman.linked_company_id {
|
||||
if !seen_company_ids.contains(&linked_company_id) {
|
||||
return Err(format!(
|
||||
"chairman_profile {} references unknown linked_company_id {}",
|
||||
chairman.profile_id, linked_company_id
|
||||
));
|
||||
}
|
||||
}
|
||||
for company_id in chairman.company_holdings.keys() {
|
||||
if !seen_company_ids.contains(company_id) {
|
||||
return Err(format!(
|
||||
"chairman_profile {} references unknown holdings company_id {}",
|
||||
chairman.profile_id, company_id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(selected_chairman_profile_id) = self.selected_chairman_profile_id {
|
||||
if !seen_chairman_profile_ids.contains(&selected_chairman_profile_id) {
|
||||
return Err(format!(
|
||||
"selected_chairman_profile_id {} does not reference a live chairman profile",
|
||||
selected_chairman_profile_id
|
||||
));
|
||||
}
|
||||
if !active_chairman_profile_ids.contains(&selected_chairman_profile_id) {
|
||||
return Err(format!(
|
||||
"selected_chairman_profile_id {} must reference an active chairman profile",
|
||||
selected_chairman_profile_id
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(selected_player_id) = self.selected_player_id {
|
||||
if !seen_player_ids.contains(&selected_player_id) {
|
||||
return Err(format!(
|
||||
|
|
@ -956,19 +1073,25 @@ impl RuntimeState {
|
|||
return Err(format!("duplicate record_id {}", record.record_id));
|
||||
}
|
||||
for (condition_index, condition) in record.conditions.iter().enumerate() {
|
||||
validate_runtime_condition(condition, &seen_company_ids, &seen_territory_ids)
|
||||
.map_err(|err| {
|
||||
validate_runtime_condition(
|
||||
condition,
|
||||
&seen_company_ids,
|
||||
&seen_chairman_profile_ids,
|
||||
&seen_territory_ids,
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
|
||||
record.record_id
|
||||
)
|
||||
})?;
|
||||
})?;
|
||||
}
|
||||
for (effect_index, effect) in record.effects.iter().enumerate() {
|
||||
validate_runtime_effect(
|
||||
effect,
|
||||
&seen_company_ids,
|
||||
&seen_player_ids,
|
||||
&seen_chairman_profile_ids,
|
||||
&seen_territory_ids,
|
||||
)
|
||||
.map_err(|err| {
|
||||
|
|
@ -1315,6 +1438,7 @@ fn validate_runtime_effect(
|
|||
effect: &RuntimeEffect,
|
||||
valid_company_ids: &BTreeSet<u32>,
|
||||
valid_player_ids: &BTreeSet<u32>,
|
||||
valid_chairman_profile_ids: &BTreeSet<u32>,
|
||||
valid_territory_ids: &BTreeSet<u32>,
|
||||
) -> Result<(), String> {
|
||||
match effect {
|
||||
|
|
@ -1343,6 +1467,10 @@ fn validate_runtime_effect(
|
|||
| RuntimeEffect::DeactivatePlayer { target } => {
|
||||
validate_player_target(target, valid_player_ids)?;
|
||||
}
|
||||
RuntimeEffect::SetChairmanCash { target, .. }
|
||||
| RuntimeEffect::DeactivateChairman { target } => {
|
||||
validate_chairman_target(target, valid_chairman_profile_ids)?;
|
||||
}
|
||||
RuntimeEffect::RetireTrains {
|
||||
company_target,
|
||||
territory_target,
|
||||
|
|
@ -1403,6 +1531,7 @@ fn validate_runtime_effect(
|
|||
record,
|
||||
valid_company_ids,
|
||||
valid_player_ids,
|
||||
valid_chairman_profile_ids,
|
||||
valid_territory_ids,
|
||||
)?;
|
||||
}
|
||||
|
|
@ -1418,23 +1547,29 @@ fn validate_event_record_template(
|
|||
record: &RuntimeEventRecordTemplate,
|
||||
valid_company_ids: &BTreeSet<u32>,
|
||||
valid_player_ids: &BTreeSet<u32>,
|
||||
valid_chairman_profile_ids: &BTreeSet<u32>,
|
||||
valid_territory_ids: &BTreeSet<u32>,
|
||||
) -> Result<(), String> {
|
||||
for (condition_index, condition) in record.conditions.iter().enumerate() {
|
||||
validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err(
|
||||
|err| {
|
||||
format!(
|
||||
"template record_id={}.conditions[{condition_index}] {err}",
|
||||
record.record_id
|
||||
)
|
||||
},
|
||||
)?;
|
||||
validate_runtime_condition(
|
||||
condition,
|
||||
valid_company_ids,
|
||||
valid_chairman_profile_ids,
|
||||
valid_territory_ids,
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"template record_id={}.conditions[{condition_index}] {err}",
|
||||
record.record_id
|
||||
)
|
||||
})?;
|
||||
}
|
||||
for (effect_index, effect) in record.effects.iter().enumerate() {
|
||||
validate_runtime_effect(
|
||||
effect,
|
||||
valid_company_ids,
|
||||
valid_player_ids,
|
||||
valid_chairman_profile_ids,
|
||||
valid_territory_ids,
|
||||
)
|
||||
.map_err(|err| {
|
||||
|
|
@ -1451,12 +1586,16 @@ fn validate_event_record_template(
|
|||
fn validate_runtime_condition(
|
||||
condition: &RuntimeCondition,
|
||||
valid_company_ids: &BTreeSet<u32>,
|
||||
valid_chairman_profile_ids: &BTreeSet<u32>,
|
||||
valid_territory_ids: &BTreeSet<u32>,
|
||||
) -> Result<(), String> {
|
||||
match condition {
|
||||
RuntimeCondition::CompanyNumericThreshold { target, .. } => {
|
||||
validate_company_target(target, valid_company_ids)
|
||||
}
|
||||
RuntimeCondition::ChairmanNumericThreshold { target, .. } => {
|
||||
validate_chairman_target(target, valid_chairman_profile_ids)
|
||||
}
|
||||
RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
|
||||
validate_territory_target(target, valid_territory_ids)
|
||||
}
|
||||
|
|
@ -1562,6 +1701,28 @@ fn validate_player_target(
|
|||
}
|
||||
}
|
||||
|
||||
fn validate_chairman_target(
|
||||
target: &RuntimeChairmanTarget,
|
||||
valid_chairman_profile_ids: &BTreeSet<u32>,
|
||||
) -> Result<(), String> {
|
||||
match target {
|
||||
RuntimeChairmanTarget::AllActive | RuntimeChairmanTarget::SelectedChairman => Ok(()),
|
||||
RuntimeChairmanTarget::Ids { ids } => {
|
||||
if ids.is_empty() {
|
||||
return Err("target ids must not be empty".to_string());
|
||||
}
|
||||
for profile_id in ids {
|
||||
if !valid_chairman_profile_ids.contains(profile_id) {
|
||||
return Err(format!(
|
||||
"target references unknown chairman profile_id {profile_id}"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_territory_target(
|
||||
target: &RuntimeTerritoryTarget,
|
||||
valid_territory_ids: &BTreeSet<u32>,
|
||||
|
|
@ -1628,6 +1789,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1689,6 +1852,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1735,6 +1900,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1794,6 +1961,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1853,6 +2022,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1963,6 +2134,8 @@ mod tests {
|
|||
selected_company_id: Some(2),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -2009,6 +2182,8 @@ mod tests {
|
|||
selected_company_id: Some(1),
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -2055,6 +2230,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: vec![
|
||||
RuntimeTrain {
|
||||
train_id: 7,
|
||||
|
|
@ -2118,6 +2295,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: vec![RuntimeTrain {
|
||||
train_id: 7,
|
||||
owner_company_id: 2,
|
||||
|
|
@ -2171,6 +2350,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: vec![RuntimeTrain {
|
||||
train_id: 7,
|
||||
owner_company_id: 1,
|
||||
|
|
@ -2228,6 +2409,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: vec![RuntimeTrain {
|
||||
train_id: 7,
|
||||
owner_company_id: 1,
|
||||
|
|
@ -2281,6 +2464,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -2340,6 +2525,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -2393,6 +2580,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{
|
||||
RuntimeCargoClass, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric,
|
||||
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect,
|
||||
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
|
||||
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
|
||||
RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
|
||||
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
|
||||
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
|
||||
RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
|
||||
};
|
||||
|
||||
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
||||
|
|
@ -1876,6 +1876,8 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
|
|||
pub target_mask_bits: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub parameter_family: Option<String>,
|
||||
#[serde(default)]
|
||||
pub grouped_target_subject: Option<String>,
|
||||
pub opcode: u8,
|
||||
pub raw_scalar_value: i32,
|
||||
pub value_byte_0x09: u8,
|
||||
|
|
@ -1901,6 +1903,15 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
|
|||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RealGroupedTargetSubject {
|
||||
Company,
|
||||
Player,
|
||||
Chairman,
|
||||
Territory,
|
||||
WholeGame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpLoadedSaveSlice {
|
||||
pub file_extension_hint: Option<String>,
|
||||
|
|
@ -2590,12 +2601,20 @@ fn parse_real_event_runtime_record_summary(
|
|||
}
|
||||
if let Some(control) = compact_control.as_ref() {
|
||||
for row in &mut grouped_effect_rows {
|
||||
row.grouped_target_subject = derive_real_grouped_target_subject(row, control)
|
||||
.map(real_grouped_target_subject_name)
|
||||
.map(str::to_string);
|
||||
let company_target_present = control
|
||||
.grouped_target_scope_ordinals_0x7fb
|
||||
.get(row.group_index)
|
||||
.copied()
|
||||
.and_then(real_grouped_company_target)
|
||||
.is_some();
|
||||
let chairman_target_present = control
|
||||
.grouped_target_scope_ordinals_0x7fb
|
||||
.get(row.group_index)
|
||||
.copied()
|
||||
.is_some_and(real_grouped_chairman_target_supported_in_runtime);
|
||||
let territory_target_present = control
|
||||
.grouped_territory_selectors_0x80f
|
||||
.get(row.group_index)
|
||||
|
|
@ -2617,6 +2636,14 @@ fn parse_real_event_runtime_record_summary(
|
|||
row.notes
|
||||
.push("territory access row is missing company or territory scope".to_string());
|
||||
}
|
||||
if matches!(
|
||||
derive_real_grouped_target_subject(row, control),
|
||||
Some(RealGroupedTargetSubject::Chairman)
|
||||
) && !chairman_target_present
|
||||
{
|
||||
row.notes
|
||||
.push("chairman row requires selected-chairman scope".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3174,6 +3201,7 @@ fn parse_real_grouped_effect_row_summary(
|
|||
descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()),
|
||||
target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits),
|
||||
parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()),
|
||||
grouped_target_subject: None,
|
||||
opcode,
|
||||
raw_scalar_value,
|
||||
value_byte_0x09,
|
||||
|
|
@ -3665,6 +3693,44 @@ fn runtime_world_flag_key_from_label(label: &str) -> String {
|
|||
key
|
||||
}
|
||||
|
||||
fn derive_real_grouped_target_subject(
|
||||
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
||||
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
||||
) -> Option<RealGroupedTargetSubject> {
|
||||
match row.target_mask_bits {
|
||||
Some(0x08) => Some(RealGroupedTargetSubject::WholeGame),
|
||||
Some(0x01) => Some(RealGroupedTargetSubject::Company),
|
||||
Some(0x02) => match compact_control
|
||||
.grouped_scope_checkboxes_0x7ff
|
||||
.get(row.group_index)
|
||||
.copied()
|
||||
{
|
||||
Some(2) => Some(RealGroupedTargetSubject::Chairman),
|
||||
_ => Some(RealGroupedTargetSubject::Player),
|
||||
},
|
||||
_ if row.descriptor_id == 3 => Some(RealGroupedTargetSubject::Territory),
|
||||
_ if row.descriptor_id == 15
|
||||
&& compact_control
|
||||
.grouped_territory_selectors_0x80f
|
||||
.get(row.group_index)
|
||||
.is_some_and(|selector| *selector >= 0) =>
|
||||
{
|
||||
Some(RealGroupedTargetSubject::Territory)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn real_grouped_target_subject_name(subject: RealGroupedTargetSubject) -> &'static str {
|
||||
match subject {
|
||||
RealGroupedTargetSubject::Company => "company",
|
||||
RealGroupedTargetSubject::Player => "player",
|
||||
RealGroupedTargetSubject::Chairman => "chairman",
|
||||
RealGroupedTargetSubject::Territory => "territory",
|
||||
RealGroupedTargetSubject::WholeGame => "whole_game",
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_real_grouped_effect_actions(
|
||||
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
|
||||
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
||||
|
|
@ -3684,17 +3750,23 @@ fn decode_real_grouped_effect_action(
|
|||
.grouped_target_scope_ordinals_0x7fb
|
||||
.get(row.group_index)
|
||||
.copied()?;
|
||||
let target_subject = derive_real_grouped_target_subject(row, compact_control);
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
&& descriptor_metadata.descriptor_id == 1
|
||||
&& row.opcode == 8
|
||||
&& row.row_shape == "multivalue_scalar"
|
||||
{
|
||||
let target = real_grouped_player_target(target_scope_ordinal)?;
|
||||
return Some(RuntimeEffect::SetPlayerCash {
|
||||
target,
|
||||
value: i64::from(row.raw_scalar_value),
|
||||
});
|
||||
return match target_subject {
|
||||
Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash {
|
||||
target: real_grouped_chairman_target(target_scope_ordinal)?,
|
||||
value: i64::from(row.raw_scalar_value),
|
||||
}),
|
||||
_ => Some(RuntimeEffect::SetPlayerCash {
|
||||
target: real_grouped_player_target(target_scope_ordinal)?,
|
||||
value: i64::from(row.raw_scalar_value),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
|
|
@ -3823,8 +3895,14 @@ fn decode_real_grouped_effect_action(
|
|||
&& row.row_shape == "bool_toggle"
|
||||
&& row.raw_scalar_value != 0
|
||||
{
|
||||
let target = real_grouped_player_target(target_scope_ordinal)?;
|
||||
return Some(RuntimeEffect::DeactivatePlayer { target });
|
||||
return match target_subject {
|
||||
Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::DeactivateChairman {
|
||||
target: real_grouped_chairman_target(target_scope_ordinal)?,
|
||||
}),
|
||||
_ => Some(RuntimeEffect::DeactivatePlayer {
|
||||
target: real_grouped_player_target(target_scope_ordinal)?,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
|
|
@ -3886,6 +3964,17 @@ fn real_grouped_player_target(ordinal: u8) -> Option<RuntimePlayerTarget> {
|
|||
}
|
||||
}
|
||||
|
||||
fn real_grouped_chairman_target(ordinal: u8) -> Option<RuntimeChairmanTarget> {
|
||||
match ordinal {
|
||||
1 => Some(RuntimeChairmanTarget::SelectedChairman),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn real_grouped_chairman_target_supported_in_runtime(ordinal: u8) -> bool {
|
||||
real_grouped_chairman_target(ordinal).is_some()
|
||||
}
|
||||
|
||||
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
|
||||
let opcode = read_u8_at(bytes, *cursor)?;
|
||||
*cursor += 1;
|
||||
|
|
@ -4033,6 +4122,13 @@ fn parse_optional_u16_len_prefixed_string(
|
|||
|
||||
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
||||
match effect {
|
||||
RuntimeEffect::SetChairmanCash { target, .. }
|
||||
| RuntimeEffect::DeactivateChairman { target } => matches!(
|
||||
target,
|
||||
RuntimeChairmanTarget::AllActive
|
||||
| RuntimeChairmanTarget::SelectedChairman
|
||||
| RuntimeChairmanTarget::Ids { .. }
|
||||
),
|
||||
RuntimeEffect::SetWorldFlag { .. }
|
||||
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
|
||||
| RuntimeEffect::SetEconomicStatusCode { .. }
|
||||
|
|
@ -4097,6 +4193,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
|||
fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool {
|
||||
match condition {
|
||||
RuntimeCondition::CompanyNumericThreshold { .. }
|
||||
| RuntimeCondition::ChairmanNumericThreshold { .. }
|
||||
| RuntimeCondition::TerritoryNumericThreshold { .. }
|
||||
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
|
||||
| RuntimeCondition::SpecialConditionThreshold { .. }
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ use std::collections::BTreeSet;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
RuntimeCargoClass, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
|
||||
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
|
||||
RuntimePlayerTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric,
|
||||
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
|
||||
RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyControllerKind,
|
||||
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
|
||||
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeState, RuntimeSummary,
|
||||
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
|
||||
calendar::BoundaryEventKind,
|
||||
};
|
||||
|
||||
|
|
@ -87,6 +87,8 @@ struct AppliedEffectsSummary {
|
|||
struct ResolvedConditionContext {
|
||||
matching_company_ids: BTreeSet<u32>,
|
||||
matching_player_ids: BTreeSet<u32>,
|
||||
#[allow(dead_code)]
|
||||
matching_chairman_profile_ids: BTreeSet<u32>,
|
||||
}
|
||||
|
||||
pub fn execute_step_command(
|
||||
|
|
@ -346,6 +348,21 @@ fn apply_runtime_effects(
|
|||
mutated_player_ids.insert(player_id);
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetChairmanCash { target, value } => {
|
||||
let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?;
|
||||
for profile_id in profile_ids {
|
||||
let chairman = state
|
||||
.chairman_profiles
|
||||
.iter_mut()
|
||||
.find(|profile| profile.profile_id == profile_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"missing chairman profile_id {profile_id} while applying cash effect"
|
||||
)
|
||||
})?;
|
||||
chairman.current_cash = *value;
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
let player_ids = resolve_player_target_ids(state, target, condition_context)?;
|
||||
for player_id in player_ids {
|
||||
|
|
@ -365,6 +382,39 @@ fn apply_runtime_effects(
|
|||
}
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivateChairman { target } => {
|
||||
let profile_ids = resolve_chairman_target_ids(state, target, condition_context)?;
|
||||
for profile_id in profile_ids.iter().copied() {
|
||||
let linked_company_id = state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.find(|profile| profile.profile_id == profile_id)
|
||||
.and_then(|profile| profile.linked_company_id);
|
||||
let chairman = state
|
||||
.chairman_profiles
|
||||
.iter_mut()
|
||||
.find(|profile| profile.profile_id == profile_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"missing chairman profile_id {profile_id} while applying deactivate effect"
|
||||
)
|
||||
})?;
|
||||
chairman.active = false;
|
||||
chairman.linked_company_id = None;
|
||||
if state.selected_chairman_profile_id == Some(profile_id) {
|
||||
state.selected_chairman_profile_id = None;
|
||||
}
|
||||
if let Some(linked_company_id) = linked_company_id {
|
||||
for other in &mut state.chairman_profiles {
|
||||
if other.profile_id != profile_id
|
||||
&& other.linked_company_id == Some(linked_company_id)
|
||||
{
|
||||
other.linked_company_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -607,6 +657,7 @@ fn evaluate_record_conditions(
|
|||
}
|
||||
|
||||
let mut company_matches: Option<BTreeSet<u32>> = None;
|
||||
let mut chairman_matches: Option<BTreeSet<u32>> = None;
|
||||
|
||||
for condition in conditions {
|
||||
match condition {
|
||||
|
|
@ -657,6 +708,41 @@ fn evaluate_record_conditions(
|
|||
return Ok(None);
|
||||
}
|
||||
}
|
||||
RuntimeCondition::ChairmanNumericThreshold {
|
||||
target,
|
||||
metric,
|
||||
comparator,
|
||||
value,
|
||||
} => {
|
||||
let resolved = resolve_chairman_target_ids(
|
||||
state,
|
||||
target,
|
||||
&ResolvedConditionContext::default(),
|
||||
)?;
|
||||
let matching = resolved
|
||||
.into_iter()
|
||||
.filter(|profile_id| {
|
||||
state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.find(|profile| profile.profile_id == *profile_id)
|
||||
.is_some_and(|profile| {
|
||||
compare_condition_value(
|
||||
chairman_metric_value(profile, *metric),
|
||||
*comparator,
|
||||
*value,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
if matching.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
intersect_chairman_matches(&mut chairman_matches, matching);
|
||||
if chairman_matches.as_ref().is_some_and(BTreeSet::is_empty) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
RuntimeCondition::CompanyTerritoryNumericThreshold {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -840,6 +926,7 @@ fn evaluate_record_conditions(
|
|||
Ok(Some(ResolvedConditionContext {
|
||||
matching_company_ids: company_matches.unwrap_or_default(),
|
||||
matching_player_ids: BTreeSet::new(),
|
||||
matching_chairman_profile_ids: chairman_matches.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -854,6 +941,17 @@ fn intersect_company_matches(company_matches: &mut Option<BTreeSet<u32>>, next:
|
|||
}
|
||||
}
|
||||
|
||||
fn intersect_chairman_matches(chairman_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
|
||||
match chairman_matches {
|
||||
Some(existing) => {
|
||||
existing.retain(|profile_id| next.contains(profile_id));
|
||||
}
|
||||
None => {
|
||||
*chairman_matches = Some(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_company_target_ids(
|
||||
state: &RuntimeState,
|
||||
target: &RuntimeCompanyTarget,
|
||||
|
|
@ -1043,6 +1141,53 @@ fn resolve_player_target_ids(
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_chairman_target_ids(
|
||||
state: &RuntimeState,
|
||||
target: &RuntimeChairmanTarget,
|
||||
_condition_context: &ResolvedConditionContext,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
match target {
|
||||
RuntimeChairmanTarget::AllActive => Ok(state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.filter(|profile| profile.active)
|
||||
.map(|profile| profile.profile_id)
|
||||
.collect()),
|
||||
RuntimeChairmanTarget::Ids { ids } => {
|
||||
let known_ids = state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.map(|profile| profile.profile_id)
|
||||
.collect::<BTreeSet<_>>();
|
||||
for profile_id in ids {
|
||||
if !known_ids.contains(profile_id) {
|
||||
return Err(format!(
|
||||
"target references unknown chairman profile_id {profile_id}"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(ids.clone())
|
||||
}
|
||||
RuntimeChairmanTarget::SelectedChairman => {
|
||||
let selected_profile_id = state.selected_chairman_profile_id.ok_or_else(|| {
|
||||
"target requires selected_chairman_profile_id context".to_string()
|
||||
})?;
|
||||
if state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.any(|profile| profile.profile_id == selected_profile_id && profile.active)
|
||||
{
|
||||
Ok(vec![selected_profile_id])
|
||||
} else {
|
||||
Err(
|
||||
"target requires selected_chairman_profile_id to reference an active chairman profile"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_territory_target_ids(
|
||||
state: &RuntimeState,
|
||||
target: &RuntimeTerritoryTarget,
|
||||
|
|
@ -1090,6 +1235,18 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
|
|||
}
|
||||
}
|
||||
|
||||
fn chairman_metric_value(
|
||||
profile: &crate::RuntimeChairmanProfile,
|
||||
metric: RuntimeChairmanMetric,
|
||||
) -> i64 {
|
||||
match metric {
|
||||
RuntimeChairmanMetric::CurrentCash => profile.current_cash,
|
||||
RuntimeChairmanMetric::HoldingsValueTotal => profile.holdings_value_total,
|
||||
RuntimeChairmanMetric::NetWorthTotal => profile.net_worth_total,
|
||||
RuntimeChairmanMetric::PurchasingPowerTotal => profile.purchasing_power_total,
|
||||
}
|
||||
}
|
||||
|
||||
fn territory_metric_value(
|
||||
state: &RuntimeState,
|
||||
territory_ids: &[u32],
|
||||
|
|
@ -1277,6 +1434,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ pub struct RuntimeSummary {
|
|||
pub company_count: usize,
|
||||
pub active_company_count: usize,
|
||||
pub player_count: usize,
|
||||
pub chairman_profile_count: usize,
|
||||
pub active_chairman_profile_count: usize,
|
||||
pub selected_chairman_profile_id: Option<u32>,
|
||||
pub train_count: usize,
|
||||
pub active_train_count: usize,
|
||||
pub retired_train_count: usize,
|
||||
|
|
@ -52,6 +55,8 @@ pub struct RuntimeSummary {
|
|||
pub packed_event_blocked_missing_player_context_count: usize,
|
||||
pub packed_event_blocked_missing_player_selection_context_count: usize,
|
||||
pub packed_event_blocked_missing_player_role_context_count: usize,
|
||||
pub packed_event_blocked_missing_chairman_context_count: usize,
|
||||
pub packed_event_blocked_chairman_target_scope_count: usize,
|
||||
pub packed_event_blocked_missing_condition_context_count: usize,
|
||||
pub packed_event_blocked_missing_player_condition_context_count: usize,
|
||||
pub packed_event_blocked_company_condition_scope_disabled_count: usize,
|
||||
|
|
@ -169,6 +174,13 @@ impl RuntimeSummary {
|
|||
.filter(|company| company.active)
|
||||
.count(),
|
||||
player_count: state.players.len(),
|
||||
chairman_profile_count: state.chairman_profiles.len(),
|
||||
active_chairman_profile_count: state
|
||||
.chairman_profiles
|
||||
.iter()
|
||||
.filter(|profile| profile.active)
|
||||
.count(),
|
||||
selected_chairman_profile_id: state.selected_chairman_profile_id,
|
||||
train_count: state.trains.len(),
|
||||
active_train_count: state.trains.iter().filter(|train| train.active).count(),
|
||||
retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
|
||||
|
|
@ -298,6 +310,34 @@ impl RuntimeSummary {
|
|||
.count()
|
||||
})
|
||||
.unwrap_or(0),
|
||||
packed_event_blocked_missing_chairman_context_count: state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.map(|summary| {
|
||||
summary
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.import_outcome.as_deref()
|
||||
== Some("blocked_missing_chairman_context")
|
||||
})
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0),
|
||||
packed_event_blocked_chairman_target_scope_count: state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.map(|summary| {
|
||||
summary
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.import_outcome.as_deref()
|
||||
== Some("blocked_chairman_target_scope")
|
||||
})
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0),
|
||||
packed_event_blocked_missing_condition_context_count: state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
|
|
@ -666,6 +706,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: vec![
|
||||
crate::RuntimeLocomotiveCatalogEntry {
|
||||
|
|
@ -909,6 +951,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: vec![
|
||||
crate::RuntimeLocomotiveCatalogEntry {
|
||||
|
|
@ -956,6 +1000,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: vec![
|
||||
crate::RuntimeLocomotiveCatalogEntry {
|
||||
|
|
@ -1009,6 +1055,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1053,6 +1101,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1092,6 +1142,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
@ -1200,6 +1252,8 @@ mod tests {
|
|||
selected_company_id: None,
|
||||
players: Vec::new(),
|
||||
selected_player_id: None,
|
||||
chairman_profiles: Vec::new(),
|
||||
selected_chairman_profile_id: None,
|
||||
trains: Vec::new(),
|
||||
locomotive_catalog: Vec::new(),
|
||||
cargo_catalog: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ The highest-value next passes are now:
|
|||
- descriptors `1` `Player Cash` and `14` `Deactivate Player` now join that executable real batch
|
||||
through the same ordinary runtime path, backed by the minimal player runtime and overlay-import
|
||||
context
|
||||
- the first chairman-targeted real grouped rows now execute too through that same path when the
|
||||
hidden grouped target-subject lane resolves to selected-chairman scope; broader chairman target
|
||||
scopes stay parity-only under `blocked_chairman_target_scope`
|
||||
- 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
|
||||
- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ Implemented today:
|
|||
names, a minimal player runtime now carries selected-player and role context, and real descriptor
|
||||
`1` = `Player Cash` and descriptor `14` = `Deactivate Player` now import and execute through the
|
||||
ordinary runtime path
|
||||
- a first-class chairman-profile runtime now exists too, with overlay-backed selected-chairman
|
||||
context and the first chairman-targeted grouped-effect subset: the same real descriptors
|
||||
`1` = `Player Cash` and `14` = `Deactivate Player` now also import and execute through the
|
||||
hidden grouped target-subject lane when it resolves to selected-chairman scope, while broader
|
||||
chairman target scopes remain explicit parity on `blocked_chairman_target_scope`
|
||||
- a minimal event-owned train surface and an opaque economic-status lane now exist in runtime
|
||||
state, and real descriptors `8` = `Economic Status`, `9` = `Confiscate All`, and `15` =
|
||||
`Retire Train` now import and execute through the ordinary runtime path when overlay context
|
||||
|
|
@ -111,8 +116,8 @@ Implemented today:
|
|||
|
||||
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
|
||||
broader real grouped-descriptor and ordinary condition-id coverage beyond the current access,
|
||||
whole-game toggle, train, player, numeric-threshold, named locomotive availability, named
|
||||
locomotive cost, world scalar override, and world-scalar condition batches.
|
||||
whole-game toggle, train, player, chairman selected-scope, numeric-threshold, named locomotive
|
||||
availability, named locomotive cost, world scalar override, and world-scalar condition batches.
|
||||
Richer runtime ownership should still be added only where a later descriptor or condition family
|
||||
needs more than the current event-owned roster.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"fixture_id": "packed-event-chairman-cash-overlay-fixture",
|
||||
"source": {
|
||||
"kind": "captured-runtime",
|
||||
"description": "Fixture proving descriptor 1 imports and executes on selected-chairman scope."
|
||||
},
|
||||
"state_import_path": "packed-event-chairman-cash-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": 2,
|
||||
"chairman_profile_count": 2,
|
||||
"active_chairman_profile_count": 2,
|
||||
"selected_chairman_profile_id": 1,
|
||||
"packed_event_collection_present": true,
|
||||
"packed_event_record_count": 1,
|
||||
"packed_event_decoded_record_count": 1,
|
||||
"packed_event_imported_runtime_record_count": 1,
|
||||
"event_runtime_record_count": 1,
|
||||
"total_event_record_service_count": 1,
|
||||
"total_trigger_dispatch_count": 1
|
||||
},
|
||||
"expected_state_fragment": {
|
||||
"chairman_profiles": [
|
||||
{
|
||||
"profile_id": 1,
|
||||
"current_cash": 999
|
||||
},
|
||||
{
|
||||
"profile_id": 2,
|
||||
"current_cash": 250
|
||||
}
|
||||
],
|
||||
"packed_event_collection": {
|
||||
"records": [
|
||||
{
|
||||
"import_outcome": "imported",
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "set_chairman_cash",
|
||||
"target": {
|
||||
"kind": "selected_chairman"
|
||||
},
|
||||
"value": 999
|
||||
}
|
||||
],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"grouped_target_subject": "chairman"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
fixtures/runtime/packed-event-chairman-cash-overlay.json
Normal file
9
fixtures/runtime/packed-event-chairman-cash-overlay.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"import_id": "packed-event-chairman-cash-overlay",
|
||||
"source": {
|
||||
"description": "Overlay import combining chairman runtime context with the real chairman-targeted cash descriptor sample."
|
||||
},
|
||||
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
|
||||
"save_slice_path": "packed-event-chairman-cash-save-slice.json"
|
||||
}
|
||||
110
fixtures/runtime/packed-event-chairman-cash-save-slice.json
Normal file
110
fixtures/runtime/packed-event-chairman-cash-save-slice.json
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"save_slice_id": "packed-event-chairman-cash-save-slice",
|
||||
"source": {
|
||||
"description": "Tracked save-slice document with a chairman-targeted Player Cash row using the hidden grouped target-subject lane.",
|
||||
"original_save_filename": "captured-chairman-cash.gms",
|
||||
"original_save_sha256": "chairman-cash-sample-sha256",
|
||||
"notes": [
|
||||
"tracked as JSON save-slice document rather than raw .smp",
|
||||
"proves selected-chairman descriptor import through the normal runtime 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": 61,
|
||||
"live_record_count": 1,
|
||||
"live_entry_ids": [61],
|
||||
"decoded_record_count": 1,
|
||||
"imported_runtime_record_count": 1,
|
||||
"records": [
|
||||
{
|
||||
"record_index": 0,
|
||||
"live_entry_id": 61,
|
||||
"payload_offset": 29296,
|
||||
"payload_len": 140,
|
||||
"decode_status": "parity_only",
|
||||
"payload_family": "real_packed_v1",
|
||||
"trigger_kind": 6,
|
||||
"one_shot": false,
|
||||
"compact_control": {
|
||||
"mode_byte_0x7ef": 6,
|
||||
"primary_selector_0x7f0": 12,
|
||||
"grouped_mode_0x7f4": 2,
|
||||
"one_shot_header_0x7f5": 0,
|
||||
"modifier_flag_0x7f9": 0,
|
||||
"modifier_flag_0x7fa": 0,
|
||||
"grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1],
|
||||
"grouped_scope_checkboxes_0x7ff": [2, 0, 0, 0],
|
||||
"summary_toggle_0x800": 1,
|
||||
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
|
||||
},
|
||||
"text_bands": [],
|
||||
"standalone_condition_row_count": 0,
|
||||
"standalone_condition_rows": [],
|
||||
"grouped_effect_row_counts": [1, 0, 0, 0],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"group_index": 0,
|
||||
"row_index": 0,
|
||||
"descriptor_id": 1,
|
||||
"descriptor_label": "Player Cash",
|
||||
"target_mask_bits": 2,
|
||||
"parameter_family": "player_cash_scalar",
|
||||
"grouped_target_subject": "chairman",
|
||||
"opcode": 8,
|
||||
"raw_scalar_value": 999,
|
||||
"value_byte_0x09": 0,
|
||||
"value_dword_0x0d": 0,
|
||||
"value_byte_0x11": 0,
|
||||
"value_byte_0x12": 0,
|
||||
"value_word_0x14": 0,
|
||||
"value_word_0x16": 0,
|
||||
"row_shape": "multivalue_scalar",
|
||||
"semantic_family": "multivalue_scalar",
|
||||
"semantic_preview": "Set Player Cash to 999 with aux [0, 0, 0, 0]",
|
||||
"locomotive_name": null,
|
||||
"notes": []
|
||||
}
|
||||
],
|
||||
"decoded_conditions": [],
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "set_chairman_cash",
|
||||
"target": {
|
||||
"kind": "selected_chairman"
|
||||
},
|
||||
"value": 999
|
||||
}
|
||||
],
|
||||
"executable_import_ready": true,
|
||||
"notes": [
|
||||
"decoded from grounded real 0x4e9a row framing",
|
||||
"hidden grouped target-subject lane resolves descriptor 1 to selected chairman scope"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": [
|
||||
"real chairman-targeted cash descriptor sample"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"fixture_id": "packed-event-chairman-missing-context-save-slice-fixture",
|
||||
"source": {
|
||||
"kind": "captured-runtime",
|
||||
"description": "Fixture proving chairman-targeted rows stay parity-only without runtime chairman context."
|
||||
},
|
||||
"state_import_path": "packed-event-chairman-cash-save-slice.json",
|
||||
"commands": [
|
||||
{
|
||||
"kind": "service_trigger_kind",
|
||||
"trigger_kind": 6
|
||||
}
|
||||
],
|
||||
"expected_summary": {
|
||||
"calendar_projection_source": "default-1830-placeholder",
|
||||
"calendar_projection_is_placeholder": true,
|
||||
"chairman_profile_count": 0,
|
||||
"packed_event_collection_present": true,
|
||||
"packed_event_record_count": 1,
|
||||
"packed_event_decoded_record_count": 1,
|
||||
"packed_event_imported_runtime_record_count": 0,
|
||||
"packed_event_blocked_missing_chairman_context_count": 1,
|
||||
"event_runtime_record_count": 0
|
||||
},
|
||||
"expected_state_fragment": {
|
||||
"packed_event_collection": {
|
||||
"records": [
|
||||
{
|
||||
"import_outcome": "blocked_missing_chairman_context",
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"grouped_target_subject": "chairman"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"snapshot_id": "packed-event-chairman-overlay-base-snapshot",
|
||||
"source": {
|
||||
"description": "Base runtime snapshot supplying selected-chairman context for chairman-targeted packed-event overlays."
|
||||
},
|
||||
"state": {
|
||||
"calendar": {
|
||||
"year": 1840,
|
||||
"month_slot": 1,
|
||||
"phase_slot": 2,
|
||||
"tick_slot": 3
|
||||
},
|
||||
"world_flags": {
|
||||
"base.only": true
|
||||
},
|
||||
"metadata": {
|
||||
"base.note": "chairman overlay context"
|
||||
},
|
||||
"companies": [
|
||||
{
|
||||
"company_id": 1,
|
||||
"current_cash": 150,
|
||||
"debt": 80,
|
||||
"credit_rating_score": 650,
|
||||
"prime_rate": 5,
|
||||
"controller_kind": "human",
|
||||
"track_piece_counts": {
|
||||
"total": 20,
|
||||
"single": 5,
|
||||
"double": 8,
|
||||
"transition": 1,
|
||||
"electric": 3,
|
||||
"non_electric": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"company_id": 2,
|
||||
"current_cash": 90,
|
||||
"debt": 40,
|
||||
"credit_rating_score": 480,
|
||||
"prime_rate": 6,
|
||||
"controller_kind": "ai",
|
||||
"track_piece_counts": {
|
||||
"total": 8,
|
||||
"single": 2,
|
||||
"double": 2,
|
||||
"transition": 0,
|
||||
"electric": 1,
|
||||
"non_electric": 7
|
||||
}
|
||||
}
|
||||
],
|
||||
"selected_company_id": 1,
|
||||
"players": [],
|
||||
"selected_player_id": null,
|
||||
"chairman_profiles": [
|
||||
{
|
||||
"profile_id": 1,
|
||||
"name": "Chairman One",
|
||||
"active": true,
|
||||
"current_cash": 500,
|
||||
"linked_company_id": 1,
|
||||
"company_holdings": {
|
||||
"1": 1000
|
||||
},
|
||||
"holdings_value_total": 700,
|
||||
"net_worth_total": 1200,
|
||||
"purchasing_power_total": 1500
|
||||
},
|
||||
{
|
||||
"profile_id": 2,
|
||||
"name": "Chairman Two",
|
||||
"active": true,
|
||||
"current_cash": 250,
|
||||
"linked_company_id": 2,
|
||||
"company_holdings": {
|
||||
"2": 900
|
||||
},
|
||||
"holdings_value_total": 600,
|
||||
"net_worth_total": 900,
|
||||
"purchasing_power_total": 1100
|
||||
}
|
||||
],
|
||||
"selected_chairman_profile_id": 1,
|
||||
"event_runtime_records": [],
|
||||
"candidate_availability": {},
|
||||
"special_conditions": {},
|
||||
"service_state": {
|
||||
"periodic_boundary_calls": 0,
|
||||
"trigger_dispatch_counts": {},
|
||||
"total_event_record_services": 0,
|
||||
"dirty_rerun_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"fixture_id": "packed-event-chairman-scope-parity-save-slice-fixture",
|
||||
"source": {
|
||||
"kind": "captured-runtime",
|
||||
"description": "Fixture proving unsupported chairman scopes stay parity-only under an explicit blocker."
|
||||
},
|
||||
"state_import_path": "packed-event-chairman-scope-parity-save-slice.json",
|
||||
"commands": [
|
||||
{
|
||||
"kind": "service_trigger_kind",
|
||||
"trigger_kind": 6
|
||||
}
|
||||
],
|
||||
"expected_summary": {
|
||||
"calendar_projection_source": "default-1830-placeholder",
|
||||
"calendar_projection_is_placeholder": true,
|
||||
"packed_event_collection_present": true,
|
||||
"packed_event_record_count": 1,
|
||||
"packed_event_decoded_record_count": 1,
|
||||
"packed_event_imported_runtime_record_count": 0,
|
||||
"packed_event_blocked_chairman_target_scope_count": 1,
|
||||
"event_runtime_record_count": 0
|
||||
},
|
||||
"expected_state_fragment": {
|
||||
"packed_event_collection": {
|
||||
"records": [
|
||||
{
|
||||
"import_outcome": "blocked_chairman_target_scope",
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"grouped_target_subject": "chairman"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"save_slice_id": "packed-event-chairman-scope-parity-save-slice",
|
||||
"source": {
|
||||
"description": "Tracked save-slice document with a chairman-targeted row on an unsupported non-selected scope.",
|
||||
"original_save_filename": "captured-chairman-scope-parity.gms",
|
||||
"original_save_sha256": "chairman-scope-parity-sample-sha256",
|
||||
"notes": [
|
||||
"tracked as JSON save-slice document rather than raw .smp",
|
||||
"pins the selected-chairman-only execution boundary"
|
||||
]
|
||||
},
|
||||
"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": 63,
|
||||
"live_record_count": 1,
|
||||
"live_entry_ids": [63],
|
||||
"decoded_record_count": 1,
|
||||
"imported_runtime_record_count": 0,
|
||||
"records": [
|
||||
{
|
||||
"record_index": 0,
|
||||
"live_entry_id": 63,
|
||||
"payload_offset": 29296,
|
||||
"payload_len": 140,
|
||||
"decode_status": "parity_only",
|
||||
"payload_family": "real_packed_v1",
|
||||
"trigger_kind": 6,
|
||||
"one_shot": false,
|
||||
"compact_control": {
|
||||
"mode_byte_0x7ef": 6,
|
||||
"primary_selector_0x7f0": 12,
|
||||
"grouped_mode_0x7f4": 2,
|
||||
"one_shot_header_0x7f5": 0,
|
||||
"modifier_flag_0x7f9": 0,
|
||||
"modifier_flag_0x7fa": 0,
|
||||
"grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1],
|
||||
"grouped_scope_checkboxes_0x7ff": [2, 0, 0, 0],
|
||||
"summary_toggle_0x800": 1,
|
||||
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
|
||||
},
|
||||
"text_bands": [],
|
||||
"standalone_condition_row_count": 0,
|
||||
"standalone_condition_rows": [],
|
||||
"grouped_effect_row_counts": [1, 0, 0, 0],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"group_index": 0,
|
||||
"row_index": 0,
|
||||
"descriptor_id": 1,
|
||||
"descriptor_label": "Player Cash",
|
||||
"target_mask_bits": 2,
|
||||
"parameter_family": "player_cash_scalar",
|
||||
"grouped_target_subject": "chairman",
|
||||
"opcode": 8,
|
||||
"raw_scalar_value": 700,
|
||||
"value_byte_0x09": 0,
|
||||
"value_dword_0x0d": 0,
|
||||
"value_byte_0x11": 0,
|
||||
"value_byte_0x12": 0,
|
||||
"value_word_0x14": 0,
|
||||
"value_word_0x16": 0,
|
||||
"row_shape": "multivalue_scalar",
|
||||
"semantic_family": "multivalue_scalar",
|
||||
"semantic_preview": "Set Player Cash to 700 with aux [0, 0, 0, 0]",
|
||||
"locomotive_name": null,
|
||||
"notes": [
|
||||
"chairman row requires selected-chairman scope"
|
||||
]
|
||||
}
|
||||
],
|
||||
"decoded_conditions": [],
|
||||
"decoded_actions": [],
|
||||
"executable_import_ready": false,
|
||||
"notes": [
|
||||
"decoded from grounded real 0x4e9a row framing",
|
||||
"selected-chairman scope is the only grounded chairman-target subset in this slice"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": [
|
||||
"real chairman-targeted scope parity sample"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"fixture_id": "packed-event-deactivate-chairman-overlay-fixture",
|
||||
"source": {
|
||||
"kind": "captured-runtime",
|
||||
"description": "Fixture proving descriptor 14 imports and executes on selected-chairman scope."
|
||||
},
|
||||
"state_import_path": "packed-event-deactivate-chairman-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": 2,
|
||||
"chairman_profile_count": 2,
|
||||
"active_chairman_profile_count": 1,
|
||||
"packed_event_collection_present": true,
|
||||
"packed_event_record_count": 1,
|
||||
"packed_event_decoded_record_count": 1,
|
||||
"packed_event_imported_runtime_record_count": 1,
|
||||
"event_runtime_record_count": 1,
|
||||
"total_event_record_service_count": 1,
|
||||
"total_trigger_dispatch_count": 1
|
||||
},
|
||||
"expected_state_fragment": {
|
||||
"selected_chairman_profile_id": null,
|
||||
"chairman_profiles": [
|
||||
{
|
||||
"profile_id": 1,
|
||||
"active": false,
|
||||
"linked_company_id": null
|
||||
},
|
||||
{
|
||||
"profile_id": 2,
|
||||
"active": true,
|
||||
"linked_company_id": 2
|
||||
}
|
||||
],
|
||||
"packed_event_collection": {
|
||||
"records": [
|
||||
{
|
||||
"import_outcome": "imported",
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "deactivate_chairman",
|
||||
"target": {
|
||||
"kind": "selected_chairman"
|
||||
}
|
||||
}
|
||||
],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"grouped_target_subject": "chairman"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"import_id": "packed-event-deactivate-chairman-overlay",
|
||||
"source": {
|
||||
"description": "Overlay import combining chairman runtime context with the real chairman-targeted deactivation descriptor sample."
|
||||
},
|
||||
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
|
||||
"save_slice_path": "packed-event-deactivate-chairman-save-slice.json"
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"save_slice_id": "packed-event-deactivate-chairman-save-slice",
|
||||
"source": {
|
||||
"description": "Tracked save-slice document with a chairman-targeted Deactivate Player row using the hidden grouped target-subject lane.",
|
||||
"original_save_filename": "captured-deactivate-chairman.gms",
|
||||
"original_save_sha256": "deactivate-chairman-sample-sha256",
|
||||
"notes": [
|
||||
"tracked as JSON save-slice document rather than raw .smp",
|
||||
"proves selected-chairman lifecycle import through the normal runtime 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": 62,
|
||||
"live_record_count": 1,
|
||||
"live_entry_ids": [62],
|
||||
"decoded_record_count": 1,
|
||||
"imported_runtime_record_count": 1,
|
||||
"records": [
|
||||
{
|
||||
"record_index": 0,
|
||||
"live_entry_id": 62,
|
||||
"payload_offset": 29296,
|
||||
"payload_len": 140,
|
||||
"decode_status": "parity_only",
|
||||
"payload_family": "real_packed_v1",
|
||||
"trigger_kind": 6,
|
||||
"one_shot": false,
|
||||
"compact_control": {
|
||||
"mode_byte_0x7ef": 6,
|
||||
"primary_selector_0x7f0": 12,
|
||||
"grouped_mode_0x7f4": 2,
|
||||
"one_shot_header_0x7f5": 0,
|
||||
"modifier_flag_0x7f9": 0,
|
||||
"modifier_flag_0x7fa": 0,
|
||||
"grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1],
|
||||
"grouped_scope_checkboxes_0x7ff": [2, 0, 0, 0],
|
||||
"summary_toggle_0x800": 1,
|
||||
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
|
||||
},
|
||||
"text_bands": [],
|
||||
"standalone_condition_row_count": 0,
|
||||
"standalone_condition_rows": [],
|
||||
"grouped_effect_row_counts": [1, 0, 0, 0],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"group_index": 0,
|
||||
"row_index": 0,
|
||||
"descriptor_id": 14,
|
||||
"descriptor_label": "Deactivate Player",
|
||||
"target_mask_bits": 2,
|
||||
"parameter_family": "player_lifecycle_toggle",
|
||||
"grouped_target_subject": "chairman",
|
||||
"opcode": 1,
|
||||
"raw_scalar_value": 1,
|
||||
"value_byte_0x09": 0,
|
||||
"value_dword_0x0d": 0,
|
||||
"value_byte_0x11": 0,
|
||||
"value_byte_0x12": 0,
|
||||
"value_word_0x14": 0,
|
||||
"value_word_0x16": 0,
|
||||
"row_shape": "bool_toggle",
|
||||
"semantic_family": "bool_toggle",
|
||||
"semantic_preview": "Set Deactivate Player to TRUE",
|
||||
"locomotive_name": null,
|
||||
"notes": []
|
||||
}
|
||||
],
|
||||
"decoded_conditions": [],
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "deactivate_chairman",
|
||||
"target": {
|
||||
"kind": "selected_chairman"
|
||||
}
|
||||
}
|
||||
],
|
||||
"executable_import_ready": true,
|
||||
"notes": [
|
||||
"decoded from grounded real 0x4e9a row framing",
|
||||
"hidden grouped target-subject lane resolves descriptor 14 to selected chairman scope"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": [
|
||||
"real chairman-targeted lifecycle descriptor sample"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue