Execute real packed event world and train descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-15 20:20:25 -07:00
commit e481274243
31 changed files with 3287 additions and 206 deletions

View file

@ -21,10 +21,12 @@ parallel packed executor. The first grounded condition-side unlock now exists fo
`raw_condition_id = -1` company scopes, and the first ordinary nonnegative condition batch now `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 executes too: numeric-threshold company finance, company track, aggregate territory track, and
company-territory track rows can import through overlay-backed runtime context. Exact company-territory track rows can import through overlay-backed runtime context. Exact
named-territory binding now executes, while descriptor `3` `Territory - Allow All` remains the named-territory binding now executes, and the runtime now also carries the minimal event-owned
explicit parity-only descriptor frontier. Mixed supported/unsupported real rows still stay train roster and opaque economic-status lane needed for real descriptors `8` `Economic Status`, `9`
parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer `Confiscate All`, and `15` `Retire Train` to execute through the same path. Descriptor `3`
the main execution milestone. `Territory - Allow All` remains the explicit parity-only descriptor frontier. Mixed
supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and
integration tooling, but it is no longer the main execution milestone.
## Project Docs ## Project Docs

View file

@ -176,6 +176,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -343,6 +344,7 @@ mod tests {
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,

View file

@ -54,6 +54,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>, pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)] #[serde(default)]
pub world_restore_economic_status_code: Option<i32>,
#[serde(default)]
pub world_restore_absolute_counter_restore_kind: Option<String>, pub world_restore_absolute_counter_restore_kind: Option<String>,
#[serde(default)] #[serde(default)]
pub world_restore_absolute_counter_adjustment_context: Option<String>, pub world_restore_absolute_counter_adjustment_context: Option<String>,
@ -66,6 +68,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub player_count: Option<usize>, pub player_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub train_count: Option<usize>,
#[serde(default)]
pub active_train_count: Option<usize>,
#[serde(default)]
pub retired_train_count: Option<usize>,
#[serde(default)]
pub territory_count: Option<usize>, pub territory_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub company_territory_track_count: Option<usize>, pub company_territory_track_count: Option<usize>,
@ -116,6 +124,16 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_territory_policy_descriptor_count: Option<usize>, pub packed_event_blocked_territory_policy_descriptor_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_train_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_train_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_confiscation_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_structural_only_count: Option<usize>, pub packed_event_blocked_structural_only_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub event_runtime_record_count: Option<usize>, pub event_runtime_record_count: Option<usize>,
@ -321,6 +339,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(code) = self.world_restore_economic_status_code {
if actual.world_restore_economic_status_code != Some(code) {
mismatches.push(format!(
"world_restore_economic_status_code mismatch: expected {code}, got {:?}",
actual.world_restore_economic_status_code
));
}
}
if let Some(kind) = &self.world_restore_absolute_counter_restore_kind { if let Some(kind) = &self.world_restore_absolute_counter_restore_kind {
if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) { if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) {
mismatches.push(format!( mismatches.push(format!(
@ -373,6 +399,30 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.train_count {
if actual.train_count != count {
mismatches.push(format!(
"train_count mismatch: expected {count}, got {}",
actual.train_count
));
}
}
if let Some(count) = self.active_train_count {
if actual.active_train_count != count {
mismatches.push(format!(
"active_train_count mismatch: expected {count}, got {}",
actual.active_train_count
));
}
}
if let Some(count) = self.retired_train_count {
if actual.retired_train_count != count {
mismatches.push(format!(
"retired_train_count mismatch: expected {count}, got {}",
actual.retired_train_count
));
}
}
if let Some(count) = self.territory_count { if let Some(count) = self.territory_count {
if actual.territory_count != count { if actual.territory_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -573,6 +623,46 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_train_context_count {
if actual.packed_event_blocked_missing_train_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_train_territory_context_count {
if actual.packed_event_blocked_missing_train_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_territory_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_confiscation_variant_count {
if actual.packed_event_blocked_confiscation_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_confiscation_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_confiscation_variant_count
));
}
}
if let Some(count) = self.packed_event_blocked_retire_train_variant_count {
if actual.packed_event_blocked_retire_train_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_variant_count
));
}
}
if let Some(count) = self.packed_event_blocked_retire_train_scope_count {
if actual.packed_event_blocked_retire_train_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_scope_count
));
}
}
if let Some(count) = self.packed_event_blocked_structural_only_count { if let Some(count) = self.packed_event_blocked_structural_only_count {
if actual.packed_event_blocked_structural_only_count != count { if actual.packed_event_blocked_structural_only_count != count {
mismatches.push(format!( mismatches.push(format!(

File diff suppressed because it is too large Load diff

View file

@ -44,7 +44,7 @@ pub use runtime::{
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric, RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain,
RuntimeWorldRestoreState, RuntimeWorldRestoreState,
}; };
pub use smp::{ pub use smp::{

View file

@ -96,6 +96,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,

View file

@ -83,6 +83,24 @@ pub struct RuntimePlayer {
pub controller_kind: RuntimeCompanyControllerKind, pub controller_kind: RuntimeCompanyControllerKind,
} }
fn runtime_train_default_active() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeTrain {
pub train_id: u32,
pub owner_company_id: u32,
#[serde(default)]
pub territory_id: Option<u32>,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default = "runtime_train_default_active")]
pub active: bool,
#[serde(default)]
pub retired: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCompanyTarget { pub enum RuntimeCompanyTarget {
@ -213,6 +231,9 @@ pub enum RuntimeEffect {
key: String, key: String,
value: bool, value: bool,
}, },
SetEconomicStatusCode {
value: i32,
},
SetCompanyCash { SetCompanyCash {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
value: i64, value: i64,
@ -221,6 +242,9 @@ pub enum RuntimeEffect {
target: RuntimePlayerTarget, target: RuntimePlayerTarget,
value: i64, value: i64,
}, },
ConfiscateCompanyAssets {
target: RuntimeCompanyTarget,
},
DeactivateCompany { DeactivateCompany {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
}, },
@ -228,6 +252,14 @@ pub enum RuntimeEffect {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
value: Option<u32>, value: Option<u32>,
}, },
RetireTrains {
#[serde(default)]
company_target: Option<RuntimeCompanyTarget>,
#[serde(default)]
territory_target: Option<RuntimeTerritoryTarget>,
#[serde(default)]
locomotive_name: Option<String>,
},
AdjustCompanyCash { AdjustCompanyCash {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
delta: i64, delta: i64,
@ -527,6 +559,8 @@ pub struct RuntimeWorldRestoreState {
#[serde(default)] #[serde(default)]
pub ai_ignore_territories_at_startup_enabled: Option<bool>, pub ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)] #[serde(default)]
pub economic_status_code: Option<i32>,
#[serde(default)]
pub absolute_counter_restore_kind: Option<String>, pub absolute_counter_restore_kind: Option<String>,
#[serde(default)] #[serde(default)]
pub absolute_counter_adjustment_context: Option<String>, pub absolute_counter_adjustment_context: Option<String>,
@ -552,6 +586,8 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub selected_player_id: Option<u32>, pub selected_player_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub trains: Vec<RuntimeTrain>,
#[serde(default)]
pub territories: Vec<RuntimeTerritory>, pub territories: Vec<RuntimeTerritory>,
#[serde(default)] #[serde(default)]
pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>, pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>,
@ -639,6 +675,42 @@ impl RuntimeState {
} }
} }
} }
let mut seen_train_ids = BTreeSet::new();
for train in &self.trains {
if !seen_train_ids.insert(train.train_id) {
return Err(format!("duplicate train_id {}", train.train_id));
}
if !seen_company_ids.contains(&train.owner_company_id) {
return Err(format!(
"train_id {} references unknown owner_company_id {}",
train.train_id, train.owner_company_id
));
}
if let Some(territory_id) = train.territory_id {
if !seen_territory_ids.contains(&territory_id) {
return Err(format!(
"train_id {} references unknown territory_id {}",
train.train_id, territory_id
));
}
}
if train.retired && train.active {
return Err(format!(
"train_id {} cannot be active and retired at the same time",
train.train_id
));
}
if train
.locomotive_name
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(format!(
"train_id {} has an empty locomotive_name",
train.train_id
));
}
}
for entry in &self.company_territory_track_piece_counts { for entry in &self.company_territory_track_piece_counts {
if !seen_company_ids.contains(&entry.company_id) { if !seen_company_ids.contains(&entry.company_id) {
return Err(format!( return Err(format!(
@ -1009,7 +1081,9 @@ fn validate_runtime_effect(
return Err("key must not be empty".to_string()); return Err("key must not be empty".to_string());
} }
} }
RuntimeEffect::SetEconomicStatusCode { .. } => {}
RuntimeEffect::SetCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::ConfiscateCompanyAssets { target }
| RuntimeEffect::DeactivateCompany { target } | RuntimeEffect::DeactivateCompany { target }
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. } | RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. }
@ -1019,6 +1093,30 @@ fn validate_runtime_effect(
RuntimeEffect::SetPlayerCash { target, .. } => { RuntimeEffect::SetPlayerCash { target, .. } => {
validate_player_target(target, valid_player_ids)?; validate_player_target(target, valid_player_ids)?;
} }
RuntimeEffect::RetireTrains {
company_target,
territory_target,
locomotive_name,
} => {
if let Some(company_target) = company_target {
validate_company_target(company_target, valid_company_ids)?;
}
if let Some(territory_target) = territory_target {
validate_territory_target(territory_target, valid_territory_ids)?;
}
if company_target.is_none() && territory_target.is_none() && locomotive_name.is_none() {
return Err(
"retire_trains requires at least one company_target, territory_target, or locomotive_name filter"
.to_string(),
);
}
if locomotive_name
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err("locomotive_name must not be empty".to_string());
}
}
RuntimeEffect::SetCandidateAvailability { name, .. } => { RuntimeEffect::SetCandidateAvailability { name, .. } => {
if name.trim().is_empty() { if name.trim().is_empty() {
return Err("name must not be empty".to_string()); return Err("name must not be empty".to_string());
@ -1092,9 +1190,7 @@ fn validate_runtime_condition(
validate_territory_target(target, valid_territory_ids) validate_territory_target(target, valid_territory_ids)
} }
RuntimeCondition::CompanyTerritoryNumericThreshold { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, target, territory, ..
territory,
..
} => { } => {
validate_company_target(target, valid_company_ids)?; validate_company_target(target, valid_company_ids)?;
validate_territory_target(territory, valid_territory_ids) validate_territory_target(territory, valid_territory_ids)
@ -1216,6 +1312,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1255,6 +1352,7 @@ mod tests {
disable_train_crashes_enabled: Some(false), disable_train_crashes_enabled: Some(false),
disable_train_crashes_and_breakdowns_enabled: Some(false), disable_train_crashes_and_breakdowns_enabled: Some(false),
ai_ignore_territories_at_startup_enabled: Some(false), ai_ignore_territories_at_startup_enabled: Some(false),
economic_status_code: None,
absolute_counter_restore_kind: Some( absolute_counter_restore_kind: Some(
"mode-adjusted-selected-year-lane".to_string(), "mode-adjusted-selected-year-lane".to_string(),
), ),
@ -1267,6 +1365,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1306,6 +1405,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1358,6 +1458,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1410,6 +1511,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
@ -1513,6 +1615,7 @@ mod tests {
selected_company_id: Some(2), selected_company_id: Some(2),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1552,6 +1655,209 @@ mod tests {
selected_company_id: Some(1), selected_company_id: Some(1),
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_duplicate_train_ids() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: vec![
RuntimeTrain {
train_id: 7,
owner_company_id: 1,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 7,
owner_company_id: 1,
territory_id: None,
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
},
],
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_train_with_unknown_owner_company() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 2,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
}],
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_train_with_unknown_territory() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 1,
territory_id: Some(9),
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
}],
territories: vec![RuntimeTerritory {
territory_id: 1,
name: Some("Appalachia".to_string()),
track_piece_counts: RuntimeTrackPieceCounts::default(),
}],
company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_train_marked_active_and_retired() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: vec![RuntimeTrain {
train_id: 7,
owner_company_id: 1,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: true,
}],
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,

View file

@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::{ use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
}; };
@ -154,14 +154,14 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
label: "Economic Status", label: "Economic Status",
target_mask_bits: 0x08, target_mask_bits: 0x08,
parameter_family: "whole_game_state_enum", parameter_family: "whole_game_state_enum",
executable_in_runtime: false, executable_in_runtime: true,
}, },
RealGroupedEffectDescriptorMetadata { RealGroupedEffectDescriptorMetadata {
descriptor_id: 9, descriptor_id: 9,
label: "Confiscate All", label: "Confiscate All",
target_mask_bits: 0x01, target_mask_bits: 0x01,
parameter_family: "company_confiscation_variant", parameter_family: "company_confiscation_variant",
executable_in_runtime: false, executable_in_runtime: true,
}, },
RealGroupedEffectDescriptorMetadata { RealGroupedEffectDescriptorMetadata {
descriptor_id: 13, descriptor_id: 13,
@ -175,7 +175,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
label: "Retire Train", label: "Retire Train",
target_mask_bits: 0x0d, target_mask_bits: 0x0d,
parameter_family: "company_or_territory_asset_toggle", parameter_family: "company_or_territory_asset_toggle",
executable_in_runtime: false, executable_in_runtime: true,
}, },
RealGroupedEffectDescriptorMetadata { RealGroupedEffectDescriptorMetadata {
descriptor_id: 16, descriptor_id: 16,
@ -269,7 +269,9 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 22] = [
RealOrdinaryConditionMetadata { RealOrdinaryConditionMetadata {
raw_condition_id: 2316, raw_condition_id: 2316,
label: "Territory Transition Track Pieces", label: "Territory Transition Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesTransition), metric: RealOrdinaryConditionMetric::Territory(
RuntimeTerritoryMetric::TrackPiecesTransition,
),
}, },
RealOrdinaryConditionMetadata { RealOrdinaryConditionMetadata {
raw_condition_id: 2317, raw_condition_id: 2317,
@ -279,7 +281,9 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 22] = [
RealOrdinaryConditionMetadata { RealOrdinaryConditionMetadata {
raw_condition_id: 2318, raw_condition_id: 2318,
label: "Territory Non-Electric Track Pieces", label: "Territory Non-Electric Track Pieces",
metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesNonElectric), metric: RealOrdinaryConditionMetric::Territory(
RuntimeTerritoryMetric::TrackPiecesNonElectric,
),
}, },
RealOrdinaryConditionMetadata { RealOrdinaryConditionMetadata {
raw_condition_id: 2323, raw_condition_id: 2323,
@ -2096,14 +2100,36 @@ fn parse_real_event_runtime_record_summary(
)?); )?);
} }
} }
if let Some(control) = compact_control.as_ref() {
for row in &mut grouped_effect_rows {
if row.descriptor_id != 15
|| row.row_shape != "bool_toggle"
|| row.raw_scalar_value == 0
{
continue;
}
let company_target_present = control
.grouped_target_scope_ordinals_0x7fb
.get(row.group_index)
.copied()
.and_then(real_grouped_company_target)
.is_some();
let territory_target_present = control
.grouped_territory_selectors_0x80f
.get(row.group_index)
.is_some_and(|selector| *selector >= 0);
if !company_target_present && !territory_target_present {
row.notes
.push("retire train row is missing company and territory scope".to_string());
}
}
}
let negative_sentinel_scope = compact_control.as_ref().and_then(|control| { let negative_sentinel_scope = compact_control.as_ref().and_then(|control| {
derive_negative_sentinel_scope_summary(&standalone_condition_rows, control) derive_negative_sentinel_scope_summary(&standalone_condition_rows, control)
}); });
let decoded_conditions = decode_real_condition_rows( let decoded_conditions =
&standalone_condition_rows, decode_real_condition_rows(&standalone_condition_rows, negative_sentinel_scope.as_ref());
negative_sentinel_scope.as_ref(),
);
let decoded_actions = compact_control let decoded_actions = compact_control
.as_ref() .as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control)) .map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
@ -2253,7 +2279,10 @@ fn parse_real_condition_row_summary(
notes.push("condition row carries candidate-name side string".to_string()); notes.push("condition row carries candidate-name side string".to_string());
} }
if ordinary_metadata.is_none() && raw_condition_id >= 0 { if ordinary_metadata.is_none() && raw_condition_id >= 0 {
notes.push("ordinary condition id is not yet recovered in the checked-in condition table".to_string()); notes.push(
"ordinary condition id is not yet recovered in the checked-in condition table"
.to_string(),
);
} }
Some(SmpLoadedPackedEventConditionRowSummary { Some(SmpLoadedPackedEventConditionRowSummary {
row_index, row_index,
@ -2462,24 +2491,23 @@ fn decode_real_condition_row(
let comparator = decode_real_condition_comparator(row.subtype)?; let comparator = decode_real_condition_comparator(row.subtype)?;
let value = decode_real_condition_threshold(&row.flag_bytes)?; let value = decode_real_condition_threshold(&row.flag_bytes)?;
match metadata.metric { match metadata.metric {
RealOrdinaryConditionMetric::Company(metric) => Some(RuntimeCondition::CompanyNumericThreshold { RealOrdinaryConditionMetric::Company(metric) => {
Some(RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany, target: RuntimeCompanyTarget::ConditionTrueCompany,
metric, metric,
comparator, comparator,
value, value,
}), })
RealOrdinaryConditionMetric::Territory(metric) => { }
negative_sentinel_scope RealOrdinaryConditionMetric::Territory(metric) => negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63) .filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::TerritoryNumericThreshold { .map(|_| RuntimeCondition::TerritoryNumericThreshold {
target: RuntimeTerritoryTarget::AllTerritories, target: RuntimeTerritoryTarget::AllTerritories,
metric, metric,
comparator, comparator,
value, value,
}) }),
} RealOrdinaryConditionMetric::CompanyTerritory(metric) => negative_sentinel_scope
RealOrdinaryConditionMetric::CompanyTerritory(metric) => {
negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63) .filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold { .map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany, target: RuntimeCompanyTarget::ConditionTrueCompany,
@ -2487,8 +2515,7 @@ fn decode_real_condition_row(
metric, metric,
comparator, comparator,
value, value,
}) }),
}
} }
} }
@ -2616,6 +2643,24 @@ fn decode_real_grouped_effect_action(
}); });
} }
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 8
&& row.row_shape == "scalar_assignment"
{
return Some(RuntimeEffect::SetEconomicStatusCode {
value: row.raw_scalar_value,
});
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 9
&& row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0
{
let target = real_grouped_company_target(target_scope_ordinal)?;
return Some(RuntimeEffect::ConfiscateCompanyAssets { target });
}
if descriptor_metadata.executable_in_runtime if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 13 && descriptor_metadata.descriptor_id == 13
&& row.row_shape == "bool_toggle" && row.row_shape == "bool_toggle"
@ -2637,6 +2682,30 @@ fn decode_real_grouped_effect_action(
}); });
} }
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 15
&& row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0
{
let company_target = real_grouped_company_target(target_scope_ordinal);
let territory_target = compact_control
.grouped_territory_selectors_0x80f
.get(row.group_index)
.copied()
.filter(|selector| *selector >= 0)
.map(|selector| RuntimeTerritoryTarget::Ids {
ids: vec![selector as u32],
});
if company_target.is_none() && territory_target.is_none() {
return None;
}
return Some(RuntimeEffect::RetireTrains {
company_target,
territory_target,
locomotive_name: row.locomotive_name.clone(),
});
}
None None
} }
@ -2808,10 +2877,13 @@ fn parse_optional_u16_len_prefixed_string(
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
match effect { match effect {
RuntimeEffect::SetWorldFlag { .. } RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ConfiscateCompanyAssets { .. }
| RuntimeEffect::DeactivateCompany { .. } | RuntimeEffect::DeactivateCompany { .. }
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. } | RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
| RuntimeEffect::RetireTrains { .. }
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => true, | RuntimeEffect::RemoveEventRecord { .. } => true,

View file

@ -6,8 +6,7 @@ use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget,
RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget,
RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrackMetric, RuntimeTrackPieceCounts, calendar::BoundaryEventKind,
calendar::BoundaryEventKind,
}; };
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -312,6 +311,9 @@ fn apply_runtime_effects(
RuntimeEffect::SetWorldFlag { key, value } => { RuntimeEffect::SetWorldFlag { key, value } => {
state.world_flags.insert(key.clone(), *value); state.world_flags.insert(key.clone(), *value);
} }
RuntimeEffect::SetEconomicStatusCode { value } => {
state.world_restore.economic_status_code = Some(*value);
}
RuntimeEffect::SetCompanyCash { target, value } => { RuntimeEffect::SetCompanyCash { target, value } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?; let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids { for company_id in company_ids {
@ -340,6 +342,28 @@ fn apply_runtime_effects(
mutated_player_ids.insert(player_id); mutated_player_ids.insert(player_id);
} }
} }
RuntimeEffect::ConfiscateCompanyAssets { target } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids.iter().copied() {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying confiscate effect"
)
})?;
company.current_cash = 0;
company.debt = 0;
company.active = false;
mutated_company_ids.insert(company_id);
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
}
}
retire_matching_trains(&mut state.trains, Some(&company_ids), None, None);
}
RuntimeEffect::DeactivateCompany { target } => { RuntimeEffect::DeactivateCompany { target } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?; let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids { for company_id in company_ids {
@ -375,6 +399,26 @@ fn apply_runtime_effects(
mutated_company_ids.insert(company_id); mutated_company_ids.insert(company_id);
} }
} }
RuntimeEffect::RetireTrains {
company_target,
territory_target,
locomotive_name,
} => {
let company_ids = company_target
.as_ref()
.map(|target| resolve_company_target_ids(state, target, condition_context))
.transpose()?;
let territory_ids = territory_target
.as_ref()
.map(|target| resolve_territory_target_ids(state, target))
.transpose()?;
retire_matching_trains(
&mut state.trains,
company_ids.as_ref(),
territory_ids.as_ref(),
locomotive_name.as_deref(),
);
}
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?; let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids { for company_id in company_ids {
@ -523,14 +567,18 @@ fn evaluate_record_conditions(
let matching = resolved let matching = resolved
.into_iter() .into_iter()
.filter(|company_id| { .filter(|company_id| {
state.companies.iter().find(|company| company.company_id == *company_id).is_some_and( state
|company| compare_condition_value( .companies
.iter()
.find(|company| company.company_id == *company_id)
.is_some_and(|company| {
compare_condition_value(
company_metric_value(company, *metric), company_metric_value(company, *metric),
*comparator, *comparator,
*value, *value,
),
) )
}) })
})
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
if matching.is_empty() { if matching.is_empty() {
return Ok(None); return Ok(None);
@ -597,10 +645,7 @@ fn evaluate_record_conditions(
})) }))
} }
fn intersect_company_matches( fn intersect_company_matches(company_matches: &mut Option<BTreeSet<u32>>, next: BTreeSet<u32>) {
company_matches: &mut Option<BTreeSet<u32>>,
next: BTreeSet<u32>,
) {
match company_matches { match company_matches {
Some(existing) => { Some(existing) => {
existing.retain(|company_id| next.contains(company_id)); existing.retain(|company_id| next.contains(company_id));
@ -790,7 +835,11 @@ fn resolve_player_target_ids(
if condition_context.matching_player_ids.is_empty() { if condition_context.matching_player_ids.is_empty() {
Err("target requires player condition-evaluation context".to_string()) Err("target requires player condition-evaluation context".to_string())
} else { } else {
Ok(condition_context.matching_player_ids.iter().copied().collect()) Ok(condition_context
.matching_player_ids
.iter()
.copied()
.collect())
} }
} }
} }
@ -801,9 +850,11 @@ fn resolve_territory_target_ids(
target: &RuntimeTerritoryTarget, target: &RuntimeTerritoryTarget,
) -> Result<Vec<u32>, String> { ) -> Result<Vec<u32>, String> {
match target { match target {
RuntimeTerritoryTarget::AllTerritories => { RuntimeTerritoryTarget::AllTerritories => Ok(state
Ok(state.territories.iter().map(|territory| territory.territory_id).collect()) .territories
} .iter()
.map(|territory| territory.territory_id)
.collect()),
RuntimeTerritoryTarget::Ids { ids } => { RuntimeTerritoryTarget::Ids { ids } => {
let known_ids = state let known_ids = state
.territories .territories
@ -812,7 +863,9 @@ fn resolve_territory_target_ids(
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
for territory_id in ids { for territory_id in ids {
if !known_ids.contains(territory_id) { if !known_ids.contains(territory_id) {
return Err(format!("territory target references unknown territory_id {territory_id}")); return Err(format!(
"territory target references unknown territory_id {territory_id}"
));
} }
} }
Ok(ids.clone()) Ok(ids.clone())
@ -832,9 +885,7 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
RuntimeCompanyMetric::TrackPiecesTransition => { RuntimeCompanyMetric::TrackPiecesTransition => {
i64::from(company.track_piece_counts.transition) i64::from(company.track_piece_counts.transition)
} }
RuntimeCompanyMetric::TrackPiecesElectric => { RuntimeCompanyMetric::TrackPiecesElectric => i64::from(company.track_piece_counts.electric),
i64::from(company.track_piece_counts.electric)
}
RuntimeCompanyMetric::TrackPiecesNonElectric => { RuntimeCompanyMetric::TrackPiecesNonElectric => {
i64::from(company.track_piece_counts.non_electric) i64::from(company.track_piece_counts.non_electric)
} }
@ -846,7 +897,8 @@ fn territory_metric_value(
territory_ids: &[u32], territory_ids: &[u32],
metric: RuntimeTerritoryMetric, metric: RuntimeTerritoryMetric,
) -> i64 { ) -> i64 {
state.territories state
.territories
.iter() .iter()
.filter(|territory| territory_ids.contains(&territory.territory_id)) .filter(|territory| territory_ids.contains(&territory.territory_id))
.map(|territory| { .map(|territory| {
@ -864,9 +916,12 @@ fn company_territory_metric_value(
territory_ids: &[u32], territory_ids: &[u32],
metric: RuntimeTrackMetric, metric: RuntimeTrackMetric,
) -> i64 { ) -> i64 {
state.company_territory_track_piece_counts state
.company_territory_track_piece_counts
.iter() .iter()
.filter(|entry| entry.company_id == company_id && territory_ids.contains(&entry.territory_id)) .filter(|entry| {
entry.company_id == company_id && territory_ids.contains(&entry.territory_id)
})
.map(|entry| track_piece_metric_value(entry.track_piece_counts, metric)) .map(|entry| track_piece_metric_value(entry.track_piece_counts, metric))
.sum() .sum()
} }
@ -920,6 +975,34 @@ fn apply_u64_delta(current: u64, delta: i64, company_id: u32) -> Result<u64, Str
} }
} }
fn retire_matching_trains(
trains: &mut [crate::RuntimeTrain],
company_ids: Option<&Vec<u32>>,
territory_ids: Option<&Vec<u32>>,
locomotive_name: Option<&str>,
) {
for train in trains.iter_mut() {
if !train.active || train.retired {
continue;
}
if company_ids.is_some_and(|company_ids| !company_ids.contains(&train.owner_company_id)) {
continue;
}
if territory_ids.is_some_and(|territory_ids| {
!train
.territory_id
.is_some_and(|territory_id| territory_ids.contains(&territory_id))
}) {
continue;
}
if locomotive_name.is_some_and(|name| train.locomotive_name.as_deref() != Some(name)) {
continue;
}
train.active = false;
train.retired = true;
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -928,7 +1011,8 @@ mod tests {
use crate::{ use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeWorldRestoreState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts,
RuntimeTrain, RuntimeWorldRestoreState,
}; };
fn state() -> RuntimeState { fn state() -> RuntimeState {
@ -957,6 +1041,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,
@ -1927,4 +2012,177 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
} }
#[test]
fn applies_economic_status_code_effect() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 90,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetEconomicStatusCode { value: 3 }],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("economic-status effect should succeed");
assert_eq!(state.world_restore.economic_status_code, Some(3));
}
#[test]
fn confiscate_company_assets_zeros_company_and_retires_owned_trains() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 50,
debt: 7,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 80,
debt: 9,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
},
],
selected_company_id: Some(1),
trains: vec![
RuntimeTrain {
train_id: 10,
owner_company_id: 1,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 11,
owner_company_id: 2,
territory_id: None,
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 91,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::ConfiscateCompanyAssets {
target: RuntimeCompanyTarget::SelectedCompany,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("confiscation effect should succeed");
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert!(!state.companies[0].active);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert!(!state.trains[1].retired);
}
#[test]
fn retire_trains_respects_company_territory_and_locomotive_filters() {
let mut state = RuntimeState {
territories: vec![
RuntimeTerritory {
territory_id: 7,
name: Some("Appalachia".to_string()),
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
RuntimeTerritory {
territory_id: 8,
name: Some("Great Plains".to_string()),
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
],
trains: vec![
RuntimeTrain {
train_id: 10,
owner_company_id: 1,
territory_id: Some(7),
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 11,
owner_company_id: 1,
territory_id: Some(7),
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
},
RuntimeTrain {
train_id: 12,
owner_company_id: 1,
territory_id: Some(8),
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 92,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::RetireTrains {
company_target: Some(RuntimeCompanyTarget::SelectedCompany),
territory_target: Some(RuntimeTerritoryTarget::Ids { ids: vec![7] }),
locomotive_name: Some("Mikado".to_string()),
}],
}],
selected_company_id: Some(1),
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("retire-trains effect should succeed");
assert!(state.trains[0].retired);
assert!(!state.trains[1].retired);
assert!(!state.trains[2].retired);
}
} }

View file

@ -24,12 +24,16 @@ pub struct RuntimeSummary {
pub world_restore_disable_train_crashes_enabled: Option<bool>, pub world_restore_disable_train_crashes_enabled: Option<bool>,
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>, pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>, pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
pub world_restore_economic_status_code: Option<i32>,
pub world_restore_absolute_counter_restore_kind: Option<String>, pub world_restore_absolute_counter_restore_kind: Option<String>,
pub world_restore_absolute_counter_adjustment_context: Option<String>, pub world_restore_absolute_counter_adjustment_context: Option<String>,
pub metadata_count: usize, pub metadata_count: usize,
pub company_count: usize, pub company_count: usize,
pub active_company_count: usize, pub active_company_count: usize,
pub player_count: usize, pub player_count: usize,
pub train_count: usize,
pub active_train_count: usize,
pub retired_train_count: usize,
pub territory_count: usize, pub territory_count: usize,
pub company_territory_track_count: usize, pub company_territory_track_count: usize,
pub packed_event_collection_present: bool, pub packed_event_collection_present: bool,
@ -55,6 +59,11 @@ pub struct RuntimeSummary {
pub packed_event_blocked_missing_compact_control_count: usize, pub packed_event_blocked_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_territory_policy_descriptor_count: usize, pub packed_event_blocked_territory_policy_descriptor_count: usize,
pub packed_event_blocked_missing_train_context_count: usize,
pub packed_event_blocked_missing_train_territory_context_count: usize,
pub packed_event_blocked_confiscation_variant_count: usize,
pub packed_event_blocked_retire_train_variant_count: usize,
pub packed_event_blocked_retire_train_scope_count: usize,
pub packed_event_blocked_structural_only_count: usize, pub packed_event_blocked_structural_only_count: usize,
pub event_runtime_record_count: usize, pub event_runtime_record_count: usize,
pub candidate_availability_count: usize, pub candidate_availability_count: usize,
@ -127,6 +136,7 @@ impl RuntimeSummary {
world_restore_ai_ignore_territories_at_startup_enabled: state world_restore_ai_ignore_territories_at_startup_enabled: state
.world_restore .world_restore
.ai_ignore_territories_at_startup_enabled, .ai_ignore_territories_at_startup_enabled,
world_restore_economic_status_code: state.world_restore.economic_status_code,
world_restore_absolute_counter_restore_kind: state world_restore_absolute_counter_restore_kind: state
.world_restore .world_restore
.absolute_counter_restore_kind .absolute_counter_restore_kind
@ -143,6 +153,9 @@ impl RuntimeSummary {
.filter(|company| company.active) .filter(|company| company.active)
.count(), .count(),
player_count: state.players.len(), player_count: state.players.len(),
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(),
territory_count: state.territories.len(), territory_count: state.territories.len(),
company_territory_track_count: state.company_territory_track_piece_counts.len(), company_territory_track_count: state.company_territory_track_piece_counts.len(),
packed_event_collection_present: state.packed_event_collection.is_some(), packed_event_collection_present: state.packed_event_collection.is_some(),
@ -421,6 +434,73 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_train_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_train_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_train_territory_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_train_territory_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_confiscation_variant_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref() == Some("blocked_confiscation_variant")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_retire_train_variant_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref() == Some("blocked_retire_train_variant")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_retire_train_scope_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref() == Some("blocked_retire_train_scope")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_structural_only_count: state packed_event_blocked_structural_only_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -481,8 +561,8 @@ mod tests {
use crate::{ use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeTrackPieceCounts, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeTrackPieceCounts,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, RuntimeWorldRestoreState,
}; };
use super::RuntimeSummary; use super::RuntimeSummary;
@ -504,6 +584,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
@ -731,6 +812,7 @@ mod tests {
selected_company_id: None, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
packed_event_collection: None, packed_event_collection: None,

View file

@ -91,6 +91,9 @@ The highest-value next passes are now:
track track
- exact named-territory binding now executes too, while named-territory no-match cases remain the - exact named-territory binding now executes too, while named-territory no-match cases remain the
explicit binding blocker frontier explicit binding blocker frontier
- real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` now join
the executable batch through the same ordinary runtime path, backed by the opaque economic-status
lane and the minimal event-owned train roster
- descriptor `3` `Territory - Allow All` remains the explicit parity-only descriptor frontier, and - descriptor `3` `Territory - Allow All` remains the explicit parity-only descriptor frontier, and
mixed supported/unsupported real rows still stay parity-only mixed supported/unsupported real rows still stay parity-only
- keep in mind that the current local `.gms` corpus still exports with no packed event collection, - keep in mind that the current local `.gms` corpus still exports with no packed event collection,

View file

@ -40,13 +40,17 @@ Implemented today:
- exact named-territory binding now lowers candidate-name ordinary rows onto tracked territory - 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 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` now imports and executes through the ordinary runtime path
- descriptor `3` = `Territory - Allow All` now has an explicit parity-only frontier label instead - a minimal event-owned train surface and an opaque economic-status lane now exist in runtime
of hiding behind the generic unmapped bucket 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
supplies the required train ownership data
- descriptor `3` = `Territory - Allow All` remains the explicit parity-only descriptor frontier
instead of hiding behind the generic unmapped bucket
That means the next implementation work is breadth, not bootstrap. The recommended next slice is That means the next implementation work is breadth, not bootstrap. The recommended next slice is
broader ordinary condition-id coverage beyond numeric thresholds, wider real grouped-descriptor broader real policy-descriptor coverage beyond `3/8/9/15`, wider ordinary condition-id coverage
coverage beyond the current company/player cash batch, and later executable territory-policy beyond the current numeric-threshold batch, and richer train/runtime simulation only if later
mutation once those semantics are grounded strongly enough to avoid guessing. descriptor families need more than the current event-owned roster.
## Why This Boundary ## Why This Boundary

View file

@ -0,0 +1,32 @@
{
"format_version": 1,
"fixture_id": "packed-event-confiscate-all-false-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture keeping the unsupported FALSE Confiscate All variant explicit."
},
"state_save_slice_path": "packed-event-confiscate-all-false-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"event_runtime_record_count": 0,
"packed_event_blocked_confiscation_variant_count": 1
},
"expected_state_fragment": {
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_confiscation_variant"
}
]
}
}
}

View file

@ -0,0 +1,101 @@
{
"format_version": 1,
"save_slice_id": "packed-event-confiscate-all-false-save-slice",
"source": {
"description": "Tracked save-slice document with an unsupported FALSE Confiscate All row.",
"original_save_filename": "captured-confiscate-all-false.gms",
"original_save_sha256": "confiscate-all-false-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"keeps the unsupported descriptor 9 variant explicit"
]
},
"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": 20,
"live_record_count": 1,
"live_entry_ids": [20],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 20,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 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": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 9,
"descriptor_label": "Confiscate All",
"target_mask_bits": 1,
"parameter_family": "company_confiscation_variant",
"opcode": 1,
"raw_scalar_value": 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",
"semantic_family": "bool_toggle",
"semantic_preview": "Set Confiscate All to FALSE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"unsupported real confiscate-all FALSE variant sample"
]
}
}

View file

@ -0,0 +1,81 @@
{
"format_version": 1,
"fixture_id": "packed-event-confiscate-all-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 9 Confiscate All imports and executes through the ordinary runtime path."
},
"state_import_path": "packed-event-confiscate-all-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"active_company_count": 1,
"train_count": 3,
"active_train_count": 1,
"retired_train_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,
"total_company_cash": 90
},
"expected_state_fragment": {
"selected_company_id": null,
"companies": [
{
"company_id": 1,
"current_cash": 0,
"debt": 0,
"active": false
},
{
"company_id": 2,
"current_cash": 90,
"debt": 40,
"active": true
}
],
"trains": [
{
"train_id": 100,
"active": false,
"retired": true
},
{
"train_id": 101,
"active": false,
"retired": true
},
{
"train_id": 102,
"active": true,
"retired": false
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "confiscate_company_assets",
"target": {
"kind": "selected_company"
}
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-confiscate-all-overlay",
"source": {
"description": "Overlay import combining world/train runtime context with the real Confiscate All descriptor sample."
},
"base_snapshot_path": "packed-event-world-train-overlay-base-snapshot.json",
"save_slice_path": "packed-event-confiscate-all-save-slice.json"
}

View file

@ -0,0 +1,108 @@
{
"format_version": 1,
"save_slice_id": "packed-event-confiscate-all-save-slice",
"source": {
"description": "Tracked save-slice document with a real Confiscate All row.",
"original_save_filename": "captured-confiscate-all.gms",
"original_save_sha256": "confiscate-all-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves descriptor 9 import into company liquidation plus owned-train retirement"
]
},
"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": 19,
"live_record_count": 1,
"live_entry_ids": [19],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 19,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 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": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 9,
"descriptor_label": "Confiscate All",
"target_mask_bits": 1,
"parameter_family": "company_confiscation_variant",
"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 Confiscate All to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "confiscate_company_assets",
"target": {
"kind": "selected_company"
}
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real confiscate-all descriptor sample"
]
}
}

View file

@ -0,0 +1,48 @@
{
"format_version": 1,
"fixture_id": "packed-event-economic-status-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 8 Economic Status imports and executes through the ordinary runtime path."
},
"state_import_path": "packed-event-economic-status-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"train_count": 3,
"territory_count": 2,
"world_restore_economic_status_code": 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": {
"world_restore": {
"economic_status_code": 2
},
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "set_economic_status_code",
"value": 2
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-economic-status-overlay",
"source": {
"description": "Overlay import combining world/train runtime context with the real Economic Status descriptor sample."
},
"base_snapshot_path": "packed-event-world-train-overlay-base-snapshot.json",
"save_slice_path": "packed-event-economic-status-save-slice.json"
}

View file

@ -0,0 +1,106 @@
{
"format_version": 1,
"save_slice_id": "packed-event-economic-status-save-slice",
"source": {
"description": "Tracked save-slice document with a real Economic Status row.",
"original_save_filename": "captured-economic-status.gms",
"original_save_sha256": "economic-status-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves descriptor 8 import into the opaque economic-status runtime lane"
]
},
"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": 18,
"live_record_count": 1,
"live_entry_ids": [18],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 18,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 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": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 8,
"descriptor_label": "Economic Status",
"target_mask_bits": 8,
"parameter_family": "whole_game_state_enum",
"opcode": 3,
"raw_scalar_value": 2,
"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": "scalar_assignment",
"semantic_family": "scalar_assignment",
"semantic_preview": "Set Economic Status to 2",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "set_economic_status_code",
"value": 2
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real economic status descriptor sample"
]
}
}

View file

@ -21,7 +21,7 @@
"packed_event_record_count": 1, "packed_event_record_count": 1,
"packed_event_decoded_record_count": 1, "packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0, "packed_event_imported_runtime_record_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 1, "packed_event_blocked_confiscation_variant_count": 1,
"event_runtime_record_count": 0, "event_runtime_record_count": 0,
"total_event_record_service_count": 0, "total_event_record_service_count": 0,
"total_trigger_dispatch_count": 1 "total_trigger_dispatch_count": 1
@ -45,7 +45,7 @@
"packed_event_collection": { "packed_event_collection": {
"records": [ "records": [
{ {
"import_outcome": "blocked_unmapped_real_descriptor" "import_outcome": "blocked_confiscation_variant"
} }
] ]
}, },

View file

@ -86,21 +86,21 @@
{ {
"group_index": 1, "group_index": 1,
"row_index": 0, "row_index": 0,
"descriptor_id": 8, "descriptor_id": 9,
"descriptor_label": "Economic Status", "descriptor_label": "Confiscate All",
"target_mask_bits": 8, "target_mask_bits": 1,
"parameter_family": "whole_game_state_enum", "parameter_family": "company_confiscation_variant",
"opcode": 3, "opcode": 1,
"raw_scalar_value": 2, "raw_scalar_value": 0,
"value_byte_0x09": 0, "value_byte_0x09": 0,
"value_dword_0x0d": 0, "value_dword_0x0d": 0,
"value_byte_0x11": 0, "value_byte_0x11": 0,
"value_byte_0x12": 0, "value_byte_0x12": 0,
"value_word_0x14": 0, "value_word_0x14": 0,
"value_word_0x16": 0, "value_word_0x16": 0,
"row_shape": "scalar_assignment", "row_shape": "bool_toggle",
"semantic_family": "scalar_assignment", "semantic_family": "bool_toggle",
"semantic_preview": "Set Economic Status to 2", "semantic_preview": "Set Confiscate All to FALSE",
"locomotive_name": null, "locomotive_name": null,
"notes": [] "notes": []
} }

View file

@ -0,0 +1,64 @@
{
"format_version": 1,
"fixture_id": "packed-event-retire-train-company-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 15 Retire Train executes against company scope."
},
"state_import_path": "packed-event-retire-train-company-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"train_count": 3,
"active_train_count": 1,
"retired_train_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": {
"trains": [
{
"train_id": 100,
"active": false,
"retired": true
},
{
"train_id": 101,
"active": false,
"retired": true
},
{
"train_id": 102,
"active": true,
"retired": false
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "retire_trains",
"company_target": {
"kind": "selected_company"
}
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-retire-train-company-overlay",
"source": {
"description": "Overlay import combining world/train runtime context with the company-scoped Retire Train descriptor sample."
},
"base_snapshot_path": "packed-event-world-train-overlay-base-snapshot.json",
"save_slice_path": "packed-event-retire-train-company-save-slice.json"
}

View file

@ -0,0 +1,108 @@
{
"format_version": 1,
"save_slice_id": "packed-event-retire-train-company-save-slice",
"source": {
"description": "Tracked save-slice document with a real company-scoped Retire Train row.",
"original_save_filename": "captured-retire-train-company.gms",
"original_save_sha256": "retire-train-company-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves descriptor 15 import through company scope"
]
},
"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": 21,
"live_record_count": 1,
"live_entry_ids": [21],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 21,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 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": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 15,
"descriptor_label": "Retire Train",
"target_mask_bits": 13,
"parameter_family": "company_or_territory_asset_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 Retire Train to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "retire_trains",
"company_target": {
"kind": "selected_company"
}
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real retire-train descriptor sample scoped by company"
]
}
}

View file

@ -0,0 +1,32 @@
{
"format_version": 1,
"fixture_id": "packed-event-retire-train-missing-scope-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture keeping the missing-scope Retire Train variant explicit."
},
"state_save_slice_path": "packed-event-retire-train-missing-scope-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"event_runtime_record_count": 0,
"packed_event_blocked_retire_train_scope_count": 1
},
"expected_state_fragment": {
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_retire_train_scope"
}
]
}
}
}

View file

@ -0,0 +1,103 @@
{
"format_version": 1,
"save_slice_id": "packed-event-retire-train-missing-scope-save-slice",
"source": {
"description": "Tracked save-slice document with a Retire Train row missing both company and territory scope.",
"original_save_filename": "captured-retire-train-missing-scope.gms",
"original_save_sha256": "retire-train-missing-scope-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"keeps the missing-scope descriptor 15 variant explicit"
]
},
"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": 23,
"live_record_count": 1,
"live_entry_ids": [23],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 23,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [8, 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": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 15,
"descriptor_label": "Retire Train",
"target_mask_bits": 13,
"parameter_family": "company_or_territory_asset_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 Retire Train to TRUE",
"locomotive_name": "Mikado",
"notes": [
"retire train row is missing company and territory scope"
]
}
],
"decoded_conditions": [],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"unsupported real retire-train missing-scope sample"
]
}
}

View file

@ -0,0 +1,66 @@
{
"format_version": 1,
"fixture_id": "packed-event-retire-train-territory-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 15 Retire Train executes against territory and locomotive filters."
},
"state_import_path": "packed-event-retire-train-territory-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"train_count": 3,
"active_train_count": 1,
"retired_train_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": {
"trains": [
{
"train_id": 100,
"active": false,
"retired": true
},
{
"train_id": 101,
"active": true,
"retired": false
},
{
"train_id": 102,
"active": false,
"retired": true
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "retire_trains",
"territory_target": {
"kind": "ids",
"ids": [7]
},
"locomotive_name": "Mikado"
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-retire-train-territory-overlay",
"source": {
"description": "Overlay import combining world/train runtime context with the territory-scoped Retire Train descriptor sample."
},
"base_snapshot_path": "packed-event-world-train-overlay-base-snapshot.json",
"save_slice_path": "packed-event-retire-train-territory-save-slice.json"
}

View file

@ -0,0 +1,112 @@
{
"format_version": 1,
"save_slice_id": "packed-event-retire-train-territory-save-slice",
"source": {
"description": "Tracked save-slice document with a real territory- and locomotive-scoped Retire Train row.",
"original_save_filename": "captured-retire-train-territory.gms",
"original_save_sha256": "retire-train-territory-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves descriptor 15 import through territory selector plus locomotive-name filtering"
]
},
"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": 22,
"live_record_count": 1,
"live_entry_ids": [22],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 22,
"payload_offset": 29280,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [8, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [7, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"negative_sentinel_scope": null,
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 15,
"descriptor_label": "Retire Train",
"target_mask_bits": 13,
"parameter_family": "company_or_territory_asset_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 Retire Train to TRUE",
"locomotive_name": "Mikado",
"notes": [
"grouped effect row carries locomotive-name side string"
]
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "retire_trains",
"territory_target": {
"kind": "ids",
"ids": [7]
},
"locomotive_name": "Mikado"
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real retire-train descriptor sample scoped by territory and locomotive name"
]
}
}

View file

@ -0,0 +1,81 @@
{
"format_version": 1,
"snapshot_id": "packed-event-world-train-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying company, territory, and train context for real descriptor 8/9/15 overlays."
},
"state": {
"calendar": {
"year": 1845,
"month_slot": 2,
"phase_slot": 1,
"tick_slot": 3
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "world-and-train overlay context"
},
"companies": [
{
"company_id": 1,
"current_cash": 150,
"debt": 80,
"credit_rating_score": 650,
"prime_rate": 5,
"controller_kind": "human"
},
{
"company_id": 2,
"current_cash": 90,
"debt": 40,
"credit_rating_score": 480,
"prime_rate": 6,
"controller_kind": "ai"
}
],
"selected_company_id": 1,
"players": [],
"trains": [
{
"train_id": 100,
"owner_company_id": 1,
"territory_id": 7,
"locomotive_name": "Mikado"
},
{
"train_id": 101,
"owner_company_id": 1,
"territory_id": 8,
"locomotive_name": "Orca"
},
{
"train_id": 102,
"owner_company_id": 2,
"territory_id": 7,
"locomotive_name": "Mikado"
}
],
"territories": [
{
"territory_id": 7,
"name": "Appalachia"
},
{
"territory_id": 8,
"name": "Great Plains"
}
],
"company_territory_track_piece_counts": [],
"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
}
}
}