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
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
frontier is real `0x4e9a` compact-control decode and descriptor-frontier tightening on top of the
existing save-slice, snapshot, and overlay-import workflows. The PE32 hook remains useful as
capture and integration tooling, but it is no longer the main execution milestone.
frontier is real grouped-descriptor semantic mapping on top of the existing save-slice, snapshot,
overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries
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

View file

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

View file

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

View file

@ -76,6 +76,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[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>,
#[serde(default)]
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 actual.packed_event_blocked_missing_compact_control_count != count {
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::{
CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
@ -99,6 +100,47 @@ enum SaveSliceProjectionMode {
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(
save_slice: &SmpLoadedSaveSlice,
import_id: &str,
@ -109,7 +151,7 @@ pub fn project_save_slice_to_runtime_state_import(
}
let projection = project_save_slice_components(
save_slice,
&BTreeSet::new(),
&ImportCompanyContext::standalone(),
SaveSliceProjectionMode::Standalone,
)?;
@ -125,6 +167,7 @@ pub fn project_save_slice_to_runtime_state_import(
world_restore: projection.world_restore,
metadata: projection.metadata,
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
@ -151,14 +194,10 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
}
base_state.validate()?;
let known_company_ids = base_state
.companies
.iter()
.map(|company| company.company_id)
.collect::<BTreeSet<_>>();
let company_context = ImportCompanyContext::from_runtime_state(base_state);
let projection = project_save_slice_components(
save_slice,
&known_company_ids,
&company_context,
SaveSliceProjectionMode::Overlay,
)?;
@ -177,6 +216,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
world_restore: projection.world_restore,
metadata,
companies: base_state.companies.clone(),
selected_company_id: base_state.selected_company_id,
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
@ -194,7 +234,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
fn project_save_slice_components(
save_slice: &SmpLoadedSaveSlice,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
mode: SaveSliceProjectionMode,
) -> Result<SaveSliceProjection, String> {
let mut world_flags = BTreeMap::new();
@ -301,7 +341,7 @@ fn project_save_slice_components(
}
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 {
metadata.insert(
"save_slice.event_runtime_collection_source_kind".to_string(),
@ -491,7 +531,7 @@ fn project_save_slice_components(
fn project_packed_event_collection(
save_slice: &SmpLoadedSaveSlice,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
) -> Result<
(
Option<RuntimePackedEventCollectionSummary>,
@ -506,9 +546,7 @@ fn project_packed_event_collection(
let mut imported_runtime_records = Vec::new();
let mut imported_record_ids = BTreeSet::new();
for record in &summary.records {
if let Some(import_result) =
smp_packed_record_to_runtime_event_record(record, known_company_ids)
{
if let Some(import_result) = smp_packed_record_to_runtime_event_record(record, company_context) {
let runtime_record = import_result?;
imported_record_ids.insert(record.live_entry_id);
imported_runtime_records.push(runtime_record);
@ -521,7 +559,7 @@ fn project_packed_event_collection(
.map(|record| {
runtime_packed_event_record_summary_from_smp(
record,
known_company_ids,
company_context,
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(
record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
imported: bool,
) -> RuntimePackedEventRecordSummary {
RuntimePackedEventRecordSummary {
@ -586,11 +624,12 @@ fn runtime_packed_event_record_summary_from_smp(
.iter()
.map(runtime_packed_event_grouped_effect_row_summary_from_smp)
.collect(),
grouped_company_targets: classify_real_grouped_company_targets(record),
decoded_actions: record.decoded_actions.clone(),
executable_import_ready: record.executable_import_ready,
import_outcome: Some(determine_packed_event_import_outcome(
record,
known_company_ids,
company_context,
imported,
)),
notes: record.notes.clone(),
@ -661,16 +700,14 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
) -> Option<Result<RuntimeEventRecord, String>> {
if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" {
return None;
}
let effects =
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, known_company_ids) {
let effects = match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
Ok(effects) => effects,
Err(err) if err.contains("unresolved company ids") => return None,
Err(_) => return None,
};
@ -713,17 +750,17 @@ fn smp_packed_record_to_runtime_event_record(
fn smp_runtime_effects_to_runtime_effects(
effects: &[RuntimeEffect],
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
) -> Result<Vec<RuntimeEffect>, String> {
effects
.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()
}
fn smp_runtime_effect_to_runtime_effect(
effect: &RuntimeEffect,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
) -> Result<RuntimeEffect, String> {
match effect {
RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag {
@ -731,23 +768,23 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value,
}),
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 {
target: target.clone(),
delta: *delta,
})
} 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 } => {
if company_target_supported_for_import(target, known_company_ids) {
if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyDebt {
target: target.clone(),
delta: *delta,
})
} 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 } => {
@ -765,7 +802,7 @@ fn smp_runtime_effect_to_runtime_effect(
RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord {
record: Box::new(smp_runtime_record_template_to_runtime(
record,
known_company_ids,
company_context,
)?),
}),
RuntimeEffect::ActivateEventRecord { record_id } => {
@ -786,7 +823,7 @@ fn smp_runtime_effect_to_runtime_effect(
fn smp_runtime_record_template_to_runtime(
record: &RuntimeEventRecordTemplate,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
) -> Result<RuntimeEventRecordTemplate, String> {
Ok(RuntimeEventRecordTemplate {
record_id: record.record_id,
@ -794,28 +831,71 @@ fn smp_runtime_record_template_to_runtime(
active: record.active,
marks_collection_dirty: record.marks_collection_dirty,
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(
target: &crate::RuntimeCompanyTarget,
known_company_ids: &BTreeSet<u32>,
) -> bool {
fn company_target_import_blocker(
target: &RuntimeCompanyTarget,
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
match target {
crate::RuntimeCompanyTarget::AllActive => true,
crate::RuntimeCompanyTarget::Ids { ids } => {
!ids.is_empty()
&& ids
RuntimeCompanyTarget::AllActive => None,
RuntimeCompanyTarget::Ids { ids } => {
if ids.is_empty()
|| ids
.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(
record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>,
company_context: &ImportCompanyContext,
imported: bool,
) -> String {
if imported {
@ -828,42 +908,105 @@ fn determine_packed_event_import_outcome(
if record.compact_control.is_none() {
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();
}
if packed_record_requires_missing_company_context(record, known_company_ids) {
return "blocked_missing_company_context".to_string();
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) {
return company_target_import_outcome(blocker).to_string();
}
"blocked_unsupported_decode".to_string()
}
fn packed_record_requires_missing_company_context(
fn packed_record_company_target_import_blocker(
record: &SmpLoadedPackedEventRecordSummary,
known_company_ids: &BTreeSet<u32>,
) -> bool {
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
record
.decoded_actions
.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,
known_company_ids: &BTreeSet<u32>,
) -> bool {
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
match effect {
RuntimeEffect::AdjustCompanyCash { 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| {
runtime_effect_requires_missing_company_context(nested, known_company_ids)
}),
RuntimeEffect::AppendEventRecord { record } => record
.effects
.iter()
.find_map(|nested| runtime_effect_company_target_import_blocker(nested, company_context)),
RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| 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(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::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> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
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]
fn loads_dump_document() {
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]
fn leaves_real_records_without_compact_control_blocked_missing_compact_control() {
let save_slice = SmpLoadedSaveSlice {
@ -1970,7 +2342,7 @@ mod tests {
}
#[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 {
file_extension_hint: Some("gms".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),
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!(
import
.state
@ -2063,9 +2505,11 @@ mod tests {
metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]),
companies: vec![crate::RuntimeCompany {
company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 500,
debt: 20,
}],
selected_company_id: Some(42),
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 1,
@ -2223,9 +2667,11 @@ mod tests {
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 42,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
}],
selected_company_id: Some(42),
packed_event_collection: None,
event_runtime_records: Vec::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,
};
pub use runtime::{
RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,

View file

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

View file

@ -4,17 +4,32 @@ use serde::{Deserialize, Serialize};
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)]
pub struct RuntimeCompany {
pub company_id: u32,
pub current_cash: i64,
pub debt: u64,
#[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCompanyTarget {
AllActive,
HumanCompanies,
AiCompanies,
SelectedCompany,
ConditionTrueCompany,
Ids { ids: Vec<u32> },
}
@ -137,6 +152,8 @@ pub struct RuntimePackedEventRecordSummary {
#[serde(default)]
pub grouped_effect_rows: Vec<RuntimePackedEventGroupedEffectRowSummary>,
#[serde(default)]
pub grouped_company_targets: Vec<Option<RuntimeCompanyTarget>>,
#[serde(default)]
pub decoded_actions: Vec<RuntimeEffect>,
#[serde(default)]
pub executable_import_ready: bool,
@ -303,6 +320,8 @@ pub struct RuntimeState {
#[serde(default)]
pub companies: Vec<RuntimeCompany>,
#[serde(default)]
pub selected_company_id: Option<u32>,
#[serde(default)]
pub packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
#[serde(default)]
pub event_runtime_records: Vec<RuntimeEventRecord>,
@ -324,6 +343,14 @@ impl RuntimeState {
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();
for record in &self.event_runtime_records {
@ -672,7 +699,11 @@ fn validate_company_target(
valid_company_ids: &BTreeSet<u32>,
) -> Result<(), String> {
match target {
RuntimeCompanyTarget::AllActive => Ok(()),
RuntimeCompanyTarget::AllActive
| RuntimeCompanyTarget::HumanCompanies
| RuntimeCompanyTarget::AiCompanies
| RuntimeCompanyTarget::SelectedCompany
| RuntimeCompanyTarget::ConditionTrueCompany => Ok(()),
RuntimeCompanyTarget::Ids { ids } => {
if ids.is_empty() {
return Err("target ids must not be empty".to_string());
@ -709,13 +740,16 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
},
RuntimeCompany {
company_id: 1,
current_cash: 200,
debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
},
],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -762,6 +796,7 @@ mod tests {
},
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -789,7 +824,9 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
@ -829,7 +866,9 @@ mod tests {
company_id: 1,
current_cash: 100,
debt: 0,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: vec![RuntimeEventRecord {
record_id: 7,
@ -875,6 +914,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -905,6 +945,7 @@ mod tests {
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
@ -927,6 +968,7 @@ mod tests {
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: None,
@ -942,4 +984,34 @@ mod tests {
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 crate::{
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState, RuntimeSummary,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary,
calendar::BoundaryEventKind,
};
@ -430,6 +431,49 @@ fn resolve_company_target_ids(
}
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 crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeServiceState,
RuntimeWorldRestoreState,
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeWorldRestoreState,
};
fn state() -> RuntimeState {
@ -470,9 +514,11 @@ mod tests {
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 0,
}],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -630,11 +676,13 @@ mod tests {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 5,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20,
debt: 8,
},
@ -674,6 +722,165 @@ mod tests {
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]
fn one_shot_record_only_fires_once() {
let mut state = RuntimeState {
@ -718,6 +925,7 @@ mod tests {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 2,
}],

View file

@ -35,6 +35,9 @@ pub struct RuntimeSummary {
pub packed_event_parity_only_record_count: usize,
pub packed_event_unsupported_record_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_unmapped_real_descriptor_count: usize,
pub packed_event_blocked_structural_only_count: usize,
@ -171,6 +174,48 @@ impl RuntimeSummary {
.count()
})
.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_collection
.as_ref()
@ -277,6 +322,7 @@ mod tests {
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -307,6 +353,7 @@ mod tests {
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_missing_compact_control".to_string()),
@ -329,6 +376,7 @@ mod tests {
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
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_structural_only_count, 0);
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
avoid shell-first implementation bets
- tighten the packed-event frontier from generic real-row structure into decoded real compact
control, so parity rows carry mode, selector, one-shot, and grouped target-scope state directly
- use overlay imports as the context bridge when selectively executable packed rows still need live
company state that save slices do not persist
- widen real packed-event executable coverage only after the compact-control and descriptor frontier
is stable, not just after row framing is parsed
- keep using overlay imports as the context bridge when selectively executable packed rows still
need live company state that save slices do not persist
- treat normalized symbolic company targets as the active packed-event frontier now that
`selected_company`, `human_companies`, and `ai_companies` import and execute through the runtime
service path
- 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,
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

View file

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

View file

@ -26,8 +26,9 @@
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_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_unmapped_real_descriptor_count": 1,
"packed_event_blocked_unmapped_real_descriptor_count": 0,
"packed_event_blocked_structural_only_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
@ -51,11 +52,19 @@
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"one_shot": true,
"import_outcome": "blocked_unmapped_real_descriptor",
"import_outcome": "blocked_missing_condition_context",
"compact_control": {
"primary_selector_0x7f0": 99,
"grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3]
},
"grouped_company_targets": [
{
"kind": "condition_true_company"
},
null,
null,
null
],
"standalone_condition_rows": [
{
"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"
]
}
}