Execute real packed event player deactivation
This commit is contained in:
parent
e44e0d5ac5
commit
991725dba8
10 changed files with 670 additions and 47 deletions
19
README.md
19
README.md
|
|
@ -15,15 +15,16 @@ frontier is broader real grouped-descriptor coverage on top of the existing save
|
|||
overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries
|
||||
selected-company and controller-role context through overlay imports, and real descriptors `2`
|
||||
`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and
|
||||
execute through the ordinary runtime path, and descriptor `1` `Player Cash` now joins that batch
|
||||
through the same service engine. Synthetic packed records still exercise the same runtime without a
|
||||
parallel packed executor. 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 through overlay-backed runtime context. Exact
|
||||
named-territory binding now executes, and the runtime now also carries the minimal event-owned
|
||||
train roster and opaque economic-status lane needed for real descriptors `8` `Economic Status`, `9`
|
||||
`Confiscate All`, and `15` `Retire Train` to execute through the same path. Descriptor `3`
|
||||
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
|
||||
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
|
||||
through overlay-backed runtime context. Exact named-territory binding now executes, and the runtime
|
||||
now also carries the minimal event-owned train roster and opaque economic-status lane needed for
|
||||
real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` to execute
|
||||
through the same path. Descriptor `3`
|
||||
`Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights
|
||||
rather than a territory-owned policy bit. Whole-game ordinary-condition execution now exists too:
|
||||
special-condition thresholds, candidate-availability thresholds, and economic-status-code
|
||||
|
|
|
|||
|
|
@ -1051,6 +1051,12 @@ fn lower_condition_targets_in_effect(
|
|||
)?,
|
||||
value: *value,
|
||||
},
|
||||
RuntimeEffect::DeactivatePlayer { target } => RuntimeEffect::DeactivatePlayer {
|
||||
target: lower_condition_true_player_target_in_player_target(
|
||||
target,
|
||||
lowered_player_target,
|
||||
)?,
|
||||
},
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -1378,6 +1384,19 @@ fn smp_runtime_effect_to_runtime_effect(
|
|||
Err(player_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
if player_target_allowed_for_import(
|
||||
target,
|
||||
company_context,
|
||||
allow_condition_true_player,
|
||||
) {
|
||||
Ok(RuntimeEffect::DeactivatePlayer {
|
||||
target: target.clone(),
|
||||
})
|
||||
} else {
|
||||
Err(player_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -2075,6 +2094,7 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
|
|||
RuntimeEffect::SetWorldFlag { .. }
|
||||
| RuntimeEffect::SetEconomicStatusCode { .. }
|
||||
| RuntimeEffect::SetPlayerCash { .. }
|
||||
| RuntimeEffect::DeactivatePlayer { .. }
|
||||
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||
| RuntimeEffect::SetSpecialCondition { .. }
|
||||
| RuntimeEffect::ActivateEventRecord { .. }
|
||||
|
|
@ -2088,6 +2108,9 @@ fn runtime_effect_uses_condition_true_player(effect: &RuntimeEffect) -> bool {
|
|||
RuntimeEffect::SetPlayerCash { target, .. } => {
|
||||
matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
|
||||
}
|
||||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
matches!(target, RuntimePlayerTarget::ConditionTruePlayer)
|
||||
}
|
||||
RuntimeEffect::AppendEventRecord { record } => record
|
||||
.effects
|
||||
.iter()
|
||||
|
|
@ -2119,7 +2142,8 @@ fn runtime_effect_company_target_import_blocker(
|
|||
company_target_import_blocker(target, company_context)
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetPlayerCash { target, .. } => {
|
||||
RuntimeEffect::SetPlayerCash { target, .. }
|
||||
| RuntimeEffect::DeactivatePlayer { target } => {
|
||||
player_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeEffect::RetireTrains {
|
||||
|
|
@ -2685,6 +2709,35 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn real_deactivate_player_row(
|
||||
enabled: bool,
|
||||
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id: 14,
|
||||
descriptor_label: Some("Deactivate Player".to_string()),
|
||||
target_mask_bits: Some(0x02),
|
||||
parameter_family: Some("player_lifecycle_toggle".to_string()),
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
value_byte_0x09: 0,
|
||||
value_dword_0x0d: 0,
|
||||
value_byte_0x11: 0,
|
||||
value_byte_0x12: 0,
|
||||
value_word_0x14: 0,
|
||||
value_word_0x16: 0,
|
||||
row_shape: "bool_toggle".to_string(),
|
||||
semantic_family: Some("bool_toggle".to_string()),
|
||||
semantic_preview: Some(format!(
|
||||
"Set Deactivate Player to {}",
|
||||
if enabled { "TRUE" } else { "FALSE" }
|
||||
)),
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn real_territory_access_row(
|
||||
enabled: bool,
|
||||
notes: Vec<String>,
|
||||
|
|
@ -4938,6 +4991,208 @@ mod tests {
|
|||
assert_eq!(import.state.selected_company_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_real_deactivate_player_descriptor_into_executable_runtime_record() {
|
||||
let base_state = RuntimeState {
|
||||
players: vec![
|
||||
crate::RuntimePlayer {
|
||||
player_id: 7,
|
||||
current_cash: 500,
|
||||
active: true,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
},
|
||||
crate::RuntimePlayer {
|
||||
player_id: 8,
|
||||
current_cash: 250,
|
||||
active: true,
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
},
|
||||
],
|
||||
selected_player_id: Some(7),
|
||||
..state()
|
||||
};
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
trailer_family: None,
|
||||
bridge_family: None,
|
||||
profile: None,
|
||||
candidate_availability_table: None,
|
||||
special_conditions_table: None,
|
||||
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
||||
source_kind: "packed-event-runtime-collection".to_string(),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
metadata_tag_offset: 0x7100,
|
||||
records_tag_offset: 0x7200,
|
||||
close_tag_offset: 0x7600,
|
||||
packed_state_version: 0x3e9,
|
||||
packed_state_version_hex: "0x000003e9".to_string(),
|
||||
live_id_bound: 18,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![18],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 18,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(120),
|
||||
decode_status: "parity_only".to_string(),
|
||||
payload_family: "real_packed_v1".to_string(),
|
||||
trigger_kind: Some(7),
|
||||
active: None,
|
||||
marks_collection_dirty: None,
|
||||
one_shot: Some(false),
|
||||
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 0,
|
||||
modifier_flag_0x7fa: 2,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 1,
|
||||
standalone_condition_rows: real_condition_rows(),
|
||||
negative_sentinel_scope: Some(
|
||||
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
||||
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
|
||||
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
|
||||
territory_scope_selector_is_0x63: false,
|
||||
source_row_indexes: vec![0],
|
||||
},
|
||||
),
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_deactivate_player_row(true)],
|
||||
decoded_conditions: Vec::new(),
|
||||
decoded_actions: vec![RuntimeEffect::DeactivatePlayer {
|
||||
target: RuntimePlayerTarget::ConditionTruePlayer,
|
||||
}],
|
||||
executable_import_ready: true,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let mut import = project_save_slice_overlay_to_runtime_state_import(
|
||||
&base_state,
|
||||
&save_slice,
|
||||
"real-deactivate-player-overlay",
|
||||
None,
|
||||
)
|
||||
.expect("overlay import should project");
|
||||
|
||||
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||
execute_step_command(
|
||||
&mut import.state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("real deactivate-player descriptor should execute");
|
||||
|
||||
assert!(!import.state.players[0].active);
|
||||
assert!(import.state.players[1].active);
|
||||
assert_eq!(import.state.selected_player_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_real_deactivate_player_false_row_parity_only() {
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
trailer_family: None,
|
||||
bridge_family: None,
|
||||
profile: None,
|
||||
candidate_availability_table: None,
|
||||
special_conditions_table: None,
|
||||
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
||||
source_kind: "packed-event-runtime-collection".to_string(),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
metadata_tag_offset: 0x7100,
|
||||
records_tag_offset: 0x7200,
|
||||
close_tag_offset: 0x7600,
|
||||
packed_state_version: 0x3e9,
|
||||
packed_state_version_hex: "0x000003e9".to_string(),
|
||||
live_id_bound: 19,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![19],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 19,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(120),
|
||||
decode_status: "parity_only".to_string(),
|
||||
payload_family: "real_packed_v1".to_string(),
|
||||
trigger_kind: Some(7),
|
||||
active: None,
|
||||
marks_collection_dirty: None,
|
||||
one_shot: Some(false),
|
||||
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 0,
|
||||
modifier_flag_0x7fa: 2,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![0, 0, 0, 0],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 1,
|
||||
standalone_condition_rows: real_condition_rows(),
|
||||
negative_sentinel_scope: Some(
|
||||
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
||||
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
|
||||
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
|
||||
territory_scope_selector_is_0x63: false,
|
||||
source_row_indexes: vec![0],
|
||||
},
|
||||
),
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_deactivate_player_row(false)],
|
||||
decoded_conditions: Vec::new(),
|
||||
decoded_actions: vec![],
|
||||
executable_import_ready: false,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let import = project_save_slice_to_runtime_state_import(
|
||||
&save_slice,
|
||||
"real-deactivate-player-false",
|
||||
None,
|
||||
)
|
||||
.expect("save slice should project");
|
||||
|
||||
assert!(import.state.event_runtime_records.is_empty());
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||
Some("blocked_unmapped_real_descriptor")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_real_deactivate_company_false_row_parity_only() {
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,9 @@ pub enum RuntimeEffect {
|
|||
target: RuntimePlayerTarget,
|
||||
value: i64,
|
||||
},
|
||||
DeactivatePlayer {
|
||||
target: RuntimePlayerTarget,
|
||||
},
|
||||
SetCompanyTerritoryAccess {
|
||||
target: RuntimeCompanyTarget,
|
||||
territory: RuntimeTerritoryTarget,
|
||||
|
|
@ -1144,7 +1147,8 @@ fn validate_runtime_effect(
|
|||
validate_company_target(target, valid_company_ids)?;
|
||||
validate_territory_target(territory, valid_territory_ids)?;
|
||||
}
|
||||
RuntimeEffect::SetPlayerCash { target, .. } => {
|
||||
RuntimeEffect::SetPlayerCash { target, .. }
|
||||
| RuntimeEffect::DeactivatePlayer { target } => {
|
||||
validate_player_target(target, valid_player_ids)?;
|
||||
}
|
||||
RuntimeEffect::RetireTrains {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ struct RealGroupedEffectDescriptorMetadata {
|
|||
executable_in_runtime: bool,
|
||||
}
|
||||
|
||||
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 11] = [
|
||||
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 12] = [
|
||||
RealGroupedEffectDescriptorMetadata {
|
||||
descriptor_id: 1,
|
||||
label: "Player Cash",
|
||||
|
|
@ -201,6 +201,14 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
|
|||
runtime_key: None,
|
||||
executable_in_runtime: true,
|
||||
},
|
||||
RealGroupedEffectDescriptorMetadata {
|
||||
descriptor_id: 14,
|
||||
label: "Deactivate Player",
|
||||
target_mask_bits: 0x02,
|
||||
parameter_family: "player_lifecycle_toggle",
|
||||
runtime_key: None,
|
||||
executable_in_runtime: true,
|
||||
},
|
||||
RealGroupedEffectDescriptorMetadata {
|
||||
descriptor_id: 15,
|
||||
label: "Retire Train",
|
||||
|
|
@ -2378,8 +2386,8 @@ fn parse_real_condition_row_summary(
|
|||
let comparator = ordinary_metadata
|
||||
.and_then(|_| decode_real_condition_comparator(subtype))
|
||||
.map(condition_comparator_label);
|
||||
let metric =
|
||||
ordinary_metadata.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref));
|
||||
let metric = ordinary_metadata
|
||||
.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref));
|
||||
let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes));
|
||||
let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| {
|
||||
matches!(
|
||||
|
|
@ -2409,7 +2417,10 @@ fn parse_real_condition_row_summary(
|
|||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability)
|
||||
) && candidate_name.is_none()
|
||||
}) {
|
||||
notes.push("candidate-availability condition row is missing its candidate-name side string".to_string());
|
||||
notes.push(
|
||||
"candidate-availability condition row is missing its candidate-name side string"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Some(SmpLoadedPackedEventConditionRowSummary {
|
||||
row_index,
|
||||
|
|
@ -2507,7 +2518,9 @@ fn real_ordinary_condition_metric_label(
|
|||
) -> String {
|
||||
match metadata.kind {
|
||||
RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(),
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { label }) => {
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
|
||||
label,
|
||||
}) => {
|
||||
format!("Special Condition: {label}")
|
||||
}
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => {
|
||||
|
|
@ -2522,7 +2535,9 @@ fn real_ordinary_condition_metric_label(
|
|||
}
|
||||
}
|
||||
|
||||
fn real_ordinary_condition_semantic_family(metadata: RealOrdinaryConditionMetadata) -> &'static str {
|
||||
fn real_ordinary_condition_semantic_family(
|
||||
metadata: RealOrdinaryConditionMetadata,
|
||||
) -> &'static str {
|
||||
match metadata.kind {
|
||||
RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold",
|
||||
RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold",
|
||||
|
|
@ -2685,16 +2700,17 @@ fn decode_real_condition_row(
|
|||
}
|
||||
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
|
||||
negative_sentinel_scope
|
||||
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
||||
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
|
||||
target: RuntimeTerritoryTarget::AllTerritories,
|
||||
metric,
|
||||
comparator,
|
||||
value,
|
||||
})
|
||||
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
||||
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
|
||||
target: RuntimeTerritoryTarget::AllTerritories,
|
||||
metric,
|
||||
comparator,
|
||||
value,
|
||||
})
|
||||
}
|
||||
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(metric)) => {
|
||||
negative_sentinel_scope
|
||||
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
||||
metric,
|
||||
)) => negative_sentinel_scope
|
||||
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
||||
.map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold {
|
||||
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
||||
|
|
@ -2702,8 +2718,7 @@ fn decode_real_condition_row(
|
|||
metric,
|
||||
comparator,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}),
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
|
||||
label,
|
||||
}) => Some(RuntimeCondition::SpecialConditionThreshold {
|
||||
|
|
@ -2711,15 +2726,14 @@ fn decode_real_condition_row(
|
|||
comparator,
|
||||
value,
|
||||
}),
|
||||
RealOrdinaryConditionKind::WorldState(
|
||||
RealWorldConditionKind::CandidateAvailability,
|
||||
) => row.candidate_name.as_ref().map(|name| {
|
||||
RuntimeCondition::CandidateAvailabilityThreshold {
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => row
|
||||
.candidate_name
|
||||
.as_ref()
|
||||
.map(|name| RuntimeCondition::CandidateAvailabilityThreshold {
|
||||
name: name.clone(),
|
||||
comparator,
|
||||
value,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
|
||||
Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value })
|
||||
}
|
||||
|
|
@ -2813,7 +2827,9 @@ fn runtime_candidate_availability_name(label: &str) -> String {
|
|||
.to_string()
|
||||
}
|
||||
|
||||
fn runtime_world_flag_key(descriptor_metadata: RealGroupedEffectDescriptorMetadata) -> Option<String> {
|
||||
fn runtime_world_flag_key(
|
||||
descriptor_metadata: RealGroupedEffectDescriptorMetadata,
|
||||
) -> Option<String> {
|
||||
descriptor_metadata.runtime_key.map(str::to_string)
|
||||
}
|
||||
|
||||
|
|
@ -2939,6 +2955,15 @@ fn decode_real_grouped_effect_action(
|
|||
return Some(RuntimeEffect::DeactivateCompany { target });
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
&& descriptor_metadata.descriptor_id == 14
|
||||
&& row.row_shape == "bool_toggle"
|
||||
&& row.raw_scalar_value != 0
|
||||
{
|
||||
let target = real_grouped_player_target(target_scope_ordinal)?;
|
||||
return Some(RuntimeEffect::DeactivatePlayer { target });
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
&& descriptor_metadata.descriptor_id == 16
|
||||
&& row.row_shape == "scalar_assignment"
|
||||
|
|
@ -3151,6 +3176,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
|||
| RuntimeEffect::SetSpecialCondition { .. }
|
||||
| RuntimeEffect::ConfiscateCompanyAssets { .. }
|
||||
| RuntimeEffect::DeactivateCompany { .. }
|
||||
| RuntimeEffect::DeactivatePlayer { .. }
|
||||
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
|
||||
| RuntimeEffect::RetireTrains { .. }
|
||||
| RuntimeEffect::ActivateEventRecord { .. }
|
||||
|
|
@ -8715,8 +8741,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() {
|
||||
let condition_row =
|
||||
build_real_condition_row_with_threshold(REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, 0, 2, Some("Mogul"));
|
||||
let condition_row = build_real_condition_row_with_threshold(
|
||||
REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID,
|
||||
0,
|
||||
2,
|
||||
Some("Mogul"),
|
||||
);
|
||||
let record_body = build_real_event_record(
|
||||
[b"World", b"", b"", b"", b"", b""],
|
||||
Some(RealCompactControlSpec {
|
||||
|
|
@ -8839,6 +8869,90 @@ mod tests {
|
|||
assert!(metadata.executable_in_runtime);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_up_checked_in_deactivate_player_descriptor_metadata() {
|
||||
let metadata =
|
||||
real_grouped_effect_descriptor_metadata(14).expect("descriptor metadata should exist");
|
||||
|
||||
assert_eq!(metadata.label, "Deactivate Player");
|
||||
assert_eq!(metadata.parameter_family, "player_lifecycle_toggle");
|
||||
assert_eq!(metadata.runtime_key, None);
|
||||
assert!(metadata.executable_in_runtime);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_real_deactivate_player_descriptor_from_checked_in_metadata() {
|
||||
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
||||
descriptor_id: 14,
|
||||
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,
|
||||
locomotive_name: None,
|
||||
});
|
||||
let group0_rows = vec![grouped_row];
|
||||
let record_body = build_real_event_record(
|
||||
[b"Players", b"", b"", b"", b"", b""],
|
||||
Some(RealCompactControlSpec {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 0,
|
||||
modifier_flag_0x7fa: 0,
|
||||
grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0],
|
||||
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
||||
}),
|
||||
&[],
|
||||
[&group0_rows, &[], &[], &[]],
|
||||
);
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
||||
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for word in header_words {
|
||||
bytes.extend_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(&[0x00, 0x00]);
|
||||
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
||||
bytes.extend_from_slice(&record_body);
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
|
||||
|
||||
let report = inspect_smp_bytes(&bytes);
|
||||
let summary = report
|
||||
.event_runtime_collection_summary
|
||||
.as_ref()
|
||||
.expect("event runtime collection summary should parse");
|
||||
|
||||
assert_eq!(
|
||||
summary.records[0].grouped_effect_rows[0]
|
||||
.descriptor_label
|
||||
.as_deref(),
|
||||
Some("Deactivate Player")
|
||||
);
|
||||
assert_eq!(
|
||||
summary.records[0].grouped_effect_rows[0]
|
||||
.parameter_family
|
||||
.as_deref(),
|
||||
Some("player_lifecycle_toggle")
|
||||
);
|
||||
assert_eq!(
|
||||
summary.records[0].decoded_actions,
|
||||
vec![RuntimeEffect::DeactivatePlayer {
|
||||
target: RuntimePlayerTarget::SelectedPlayer,
|
||||
}]
|
||||
);
|
||||
assert!(summary.records[0].executable_import_ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_real_world_flag_descriptor_with_checked_in_runtime_key() {
|
||||
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
||||
|
|
|
|||
|
|
@ -342,6 +342,25 @@ fn apply_runtime_effects(
|
|||
mutated_player_ids.insert(player_id);
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivatePlayer { target } => {
|
||||
let player_ids = resolve_player_target_ids(state, target, condition_context)?;
|
||||
for player_id in player_ids {
|
||||
let player = state
|
||||
.players
|
||||
.iter_mut()
|
||||
.find(|player| player.player_id == player_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"missing player_id {player_id} while applying deactivate effect"
|
||||
)
|
||||
})?;
|
||||
player.active = false;
|
||||
mutated_player_ids.insert(player_id);
|
||||
if state.selected_player_id == Some(player_id) {
|
||||
state.selected_player_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTerritoryAccess {
|
||||
target,
|
||||
territory,
|
||||
|
|
@ -1092,9 +1111,9 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
|
||||
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
|
||||
RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts,
|
||||
RuntimeTrain, RuntimeWorldRestoreState,
|
||||
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePlayer,
|
||||
RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget,
|
||||
RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState,
|
||||
};
|
||||
|
||||
fn state() -> RuntimeState {
|
||||
|
|
@ -1631,6 +1650,43 @@ mod tests {
|
|||
assert_eq!(result.service_events[0].mutated_company_ids, vec![1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deactivating_selected_player_clears_selection() {
|
||||
let mut state = RuntimeState {
|
||||
players: vec![RuntimePlayer {
|
||||
player_id: 1,
|
||||
current_cash: 500,
|
||||
active: true,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
}],
|
||||
selected_player_id: Some(1),
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 19,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
conditions: Vec::new(),
|
||||
effects: vec![RuntimeEffect::DeactivatePlayer {
|
||||
target: crate::RuntimePlayerTarget::SelectedPlayer,
|
||||
}],
|
||||
}],
|
||||
..state()
|
||||
};
|
||||
|
||||
let result = execute_step_command(
|
||||
&mut state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("deactivate player effect should succeed");
|
||||
|
||||
assert!(!state.players[0].active);
|
||||
assert_eq!(state.selected_player_id, None);
|
||||
assert_eq!(result.service_events[0].mutated_player_ids, vec![1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_track_laying_capacity_for_resolved_targets() {
|
||||
let mut state = RuntimeState {
|
||||
|
|
|
|||
|
|
@ -81,8 +81,9 @@ The highest-value next passes are now:
|
|||
first company-scoped batch already parses, summarizes, and executes through the ordinary runtime
|
||||
path when overlay context resolves its symbolic company scope: descriptor `2` `Company Cash`,
|
||||
descriptor `13` `Deactivate Company`, and descriptor `16` `Company Track Pieces Buildable`
|
||||
- descriptor `1` `Player Cash` now joins that executable real batch through the same ordinary
|
||||
runtime path, backed by the minimal player runtime and overlay-import context
|
||||
- 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
|
||||
- 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`
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ Implemented today:
|
|||
through overlay-backed runtime context
|
||||
- exact named-territory binding now lowers candidate-name ordinary rows onto tracked territory
|
||||
names, a minimal player runtime now carries selected-player and role context, and real descriptor
|
||||
`1` = `Player Cash` now imports and executes through the ordinary runtime path
|
||||
`1` = `Player Cash` and descriptor `14` = `Deactivate Player` now import and execute through the
|
||||
ordinary runtime path
|
||||
- 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
|
||||
|
|
@ -65,10 +66,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, train, player, and numeric-threshold batches, with the world-side frontier now shifted
|
||||
away from the first world-flag unlock and onto broader descriptor and condition recovery for later
|
||||
state families that still need stronger checked-in metadata. Richer runtime ownership should still
|
||||
be added only where a later descriptor or condition family needs more than the current event-owned
|
||||
whole-game, train, player, and numeric-threshold batches. Richer runtime ownership should still be
|
||||
added only where a later descriptor or condition family needs more than the current event-owned
|
||||
roster.
|
||||
|
||||
## Why This Boundary
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"fixture_id": "packed-event-deactivate-player-overlay-fixture",
|
||||
"source": {
|
||||
"kind": "captured-runtime",
|
||||
"description": "Fixture proving descriptor 14 Deactivate Player imports and executes through the ordinary runtime path."
|
||||
},
|
||||
"state_import_path": "packed-event-deactivate-player-overlay.json",
|
||||
"commands": [
|
||||
{
|
||||
"kind": "service_trigger_kind",
|
||||
"trigger_kind": 6
|
||||
}
|
||||
],
|
||||
"expected_summary": {
|
||||
"calendar_projection_source": "base-snapshot-preserved",
|
||||
"calendar_projection_is_placeholder": false,
|
||||
"company_count": 3,
|
||||
"player_count": 2,
|
||||
"territory_count": 2,
|
||||
"packed_event_collection_present": true,
|
||||
"packed_event_record_count": 1,
|
||||
"packed_event_decoded_record_count": 1,
|
||||
"packed_event_imported_runtime_record_count": 1,
|
||||
"event_runtime_record_count": 1,
|
||||
"total_event_record_service_count": 1,
|
||||
"total_trigger_dispatch_count": 1
|
||||
},
|
||||
"expected_state_fragment": {
|
||||
"selected_player_id": null,
|
||||
"players": [
|
||||
{
|
||||
"player_id": 1,
|
||||
"current_cash": 500,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"player_id": 2,
|
||||
"current_cash": 250,
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"packed_event_collection": {
|
||||
"records": [
|
||||
{
|
||||
"import_outcome": "imported",
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "deactivate_player",
|
||||
"target": {
|
||||
"kind": "selected_player"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"import_id": "packed-event-deactivate-player-overlay",
|
||||
"source": {
|
||||
"description": "Overlay import combining player runtime context with the real Deactivate Player descriptor sample."
|
||||
},
|
||||
"base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json",
|
||||
"save_slice_path": "packed-event-deactivate-player-save-slice.json"
|
||||
}
|
||||
125
fixtures/runtime/packed-event-deactivate-player-save-slice.json
Normal file
125
fixtures/runtime/packed-event-deactivate-player-save-slice.json
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{
|
||||
"format_version": 1,
|
||||
"save_slice_id": "packed-event-deactivate-player-save-slice",
|
||||
"source": {
|
||||
"description": "Tracked save-slice document with a real player-scoped Deactivate Player row.",
|
||||
"original_save_filename": "captured-deactivate-player.gms",
|
||||
"original_save_sha256": "deactivate-player-sample-sha256",
|
||||
"notes": [
|
||||
"tracked as JSON save-slice document rather than raw .smp",
|
||||
"proves descriptor 14 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": 48,
|
||||
"live_record_count": 1,
|
||||
"live_entry_ids": [48],
|
||||
"decoded_record_count": 1,
|
||||
"imported_runtime_record_count": 1,
|
||||
"records": [
|
||||
{
|
||||
"record_index": 0,
|
||||
"live_entry_id": 48,
|
||||
"payload_offset": 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": 2,
|
||||
"grouped_target_scope_ordinals_0x7fb": [0, 1, 1, 1],
|
||||
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
|
||||
"summary_toggle_0x800": 1,
|
||||
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
|
||||
},
|
||||
"text_bands": [],
|
||||
"standalone_condition_row_count": 1,
|
||||
"standalone_condition_rows": [
|
||||
{
|
||||
"row_index": 0,
|
||||
"raw_condition_id": -1,
|
||||
"subtype": 4,
|
||||
"flag_bytes": [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72],
|
||||
"candidate_name": null,
|
||||
"notes": [
|
||||
"negative sentinel-style condition row id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"negative_sentinel_scope": {
|
||||
"company_test_scope": "disabled",
|
||||
"player_test_scope": "selected_player_only",
|
||||
"territory_scope_selector_is_0x63": false,
|
||||
"source_row_indexes": [0]
|
||||
},
|
||||
"grouped_effect_row_counts": [1, 0, 0, 0],
|
||||
"grouped_effect_rows": [
|
||||
{
|
||||
"group_index": 0,
|
||||
"row_index": 0,
|
||||
"descriptor_id": 14,
|
||||
"descriptor_label": "Deactivate Player",
|
||||
"target_mask_bits": 2,
|
||||
"parameter_family": "player_lifecycle_toggle",
|
||||
"opcode": 1,
|
||||
"raw_scalar_value": 1,
|
||||
"value_byte_0x09": 0,
|
||||
"value_dword_0x0d": 0,
|
||||
"value_byte_0x11": 0,
|
||||
"value_byte_0x12": 0,
|
||||
"value_word_0x14": 0,
|
||||
"value_word_0x16": 0,
|
||||
"row_shape": "bool_toggle",
|
||||
"semantic_family": "bool_toggle",
|
||||
"semantic_preview": "Set Deactivate Player to TRUE",
|
||||
"locomotive_name": null,
|
||||
"notes": []
|
||||
}
|
||||
],
|
||||
"decoded_conditions": [],
|
||||
"decoded_actions": [
|
||||
{
|
||||
"kind": "deactivate_player",
|
||||
"target": {
|
||||
"kind": "condition_true_player"
|
||||
}
|
||||
}
|
||||
],
|
||||
"executable_import_ready": true,
|
||||
"notes": [
|
||||
"decoded from grounded real 0x4e9a row framing",
|
||||
"player-side negative-sentinel scope lowers deactivate-player at import time"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"notes": [
|
||||
"real deactivate player descriptor sample"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue