Add territory and player packed event import

This commit is contained in:
Jan Petykiewicz 2026-04-15 19:15:47 -07:00
commit ca208f74e0
26 changed files with 1912 additions and 272 deletions

View file

@ -15,14 +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 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` selected-company and controller-role context through overlay imports, and real descriptors `2`
`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and `Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and
execute through the ordinary runtime path. Synthetic packed records still exercise the same service execute through the ordinary runtime path, and descriptor `1` `Player Cash` now joins that batch
engine without a parallel packed executor. The first grounded condition-side unlock now exists for through the same service engine. Synthetic packed records still exercise the same runtime without a
negative-sentinel `raw_condition_id = -1` company scopes, and the first ordinary nonnegative parallel packed executor. The first grounded condition-side unlock now exists for negative-sentinel
condition batch now executes too: numeric-threshold company finance, company track, aggregate `raw_condition_id = -1` company scopes, and the first ordinary nonnegative condition batch now
territory track, and company-territory track rows can import through overlay-backed runtime executes too: numeric-threshold company finance, company track, aggregate territory track, and
context. Named-territory bindings and player-owned condition scope still remain blocked. Mixed company-territory track rows can import through overlay-backed runtime context. Exact
supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and named-territory binding now executes, while descriptor `3` `Territory - Allow All` remains the
integration tooling, but it is no longer the main execution milestone. 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

@ -174,6 +174,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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,
@ -339,6 +341,8 @@ mod tests {
available_track_laying_capacity: None, available_track_laying_capacity: None,
}], }],
selected_company_id: Some(42), selected_company_id: Some(42),
players: Vec::new(),
selected_player_id: None,
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

@ -64,6 +64,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub active_company_count: Option<usize>, pub active_company_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub player_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>,
@ -86,8 +88,16 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_company_role_context_count: Option<usize>, pub packed_event_blocked_missing_company_role_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_player_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>, pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_player_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>, pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_player_condition_scope_count: Option<usize>, pub packed_event_blocked_player_condition_scope_count: Option<usize>,
@ -104,6 +114,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>, pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_territory_policy_descriptor_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>,
@ -353,6 +365,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.player_count {
if actual.player_count != count {
mismatches.push(format!(
"player_count mismatch: expected {count}, got {}",
actual.player_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!(
@ -441,6 +461,30 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_player_context_count {
if actual.packed_event_blocked_missing_player_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_player_selection_context_count {
if actual.packed_event_blocked_missing_player_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_selection_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_player_role_context_count {
if actual.packed_event_blocked_missing_player_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_role_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_condition_context_count { if let Some(count) = self.packed_event_blocked_missing_condition_context_count {
if actual.packed_event_blocked_missing_condition_context_count != count { if actual.packed_event_blocked_missing_condition_context_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -449,6 +493,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_player_condition_context_count {
if actual.packed_event_blocked_missing_player_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_condition_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_company_condition_scope_disabled_count { if let Some(count) = self.packed_event_blocked_company_condition_scope_disabled_count {
if actual.packed_event_blocked_company_condition_scope_disabled_count != count { if actual.packed_event_blocked_company_condition_scope_disabled_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -513,6 +565,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_territory_policy_descriptor_count {
if actual.packed_event_blocked_territory_policy_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_policy_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_policy_descriptor_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

@ -41,9 +41,10 @@ pub use runtime::{
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer,
RuntimePlayerConditionTestScope, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
RuntimeWorldRestoreState, RuntimeWorldRestoreState,
}; };
pub use smp::{ pub use smp::{

View file

@ -94,6 +94,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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

@ -56,6 +56,8 @@ pub struct RuntimeTrackPieceCounts {
pub struct RuntimeTerritory { pub struct RuntimeTerritory {
pub territory_id: u32, pub territory_id: u32,
#[serde(default)] #[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub track_piece_counts: RuntimeTrackPieceCounts, pub track_piece_counts: RuntimeTrackPieceCounts,
} }
@ -67,6 +69,20 @@ pub struct RuntimeCompanyTerritoryTrackPieceCount {
pub track_piece_counts: RuntimeTrackPieceCounts, pub track_piece_counts: RuntimeTrackPieceCounts,
} }
fn runtime_player_default_active() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimePlayer {
pub player_id: u32,
pub current_cash: i64,
#[serde(default = "runtime_player_default_active")]
pub active: bool,
#[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind,
}
#[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 {
@ -78,6 +94,24 @@ pub enum RuntimeCompanyTarget {
Ids { ids: Vec<u32> }, Ids { ids: Vec<u32> },
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimePlayerTarget {
AllActive,
HumanPlayers,
AiPlayers,
SelectedPlayer,
ConditionTruePlayer,
Ids { ids: Vec<u32> },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeTerritoryTarget {
AllTerritories,
Ids { ids: Vec<u32> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RuntimeCompanyConditionTestScope { pub enum RuntimeCompanyConditionTestScope {
@ -158,12 +192,14 @@ pub enum RuntimeCondition {
value: i64, value: i64,
}, },
TerritoryNumericThreshold { TerritoryNumericThreshold {
target: RuntimeTerritoryTarget,
metric: RuntimeTerritoryMetric, metric: RuntimeTerritoryMetric,
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
}, },
CompanyTerritoryNumericThreshold { CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
territory: RuntimeTerritoryTarget,
metric: RuntimeTrackMetric, metric: RuntimeTrackMetric,
comparator: RuntimeConditionComparator, comparator: RuntimeConditionComparator,
value: i64, value: i64,
@ -181,6 +217,10 @@ pub enum RuntimeEffect {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
value: i64, value: i64,
}, },
SetPlayerCash {
target: RuntimePlayerTarget,
value: i64,
},
DeactivateCompany { DeactivateCompany {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
}, },
@ -508,6 +548,10 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub selected_company_id: Option<u32>, pub selected_company_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub players: Vec<RuntimePlayer>,
#[serde(default)]
pub selected_player_id: Option<u32>,
#[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>,
@ -552,11 +596,48 @@ impl RuntimeState {
} }
} }
let mut seen_player_ids = BTreeSet::new();
let mut active_player_ids = BTreeSet::new();
for player in &self.players {
if !seen_player_ids.insert(player.player_id) {
return Err(format!("duplicate player_id {}", player.player_id));
}
if player.active {
active_player_ids.insert(player.player_id);
}
}
if let Some(selected_player_id) = self.selected_player_id {
if !seen_player_ids.contains(&selected_player_id) {
return Err(format!(
"selected_player_id {} does not reference a live player",
selected_player_id
));
}
if !active_player_ids.contains(&selected_player_id) {
return Err(format!(
"selected_player_id {} must reference an active player",
selected_player_id
));
}
}
let mut seen_territory_ids = BTreeSet::new(); let mut seen_territory_ids = BTreeSet::new();
let mut seen_territory_names = BTreeSet::new();
for territory in &self.territories { for territory in &self.territories {
if !seen_territory_ids.insert(territory.territory_id) { if !seen_territory_ids.insert(territory.territory_id) {
return Err(format!("duplicate territory_id {}", territory.territory_id)); return Err(format!("duplicate territory_id {}", territory.territory_id));
} }
if let Some(name) = territory.name.as_deref() {
if name.trim().is_empty() {
return Err(format!(
"territory_id {} has an empty name",
territory.territory_id
));
}
if !seen_territory_names.insert(name.to_string()) {
return Err(format!("duplicate territory name {name:?}"));
}
}
} }
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) {
@ -579,7 +660,8 @@ impl RuntimeState {
return Err(format!("duplicate record_id {}", record.record_id)); return Err(format!("duplicate record_id {}", record.record_id));
} }
for (condition_index, condition) in record.conditions.iter().enumerate() { for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, &seen_company_ids).map_err(|err| { validate_runtime_condition(condition, &seen_company_ids, &seen_territory_ids)
.map_err(|err| {
format!( format!(
"event_runtime_records[record_id={}].conditions[{condition_index}] {err}", "event_runtime_records[record_id={}].conditions[{condition_index}] {err}",
record.record_id record.record_id
@ -587,7 +669,13 @@ impl RuntimeState {
})?; })?;
} }
for (effect_index, effect) in record.effects.iter().enumerate() { for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, &seen_company_ids).map_err(|err| { validate_runtime_effect(
effect,
&seen_company_ids,
&seen_player_ids,
&seen_territory_ids,
)
.map_err(|err| {
format!( format!(
"event_runtime_records[record_id={}].effects[{effect_index}] {err}", "event_runtime_records[record_id={}].effects[{effect_index}] {err}",
record.record_id record.record_id
@ -912,6 +1000,8 @@ impl RuntimeState {
fn validate_runtime_effect( fn validate_runtime_effect(
effect: &RuntimeEffect, effect: &RuntimeEffect,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match effect { match effect {
RuntimeEffect::SetWorldFlag { key, .. } => { RuntimeEffect::SetWorldFlag { key, .. } => {
@ -926,6 +1016,9 @@ fn validate_runtime_effect(
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
validate_company_target(target, valid_company_ids)?; validate_company_target(target, valid_company_ids)?;
} }
RuntimeEffect::SetPlayerCash { target, .. } => {
validate_player_target(target, valid_player_ids)?;
}
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());
@ -937,7 +1030,12 @@ fn validate_runtime_effect(
} }
} }
RuntimeEffect::AppendEventRecord { record } => { RuntimeEffect::AppendEventRecord { record } => {
validate_event_record_template(record, valid_company_ids)?; validate_event_record_template(
record,
valid_company_ids,
valid_player_ids,
valid_territory_ids,
)?;
} }
RuntimeEffect::ActivateEventRecord { .. } RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
@ -950,17 +1048,27 @@ fn validate_runtime_effect(
fn validate_event_record_template( fn validate_event_record_template(
record: &RuntimeEventRecordTemplate, record: &RuntimeEventRecordTemplate,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_player_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
for (condition_index, condition) in record.conditions.iter().enumerate() { for (condition_index, condition) in record.conditions.iter().enumerate() {
validate_runtime_condition(condition, valid_company_ids).map_err(|err| { validate_runtime_condition(condition, valid_company_ids, valid_territory_ids).map_err(
|err| {
format!( format!(
"template record_id={}.conditions[{condition_index}] {err}", "template record_id={}.conditions[{condition_index}] {err}",
record.record_id record.record_id
) )
})?; },
)?;
} }
for (effect_index, effect) in record.effects.iter().enumerate() { for (effect_index, effect) in record.effects.iter().enumerate() {
validate_runtime_effect(effect, valid_company_ids).map_err(|err| { validate_runtime_effect(
effect,
valid_company_ids,
valid_player_ids,
valid_territory_ids,
)
.map_err(|err| {
format!( format!(
"template record_id={}.effects[{effect_index}] {err}", "template record_id={}.effects[{effect_index}] {err}",
record.record_id record.record_id
@ -974,13 +1082,23 @@ fn validate_event_record_template(
fn validate_runtime_condition( fn validate_runtime_condition(
condition: &RuntimeCondition, condition: &RuntimeCondition,
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match condition { match condition {
RuntimeCondition::CompanyNumericThreshold { target, .. } RuntimeCondition::CompanyNumericThreshold { target, .. } => {
| RuntimeCondition::CompanyTerritoryNumericThreshold { target, .. } => {
validate_company_target(target, valid_company_ids) validate_company_target(target, valid_company_ids)
} }
RuntimeCondition::TerritoryNumericThreshold { .. } => Ok(()), RuntimeCondition::TerritoryNumericThreshold { target, .. } => {
validate_territory_target(target, valid_territory_ids)
}
RuntimeCondition::CompanyTerritoryNumericThreshold {
target,
territory,
..
} => {
validate_company_target(target, valid_company_ids)?;
validate_territory_target(territory, valid_territory_ids)
}
} }
} }
@ -1008,6 +1126,52 @@ fn validate_company_target(
} }
} }
fn validate_player_target(
target: &RuntimePlayerTarget,
valid_player_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match target {
RuntimePlayerTarget::AllActive
| RuntimePlayerTarget::HumanPlayers
| RuntimePlayerTarget::AiPlayers
| RuntimePlayerTarget::SelectedPlayer
| RuntimePlayerTarget::ConditionTruePlayer => Ok(()),
RuntimePlayerTarget::Ids { ids } => {
if ids.is_empty() {
return Err("target ids must not be empty".to_string());
}
for player_id in ids {
if !valid_player_ids.contains(player_id) {
return Err(format!("target references unknown player_id {player_id}"));
}
}
Ok(())
}
}
}
fn validate_territory_target(
target: &RuntimeTerritoryTarget,
valid_territory_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match target {
RuntimeTerritoryTarget::AllTerritories => Ok(()),
RuntimeTerritoryTarget::Ids { ids } => {
if ids.is_empty() {
return Err("territory target ids must not be empty".to_string());
}
for territory_id in ids {
if !valid_territory_ids.contains(territory_id) {
return Err(format!(
"territory target references unknown territory_id {territory_id}"
));
}
}
Ok(())
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -1050,6 +1214,8 @@ mod tests {
}, },
], ],
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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,
@ -1099,6 +1265,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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,
@ -1136,6 +1304,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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,
@ -1186,6 +1356,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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,
@ -1236,6 +1408,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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 {
@ -1337,6 +1511,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(2), selected_company_id: Some(2),
players: Vec::new(),
selected_player_id: None,
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,
@ -1374,6 +1550,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(1), selected_company_id: Some(1),
players: Vec::new(),
selected_player_id: None,
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

@ -7,7 +7,8 @@ use sha2::{Digest, Sha256};
use crate::{ use crate::{
RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric,
}; };
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -132,7 +133,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
label: "Player Cash", label: "Player Cash",
target_mask_bits: 0x02, target_mask_bits: 0x02,
parameter_family: "player_finance_scalar", parameter_family: "player_finance_scalar",
executable_in_runtime: false, executable_in_runtime: true,
}, },
RealGroupedEffectDescriptorMetadata { RealGroupedEffectDescriptorMetadata {
descriptor_id: 2, descriptor_id: 2,
@ -2471,6 +2472,7 @@ fn decode_real_condition_row(
negative_sentinel_scope 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,
metric, metric,
comparator, comparator,
value, value,
@ -2481,6 +2483,7 @@ fn decode_real_condition_row(
.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,
territory: RuntimeTerritoryTarget::AllTerritories,
metric, metric,
comparator, comparator,
value, value,
@ -2588,19 +2591,25 @@ fn decode_real_grouped_effect_action(
.grouped_target_scope_ordinals_0x7fb .grouped_target_scope_ordinals_0x7fb
.get(row.group_index) .get(row.group_index)
.copied()?; .copied()?;
let target = match target_scope_ordinal {
0 => RuntimeCompanyTarget::ConditionTrueCompany, if descriptor_metadata.executable_in_runtime
1 => RuntimeCompanyTarget::SelectedCompany, && descriptor_metadata.descriptor_id == 1
2 => RuntimeCompanyTarget::HumanCompanies, && row.opcode == 8
3 => RuntimeCompanyTarget::AiCompanies, && row.row_shape == "multivalue_scalar"
_ => return None, {
}; let target = real_grouped_player_target(target_scope_ordinal)?;
return Some(RuntimeEffect::SetPlayerCash {
target,
value: i64::from(row.raw_scalar_value),
});
}
if descriptor_metadata.executable_in_runtime if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 2 && descriptor_metadata.descriptor_id == 2
&& row.opcode == 8 && row.opcode == 8
&& row.row_shape == "multivalue_scalar" && row.row_shape == "multivalue_scalar"
{ {
let target = real_grouped_company_target(target_scope_ordinal)?;
return Some(RuntimeEffect::SetCompanyCash { return Some(RuntimeEffect::SetCompanyCash {
target, target,
value: i64::from(row.raw_scalar_value), value: i64::from(row.raw_scalar_value),
@ -2612,6 +2621,7 @@ fn decode_real_grouped_effect_action(
&& row.row_shape == "bool_toggle" && row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0 && row.raw_scalar_value != 0
{ {
let target = real_grouped_company_target(target_scope_ordinal)?;
return Some(RuntimeEffect::DeactivateCompany { target }); return Some(RuntimeEffect::DeactivateCompany { target });
} }
@ -2620,6 +2630,7 @@ fn decode_real_grouped_effect_action(
&& row.row_shape == "scalar_assignment" && row.row_shape == "scalar_assignment"
&& row.raw_scalar_value >= 0 && row.raw_scalar_value >= 0
{ {
let target = real_grouped_company_target(target_scope_ordinal)?;
return Some(RuntimeEffect::SetCompanyTrackLayingCapacity { return Some(RuntimeEffect::SetCompanyTrackLayingCapacity {
target, target,
value: Some(row.raw_scalar_value as u32), value: Some(row.raw_scalar_value as u32),
@ -2629,6 +2640,26 @@ fn decode_real_grouped_effect_action(
None None
} }
fn real_grouped_company_target(ordinal: u8) -> Option<RuntimeCompanyTarget> {
match ordinal {
0 => Some(RuntimeCompanyTarget::ConditionTrueCompany),
1 => Some(RuntimeCompanyTarget::SelectedCompany),
2 => Some(RuntimeCompanyTarget::HumanCompanies),
3 => Some(RuntimeCompanyTarget::AiCompanies),
_ => None,
}
}
fn real_grouped_player_target(ordinal: u8) -> Option<RuntimePlayerTarget> {
match ordinal {
0 => Some(RuntimePlayerTarget::ConditionTruePlayer),
1 => Some(RuntimePlayerTarget::SelectedPlayer),
2 => Some(RuntimePlayerTarget::HumanPlayers),
3 => Some(RuntimePlayerTarget::AiPlayers),
_ => None,
}
}
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> { fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
let opcode = read_u8_at(bytes, *cursor)?; let opcode = read_u8_at(bytes, *cursor)?;
*cursor += 1; *cursor += 1;
@ -2784,6 +2815,15 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => true, | RuntimeEffect::RemoveEventRecord { .. } => true,
RuntimeEffect::SetPlayerCash { target, .. } => matches!(
target,
RuntimePlayerTarget::AllActive
| RuntimePlayerTarget::Ids { .. }
| RuntimePlayerTarget::HumanPlayers
| RuntimePlayerTarget::AiPlayers
| RuntimePlayerTarget::SelectedPlayer
| RuntimePlayerTarget::ConditionTruePlayer
),
RuntimeEffect::SetCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!( | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!(

View file

@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget,
RuntimeSummary, RuntimeTerritoryMetric, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeState, RuntimeSummary, RuntimeTerritoryMetric, RuntimeTerritoryTarget,
RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind, calendar::BoundaryEventKind,
}; };
@ -48,6 +49,7 @@ pub struct ServiceEvent {
pub serviced_record_ids: Vec<u32>, pub serviced_record_ids: Vec<u32>,
pub applied_effect_count: u32, pub applied_effect_count: u32,
pub mutated_company_ids: Vec<u32>, pub mutated_company_ids: Vec<u32>,
pub mutated_player_ids: Vec<u32>,
pub appended_record_ids: Vec<u32>, pub appended_record_ids: Vec<u32>,
pub activated_record_ids: Vec<u32>, pub activated_record_ids: Vec<u32>,
pub deactivated_record_ids: Vec<u32>, pub deactivated_record_ids: Vec<u32>,
@ -84,6 +86,7 @@ struct AppliedEffectsSummary {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct ResolvedConditionContext { struct ResolvedConditionContext {
matching_company_ids: BTreeSet<u32>, matching_company_ids: BTreeSet<u32>,
matching_player_ids: BTreeSet<u32>,
} }
pub fn execute_step_command( pub fn execute_step_command(
@ -205,6 +208,7 @@ fn service_trigger_kind(
let mut serviced_record_ids = Vec::new(); let mut serviced_record_ids = Vec::new();
let mut applied_effect_count = 0_u32; let mut applied_effect_count = 0_u32;
let mut mutated_company_ids = BTreeSet::new(); let mut mutated_company_ids = BTreeSet::new();
let mut mutated_player_ids = BTreeSet::new();
let mut appended_record_ids = Vec::new(); let mut appended_record_ids = Vec::new();
let mut activated_record_ids = Vec::new(); let mut activated_record_ids = Vec::new();
let mut deactivated_record_ids = Vec::new(); let mut deactivated_record_ids = Vec::new();
@ -245,6 +249,7 @@ fn service_trigger_kind(
&record_effects, &record_effects,
&condition_context, &condition_context,
&mut mutated_company_ids, &mut mutated_company_ids,
&mut mutated_player_ids,
&mut staged_event_graph_mutations, &mut staged_event_graph_mutations,
)?; )?;
applied_effect_count += effect_summary.applied_effect_count; applied_effect_count += effect_summary.applied_effect_count;
@ -276,6 +281,7 @@ fn service_trigger_kind(
serviced_record_ids, serviced_record_ids,
applied_effect_count, applied_effect_count,
mutated_company_ids: mutated_company_ids.into_iter().collect(), mutated_company_ids: mutated_company_ids.into_iter().collect(),
mutated_player_ids: mutated_player_ids.into_iter().collect(),
appended_record_ids, appended_record_ids,
activated_record_ids, activated_record_ids,
deactivated_record_ids, deactivated_record_ids,
@ -296,6 +302,7 @@ fn apply_runtime_effects(
effects: &[RuntimeEffect], effects: &[RuntimeEffect],
condition_context: &ResolvedConditionContext, condition_context: &ResolvedConditionContext,
mutated_company_ids: &mut BTreeSet<u32>, mutated_company_ids: &mut BTreeSet<u32>,
mutated_player_ids: &mut BTreeSet<u32>,
staged_event_graph_mutations: &mut Vec<EventGraphMutation>, staged_event_graph_mutations: &mut Vec<EventGraphMutation>,
) -> Result<AppliedEffectsSummary, String> { ) -> Result<AppliedEffectsSummary, String> {
let mut summary = AppliedEffectsSummary::default(); let mut summary = AppliedEffectsSummary::default();
@ -319,6 +326,20 @@ fn apply_runtime_effects(
mutated_company_ids.insert(company_id); mutated_company_ids.insert(company_id);
} }
} }
RuntimeEffect::SetPlayerCash { target, value } => {
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 cash effect")
})?;
player.current_cash = *value;
mutated_player_ids.insert(player_id);
}
}
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 {
@ -520,21 +541,25 @@ fn evaluate_record_conditions(
} }
} }
RuntimeCondition::TerritoryNumericThreshold { RuntimeCondition::TerritoryNumericThreshold {
target,
metric, metric,
comparator, comparator,
value, value,
} => { } => {
let actual = territory_metric_value(state, *metric); let territory_ids = resolve_territory_target_ids(state, target)?;
let actual = territory_metric_value(state, &territory_ids, *metric);
if !compare_condition_value(actual, *comparator, *value) { if !compare_condition_value(actual, *comparator, *value) {
return Ok(None); return Ok(None);
} }
} }
RuntimeCondition::CompanyTerritoryNumericThreshold { RuntimeCondition::CompanyTerritoryNumericThreshold {
target, target,
territory,
metric, metric,
comparator, comparator,
value, value,
} => { } => {
let territory_ids = resolve_territory_target_ids(state, territory)?;
let resolved = resolve_company_target_ids( let resolved = resolve_company_target_ids(
state, state,
target, target,
@ -544,7 +569,12 @@ fn evaluate_record_conditions(
.into_iter() .into_iter()
.filter(|company_id| { .filter(|company_id| {
compare_condition_value( compare_condition_value(
company_territory_metric_value(state, *company_id, *metric), company_territory_metric_value(
state,
*company_id,
&territory_ids,
*metric,
),
*comparator, *comparator,
*value, *value,
) )
@ -563,6 +593,7 @@ fn evaluate_record_conditions(
Ok(Some(ResolvedConditionContext { Ok(Some(ResolvedConditionContext {
matching_company_ids: company_matches.unwrap_or_default(), matching_company_ids: company_matches.unwrap_or_default(),
matching_player_ids: BTreeSet::new(),
})) }))
} }
@ -676,6 +707,119 @@ fn resolve_company_target_ids(
} }
} }
fn resolve_player_target_ids(
state: &RuntimeState,
target: &RuntimePlayerTarget,
condition_context: &ResolvedConditionContext,
) -> Result<Vec<u32>, String> {
match target {
RuntimePlayerTarget::AllActive => Ok(state
.players
.iter()
.filter(|player| player.active)
.map(|player| player.player_id)
.collect()),
RuntimePlayerTarget::Ids { ids } => {
let known_ids = state
.players
.iter()
.map(|player| player.player_id)
.collect::<BTreeSet<_>>();
for player_id in ids {
if !known_ids.contains(player_id) {
return Err(format!("target references unknown player_id {player_id}"));
}
}
Ok(ids.clone())
}
RuntimePlayerTarget::HumanPlayers => {
if state
.players
.iter()
.any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires player role context but at least one player has unknown controller_kind"
.to_string(),
);
}
Ok(state
.players
.iter()
.filter(|player| {
player.active && player.controller_kind == RuntimeCompanyControllerKind::Human
})
.map(|player| player.player_id)
.collect())
}
RuntimePlayerTarget::AiPlayers => {
if state
.players
.iter()
.any(|player| player.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires player role context but at least one player has unknown controller_kind"
.to_string(),
);
}
Ok(state
.players
.iter()
.filter(|player| {
player.active && player.controller_kind == RuntimeCompanyControllerKind::Ai
})
.map(|player| player.player_id)
.collect())
}
RuntimePlayerTarget::SelectedPlayer => {
let selected_player_id = state
.selected_player_id
.ok_or_else(|| "target requires selected_player_id context".to_string())?;
if state
.players
.iter()
.any(|player| player.player_id == selected_player_id && player.active)
{
Ok(vec![selected_player_id])
} else {
Err("target requires selected_player_id to reference an active player".to_string())
}
}
RuntimePlayerTarget::ConditionTruePlayer => {
if condition_context.matching_player_ids.is_empty() {
Err("target requires player condition-evaluation context".to_string())
} else {
Ok(condition_context.matching_player_ids.iter().copied().collect())
}
}
}
}
fn resolve_territory_target_ids(
state: &RuntimeState,
target: &RuntimeTerritoryTarget,
) -> Result<Vec<u32>, String> {
match target {
RuntimeTerritoryTarget::AllTerritories => {
Ok(state.territories.iter().map(|territory| territory.territory_id).collect())
}
RuntimeTerritoryTarget::Ids { ids } => {
let known_ids = state
.territories
.iter()
.map(|territory| territory.territory_id)
.collect::<BTreeSet<_>>();
for territory_id in ids {
if !known_ids.contains(territory_id) {
return Err(format!("territory target references unknown territory_id {territory_id}"));
}
}
Ok(ids.clone())
}
}
}
fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyMetric) -> i64 { fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyMetric) -> i64 {
match metric { match metric {
RuntimeCompanyMetric::CurrentCash => company.current_cash, RuntimeCompanyMetric::CurrentCash => company.current_cash,
@ -697,9 +841,14 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
} }
} }
fn territory_metric_value(state: &RuntimeState, metric: RuntimeTerritoryMetric) -> i64 { fn territory_metric_value(
state: &RuntimeState,
territory_ids: &[u32],
metric: RuntimeTerritoryMetric,
) -> i64 {
state.territories state.territories
.iter() .iter()
.filter(|territory| territory_ids.contains(&territory.territory_id))
.map(|territory| { .map(|territory| {
track_piece_metric_value( track_piece_metric_value(
territory.track_piece_counts, territory.track_piece_counts,
@ -712,11 +861,12 @@ fn territory_metric_value(state: &RuntimeState, metric: RuntimeTerritoryMetric)
fn company_territory_metric_value( fn company_territory_metric_value(
state: &RuntimeState, state: &RuntimeState,
company_id: u32, company_id: 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) .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()
} }
@ -805,6 +955,8 @@ mod tests {
available_track_laying_capacity: None, available_track_laying_capacity: None,
}], }],
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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

@ -29,6 +29,7 @@ pub struct RuntimeSummary {
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 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,
@ -40,7 +41,11 @@ pub struct RuntimeSummary {
pub packed_event_blocked_missing_company_context_count: usize, pub packed_event_blocked_missing_company_context_count: usize,
pub packed_event_blocked_missing_selection_context_count: usize, pub packed_event_blocked_missing_selection_context_count: usize,
pub packed_event_blocked_missing_company_role_context_count: usize, pub packed_event_blocked_missing_company_role_context_count: usize,
pub packed_event_blocked_missing_player_context_count: usize,
pub packed_event_blocked_missing_player_selection_context_count: usize,
pub packed_event_blocked_missing_player_role_context_count: usize,
pub packed_event_blocked_missing_condition_context_count: usize, pub packed_event_blocked_missing_condition_context_count: usize,
pub packed_event_blocked_missing_player_condition_context_count: usize,
pub packed_event_blocked_company_condition_scope_disabled_count: usize, pub packed_event_blocked_company_condition_scope_disabled_count: usize,
pub packed_event_blocked_player_condition_scope_count: usize, pub packed_event_blocked_player_condition_scope_count: usize,
pub packed_event_blocked_territory_condition_scope_count: usize, pub packed_event_blocked_territory_condition_scope_count: usize,
@ -49,6 +54,7 @@ pub struct RuntimeSummary {
pub packed_event_blocked_unmapped_ordinary_condition_count: usize, pub packed_event_blocked_unmapped_ordinary_condition_count: usize,
pub packed_event_blocked_missing_compact_control_count: usize, pub packed_event_blocked_missing_compact_control_count: usize,
pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_territory_policy_descriptor_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,
@ -136,6 +142,7 @@ impl RuntimeSummary {
.iter() .iter()
.filter(|company| company.active) .filter(|company| company.active)
.count(), .count(),
player_count: state.players.len(),
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(),
@ -218,6 +225,48 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_player_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_player_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_player_selection_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_player_selection_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_player_role_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_player_role_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_condition_context_count: state packed_event_blocked_missing_condition_context_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -232,6 +281,20 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_player_condition_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_player_condition_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_company_condition_scope_disabled_count: state packed_event_blocked_company_condition_scope_disabled_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -344,6 +407,20 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_territory_policy_descriptor_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_territory_policy_descriptor")
})
.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()
@ -425,6 +502,8 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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 {
@ -650,6 +729,8 @@ mod tests {
}, },
], ],
selected_company_id: None, selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
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

@ -81,14 +81,18 @@ The highest-value next passes are now:
first company-scoped batch already parses, summarizes, and executes through the ordinary runtime 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`, 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 `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
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask, - widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
and normalized effect semantics are all grounded, not just after row framing is parsed and normalized effect semantics are all grounded, not just after row framing is parsed
- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` - 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 company scopes, and the first ordinary nonnegative condition batch now executes too: numeric
thresholds for company finance, company track, aggregate territory track, and company-territory thresholds for company finance, company track, aggregate territory track, and company-territory
track track
- named-territory ordinary rows and player-owned condition scope are still the remaining condition - exact named-territory binding now executes too, while named-territory no-match cases remain the
frontier, and mixed supported/unsupported real rows stay parity-only explicit binding blocker frontier
- descriptor `3` `Territory - Allow All` remains the explicit parity-only descriptor frontier, and
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,
so real descriptor mapping needs to stay plumbing-first until better captures exist so real descriptor mapping needs to stay plumbing-first until better captures exist
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution

View file

@ -33,17 +33,20 @@ Implemented today:
descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable` descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable`
- the first grounded condition-side unlock now exists for real packed rows: negative-sentinel - the first grounded condition-side unlock now exists for real packed rows: negative-sentinel
`raw_condition_id = -1` company scope lowers `condition_true_company` into normalized company `raw_condition_id = -1` company scope lowers `condition_true_company` into normalized company
targets during import, while player and territory scope variants remain parity-visible and targets during import
explicitly blocked
- the first ordinary nonnegative condition-id batch now executes too: numeric-threshold company - the first ordinary nonnegative condition-id batch now executes too: numeric-threshold company
finance, company track, aggregate territory track, and company-territory track rows can import finance, company track, aggregate territory track, and company-territory track rows can import
through overlay-backed runtime context, while named-territory bindings stay parity-only and through overlay-backed runtime context
player-owned condition scope still has no runtime owner - 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
- descriptor `3` = `Territory - Allow All` now has an explicit parity-only frontier label 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, plus runtime ownership for the broader ordinary condition-id coverage beyond numeric thresholds, wider real grouped-descriptor
still-blocked player-scoped and named-territory condition families, alongside wider real coverage beyond the current company/player cash batch, and later executable territory-policy
grouped-descriptor coverage beyond the current company-scoped batch. mutation once those semantics are grounded strongly enough to avoid guessing.
## Why This Boundary ## Why This Boundary
@ -241,9 +244,10 @@ Current status:
- overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic - overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic
selected-company and controller-role scopes without inventing company state from save bytes alone selected-company and controller-role scopes without inventing company state from save bytes alone
- aggregate territory context and company-territory track counters now flow through tracked overlay - aggregate territory context and company-territory track counters now flow through tracked overlay
snapshots, so the remaining gap is broader ordinary condition-id coverage beyond numeric snapshots, named-territory binding now executes on exact matches, and a minimal player runtime is
thresholds, named-territory binding, player runtime ownership, and wider real grouped-descriptor now present, so the remaining gap is broader ordinary condition-id coverage beyond numeric
semantic coverage, not first-pass captured-runtime plumbing thresholds plus wider real grouped-descriptor and territory-policy semantic coverage, not
first-pass captured-runtime plumbing
### Milestone 4: Domain Expansion ### Milestone 4: Domain Expansion
@ -398,10 +402,10 @@ Target behavior:
- extend ordinary condition coverage beyond numeric thresholds only when comparator semantics, - extend ordinary condition coverage beyond numeric thresholds only when comparator semantics,
runtime ownership, and binding rules are grounded enough to lower honestly into the normalized runtime ownership, and binding rules are grounded enough to lower honestly into the normalized
runtime path runtime path
- keep named-territory ordinary rows explicit and parity-visible until candidate-name territory - keep named-territory ordinary rows on exact case-sensitive binding until captured evidence
binding is grounded justifies alias tables or fuzzier matching
- keep player-owned condition scope explicit and parity-visible until there is a first-class player - keep player-owned condition scope within the minimal event runtime model until later slices need
runtime model richer player metrics or profile/chairman ownership
- continue widening real grouped-descriptor execution only when both descriptor identity and - continue widening real grouped-descriptor execution only when both descriptor identity and
runtime effect semantics are grounded enough to map into the normalized runtime path honestly runtime effect semantics are grounded enough to map into the normalized runtime path honestly
@ -409,8 +413,8 @@ Public-model expectations for that slice:
- additional checked-in ordinary-condition metadata entries beyond the current numeric-threshold - additional checked-in ordinary-condition metadata entries beyond the current numeric-threshold
allowlist allowlist
- richer runtime ownership for still-blocked condition domains such as named territory and player - richer ordinary-condition metadata and later runtime ownership only where new condition domains
scope still remain blocked after the current named-territory and player-runtime unlocks
- more selective real-row `decoded_conditions` and `decoded_actions` only where the - more selective real-row `decoded_conditions` and `decoded_actions` only where the
condition/effect-to-runtime mapping is supported end to end condition/effect-to-runtime mapping is supported end to end
@ -418,7 +422,9 @@ Fixture work for that slice:
- preserve the new ordinary-condition tracked overlays for executable company finance, company - preserve the new ordinary-condition tracked overlays for executable company finance, company
track, aggregate territory track, and company-territory track thresholds track, aggregate territory track, and company-territory track thresholds
- preserve the named-territory tracked overlay as the explicit binding blocker frontier - preserve the named-territory no-match tracked overlay as the explicit binding blocker frontier
- preserve the territory-policy tracked sample as the explicit descriptor frontier until mutation
semantics are grounded strongly enough to move beyond parity-only
- keep the older negative-sentinel, mixed real-row, and company-scoped descriptor fixtures green so - keep the older negative-sentinel, mixed real-row, and company-scoped descriptor fixtures green so
ordinary-condition breadth does not regress descriptor-side execution ordinary-condition breadth does not regress descriptor-side execution
- keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens - keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens

View file

@ -0,0 +1,71 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-named-company-territory-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving named-territory company-territory ordinary rows bind exactly and execute through the normal runtime path."
},
"state_import_path": "packed-event-ordinary-named-company-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": 3,
"active_company_count": 3,
"player_count": 2,
"territory_count": 2,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 734
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 444
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "company_territory_numeric_threshold",
"target": {
"kind": "selected_company"
},
"territory": {
"kind": "ids",
"ids": [7]
},
"metric": "total",
"comparator": "ge",
"value": 10
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-named-company-territory-overlay",
"source": {
"description": "Overlay import combining named-territory runtime context with the real company-territory threshold sample."
},
"base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-named-company-territory-save-slice.json"
}

View file

@ -0,0 +1,144 @@
{
"format_version": 1,
"save_slice_id": "packed-event-ordinary-named-company-territory-save-slice",
"source": {
"description": "Tracked save-slice document with a real named-territory company-territory threshold row gating Company Cash.",
"original_save_filename": "captured-ordinary-named-company-territory.gms",
"original_save_sha256": "ordinary-named-company-territory-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves exact named-territory binding for company-territory thresholds"
]
},
"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": 46,
"live_record_count": 1,
"live_entry_ids": [46],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 46,
"payload_offset": 29240,
"payload_len": 176,
"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": 2,
"modifier_flag_0x7fa": 0,
"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": 2323,
"subtype": 0,
"flag_bytes": [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"candidate_name": "Appalachia",
"comparator": "ge",
"metric": "Company-Territory Track Pieces",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Company-Territory Track Pieces >= 10",
"requires_candidate_name_binding": true,
"notes": [
"condition row carries candidate-name side string"
]
}
],
"negative_sentinel_scope": {
"company_test_scope": "selected_company_only",
"player_test_scope": "disabled",
"territory_scope_selector_is_0x63": true,
"source_row_indexes": [0]
},
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 444,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 444 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "company_territory_numeric_threshold",
"target": {
"kind": "condition_true_company"
},
"territory": {
"kind": "all_territories"
},
"metric": "total",
"comparator": "ge",
"value": 10
}
],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 444
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"named company-territory threshold lowers both company and territory scope at import time"
]
}
]
},
"notes": [
"real named company-territory threshold sample"
]
}
}

