Add symbolic company target runtime import

This commit is contained in:
Jan Petykiewicz 2026-04-15 09:13:51 -07:00
commit f918d0c4f7
17 changed files with 1230 additions and 80 deletions

View file

@ -11,9 +11,12 @@ The long-term direction is still a DLL we can inject into the original executabl
individual functions as we build them out. The active implementation milestone is now a headless individual functions as we build them out. The active implementation milestone is now a headless
runtime rehost layer that can execute deterministic world work, compare normalized state, and grow runtime rehost layer that can execute deterministic world work, compare normalized state, and grow
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 real `0x4e9a` compact-control decode and descriptor-frontier tightening on top of the frontier is real grouped-descriptor semantic mapping on top of the existing save-slice, snapshot,
existing save-slice, snapshot, and overlay-import workflows. The PE32 hook remains useful as overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries
capture and integration tooling, but it is no longer the main execution milestone. selected-company and controller-role context through overlay imports so synthetic packed records can
execute `selected_company`, `human_companies`, and `ai_companies` scopes without a parallel packed
executor, while condition-relative company scopes remain explicitly blocked. 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

@ -4440,6 +4440,8 @@ mod tests {
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json"); .join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.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"))
.join("../../fixtures/runtime/packed-event-symbolic-company-scope-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");
@ -4447,6 +4449,8 @@ mod tests {
.expect("save-slice-backed selective-import fixture should summarize"); .expect("save-slice-backed selective-import fixture should summarize");
run_runtime_summarize_fixture(&overlay_fixture) run_runtime_summarize_fixture(&overlay_fixture)
.expect("overlay-backed selective-import fixture should summarize"); .expect("overlay-backed selective-import fixture should summarize");
run_runtime_summarize_fixture(&symbolic_overlay_fixture)
.expect("overlay-backed symbolic-target fixture should summarize");
} }
#[test] #[test]

View file

@ -173,6 +173,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(), world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -326,9 +327,11 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: vec![rrt_runtime::RuntimeCompany { companies: vec![rrt_runtime::RuntimeCompany {
company_id: 42, company_id: 42,
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
}], }],
selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),

View file

