Implement real company-scoped event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-15 12:11:29 -07:00
commit 780e739daa
21 changed files with 1483 additions and 56 deletions

View file

@ -13,12 +13,13 @@ runtime rehost layer that can execute deterministic world work, compare normaliz
subsystem breadth without depending on the shell or presentation path. The current packed-event subsystem breadth without depending on the shell or presentation path. The current packed-event
frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot, frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot,
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, real descriptor `2` selected-company and controller-role context through overlay imports, and real descriptors `2`
`Company Cash` now parses and executes through the ordinary runtime path, and synthetic packed `Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and
records still exercise the same service engine without a parallel packed executor. Condition- execute through the ordinary runtime path. Synthetic packed records still exercise the same service
relative company scopes remain explicitly blocked until condition evaluation is grounded. The PE32 engine without a parallel packed executor. Condition-relative company scopes remain explicitly
hook remains useful as capture and integration tooling, but it is no longer the main execution blocked until condition evaluation is grounded, and mixed supported/unsupported real rows stay
milestone. 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

@ -4442,6 +4442,12 @@ mod tests {
.join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json"); .join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json");
let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json"); .join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json");
let deactivate_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json");
let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json");
let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json");
run_runtime_summarize_fixture(&parity_fixture) run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize"); .expect("save-slice-backed parity fixture should summarize");
@ -4451,6 +4457,12 @@ mod tests {
.expect("overlay-backed selective-import fixture should summarize"); .expect("overlay-backed selective-import fixture should summarize");
run_runtime_summarize_fixture(&symbolic_overlay_fixture) run_runtime_summarize_fixture(&symbolic_overlay_fixture)
.expect("overlay-backed symbolic-target fixture should summarize"); .expect("overlay-backed symbolic-target fixture should summarize");
run_runtime_summarize_fixture(&deactivate_overlay_fixture)
.expect("overlay-backed deactivate-company fixture should summarize");
run_runtime_summarize_fixture(&track_capacity_overlay_fixture)
.expect("overlay-backed track-capacity fixture should summarize");
run_runtime_summarize_fixture(&mixed_overlay_fixture)
.expect("overlay-backed mixed real-row fixture should summarize");
} }
#[test] #[test]

View file

@ -330,6 +330,8 @@ mod tests {
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human, controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
}], }],
selected_company_id: Some(42), selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,

View file

@ -62,6 +62,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub company_count: Option<usize>, pub company_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub active_company_count: Option<usize>,
#[serde(default)]
pub packed_event_collection_present: Option<bool>, pub packed_event_collection_present: Option<bool>,
#[serde(default)] #[serde(default)]
pub packed_event_record_count: Option<usize>, pub packed_event_record_count: Option<usize>,
@ -327,6 +329,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.active_company_count {
if actual.active_company_count != count {
mismatches.push(format!(
"active_company_count mismatch: expected {count}, got {}",
actual.active_company_count
));
}
}
if let Some(present) = self.packed_event_collection_present { if let Some(present) = self.packed_event_collection_present {
if actual.packed_event_collection_present != present { if actual.packed_event_collection_present != present {
mismatches.push(format!( mismatches.push(format!(

View file

@ -710,7 +710,7 @@ fn smp_packed_record_to_runtime_event_record(
if record.decode_status == "unsupported_framing" { if record.decode_status == "unsupported_framing" {
return None; return None;
} }
if record.payload_family == "real_packed_v1" && record.decoded_actions.is_empty() { if record.payload_family == "real_packed_v1" && !record.executable_import_ready {
return None; return None;
} }
@ -771,6 +771,25 @@ fn smp_runtime_effect_to_runtime_effect(
Err(company_target_import_error_message(target, company_context)) Err(company_target_import_error_message(target, company_context))
} }
} }
RuntimeEffect::DeactivateCompany { target } => {
if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::DeactivateCompany {
target: target.clone(),
})
} else {
Err(company_target_import_error_message(target, company_context))
}
}
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::SetCompanyTrackLayingCapacity {
target: target.clone(),
value: *value,
})
} else {
Err(company_target_import_error_message(target, company_context))
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
if company_target_import_blocker(target, company_context).is_none() { if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyCash { Ok(RuntimeEffect::AdjustCompanyCash {
@ -912,18 +931,15 @@ fn determine_packed_event_import_outcome(
if record.compact_control.is_none() { if record.compact_control.is_none() {
return "blocked_missing_compact_control".to_string(); return "blocked_missing_compact_control".to_string();
} }
if !record.executable_import_ready {
return "blocked_unmapped_real_descriptor".to_string();
}
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context)
{ {
return company_target_import_outcome(blocker).to_string(); return company_target_import_outcome(blocker).to_string();
} }
if !record.decoded_actions.is_empty() {
return "blocked_unsupported_decode".to_string(); return "blocked_unsupported_decode".to_string();
} }
if let Some(blocker) = real_record_company_target_import_blocker(record, company_context) {
return company_target_import_outcome(blocker).to_string();
}
return "blocked_unmapped_real_descriptor".to_string();
}
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) {
return company_target_import_outcome(blocker).to_string(); return company_target_import_outcome(blocker).to_string();
} }
@ -946,6 +962,8 @@ fn runtime_effect_company_target_import_blocker(
) -> Option<CompanyTargetImportBlocker> { ) -> Option<CompanyTargetImportBlocker> {
match effect { match effect {
RuntimeEffect::SetCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::DeactivateCompany { target }
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
company_target_import_blocker(target, company_context) company_target_import_blocker(target, company_context)
@ -996,16 +1014,6 @@ fn classify_real_grouped_company_target(ordinal: u8) -> Option<RuntimeCompanyTar
} }
} }
fn real_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
classify_real_grouped_company_targets(record)
.into_iter()
.flatten()
.find_map(|target| company_target_import_blocker(&target, company_context))
}
fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str { fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str {
match blocker { match blocker {
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context", CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
@ -1417,6 +1425,83 @@ mod tests {
}] }]
} }
fn real_deactivate_company_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id: 13,
descriptor_label: Some("Deactivate Company".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_lifecycle_toggle".to_string()),
opcode: 1,
raw_scalar_value: if enabled { 1 } else { 0 },
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
row_shape: "bool_toggle".to_string(),
semantic_family: Some("bool_toggle".to_string()),
semantic_preview: Some(format!(
"Set Deactivate Company to {}",
if enabled { "TRUE" } else { "FALSE" }
)),
locomotive_name: None,
notes: vec![],
}
}
fn real_track_capacity_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id: 16,
descriptor_label: Some("Company Track Pieces Buildable".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_build_limit_scalar".to_string()),
opcode: 3,
raw_scalar_value: value,
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".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!(
"Set Company Track Pieces Buildable to {value}"
)),
locomotive_name: None,
notes: vec![],
}
}
fn unsupported_real_grouped_row() -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 1,
row_index: 0,
descriptor_id: 8,
descriptor_label: Some("Economic Status".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("whole_game_state_enum".to_string()),
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".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some("Set Economic Status to 2".to_string()),
locomotive_name: None,
notes: vec![],
}
}
fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary { fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary {
crate::SmpLoadedPackedEventCompactControlSummary { crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6, mode_byte_0x7ef: 6,
@ -2173,12 +2258,16 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100, current_cash: 100,
debt: 10, debt: 10,
active: true,
available_track_laying_capacity: None,
}, },
crate::RuntimeCompany { crate::RuntimeCompany {
company_id: 2, company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 50, current_cash: 50,
debt: 20, debt: 20,
active: true,
available_track_laying_capacity: None,
}, },
], ],
selected_company_id: Some(1), selected_company_id: Some(1),
@ -2312,7 +2401,7 @@ mod tests {
target: RuntimeCompanyTarget::ConditionTrueCompany, target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7, value: 7,
}], }],
executable_import_ready: false, executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}], }],
}), }),
@ -2398,8 +2487,11 @@ mod tests {
standalone_condition_rows: real_condition_rows(), standalone_condition_rows: real_condition_rows(),
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(), grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![], decoded_actions: vec![RuntimeEffect::SetCompanyCash {
executable_import_ready: false, target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}], }],
}), }),
@ -2521,6 +2613,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500, current_cash: 500,
debt: 20, debt: 20,
active: true,
available_track_laying_capacity: None,
}], }],
selected_company_id: Some(42), selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,
@ -2610,7 +2704,7 @@ mod tests {
target: RuntimeCompanyTarget::SelectedCompany, target: RuntimeCompanyTarget::SelectedCompany,
value: 250, value: 250,
}], }],
executable_import_ready: false, executable_import_ready: true,
notes: vec![ notes: vec![
"decoded from grounded real 0x4e9a row framing".to_string(), "decoded from grounded real 0x4e9a row framing".to_string(),
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(), "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(),
@ -2647,6 +2741,381 @@ mod tests {
assert_eq!(import.state.companies[0].current_cash, 250); assert_eq!(import.state.companies[0].current_cash, 250);
} }
#[test]
fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() {
let base_state = RuntimeState {
companies: vec![crate::RuntimeCompany {
company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
..state()
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 13,
live_record_count: 1,
live_entry_ids: vec![13],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 13,
payload_offset: Some(0x7202),
payload_len: Some(120),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
}),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(true)],
decoded_actions: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany,
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"real-deactivate-company-overlay",
None,
)
.expect("overlay import should project");
assert_eq!(import.state.event_runtime_records.len(), 1);
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("real deactivate-company descriptor should execute");
assert!(!import.state.companies[0].active);
assert_eq!(import.state.selected_company_id, None);
}
#[test]
fn keeps_real_deactivate_company_false_row_parity_only() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 14,
live_record_count: 1,
live_entry_ids: vec![14],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 14,
payload_offset: Some(0x7202),
payload_len: Some(120),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
}),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_deactivate_company_row(false)],
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"real-deactivate-company-false",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_real_descriptor")
);
}
#[test]
fn overlays_real_track_capacity_descriptor_into_executable_runtime_record() {
let base_state = RuntimeState {
companies: vec![crate::RuntimeCompany {
company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
..state()
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 16,
live_record_count: 1,
live_entry_ids: vec![16],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 16,
payload_offset: Some(0x7202),
payload_len: Some(120),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
}),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_track_capacity_row(18)],
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::SelectedCompany,
value: Some(18),
}],
executable_import_ready: true,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"real-track-capacity-overlay",
None,
)
.expect("overlay import should project");
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("real track-capacity descriptor should execute");
assert_eq!(
import.state.companies[0].available_track_laying_capacity,
Some(18)
);
}
#[test]
fn keeps_mixed_real_records_out_of_event_runtime_records() {
let base_state = RuntimeState {
companies: vec![crate::RuntimeCompany {
company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: Some(42),
..state()
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 17,
live_record_count: 1,
live_entry_ids: vec![17],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 17,
payload_offset: Some(0x7202),
payload_len: Some(160),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: vec![1, 1, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
}),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![1, 1, 0, 0],
grouped_effect_rows: vec![
real_track_capacity_row(18),
unsupported_real_grouped_row(),
],
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::SelectedCompany,
value: Some(18),
}],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
}),
notes: vec![],
};
let import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"mixed-real-record-overlay",
None,
)
.expect("overlay import should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_real_descriptor")
);
}
#[test] #[test]
fn overlays_save_slice_events_onto_base_company_context() { fn overlays_save_slice_events_onto_base_company_context() {
let base_state = RuntimeState { let base_state = RuntimeState {
@ -2665,6 +3134,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500, current_cash: 500,
debt: 20, debt: 20,
active: true,
available_track_laying_capacity: None,
}], }],
selected_company_id: Some(42), selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,
@ -2827,6 +3298,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
}], }],
selected_company_id: Some(42), selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,

View file

@ -13,11 +13,19 @@ pub enum RuntimeCompanyControllerKind {
Ai, Ai,
} }
fn runtime_company_default_active() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompany { pub struct RuntimeCompany {
pub company_id: u32, pub company_id: u32,
pub current_cash: i64, pub current_cash: i64,
pub debt: u64, pub debt: u64,
#[serde(default = "runtime_company_default_active")]
pub active: bool,
#[serde(default)]
pub available_track_laying_capacity: Option<u32>,
#[serde(default)] #[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind, pub controller_kind: RuntimeCompanyControllerKind,
} }
@ -44,6 +52,13 @@ pub enum RuntimeEffect {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
value: i64, value: i64,
}, },
DeactivateCompany {
target: RuntimeCompanyTarget,
},
SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget,
value: Option<u32>,
},
AdjustCompanyCash { AdjustCompanyCash {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
delta: i64, delta: i64,
@ -352,10 +367,14 @@ impl RuntimeState {
self.calendar.validate()?; self.calendar.validate()?;
let mut seen_company_ids = BTreeSet::new(); let mut seen_company_ids = BTreeSet::new();
let mut active_company_ids = BTreeSet::new();
for company in &self.companies { for company in &self.companies {
if !seen_company_ids.insert(company.company_id) { if !seen_company_ids.insert(company.company_id) {
return Err(format!("duplicate company_id {}", company.company_id)); return Err(format!("duplicate company_id {}", company.company_id));
} }
if company.active {
active_company_ids.insert(company.company_id);
}
} }
if let Some(selected_company_id) = self.selected_company_id { if let Some(selected_company_id) = self.selected_company_id {
if !seen_company_ids.contains(&selected_company_id) { if !seen_company_ids.contains(&selected_company_id) {
@ -364,6 +383,12 @@ impl RuntimeState {
selected_company_id selected_company_id
)); ));
} }
if !active_company_ids.contains(&selected_company_id) {
return Err(format!(
"selected_company_id {} must reference an active company",
selected_company_id
));
}
} }
let mut seen_record_ids = BTreeSet::new(); let mut seen_record_ids = BTreeSet::new();
@ -669,6 +694,8 @@ fn validate_runtime_effect(
} }
} }
RuntimeEffect::SetCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::DeactivateCompany { target }
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
validate_company_target(target, valid_company_ids)?; validate_company_target(target, valid_company_ids)?;
@ -756,12 +783,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
RuntimeCompany { RuntimeCompany {
company_id: 1, company_id: 1,
current_cash: 200, current_cash: 200,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
], ],
@ -840,6 +871,8 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
@ -882,6 +915,8 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
@ -1018,6 +1053,8 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(2), selected_company_id: Some(2),
@ -1030,4 +1067,36 @@ mod tests {
assert!(state.validate().is_err()); assert!(state.validate().is_err());
} }
#[test]
fn rejects_selected_company_id_that_is_inactive() {
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,
active: false,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(1),
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());
}
} }

View file

@ -163,7 +163,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
label: "Deactivate Company", label: "Deactivate Company",
target_mask_bits: 0x01, target_mask_bits: 0x01,
parameter_family: "company_lifecycle_toggle", parameter_family: "company_lifecycle_toggle",
executable_in_runtime: false, executable_in_runtime: true,
}, },
RealGroupedEffectDescriptorMetadata { RealGroupedEffectDescriptorMetadata {
descriptor_id: 15, descriptor_id: 15,
@ -177,7 +177,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
label: "Company Track Pieces Buildable", label: "Company Track Pieces Buildable",
target_mask_bits: 0x01, target_mask_bits: 0x01,
parameter_family: "company_build_limit_scalar", parameter_family: "company_build_limit_scalar",
executable_in_runtime: false, executable_in_runtime: true,
}, },
]; ];
@ -1944,7 +1944,8 @@ fn parse_real_event_runtime_record_summary(
.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))
.unwrap_or_default(); .unwrap_or_default();
let executable_import_ready = !decoded_actions.is_empty() let executable_import_ready = !grouped_effect_rows.is_empty()
&& decoded_actions.len() == grouped_effect_rows.len()
&& decoded_actions && decoded_actions
.iter() .iter()
.all(runtime_effect_supported_for_save_import); .all(runtime_effect_supported_for_save_import);
@ -2265,6 +2266,25 @@ fn decode_real_grouped_effect_action(
}); });
} }
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 13
&& row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0
{
return Some(RuntimeEffect::DeactivateCompany { target });
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 16
&& row.row_shape == "scalar_assignment"
&& row.raw_scalar_value >= 0
{
return Some(RuntimeEffect::SetCompanyTrackLayingCapacity {
target,
value: Some(row.raw_scalar_value as u32),
});
}
None None
} }
@ -2417,14 +2437,22 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
RuntimeEffect::SetWorldFlag { .. } RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::DeactivateCompany { .. }
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => true, | RuntimeEffect::RemoveEventRecord { .. } => true,
RuntimeEffect::SetCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. } | RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!(
matches!(target, RuntimeCompanyTarget::AllActive) target,
} RuntimeCompanyTarget::AllActive
| RuntimeCompanyTarget::Ids { .. }
| RuntimeCompanyTarget::HumanCompanies
| RuntimeCompanyTarget::AiCompanies
| RuntimeCompanyTarget::SelectedCompany
| RuntimeCompanyTarget::ConditionTrueCompany
),
RuntimeEffect::AppendEventRecord { record } => record RuntimeEffect::AppendEventRecord { record } => record
.effects .effects
.iter() .iter()
@ -7513,10 +7541,10 @@ mod tests {
.expect("event runtime collection summary should parse"); .expect("event runtime collection summary should parse");
assert_eq!(summary.decoded_record_count, 1); assert_eq!(summary.decoded_record_count, 1);
assert_eq!(summary.imported_runtime_record_count, 0); assert_eq!(summary.imported_runtime_record_count, 1);
assert_eq!(summary.records[0].decode_status, "parity_only"); assert_eq!(summary.records[0].decode_status, "executable");
assert_eq!(summary.records[0].payload_family, "synthetic_harness"); assert_eq!(summary.records[0].payload_family, "synthetic_harness");
assert!(!summary.records[0].executable_import_ready); assert!(summary.records[0].executable_import_ready);
} }
#[test] #[test]

View file

@ -299,6 +299,41 @@ fn apply_runtime_effects(
mutated_company_ids.insert(company_id); mutated_company_ids.insert(company_id);
} }
} }
RuntimeEffect::DeactivateCompany { target } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying deactivate effect"
)
})?;
company.active = false;
mutated_company_ids.insert(company_id);
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
}
}
}
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying track capacity effect"
)
})?;
company.available_track_laying_capacity = *value;
mutated_company_ids.insert(company_id);
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target)?; let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids { for company_id in company_ids {
@ -429,6 +464,7 @@ fn resolve_company_target_ids(
RuntimeCompanyTarget::AllActive => Ok(state RuntimeCompanyTarget::AllActive => Ok(state
.companies .companies
.iter() .iter()
.filter(|company| company.active)
.map(|company| company.company_id) .map(|company| company.company_id)
.collect()), .collect()),
RuntimeCompanyTarget::Ids { ids } => { RuntimeCompanyTarget::Ids { ids } => {
@ -458,7 +494,10 @@ fn resolve_company_target_ids(
Ok(state Ok(state
.companies .companies
.iter() .iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human) .filter(|company| {
company.active
&& company.controller_kind == RuntimeCompanyControllerKind::Human
})
.map(|company| company.company_id) .map(|company| company.company_id)
.collect()) .collect())
} }
@ -476,14 +515,27 @@ fn resolve_company_target_ids(
Ok(state Ok(state
.companies .companies
.iter() .iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai) .filter(|company| {
company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai
})
.map(|company| company.company_id) .map(|company| company.company_id)
.collect()) .collect())
} }
RuntimeCompanyTarget::SelectedCompany => state RuntimeCompanyTarget::SelectedCompany => {
let selected_company_id = state
.selected_company_id .selected_company_id
.map(|company_id| vec![company_id]) .ok_or_else(|| "target requires selected_company_id context".to_string())?;
.ok_or_else(|| "target requires selected_company_id context".to_string()), if state
.companies
.iter()
.any(|company| company.company_id == selected_company_id && company.active)
{
Ok(vec![selected_company_id])
} else {
Err("target requires selected_company_id to reference an active company"
.to_string())
}
}
RuntimeCompanyTarget::ConditionTrueCompany => { RuntimeCompanyTarget::ConditionTrueCompany => {
Err("target requires condition-evaluation context".to_string()) Err("target requires condition-evaluation context".to_string())
} }
@ -530,6 +582,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
}], }],
selected_company_id: None, selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
@ -692,12 +746,16 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 5, debt: 5,
active: true,
available_track_laying_capacity: None,
}, },
RuntimeCompany { RuntimeCompany {
company_id: 2, company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20, current_cash: 20,
debt: 8, debt: 8,
active: true,
available_track_laying_capacity: None,
}, },
], ],
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
@ -744,12 +802,16 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human, controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
active: true,
available_track_laying_capacity: None,
}, },
RuntimeCompany { RuntimeCompany {
company_id: 2, company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai, controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20, current_cash: 20,
debt: 2, debt: 2,
active: true,
available_track_laying_capacity: None,
}, },
], ],
selected_company_id: Some(1), selected_company_id: Some(1),
@ -866,6 +928,179 @@ mod tests {
assert!(error.contains("controller_kind")); assert!(error.contains("controller_kind"));
} }
#[test]
fn all_active_and_role_targets_exclude_inactive_companies() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 1,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 20,
debt: 2,
active: false,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 3,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 30,
debt: 3,
active: true,
available_track_laying_capacity: None,
},
],
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 16,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive,
delta: 5,
}],
},
RuntimeEventRecord {
record_id: 17,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 4,
}],
},
RuntimeEventRecord {
record_id: 18,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 6,
}],
},
],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("active-company filtering should succeed");
assert_eq!(state.companies[0].current_cash, 15);
assert_eq!(state.companies[1].current_cash, 20);
assert_eq!(state.companies[2].current_cash, 35);
assert_eq!(state.companies[0].debt, 5);
assert_eq!(state.companies[1].debt, 2);
assert_eq!(state.companies[2].debt, 9);
}
#[test]
fn deactivating_selected_company_clears_selection() {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: Some(8),
}],
selected_company_id: Some(1),
event_runtime_records: vec![RuntimeEventRecord {
record_id: 19,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany,
}],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("deactivate company effect should succeed");
assert!(!state.companies[0].active);
assert_eq!(state.selected_company_id, None);
assert_eq!(result.service_events[0].mutated_company_ids, vec![1]);
}
#[test]
fn sets_track_laying_capacity_for_resolved_targets() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 0,
active: true,
available_track_laying_capacity: None,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 20,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
value: Some(14),
}],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("track capacity effect should succeed");
assert_eq!(state.companies[0].available_track_laying_capacity, None);
assert_eq!(state.companies[1].available_track_laying_capacity, Some(14));
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
}
#[test] #[test]
fn rejects_condition_true_company_target_without_condition_context() { fn rejects_condition_true_company_target_without_condition_context() {
let mut state = RuntimeState { let mut state = RuntimeState {
@ -941,6 +1176,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown, controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 2, debt: 2,
active: true,
available_track_laying_capacity: None,
}], }],
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 30, record_id: 30,

View file

@ -28,6 +28,7 @@ pub struct RuntimeSummary {
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 packed_event_collection_present: bool, pub packed_event_collection_present: bool,
pub packed_event_record_count: usize, pub packed_event_record_count: usize,
pub packed_event_decoded_record_count: usize, pub packed_event_decoded_record_count: usize,
@ -122,6 +123,7 @@ impl RuntimeSummary {
.clone(), .clone(),
metadata_count: state.metadata.len(), metadata_count: state.metadata.len(),
company_count: state.companies.len(), company_count: state.companies.len(),
active_company_count: state.companies.iter().filter(|company| company.active).count(),
packed_event_collection_present: state.packed_event_collection.is_some(), packed_event_collection_present: state.packed_event_collection.is_some(),
packed_event_record_count: state packed_event_record_count: state
.packed_event_collection .packed_event_collection
@ -302,7 +304,8 @@ mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use crate::{ use crate::{
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
}; };
@ -399,4 +402,48 @@ mod tests {
assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0); assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0);
assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0); assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0);
} }
#[test]
fn counts_active_companies_separately_from_total_companies() {
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: 10,
debt: 0,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
},
RuntimeCompany {
company_id: 2,
current_cash: 20,
debt: 0,
active: false,
available_track_laying_capacity: Some(7),
controller_kind: RuntimeCompanyControllerKind::Ai,
},
],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.company_count, 2);
assert_eq!(summary.active_company_count, 1);
}
} }

View file

@ -77,13 +77,14 @@ The highest-value next passes are now:
avoid shell-first implementation bets avoid shell-first implementation bets
- keep using overlay imports as the context bridge when selectively executable packed rows still - keep using overlay imports as the context bridge when selectively executable packed rows still
need live company state that save slices do not persist need live company state that save slices do not persist
- treat broader real grouped-descriptor recovery as the active packed-event frontier now that - treat broader real grouped-descriptor recovery as the active packed-event frontier now that the
descriptor `2` `Company Cash` already parses, summarizes, and executes through the ordinary first company-scoped batch already parses, summarizes, and executes through the ordinary runtime
runtime path when overlay context resolves its symbolic company scope path when overlay context resolves its symbolic company scope: descriptor `2` `Company Cash`,
descriptor `13` `Deactivate Company`, and descriptor `16` `Company Track Pieces Buildable`
- 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
- leave condition-relative company scopes explicit and blocked until condition evaluation has - leave condition-relative company scopes explicit and blocked until condition evaluation has
grounded runtime semantics grounded runtime semantics, and keep mixed supported/unsupported real rows 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

@ -29,11 +29,13 @@ Implemented today:
symbolic scopes through the ordinary runtime service path while keeping condition-relative symbolic scopes through the ordinary runtime service path while keeping condition-relative
company scopes explicitly blocked company scopes explicitly blocked
- real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview - real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview
summaries, and one recovered executable family: descriptor `2` = `Company Cash` summaries, and three recovered executable company-scoped families: descriptor `2` = `Company Cash`,
descriptor `13` = `Deactivate Company`, and descriptor `16` = `Company Track Pieces Buildable`
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 real grouped-descriptor coverage beyond `Company Cash`, plus condition-relative execution broader real grouped-descriptor coverage beyond the current company-scoped batch, plus
for the still-blocked symbolic scopes, not another persistence scaffold pass. condition-relative execution for the still-blocked symbolic scopes, not another persistence
scaffold pass.
## Why This Boundary ## Why This Boundary
@ -375,11 +377,12 @@ Checked-in fixture families already include:
## Next Slice ## Next Slice
The recommended next implementation slice is broader real grouped-descriptor coverage on top of the The recommended next implementation slice is broader real grouped-descriptor coverage on top of the
now-stable compact-control, symbolic-target, and first recovered real-family path. now-stable compact-control, symbolic-target, and current company-scoped real-family batch.
Target behavior: Target behavior:
- keep descriptor `2` `Company Cash` as the proof that real grouped rows can cross the whole path: - keep descriptors `2` `Company Cash`, `13` `Deactivate Company`, and `16`
`Company Track Pieces Buildable` as the proof that real grouped rows can cross the whole path:
parse, semantic summary, overlay-backed import, and ordinary trigger execution parse, semantic summary, overlay-backed import, and ordinary trigger execution
- recover more real descriptor identities from the checked-in effect table and expose their target - recover more real descriptor identities from the checked-in effect table and expose their target
masks and conservative semantic previews without guessing unsupported behavior masks and conservative semantic previews without guessing unsupported behavior
@ -399,8 +402,11 @@ Public-model expectations for that slice:
Fixture work for that slice: Fixture work for that slice:
- preserve the parity-heavy tracked sample as the condition-relative blocked frontier while it now - preserve the parity-heavy tracked sample as the condition-relative blocked frontier while it now
carries recovered `Company Cash` semantics carries recovered `Company Cash` semantics with executable import readiness
- add overlay-backed captured fixtures whenever a new real descriptor family becomes executable - keep overlay-backed captured fixtures for the executable company-scoped real families:
`Company Cash`, `Deactivate Company`, and `Company Track Pieces Buildable`
- keep a mixed real-row overlay fixture to lock the all-or-nothing parity rule for partially
supported real records
- 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
Current local constraint: Current local constraint:

View file

@ -0,0 +1,58 @@
{
"format_version": 1,
"fixture_id": "packed-event-deactivate-company-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by an overlay import document so the real Deactivate Company row executes against selected-company context."
},
"state_import_path": "packed-event-deactivate-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": 3,
"active_company_count": 2,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"selected_company_id": null,
"companies": [
{
"company_id": 1,
"active": true
},
{
"company_id": 2,
"active": true
},
{
"company_id": 3,
"active": false
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported"
}
]
},
"event_runtime_records": [
{
"record_id": 31,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,10 @@
{
"format_version": 1,
"import_id": "packed-event-deactivate-company-overlay",
"source": {
"description": "Overlay import document for the real Deactivate Company descriptor sample.",
"notes": []
},
"base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json",
"save_slice_path": "packed-event-deactivate-company-save-slice.json"
}

View file

@ -0,0 +1,106 @@
{
"format_version": 1,
"save_slice_id": "packed-event-deactivate-company-save-slice",
"source": {
"description": "Tracked save-slice document with a real packed-event row for Deactivate Company.",
"original_save_filename": "captured-deactivate-company.gms",
"original_save_sha256": "deactivate-company-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks executable import for real descriptor 13"
]
},
"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": 31,
"live_record_count": 1,
"live_entry_ids": [31],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 31,
"payload_offset": 29186,
"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": [],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 13,
"descriptor_label": "Deactivate Company",
"target_mask_bits": 1,
"parameter_family": "company_lifecycle_toggle",
"opcode": 1,
"raw_scalar_value": 1,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "bool_toggle",
"semantic_family": "bool_toggle",
"semantic_preview": "Set Deactivate Company to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_actions": [
{
"kind": "deactivate_company",
"target": {
"kind": "selected_company"
}
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real descriptor 13 sample"
]
}
}

View file

@ -0,0 +1,54 @@
{
"format_version": 1,
"fixture_id": "packed-event-mixed-company-descriptor-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by an overlay import document proving mixed real grouped-row records stay parity-only."
},
"state_import_path": "packed-event-mixed-company-descriptor-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,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 1,
"event_runtime_record_count": 0,
"total_event_record_service_count": 0,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"selected_company_id": 3,
"companies": [
{
"company_id": 1,
"available_track_laying_capacity": null
},
{
"company_id": 2,
"available_track_laying_capacity": null
},
{
"company_id": 3,
"available_track_laying_capacity": null
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "blocked_unmapped_real_descriptor"
}
]
},
"event_runtime_records": []
}
}

View file

@ -0,0 +1,10 @@
{
"format_version": 1,
"import_id": "packed-event-mixed-company-descriptor-overlay",
"source": {
"description": "Overlay import document for a mixed real grouped-row record that should remain parity-only.",
"notes": []
},
"base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json",
"save_slice_path": "packed-event-mixed-company-descriptor-save-slice.json"
}

View file

@ -0,0 +1,128 @@
{
"format_version": 1,
"save_slice_id": "packed-event-mixed-company-descriptor-save-slice",
"source": {
"description": "Tracked save-slice document with one supported and one unsupported real company-scoped grouped row.",
"original_save_filename": "captured-mixed-company-descriptor.gms",
"original_save_sha256": "mixed-company-descriptor-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks all-or-nothing import for mixed real grouped-row records"
]
},
"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": 33,
"live_record_count": 1,
"live_entry_ids": [33],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 33,
"payload_offset": 29186,
"payload_len": 160,
"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, 1, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [1, 1, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 16,
"descriptor_label": "Company Track Pieces Buildable",
"target_mask_bits": 1,
"parameter_family": "company_build_limit_scalar",
"opcode": 3,
"raw_scalar_value": 18,
"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 Company Track Pieces Buildable to 18",
"locomotive_name": null,
"notes": []
},
{
"group_index": 1,
"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_actions": [
{
"kind": "set_company_track_laying_capacity",
"target": {
"kind": "selected_company"
},
"value": 18
}
],
"executable_import_ready": false,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"mixed real descriptor sample"
]
}
}

View file

@ -162,7 +162,7 @@
"value": 7 "value": 7
} }
], ],
"executable_import_ready": false, "executable_import_ready": true,
"notes": [ "notes": [
"decoded from grounded real 0x4e9a row framing", "decoded from grounded real 0x4e9a row framing",
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0" "grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0"

View file

@ -0,0 +1,58 @@
{
"format_version": 1,
"fixture_id": "packed-event-track-capacity-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by an overlay import document so the real Company Track Pieces Buildable row executes against selected-company context."
},
"state_import_path": "packed-event-track-capacity-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,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"selected_company_id": 3,
"companies": [
{
"company_id": 1,
"available_track_laying_capacity": null
},
{
"company_id": 2,
"available_track_laying_capacity": null
},
{
"company_id": 3,
"available_track_laying_capacity": 18
}
],
"packed_event_collection": {
"records": [
{
"import_outcome": "imported"
}
]
},
"event_runtime_records": [
{
"record_id": 32,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,10 @@
{
"format_version": 1,
"import_id": "packed-event-track-capacity-overlay",
"source": {
"description": "Overlay import document for the real Company Track Pieces Buildable descriptor sample.",
"notes": []
},
"base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json",
"save_slice_path": "packed-event-track-capacity-save-slice.json"
}

View file

@ -0,0 +1,107 @@
{
"format_version": 1,
"save_slice_id": "packed-event-track-capacity-save-slice",
"source": {
"description": "Tracked save-slice document with a real packed-event row for Company Track Pieces Buildable.",
"original_save_filename": "captured-track-capacity.gms",
"original_save_sha256": "track-capacity-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks executable import for real descriptor 16"
]
},
"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": 32,
"live_record_count": 1,
"live_entry_ids": [32],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 32,
"payload_offset": 29186,
"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": [],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 16,
"descriptor_label": "Company Track Pieces Buildable",
"target_mask_bits": 1,
"parameter_family": "company_build_limit_scalar",
"opcode": 3,
"raw_scalar_value": 18,
"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 Company Track Pieces Buildable to 18",
"locomotive_name": null,
"notes": []
}
],
"decoded_actions": [
{
"kind": "set_company_track_laying_capacity",
"target": {
"kind": "selected_company"
},
"value": 18
}
],
"executable_import_ready": true,
"notes": [
"decoded from grounded real 0x4e9a row framing"
]
}
]
},
"notes": [
"real descriptor 16 sample"
]
}
}