View file

@ -0,0 +1,75 @@
{
"format_version": 1,
"fixture_id": "packed-event-ordinary-named-territory-executable-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving named-territory ordinary rows bind exactly and execute through the normal runtime path."
},
"state_import_path": "packed-event-ordinary-named-territory-executable-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": 3,
"active_company_count": 3,
"player_count": 2,
"territory_count": 2,
"company_territory_track_count": 3,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_named_territory_binding_count": 0,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"total_company_cash": 1067
},
"expected_state_fragment": {
"companies": [
{
"company_id": 1,
"current_cash": 777
},
{
"company_id": 2,
"current_cash": 90
},
{
"company_id": 3,
"current_cash": 200
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "territory_numeric_threshold",
"target": {
"kind": "ids",
"ids": [7]
},
"metric": "track_pieces_total",
"comparator": "ge",
"value": 10
}
]
}
]
},
"event_runtime_records": [
{
"record_id": 45,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-ordinary-named-territory-executable-overlay",
"source": {
"description": "Overlay import combining named-territory runtime context with the real named-territory threshold sample."
},
"base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json",
"save_slice_path": "packed-event-ordinary-named-territory-save-slice.json"
}

View file

@ -103,6 +103,9 @@
"decoded_conditions": [ "decoded_conditions": [
{ {
"kind": "territory_numeric_threshold", "kind": "territory_numeric_threshold",
"target": {
"kind": "all_territories"
},
"metric": "track_pieces_total", "metric": "track_pieces_total",
"comparator": "ge", "comparator": "ge",
"value": 10 "value": 10

View file

@ -23,15 +23,15 @@
"packed_event_collection_present": true, "packed_event_collection_present": true,
"packed_event_record_count": 2, "packed_event_record_count": 2,
"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": 1,
"packed_event_parity_only_record_count": 1, "packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 1, "packed_event_unsupported_record_count": 1,
"packed_event_blocked_missing_condition_context_count": 0, "packed_event_blocked_missing_condition_context_count": 0,
"packed_event_blocked_territory_condition_scope_count": 1, "packed_event_blocked_territory_condition_scope_count": 0,
"packed_event_blocked_missing_compact_control_count": 0, "packed_event_blocked_missing_compact_control_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 0, "packed_event_blocked_unmapped_real_descriptor_count": 0,
"packed_event_blocked_structural_only_count": 0, "packed_event_blocked_structural_only_count": 0,
"event_runtime_record_count": 0, "event_runtime_record_count": 1,
"total_company_cash": 0 "total_company_cash": 0
}, },
"expected_state_fragment": { "expected_state_fragment": {
@ -53,7 +53,7 @@
"payload_family": "real_packed_v1", "payload_family": "real_packed_v1",
"trigger_kind": 6, "trigger_kind": 6,
"one_shot": true, "one_shot": true,
"import_outcome": "blocked_territory_condition_scope", "import_outcome": "imported",
"compact_control": { "compact_control": {
"primary_selector_0x7f0": 99, "primary_selector_0x7f0": 99,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3] "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3]
@ -81,7 +81,7 @@
{ {
"kind": "set_company_cash", "kind": "set_company_cash",
"target": { "target": {
"kind": "condition_true_company" "kind": "all_active"
}, },
"value": 7 "value": 7
} }

View file

@ -34,7 +34,7 @@
"live_record_count": 2, "live_record_count": 2,
"live_entry_ids": [3, 5], "live_entry_ids": [3, 5],
"decoded_record_count": 1, "decoded_record_count": 1,
"imported_runtime_record_count": 0, "imported_runtime_record_count": 1,
"records": [ "records": [
{ {
"record_index": 0, "record_index": 0,

View file

@ -0,0 +1,57 @@
{
"format_version": 1,
"fixture_id": "packed-event-player-cash-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 1 Player Cash imports and executes through the ordinary runtime path."
},
"state_import_path": "packed-event-player-cash-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 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": {
"players": [
{
"player_id": 1,
"current_cash": 888
},
{
"player_id": 2,
"current_cash": 250
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_actions": [
{
"kind": "set_player_cash",
"target": {
"kind": "selected_player"
},
"value": 888
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-player-cash-overlay",
"source": {
"description": "Overlay import combining player runtime context with the real Player Cash descriptor sample."
},
"base_snapshot_path": "packed-event-territory-player-overlay-base-snapshot.json",
"save_slice_path": "packed-event-player-cash-save-slice.json"
}

View file

@ -0,0 +1,126 @@
{
"format_version": 1,
"save_slice_id": "packed-event-player-cash-save-slice",
"source": {
"description": "Tracked save-slice document with a real player-scoped Player Cash row.",
"original_save_filename": "captured-player-cash.gms",
"original_save_sha256": "player-cash-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"proves descriptor 1 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": 47,
"live_record_count": 1,
"live_entry_ids": [47],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 47,
"payload_offset": 29280,
"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": 1,
"descriptor_label": "Player Cash",
"target_mask_bits": 2,
"parameter_family": "player_finance_scalar",
"opcode": 8,
"raw_scalar_value": 888,
"value_byte_0x09": 1,
"value_dword_0x0d": 12,
"value_byte_0x11": 2,
"value_byte_0x12": 3,
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Player Cash to 888 with aux [2, 3, 24, 36]",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [
{
"kind": "set_player_cash",
"target": {
"kind": "condition_true_player"
},
"value": 888
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"player-side negative-sentinel scope lowers player cash at import time"
]
}
]
},
"notes": [
"real player cash descriptor sample"
]
}
}

View file

@ -0,0 +1,158 @@
{
"format_version": 1,
"snapshot_id": "packed-event-territory-player-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying named-territory and player-selection context for packed-event overlays."
},
"state": {
"calendar": {
"year": 1840,
"month_slot": 1,
"phase_slot": 2,
"tick_slot": 3
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "territory-and-player overlay context"
},
"companies": [
{
"company_id": 1,
"current_cash": 150,
"debt": 80,
"credit_rating_score": 650,
"prime_rate": 5,
"controller_kind": "human",
"track_piece_counts": {
"total": 20,
"single": 5,
"double": 8,
"transition": 1,
"electric": 3,
"non_electric": 17
}
},
{
"company_id": 2,
"current_cash": 90,
"debt": 40,
"credit_rating_score": 480,
"prime_rate": 6,
"controller_kind": "ai",
"track_piece_counts": {
"total": 8,
"single": 2,
"double": 2,
"transition": 0,
"electric": 1,
"non_electric": 7
}
},
{
"company_id": 3,
"current_cash": 200,
"debt": 10,
"credit_rating_score": 720,
"prime_rate": 4,
"controller_kind": "human",
"track_piece_counts": {
"total": 30,
"single": 10,
"double": 12,
"transition": 2,
"electric": 8,
"non_electric": 22
}
}
],
"selected_company_id": 1,
"players": [
{
"player_id": 1,
"current_cash": 500,
"controller_kind": "human"
},
{
"player_id": 2,
"current_cash": 250,
"controller_kind": "ai"
}
],
"selected_player_id": 1,
"territories": [
{
"territory_id": 7,
"name": "Appalachia",
"track_piece_counts": {
"total": 50,
"single": 10,
"double": 20,
"transition": 5,
"electric": 15,
"non_electric": 35
}
},
{
"territory_id": 8,
"name": "Great Plains",
"track_piece_counts": {
"total": 12,
"single": 4,
"double": 3,
"transition": 1,
"electric": 2,
"non_electric": 10
}
}
],
"company_territory_track_piece_counts": [
{
"company_id": 1,
"territory_id": 7,
"track_piece_counts": {
"total": 12,
"single": 3,
"double": 5,
"transition": 1,
"electric": 4,
"non_electric": 8
}
},
{
"company_id": 2,
"territory_id": 7,
"track_piece_counts": {
"total": 7,
"single": 2,
"double": 2,
"transition": 0,
"electric": 1,
"non_electric": 6
}
},
{
"company_id": 3,
"territory_id": 7,
"track_piece_counts": {
"total": 15,
"single": 5,
"double": 6,
"transition": 2,
"electric": 5,
"non_electric": 10
}
}
],
"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
}
}
}

View file

@ -0,0 +1,39 @@
{
"format_version": 1,
"fixture_id": "packed-event-territory-policy-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving descriptor 3 Territory - Allow All stays parity-only with an explicit blocker."
},
"state_save_slice_path": "packed-event-territory-policy-save-slice.json",
"commands": [
{
"kind": "step_count",
"steps": 1
}
],
"expected_summary": {
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_territory_policy_descriptor_count": 1,
"event_runtime_record_count": 0
},
"expected_state_fragment": {
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_territory_policy_descriptor",
"grouped_effect_rows": [
{
"descriptor_label": "Territory - Allow All"
}
]
}
]
}
}
}

View file

@ -0,0 +1,102 @@
{
"format_version": 1,
"save_slice_id": "packed-event-territory-policy-save-slice",
"source": {
"description": "Tracked save-slice document with a real Territory - Allow All row that stays parity-only.",
"original_save_filename": "captured-territory-policy.gms",
"original_save_sha256": "territory-policy-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"keeps descriptor 3 explicit without guessing territory policy mutation semantics"
]
},
"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": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 48,
"payload_offset": 29320,
"payload_len": 132,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"primary_selector_0x7f0": 12,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 0,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [1, 1, 1, 1],
"grouped_scope_checkboxes_0x7ff": [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": 3,
"descriptor_label": "Territory - Allow All",
"target_mask_bits": 5,
"parameter_family": "territory_access_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 Territory - Allow All to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"decoded from grounded real 0x4e9a row framing",
"territory policy mutation remains parity-only in this slice"
]
}
]
},
"notes": [
"real territory policy descriptor sample"
]
}
}