@ -76,6 +76,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>, pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_company_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_compact_control_count: Option<usize>, pub packed_event_blocked_missing_compact_control_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>, pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
@ -377,6 +383,30 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_selection_context_count {
if actual.packed_event_blocked_missing_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_selection_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_company_role_context_count {
if actual.packed_event_blocked_missing_company_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_company_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_company_role_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_condition_context_count {
if actual.packed_event_blocked_missing_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_condition_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_missing_compact_control_count { if let Some(count) = self.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count { if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{ use crate::{
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCompactControlSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
@ -99,6 +100,47 @@ enum SaveSliceProjectionMode {
Overlay, Overlay,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
struct ImportCompanyContext {
known_company_ids: BTreeSet<u32>,
selected_company_id: Option<u32>,
has_complete_controller_context: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompanyTargetImportBlocker {
MissingCompanyContext,
MissingSelectionContext,
MissingCompanyRoleContext,
MissingConditionContext,
}
impl ImportCompanyContext {
fn standalone() -> Self {
Self {
known_company_ids: BTreeSet::new(),
selected_company_id: None,
has_complete_controller_context: false,
}
}
fn from_runtime_state(state: &RuntimeState) -> Self {
Self {
known_company_ids: state
.companies
.iter()
.map(|company| company.company_id)
.collect(),
selected_company_id: state.selected_company_id,
has_complete_controller_context: !state.companies.is_empty()
&& state
.companies
.iter()
.all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown),
}
}
}
pub fn project_save_slice_to_runtime_state_import( pub fn project_save_slice_to_runtime_state_import(
save_slice: &SmpLoadedSaveSlice, save_slice: &SmpLoadedSaveSlice,
import_id: &str, import_id: &str,
@ -109,7 +151,7 @@ pub fn project_save_slice_to_runtime_state_import(
} }
let projection = project_save_slice_components( let projection = project_save_slice_components(
save_slice, save_slice,
&BTreeSet::new(), &ImportCompanyContext::standalone(),
SaveSliceProjectionMode::Standalone, SaveSliceProjectionMode::Standalone,
)?; )?;
@ -125,6 +167,7 @@ pub fn project_save_slice_to_runtime_state_import(
world_restore: projection.world_restore, world_restore: projection.world_restore,
metadata: projection.metadata, metadata: projection.metadata,
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: projection.packed_event_collection, packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records, event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability, candidate_availability: projection.candidate_availability,
@ -151,14 +194,10 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
} }
base_state.validate()?; base_state.validate()?;
let known_company_ids = base_state let company_context = ImportCompanyContext::from_runtime_state(base_state);
.companies
.iter()
.map(|company| company.company_id)
.collect::<BTreeSet<_>>();
let projection = project_save_slice_components( let projection = project_save_slice_components(
save_slice, save_slice,
&known_company_ids, &company_context,
SaveSliceProjectionMode::Overlay, SaveSliceProjectionMode::Overlay,
)?; )?;
@ -177,6 +216,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
world_restore: projection.world_restore, world_restore: projection.world_restore,
metadata, metadata,
companies: base_state.companies.clone(), companies: base_state.companies.clone(),
selected_company_id: base_state.selected_company_id,
packed_event_collection: projection.packed_event_collection, packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records, event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability, candidate_availability: projection.candidate_availability,
@ -194,7 +234,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
fn project_save_slice_components( fn project_save_slice_components(
save_slice: &SmpLoadedSaveSlice, save_slice: &SmpLoadedSaveSlice,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
mode: SaveSliceProjectionMode, mode: SaveSliceProjectionMode,
) -> Result<SaveSliceProjection, String> { ) -> Result<SaveSliceProjection, String> {
let mut world_flags = BTreeMap::new(); let mut world_flags = BTreeMap::new();
@ -301,7 +341,7 @@ fn project_save_slice_components(
} }
let (packed_event_collection, event_runtime_records) = let (packed_event_collection, event_runtime_records) =
project_packed_event_collection(save_slice, known_company_ids)?; project_packed_event_collection(save_slice, company_context)?;
if let Some(summary) = &save_slice.event_runtime_collection { if let Some(summary) = &save_slice.event_runtime_collection {
metadata.insert( metadata.insert(
"save_slice.event_runtime_collection_source_kind".to_string(), "save_slice.event_runtime_collection_source_kind".to_string(),
@ -491,7 +531,7 @@ fn project_save_slice_components(
fn project_packed_event_collection( fn project_packed_event_collection(
save_slice: &SmpLoadedSaveSlice, save_slice: &SmpLoadedSaveSlice,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> Result< ) -> Result<
( (
Option<RuntimePackedEventCollectionSummary>, Option<RuntimePackedEventCollectionSummary>,
@ -506,9 +546,7 @@ fn project_packed_event_collection(
let mut imported_runtime_records = Vec::new(); let mut imported_runtime_records = Vec::new();
let mut imported_record_ids = BTreeSet::new(); let mut imported_record_ids = BTreeSet::new();
for record in &summary.records { for record in &summary.records {
if let Some(import_result) = if let Some(import_result) = smp_packed_record_to_runtime_event_record(record, company_context) {
smp_packed_record_to_runtime_event_record(record, known_company_ids)
{
let runtime_record = import_result?; let runtime_record = import_result?;
imported_record_ids.insert(record.live_entry_id); imported_record_ids.insert(record.live_entry_id);
imported_runtime_records.push(runtime_record); imported_runtime_records.push(runtime_record);
@ -521,7 +559,7 @@ fn project_packed_event_collection(
.map(|record| { .map(|record| {
runtime_packed_event_record_summary_from_smp( runtime_packed_event_record_summary_from_smp(
record, record,
known_company_ids, company_context,
imported_record_ids.contains(&record.live_entry_id), imported_record_ids.contains(&record.live_entry_id),
) )
}) })
@ -551,7 +589,7 @@ fn project_packed_event_collection(
fn runtime_packed_event_record_summary_from_smp( fn runtime_packed_event_record_summary_from_smp(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
imported: bool, imported: bool,
) -> RuntimePackedEventRecordSummary { ) -> RuntimePackedEventRecordSummary {
RuntimePackedEventRecordSummary { RuntimePackedEventRecordSummary {
@ -586,11 +624,12 @@ fn runtime_packed_event_record_summary_from_smp(
.iter() .iter()
.map(runtime_packed_event_grouped_effect_row_summary_from_smp) .map(runtime_packed_event_grouped_effect_row_summary_from_smp)
.collect(), .collect(),
grouped_company_targets: classify_real_grouped_company_targets(record),
decoded_actions: record.decoded_actions.clone(), decoded_actions: record.decoded_actions.clone(),
executable_import_ready: record.executable_import_ready, executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome( import_outcome: Some(determine_packed_event_import_outcome(
record, record,
known_company_ids, company_context,
imported, imported,
)), )),
notes: record.notes.clone(), notes: record.notes.clone(),
@ -661,18 +700,16 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
fn smp_packed_record_to_runtime_event_record( fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> Option<Result<RuntimeEventRecord, String>> { ) -> Option<Result<RuntimeEventRecord, String>> {
if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" { if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" {
return None; return None;
} }
let effects = let effects = match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids) { Ok(effects) => effects,
Ok(effects) => effects, Err(_) => return None,
Err(err) if err.contains("unresolved company ids") => return None, };
Err(_) => return None,
};
Some((|| { Some((|| {
let trigger_kind = record.trigger_kind.ok_or_else(|| { let trigger_kind = record.trigger_kind.ok_or_else(|| {
@ -713,17 +750,17 @@ fn smp_packed_record_to_runtime_event_record(
fn smp_runtime_effects_to_runtime_effects( fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect], effects: &[RuntimeEffect],
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> Result<Vec<RuntimeEffect>, String> { ) -> Result<Vec<RuntimeEffect>, String> {
effects effects
.iter() .iter()
.map(|effect| smp_runtime_effect_to_runtime_effect(effect, known_company_ids)) .map(|effect| smp_runtime_effect_to_runtime_effect(effect, company_context))
.collect() .collect()
} }
fn smp_runtime_effect_to_runtime_effect( fn smp_runtime_effect_to_runtime_effect(
effect: &RuntimeEffect, effect: &RuntimeEffect,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> Result<RuntimeEffect, String> { ) -> Result<RuntimeEffect, String> {
match effect { match effect {
RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag { RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag {
@ -731,23 +768,23 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value, value: *value,
}), }),
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
if company_target_supported_for_import(target, known_company_ids) { if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyCash { Ok(RuntimeEffect::AdjustCompanyCash {
target: target.clone(), target: target.clone(),
delta: *delta, delta: *delta,
}) })
} else { } else {
Err("packed company-cash effect requires unresolved company ids".to_string()) Err(company_target_import_error_message(target, company_context))
} }
} }
RuntimeEffect::AdjustCompanyDebt { target, delta } => { RuntimeEffect::AdjustCompanyDebt { target, delta } => {
if company_target_supported_for_import(target, known_company_ids) { if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyDebt { Ok(RuntimeEffect::AdjustCompanyDebt {
target: target.clone(), target: target.clone(),
delta: *delta, delta: *delta,
}) })
} else { } else {
Err("packed company-debt effect requires unresolved company ids".to_string()) Err(company_target_import_error_message(target, company_context))
} }
} }
RuntimeEffect::SetCandidateAvailability { name, value } => { RuntimeEffect::SetCandidateAvailability { name, value } => {
@ -765,7 +802,7 @@ fn smp_runtime_effect_to_runtime_effect(
RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord { RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord {
record: Box::new(smp_runtime_record_template_to_runtime( record: Box::new(smp_runtime_record_template_to_runtime(
record, record,
known_company_ids, company_context,
)?), )?),
}), }),
RuntimeEffect::ActivateEventRecord { record_id } => { RuntimeEffect::ActivateEventRecord { record_id } => {
@ -786,7 +823,7 @@ fn smp_runtime_effect_to_runtime_effect(
fn smp_runtime_record_template_to_runtime( fn smp_runtime_record_template_to_runtime(
record: &RuntimeEventRecordTemplate, record: &RuntimeEventRecordTemplate,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> Result<RuntimeEventRecordTemplate, String> { ) -> Result<RuntimeEventRecordTemplate, String> {
Ok(RuntimeEventRecordTemplate { Ok(RuntimeEventRecordTemplate {
record_id: record.record_id, record_id: record.record_id,
@ -794,28 +831,71 @@ fn smp_runtime_record_template_to_runtime(
active: record.active, active: record.active,
marks_collection_dirty: record.marks_collection_dirty, marks_collection_dirty: record.marks_collection_dirty,
one_shot: record.one_shot, one_shot: record.one_shot,
effects: smp_runtime_effects_to_runtime_effects(&record.effects, known_company_ids)?, effects: smp_runtime_effects_to_runtime_effects(&record.effects, company_context)?,
}) })
} }
fn company_target_supported_for_import( fn company_target_import_blocker(
target: &crate::RuntimeCompanyTarget, target: &RuntimeCompanyTarget,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> bool { ) -> Option<CompanyTargetImportBlocker> {
match target { match target {
crate::RuntimeCompanyTarget::AllActive => true, RuntimeCompanyTarget::AllActive => None,
crate::RuntimeCompanyTarget::Ids { ids } => { RuntimeCompanyTarget::Ids { ids } => {
!ids.is_empty() if ids.is_empty()
&& ids || ids
.iter() .iter()
.all(|company_id| known_company_ids.contains(company_id)) .any(|company_id| !company_context.known_company_ids.contains(company_id))
{
Some(CompanyTargetImportBlocker::MissingCompanyContext)
} else {
None
}
} }
RuntimeCompanyTarget::HumanCompanies | RuntimeCompanyTarget::AiCompanies => {
if !company_context.has_complete_controller_context {
Some(CompanyTargetImportBlocker::MissingCompanyRoleContext)
} else {
None
}
}
RuntimeCompanyTarget::SelectedCompany => {
if company_context.selected_company_id.is_some() {
None
} else {
Some(CompanyTargetImportBlocker::MissingSelectionContext)
}
}
RuntimeCompanyTarget::ConditionTrueCompany => {
Some(CompanyTargetImportBlocker::MissingConditionContext)
}
}
}
fn company_target_import_error_message(
target: &RuntimeCompanyTarget,
company_context: &ImportCompanyContext,
) -> String {
match company_target_import_blocker(target, company_context) {
Some(CompanyTargetImportBlocker::MissingCompanyContext) => {
"packed company effect requires resolved company ids".to_string()
}
Some(CompanyTargetImportBlocker::MissingSelectionContext) => {
"packed company effect requires selected_company_id context".to_string()
}
Some(CompanyTargetImportBlocker::MissingCompanyRoleContext) => {
"packed company effect requires company controller role context".to_string()
}
Some(CompanyTargetImportBlocker::MissingConditionContext) => {
"packed company effect requires condition-relative context".to_string()
}
None => "packed company effect is importable".to_string(),
} }
} }
fn determine_packed_event_import_outcome( fn determine_packed_event_import_outcome(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
imported: bool, imported: bool,
) -> String { ) -> String {
if imported { if imported {
@ -828,42 +908,105 @@ 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 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(); return "blocked_unmapped_real_descriptor".to_string();
} }
if packed_record_requires_missing_company_context(record, known_company_ids) { if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) {
return "blocked_missing_company_context".to_string(); return company_target_import_outcome(blocker).to_string();
} }
"blocked_unsupported_decode".to_string() "blocked_unsupported_decode".to_string()
} }
fn packed_record_requires_missing_company_context( fn packed_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> bool { ) -> Option<CompanyTargetImportBlocker> {
record record
.decoded_actions .decoded_actions
.iter() .iter()
.any(|effect| runtime_effect_requires_missing_company_context(effect, known_company_ids)) .find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context))
} }
fn runtime_effect_requires_missing_company_context( fn runtime_effect_company_target_import_blocker(
effect: &RuntimeEffect, effect: &RuntimeEffect,
known_company_ids: &BTreeSet<u32>, company_context: &ImportCompanyContext,
) -> bool { ) -> Option<CompanyTargetImportBlocker> {
match effect { match effect {
RuntimeEffect::AdjustCompanyCash { target, .. } RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
!company_target_supported_for_import(target, known_company_ids) company_target_import_blocker(target, company_context)
} }
RuntimeEffect::AppendEventRecord { record } => record.effects.iter().any(|nested| { RuntimeEffect::AppendEventRecord { record } => record
runtime_effect_requires_missing_company_context(nested, known_company_ids) .effects
}), .iter()
.find_map(|nested| runtime_effect_company_target_import_blocker(nested, company_context)),
RuntimeEffect::SetWorldFlag { .. } RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => false, | RuntimeEffect::RemoveEventRecord { .. } => None,
}
}
fn classify_real_grouped_company_targets(
record: &SmpLoadedPackedEventRecordSummary,
) -> Vec<Option<RuntimeCompanyTarget>> {
let Some(control) = &record.compact_control else {
return Vec::new();
};
control
.grouped_target_scope_ordinals_0x7fb
.iter()
.enumerate()
.map(|(group_index, ordinal)| {
if !record
.grouped_effect_rows
.iter()
.any(|row| row.group_index == group_index)
{
return None;
}
classify_real_grouped_company_target(*ordinal)
})
.collect()
}
fn classify_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_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 {
match blocker {
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
CompanyTargetImportBlocker::MissingSelectionContext => {
"blocked_missing_selection_context"
}
CompanyTargetImportBlocker::MissingCompanyRoleContext => {
"blocked_missing_company_role_context"
}
CompanyTargetImportBlocker::MissingConditionContext => {
"blocked_missing_condition_context"
}
} }
} }
@ -1154,6 +1297,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(), world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -1214,6 +1358,34 @@ mod tests {
}] }]
} }
fn synthetic_packed_record(
record_index: usize,
live_entry_id: u32,
effect: RuntimeEffect,
) -> crate::SmpLoadedPackedEventRecordSummary {
crate::SmpLoadedPackedEventRecordSummary {
record_index,
live_entry_id,
payload_offset: Some(0x7200 + (live_entry_id as usize * 0x20)),
payload_len: Some(64),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
compact_control: None,
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_actions: vec![effect],
executable_import_ready: false,
notes: vec!["synthetic test record".to_string()],
}
}
fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> { fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0, group_index: 0,
@ -1248,6 +1420,22 @@ mod tests {
} }
} }
fn real_compact_control_without_symbolic_company_scope(
) -> crate::SmpLoadedPackedEventCompactControlSummary {
crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 1,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![8, 9, 10, 11],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22],
}
}
#[test] #[test]
fn loads_dump_document() { fn loads_dump_document() {
let text = serde_json::to_string(&RuntimeStateDumpDocument { let text = serde_json::to_string(&RuntimeStateDumpDocument {
@ -1883,6 +2071,190 @@ mod tests {
); );
} }
#[test]
fn classifies_symbolic_company_target_blockers_for_standalone_import() {
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: 12,
live_record_count: 3,
live_entry_ids: vec![10, 11, 12],
decoded_record_count: 3,
imported_runtime_record_count: 0,
records: vec![
synthetic_packed_record(
0,
10,
RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 1,
},
),
synthetic_packed_record(
1,
11,
RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 2,
},
),
synthetic_packed_record(
2,
12,
RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::ConditionTrueCompany,
delta: 3,
},
),
],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"symbolic-blockers",
None,
)
.expect("standalone projection should succeed");
assert!(import.state.event_runtime_records.is_empty());
let outcomes = import
.state
.packed_event_collection
.as_ref()
.expect("packed event collection should be present")
.records
.iter()
.map(|record| record.import_outcome.clone())
.collect::<Vec<_>>();
assert_eq!(
outcomes,
vec![
Some("blocked_missing_selection_context".to_string()),
Some("blocked_missing_company_role_context".to_string()),
Some("blocked_missing_condition_context".to_string()),
]
);
}
#[test]
fn overlays_symbolic_company_targets_into_executable_runtime_records() {
let base_state = RuntimeState {
companies: vec![
crate::RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 10,
},
crate::RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 50,
debt: 20,
},
],
selected_company_id: Some(1),
..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: 22,
live_record_count: 2,
live_entry_ids: vec![21, 22],
decoded_record_count: 2,
imported_runtime_record_count: 0,
records: vec![
synthetic_packed_record(
0,
21,
RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 15,
},
),
synthetic_packed_record(
1,
22,
RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 4,
},
),
],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"symbolic-overlay",
None,
)
.expect("overlay projection should succeed");
assert_eq!(import.state.event_runtime_records.len(), 2);
let outcomes = import
.state
.packed_event_collection
.as_ref()
.expect("packed event collection should be present")
.records
.iter()
.map(|record| record.import_outcome.clone())
.collect::<Vec<_>>();
assert_eq!(
outcomes,
vec![Some("imported".to_string()), Some("imported".to_string())]
);
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("symbolic overlay dispatch should succeed");
assert_eq!(import.state.companies[0].current_cash, 115);
assert_eq!(import.state.companies[1].debt, 24);
}
#[test] #[test]
fn leaves_real_records_without_compact_control_blocked_missing_compact_control() { fn leaves_real_records_without_compact_control_blocked_missing_compact_control() {
let save_slice = SmpLoadedSaveSlice { let save_slice = SmpLoadedSaveSlice {
@ -1970,7 +2342,7 @@ mod tests {
} }
#[test] #[test]
fn leaves_real_records_with_compact_control_blocked_unmapped_real_descriptor() { fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context() {
let save_slice = SmpLoadedSaveSlice { let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()), file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
@ -2038,6 +2410,76 @@ mod tests {
.map(|control| control.mode_byte_0x7ef), .map(|control| control.mode_byte_0x7ef),
Some(6) Some(6)
); );
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_missing_condition_context")
);
}
#[test]
fn leaves_real_records_with_unclassified_scope_blocked_unmapped_real_descriptor() {
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: 7,
live_record_count: 1,
live_entry_ids: vec![7],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(true),
compact_control: Some(real_compact_control_without_symbolic_company_scope()),
text_bands: packed_text_bands(),
standalone_condition_row_count: 1,
standalone_condition_rows: real_condition_rows(),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
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,
"packed-events-real-descriptor-frontier",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!( assert_eq!(
import import
.state .state
@ -2063,9 +2505,11 @@ mod tests {
metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]), metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]),
companies: vec![crate::RuntimeCompany { companies: vec![crate::RuntimeCompany {
company_id: 42, company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500, current_cash: 500,
debt: 20, debt: 20,
}], }],
selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 1, record_id: 1,
@ -2223,9 +2667,11 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany { companies: vec![crate::RuntimeCompany {
company_id: 42, company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
}], }],
selected_company_id: Some(42),
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),

View file

@ -35,7 +35,7 @@ pub use pk4::{
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file, extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
}; };
pub use runtime::{ pub use runtime::{
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,

View file

@ -93,6 +93,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(), world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),

View file

@ -4,17 +4,32 @@ use serde::{Deserialize, Serialize};
use crate::CalendarPoint; use crate::CalendarPoint;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeCompanyControllerKind {
#[default]
Unknown,
Human,
Ai,
}
#[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)]
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 {
AllActive, AllActive,
HumanCompanies,
AiCompanies,
SelectedCompany,
ConditionTrueCompany,
Ids { ids: Vec<u32> }, Ids { ids: Vec<u32> },
} }
@ -137,6 +152,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)] #[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>, pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
#[serde(default)] #[serde(default)]
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>, pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)] #[serde(default)]
pub executable_import_ready: bool, pub executable_import_ready: bool,
@ -303,6 +320,8 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub companies: Vec<RuntimeCompany>, pub companies: Vec<RuntimeCompany>,
#[serde(default)] #[serde(default)]
pub selected_company_id: Option<u32>,
#[serde(default)]
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>, pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
#[serde(default)] #[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>, pub event_runtime_records: Vec<RuntimeEventRecord>,
@ -324,6 +343,14 @@ impl RuntimeState {
return Err(format!("duplicate company_id {}", company.company_id)); return Err(format!("duplicate company_id {}", company.company_id));
} }
} }
if let Some(selected_company_id) = self.selected_company_id {
if !seen_company_ids.contains(&selected_company_id) {
return Err(format!(
"selected_company_id {} does not reference a live company",
selected_company_id
));
}
}
let mut seen_record_ids = BTreeSet::new(); let mut seen_record_ids = BTreeSet::new();
for record in &self.event_runtime_records { for record in &self.event_runtime_records {
@ -672,7 +699,11 @@ fn validate_company_target(
valid_company_ids: &BTreeSet<u32>, valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> { ) -> Result<(), String> {
match target { match target {
RuntimeCompanyTarget::AllActive => Ok(()), RuntimeCompanyTarget::AllActive
| RuntimeCompanyTarget::HumanCompanies
| RuntimeCompanyTarget::AiCompanies
| RuntimeCompanyTarget::SelectedCompany
| RuntimeCompanyTarget::ConditionTrueCompany => Ok(()),
RuntimeCompanyTarget::Ids { ids } => { RuntimeCompanyTarget::Ids { ids } => {
if ids.is_empty() { if ids.is_empty() {
return Err("target ids must not be empty".to_string()); return Err("target ids must not be empty".to_string());
@ -709,13 +740,16 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
RuntimeCompany { RuntimeCompany {
company_id: 1, company_id: 1,
current_cash: 200, current_cash: 200,
debt: 0, debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
], ],
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -762,6 +796,7 @@ mod tests {
}, },
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -789,7 +824,9 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 7, record_id: 7,
@ -829,7 +866,9 @@ mod tests {
company_id: 1, company_id: 1,
current_cash: 100, current_cash: 100,
debt: 0, debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
record_id: 7, record_id: 7,
@ -875,6 +914,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(), world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(), mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -905,6 +945,7 @@ mod tests {
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, import_outcome: None,
@ -927,6 +968,7 @@ mod tests {
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: None, import_outcome: None,
@ -942,4 +984,34 @@ mod tests {
assert!(state.validate().is_err()); assert!(state.validate().is_err());
} }
#[test]
fn rejects_selected_company_id_that_does_not_exist() {
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,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: Some(2),
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

@ -3,7 +3,8 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState, RuntimeSummary, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary,
calendar::BoundaryEventKind, calendar::BoundaryEventKind,
}; };
@ -430,6 +431,49 @@ fn resolve_company_target_ids(
} }
Ok(ids.clone()) Ok(ids.clone())
} }
RuntimeCompanyTarget::HumanCompanies => {
if state
.companies
.iter()
.any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires company role context but at least one company has unknown controller_kind"
.to_string(),
);
}
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human)
.map(|company| company.company_id)
.collect())
}
RuntimeCompanyTarget::AiCompanies => {
if state
.companies
.iter()
.any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires company role context but at least one company has unknown controller_kind"
.to_string(),
);
}
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai)
.map(|company| company.company_id)
.collect())
}
RuntimeCompanyTarget::SelectedCompany => state
.selected_company_id
.map(|company_id| vec![company_id])
.ok_or_else(|| "target requires selected_company_id context".to_string()),
RuntimeCompanyTarget::ConditionTrueCompany => {
Err("target requires condition-evaluation context".to_string())
}
} }
} }
@ -451,9 +495,9 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeServiceState, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
RuntimeWorldRestoreState, RuntimeServiceState, RuntimeWorldRestoreState,
}; };
fn state() -> RuntimeState { fn state() -> RuntimeState {
@ -470,9 +514,11 @@ mod tests {
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: vec![RuntimeCompany { companies: vec![RuntimeCompany {
company_id: 1, company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 0, debt: 0,
}], }],
selected_company_id: None,
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
@ -630,11 +676,13 @@ mod tests {
companies: vec![ companies: vec![
RuntimeCompany { RuntimeCompany {
company_id: 1, company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 5, debt: 5,
}, },
RuntimeCompany { RuntimeCompany {
company_id: 2, company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20, current_cash: 20,
debt: 8, debt: 8,
}, },
@ -674,6 +722,165 @@ mod tests {
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
} }
#[test]
fn resolves_symbolic_company_targets() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 2,
},
],
selected_company_id: Some(1),
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 11,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 5,
}],
},
RuntimeEventRecord {
record_id: 12,
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: 3,
}],
},
RuntimeEventRecord {
record_id: 13,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 7,
}],
},
],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("symbolic target effects should succeed");
assert_eq!(state.companies[0].current_cash, 22);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.companies[1].current_cash, 20);
assert_eq!(state.companies[1].debt, 5);
assert_eq!(result.service_events[0].mutated_company_ids, vec![1, 2]);
}
#[test]
fn rejects_selected_company_target_without_selection_context() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 14,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("selected company target should require selection context");
assert!(error.contains("selected_company_id"));
}
#[test]
fn rejects_human_or_ai_targets_without_role_context() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 15,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("human target should require controller metadata");
assert!(error.contains("controller_kind"));
}
#[test]
fn rejects_condition_true_company_target_without_condition_context() {
let mut state = RuntimeState {
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::ConditionTrueCompany,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("condition-relative target should remain blocked");
assert!(error.contains("condition-evaluation context"));
}
#[test] #[test]
fn one_shot_record_only_fires_once() { fn one_shot_record_only_fires_once() {
let mut state = RuntimeState { let mut state = RuntimeState {
@ -718,6 +925,7 @@ mod tests {
let mut state = RuntimeState { let mut state = RuntimeState {
companies: vec![RuntimeCompany { companies: vec![RuntimeCompany {
company_id: 1, company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10, current_cash: 10,
debt: 2, debt: 2,
}], }],

View file

@ -35,6 +35,9 @@ pub struct RuntimeSummary {
pub packed_event_parity_only_record_count: usize, pub packed_event_parity_only_record_count: usize,
pub packed_event_unsupported_record_count: usize, pub packed_event_unsupported_record_count: usize,
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_company_role_context_count: usize,
pub packed_event_blocked_missing_condition_context_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_structural_only_count: usize, pub packed_event_blocked_structural_only_count: usize,
@ -171,6 +174,48 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_selection_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_selection_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_company_role_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_company_role_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_condition_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_condition_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_missing_compact_control_count: state packed_event_blocked_missing_compact_control_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -277,6 +322,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(), world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(), metadata: BTreeMap::new(),
companies: Vec::new(), companies: Vec::new(),
selected_company_id: None,
packed_event_collection: Some(RuntimePackedEventCollectionSummary { packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(), mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -307,6 +353,7 @@ mod tests {
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_missing_compact_control".to_string()), import_outcome: Some("blocked_missing_compact_control".to_string()),
@ -329,6 +376,7 @@ mod tests {
standalone_condition_rows: Vec::new(), standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(), grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(), decoded_actions: Vec::new(),
executable_import_ready: false, executable_import_ready: false,
import_outcome: Some("blocked_missing_company_context".to_string()), import_outcome: Some("blocked_missing_company_context".to_string()),
@ -347,5 +395,8 @@ mod tests {
assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0); assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0);
assert_eq!(summary.packed_event_blocked_structural_only_count, 0); assert_eq!(summary.packed_event_blocked_structural_only_count, 0);
assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1); assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1);
assert_eq!(summary.packed_event_blocked_missing_selection_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);
} }
} }

View file

@ -75,12 +75,15 @@ The highest-value next passes are now:
- preserve the atlas and function map as the source of subsystem boundaries while continuing to - preserve the atlas and function map as the source of subsystem boundaries while continuing to
avoid shell-first implementation bets avoid shell-first implementation bets
- tighten the packed-event frontier from generic real-row structure into decoded real compact - keep using overlay imports as the context bridge when selectively executable packed rows still
control, so parity rows carry mode, selector, one-shot, and grouped target-scope state directly need live company state that save slices do not persist
- use overlay imports as the context bridge when selectively executable packed rows still need live - treat normalized symbolic company targets as the active packed-event frontier now that
company state that save slices do not persist `selected_company`, `human_companies`, and `ai_companies` import and execute through the runtime
- widen real packed-event executable coverage only after the compact-control and descriptor frontier service path
is stable, not just after row framing is parsed - widen real packed-event executable coverage only after the compact-control, symbolic target, and
descriptor frontier is stable, not just after row framing is parsed
- leave condition-relative company scopes explicit and blocked until condition evaluation has
grounded runtime semantics
- 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

@ -24,10 +24,14 @@ Implemented today:
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger - checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
service, snapshot-backed inputs, save-slice-backed inputs, overlay-import-backed inputs, service, snapshot-backed inputs, save-slice-backed inputs, overlay-import-backed inputs,
normalized state-fragment assertions, and imported packed-event execution normalized state-fragment assertions, and imported packed-event execution
- overlay imports now preserve selected-company and controller-role context, and the normalized
company-target model can execute `selected_company`, `human_companies`, and `ai_companies`
symbolic scopes through the ordinary runtime service path while keeping condition-relative
company scopes explicitly blocked
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
real `0x4e9a` compact-control decode and descriptor-frontier tightening, not another persistence real grouped-descriptor semantic mapping on top of the now-stable compact-control and symbolic
scaffold pass. target frontier, not another persistence scaffold pass.
## Why This Boundary ## Why This Boundary
@ -222,8 +226,10 @@ Current status:
decoded actions fit the current normalized runtime-effect model decoded actions fit the current normalized runtime-effect model
- tracked save-slice documents now provide a repo-friendly captured-runtime path without checking in - tracked save-slice documents now provide a repo-friendly captured-runtime path without checking in
raw `.smp` binaries raw `.smp` binaries
- the remaining gap is wider packed target-family coverage plus company-import depth, not - overlay-backed captured-runtime inputs now provide enough runtime company context for symbolic
first-pass captured-runtime plumbing selected-company and controller-role scopes without inventing company state from save bytes alone
- the remaining gap is wider real grouped-descriptor semantic coverage plus condition evaluation,
not first-pass captured-runtime plumbing
### Milestone 4: Domain Expansion ### Milestone 4: Domain Expansion

View file

@ -26,8 +26,9 @@
"packed_event_imported_runtime_record_count": 0, "packed_event_imported_runtime_record_count": 0,
"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": 1,
"packed_event_blocked_missing_compact_control_count": 0, "packed_event_blocked_missing_compact_control_count": 0,
"packed_event_blocked_unmapped_real_descriptor_count": 1, "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": 0,
"total_company_cash": 0 "total_company_cash": 0
@ -51,11 +52,19 @@
"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_unmapped_real_descriptor", "import_outcome": "blocked_missing_condition_context",
"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]
}, },
"grouped_company_targets": [
{
"kind": "condition_true_company"
},
null,
null,
null
],
"standalone_condition_rows": [ "standalone_condition_rows": [
{ {
"candidate_name": "AutoPlant" "candidate_name": "AutoPlant"

View file

@ -0,0 +1,51 @@
{
"format_version": 1,
"snapshot_id": "packed-event-symbolic-company-scope-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying selected-company and controller-role context for symbolic packed-event targets."
},
"state": {
"calendar": {
"year": 1840,
"month_slot": 0,
"phase_slot": 1,
"tick_slot": 2
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "symbolic target context"
},
"companies": [
{
"company_id": 1,
"controller_kind": "human",
"current_cash": 100,
"debt": 10
},
{
"company_id": 2,
"controller_kind": "ai",
"current_cash": 50,
"debt": 20
},
{
"company_id": 3,
"controller_kind": "human",
"current_cash": 70,
"debt": 30
}
],
"selected_company_id": 3,
"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,86 @@
{
"format_version": 1,
"fixture_id": "packed-event-symbolic-company-scope-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by an overlay import document so symbolic company-target packed events execute against selected-company and controller-role context."
},
"state_import_path": "packed-event-symbolic-company-scope-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,
"packed_event_collection_present": true,
"packed_event_record_count": 4,
"packed_event_decoded_record_count": 4,
"packed_event_imported_runtime_record_count": 3,
"packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 0,
"packed_event_blocked_missing_condition_context_count": 1,
"event_runtime_record_count": 3,
"total_event_record_service_count": 3,
"total_trigger_dispatch_count": 1,
"dirty_rerun_count": 0,
"total_company_cash": 244
},
"expected_state_fragment": {
"selected_company_id": 3,
"companies": [
{
"company_id": 1,
"controller_kind": "human",
"current_cash": 100,
"debt": 14
},
{
"company_id": 2,
"controller_kind": "ai",
"current_cash": 59,
"debt": 20
},
{
"company_id": 3,
"controller_kind": "human",
"current_cash": 85,
"debt": 34
}
],
"packed_event_collection": {
"live_entry_ids": [21, 22, 23, 24],
"records": [
{
"import_outcome": "imported"
},
{
"import_outcome": "imported"
},
{
"import_outcome": "imported"
},
{
"import_outcome": "blocked_missing_condition_context"
}
]
},
"event_runtime_records": [
{
"record_id": 21,
"service_count": 1
},
{
"record_id": 22,
"service_count": 1
},
{
"record_id": 23,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,12 @@
{
"format_version": 1,
"import_id": "packed-event-symbolic-company-scope-overlay",
"source": {
"description": "Overlay import that combines a selected-company snapshot with symbolic company-target packed events.",
"notes": [
"used to prove selected, human, and ai symbolic company targets import through the normalized runtime path"
]
},
"base_snapshot_path": "packed-event-symbolic-company-scope-overlay-base-snapshot.json",
"save_slice_path": "packed-event-symbolic-company-scope-save-slice.json"
}

View file

@ -0,0 +1,165 @@
{
"format_version": 1,
"save_slice_id": "packed-event-symbolic-company-scope-save-slice",
"source": {
"description": "Tracked save-slice document with synthetic packed-event records that use symbolic company targets.",
"original_save_filename": "captured-symbolic-company-scope.gms",
"original_save_sha256": "symbolic-company-scope-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks selected-company and controller-role import behavior"
]
},
"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": 29952,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 24,
"live_record_count": 4,
"live_entry_ids": [21, 22, 23, 24],
"decoded_record_count": 4,
"imported_runtime_record_count": 3,
"records": [
{
"record_index": 0,
"live_entry_id": 21,
"payload_offset": 29186,
"payload_len": 56,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 0, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "adjust_company_cash",
"target": {
"kind": "selected_company"
},
"delta": 15
}
],
"executable_import_ready": true,
"notes": [
"selected-company symbolic target"
]
},
{
"record_index": 1,
"live_entry_id": 22,
"payload_offset": 29242,
"payload_len": 56,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 0, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "adjust_company_debt",
"target": {
"kind": "human_companies"
},
"delta": 4
}
],
"executable_import_ready": true,
"notes": [
"human-company symbolic target"
]
},
{
"record_index": 2,
"live_entry_id": 23,
"payload_offset": 29298,
"payload_len": 56,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 0, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "adjust_company_cash",
"target": {
"kind": "ai_companies"
},
"delta": 9
}
],
"executable_import_ready": true,
"notes": [
"ai-company symbolic target"
]
},
{
"record_index": 3,
"live_entry_id": 24,
"payload_offset": 29354,
"payload_len": 56,
"decode_status": "parity_only",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 0, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "adjust_company_debt",
"target": {
"kind": "condition_true_company"
},
"delta": 1
}
],
"executable_import_ready": false,
"notes": [
"condition-relative symbolic target remains blocked"
]
}
]
},
"notes": [
"symbolic company target sample"
]
}
}