Recover real packed event descriptor semantics

This commit is contained in:
Jan Petykiewicz 2026-04-15 09:50:58 -07:00
commit eb6c4833af
9 changed files with 719 additions and 120 deletions

View file

@ -11,12 +11,14 @@ 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 grouped-descriptor semantic mapping on top of the existing save-slice, snapshot, frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot,
overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries
selected-company and controller-role context through overlay imports so synthetic packed records can selected-company and controller-role context through overlay imports, real descriptor `2`
execute `selected_company`, `human_companies`, and `ai_companies` scopes without a parallel packed `Company Cash` now parses and executes through the ordinary runtime path, and synthetic packed
executor, while condition-relative company scopes remain explicitly blocked. The PE32 hook remains records still exercise the same service engine without a parallel packed executor. Condition-
useful as capture and integration tooling, but it is no longer the main execution milestone. relative company scopes remain explicitly blocked until condition evaluation is grounded. 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

@ -6,10 +6,9 @@ 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, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary, RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
@ -133,10 +132,9 @@ impl ImportCompanyContext {
.collect(), .collect(),
selected_company_id: state.selected_company_id, selected_company_id: state.selected_company_id,
has_complete_controller_context: !state.companies.is_empty() has_complete_controller_context: !state.companies.is_empty()
&& state && state.companies.iter().all(|company| {
.companies company.controller_kind != RuntimeCompanyControllerKind::Unknown
.iter() }),
.all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown),
} }
} }
} }
@ -546,7 +544,9 @@ 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) = smp_packed_record_to_runtime_event_record(record, company_context) { if let Some(import_result) =
smp_packed_record_to_runtime_event_record(record, company_context)
{
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);
@ -684,6 +684,9 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
group_index: row.group_index, group_index: row.group_index,
row_index: row.row_index, row_index: row.row_index,
descriptor_id: row.descriptor_id, descriptor_id: row.descriptor_id,
descriptor_label: row.descriptor_label.clone(),
target_mask_bits: row.target_mask_bits,
parameter_family: row.parameter_family.clone(),
opcode: row.opcode, opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value, raw_scalar_value: row.raw_scalar_value,
value_byte_0x09: row.value_byte_0x09, value_byte_0x09: row.value_byte_0x09,
@ -693,6 +696,8 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
value_word_0x14: row.value_word_0x14, value_word_0x14: row.value_word_0x14,
value_word_0x16: row.value_word_0x16, value_word_0x16: row.value_word_0x16,
row_shape: row.row_shape.clone(), row_shape: row.row_shape.clone(),
semantic_family: row.semantic_family.clone(),
semantic_preview: row.semantic_preview.clone(),
locomotive_name: row.locomotive_name.clone(), locomotive_name: row.locomotive_name.clone(),
notes: row.notes.clone(), notes: row.notes.clone(),
} }
@ -702,11 +707,15 @@ fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext, 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" {
return None;
}
if record.payload_family == "real_packed_v1" && record.decoded_actions.is_empty() {
return None; return None;
} }
let effects = match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) { let effects =
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
Ok(effects) => effects, Ok(effects) => effects,
Err(_) => return None, Err(_) => return None,
}; };
@ -718,24 +727,9 @@ fn smp_packed_record_to_runtime_event_record(
record.live_entry_id record.live_entry_id
) )
})?; })?;
let active = record.active.ok_or_else(|| { let active = record.active.unwrap_or(true);
format!( let marks_collection_dirty = record.marks_collection_dirty.unwrap_or(false);
"packed event record {} is missing active flag", let one_shot = record.one_shot.unwrap_or(false);
record.live_entry_id
)
})?;
let marks_collection_dirty = record.marks_collection_dirty.ok_or_else(|| {
format!(
"packed event record {} is missing dirty flag",
record.live_entry_id
)
})?;
let one_shot = record.one_shot.ok_or_else(|| {
format!(
"packed event record {} is missing one_shot flag",
record.live_entry_id
)
})?;
Ok(RuntimeEventRecordTemplate { Ok(RuntimeEventRecordTemplate {
record_id: record.live_entry_id, record_id: record.live_entry_id,
trigger_kind, trigger_kind,
@ -767,6 +761,16 @@ fn smp_runtime_effect_to_runtime_effect(
key: key.clone(), key: key.clone(),
value: *value, value: *value,
}), }),
RuntimeEffect::SetCompanyCash { target, value } => {
if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::SetCompanyCash {
target: target.clone(),
value: *value,
})
} else {
Err(company_target_import_error_message(target, company_context))
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
if company_target_import_blocker(target, company_context).is_none() { if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyCash { Ok(RuntimeEffect::AdjustCompanyCash {
@ -908,6 +912,13 @@ 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) = packed_record_company_target_import_blocker(record, company_context)
{
return company_target_import_outcome(blocker).to_string();
}
if !record.decoded_actions.is_empty() {
return "blocked_unsupported_decode".to_string();
}
if let Some(blocker) = real_record_company_target_import_blocker(record, company_context) { if let Some(blocker) = real_record_company_target_import_blocker(record, company_context) {
return company_target_import_outcome(blocker).to_string(); return company_target_import_outcome(blocker).to_string();
} }
@ -934,14 +945,14 @@ fn runtime_effect_company_target_import_blocker(
company_context: &ImportCompanyContext, company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> { ) -> Option<CompanyTargetImportBlocker> {
match effect { match effect {
RuntimeEffect::AdjustCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
company_target_import_blocker(target, company_context) company_target_import_blocker(target, company_context)
} }
RuntimeEffect::AppendEventRecord { record } => record RuntimeEffect::AppendEventRecord { record } => record.effects.iter().find_map(|nested| {
.effects runtime_effect_company_target_import_blocker(nested, company_context)
.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 { .. }
@ -998,15 +1009,11 @@ fn real_record_company_target_import_blocker(
fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str { fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str {
match blocker { match blocker {
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context", CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
CompanyTargetImportBlocker::MissingSelectionContext => { CompanyTargetImportBlocker::MissingSelectionContext => "blocked_missing_selection_context",
"blocked_missing_selection_context"
}
CompanyTargetImportBlocker::MissingCompanyRoleContext => { CompanyTargetImportBlocker::MissingCompanyRoleContext => {
"blocked_missing_company_role_context" "blocked_missing_company_role_context"
} }
CompanyTargetImportBlocker::MissingConditionContext => { CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
"blocked_missing_condition_context"
}
} }
} }
@ -1391,6 +1398,9 @@ mod tests {
group_index: 0, group_index: 0,
row_index: 0, row_index: 0,
descriptor_id: 2, descriptor_id: 2,
descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()),
opcode: 8, opcode: 8,
raw_scalar_value: 7, raw_scalar_value: 7,
value_byte_0x09: 1, value_byte_0x09: 1,
@ -1400,6 +1410,8 @@ mod tests {
value_word_0x14: 24, value_word_0x14: 24,
value_word_0x16: 36, value_word_0x16: 36,
row_shape: "multivalue_scalar".to_string(), row_shape: "multivalue_scalar".to_string(),
semantic_family: Some("multivalue_scalar".to_string()),
semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()),
locomotive_name: Some("Mikado".to_string()), locomotive_name: Some("Mikado".to_string()),
notes: vec!["grouped effect row carries locomotive-name side string".to_string()], notes: vec!["grouped effect row carries locomotive-name side string".to_string()],
}] }]
@ -1420,8 +1432,8 @@ mod tests {
} }
} }
fn real_compact_control_without_symbolic_company_scope( fn real_compact_control_without_symbolic_company_scope()
) -> crate::SmpLoadedPackedEventCompactControlSummary { -> crate::SmpLoadedPackedEventCompactControlSummary {
crate::SmpLoadedPackedEventCompactControlSummary { crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6, mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x63, primary_selector_0x7f0: 0x63,
@ -2128,11 +2140,8 @@ mod tests {
notes: vec![], notes: vec![],
}; };
let import = project_save_slice_to_runtime_state_import( let import =
&save_slice, project_save_slice_to_runtime_state_import(&save_slice, "symbolic-blockers", None)
"symbolic-blockers",
None,
)
.expect("standalone projection should succeed"); .expect("standalone projection should succeed");
assert!(import.state.event_runtime_records.is_empty()); assert!(import.state.event_runtime_records.is_empty());
@ -2299,7 +2308,10 @@ mod tests {
standalone_condition_rows: real_condition_rows(), standalone_condition_rows: real_condition_rows(),
grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(), grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![], decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
}],
executable_import_ready: false, executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}], }],
@ -2342,7 +2354,8 @@ mod tests {
} }
#[test] #[test]
fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context() { 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()),
@ -2490,6 +2503,150 @@ mod tests {
); );
} }
#[test]
fn overlays_real_company_cash_descriptor_into_executable_runtime_record() {
let base_state = RuntimeState {
calendar: CalendarPoint {
year: 1845,
month_slot: 2,
phase_slot: 1,
tick_slot: 3,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
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::new(),
candidate_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
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: 9,
live_record_count: 1,
live_entry_ids: vec![9],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 9,
payload_offset: Some(0x7202),
payload_len: Some(133),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 1,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
}),
text_bands: packed_text_bands(),
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id: 2,
descriptor_label: Some("Company Cash".to_string()),
target_mask_bits: Some(0x01),
parameter_family: Some("company_finance_scalar".to_string()),
opcode: 8,
raw_scalar_value: 250,
value_byte_0x09: 1,
value_dword_0x0d: 12,
value_byte_0x11: 2,
value_byte_0x12: 3,
value_word_0x14: 24,
value_word_0x16: 36,
row_shape: "multivalue_scalar".to_string(),
semantic_family: Some("multivalue_scalar".to_string()),
semantic_preview: Some(
"Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(),
),
locomotive_name: Some("Mikado".to_string()),
notes: vec![
"grouped effect row carries locomotive-name side string".to_string(),
],
}],
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
value: 250,
}],
executable_import_ready: false,
notes: vec![
"decoded from grounded real 0x4e9a row framing".to_string(),
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(),
],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"real-company-cash-overlay",
None,
)
.expect("overlay import should project");
assert_eq!(import.state.event_runtime_records.len(), 1);
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("imported")
);
execute_step_command(
&mut import.state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("real company-cash descriptor should execute through the normal trigger path");
assert_eq!(import.state.companies[0].current_cash, 250);
}
#[test] #[test]
fn overlays_save_slice_events_onto_base_company_context() { fn overlays_save_slice_events_onto_base_company_context() {
let base_state = RuntimeState { let base_state = RuntimeState {

View file

@ -40,6 +40,10 @@ pub enum RuntimeEffect {
key: String, key: String,
value: bool, value: bool,
}, },
SetCompanyCash {
target: RuntimeCompanyTarget,
value: i64,
},
AdjustCompanyCash { AdjustCompanyCash {
target: RuntimeCompanyTarget, target: RuntimeCompanyTarget,
delta: i64, delta: i64,
@ -206,6 +210,12 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub group_index: usize, pub group_index: usize,
pub row_index: usize, pub row_index: usize,
pub descriptor_id: u32, pub descriptor_id: u32,
#[serde(default)]
pub descriptor_label: Option<String>,
#[serde(default)]
pub target_mask_bits: Option<u8>,
#[serde(default)]
pub parameter_family: Option<String>,
pub opcode: u8, pub opcode: u8,
pub raw_scalar_value: i32, pub raw_scalar_value: i32,
pub value_byte_0x09: u8, pub value_byte_0x09: u8,
@ -216,6 +226,10 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub value_word_0x16: u16, pub value_word_0x16: u16,
pub row_shape: String, pub row_shape: String,
#[serde(default)] #[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub locomotive_name: Option<String>, pub locomotive_name: Option<String>,
#[serde(default)] #[serde(default)]
pub notes: Vec<String>, pub notes: Vec<String>,
@ -492,7 +506,8 @@ impl RuntimeState {
)); ));
} }
if record.payload_family == "real_packed_v1" if record.payload_family == "real_packed_v1"
&& record.standalone_condition_rows.len() != record.standalone_condition_row_count && record.standalone_condition_rows.len()
!= record.standalone_condition_row_count
{ {
return Err(format!( return Err(format!(
"packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count" "packed_event_collection.records[{record_index}].standalone_condition_rows must match standalone_condition_row_count"
@ -653,7 +668,8 @@ fn validate_runtime_effect(
return Err("key must not be empty".to_string()); return Err("key must not be empty".to_string());
} }
} }
RuntimeEffect::AdjustCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
validate_company_target(target, valid_company_ids)?; validate_company_target(target, valid_company_ids)?;
} }

View file

@ -113,6 +113,74 @@ const SHARED_SIGNATURE_WORDS_1_TO_7: [u32; 7] = [
0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771, 0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771,
]; ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RealGroupedEffectDescriptorMetadata {
descriptor_id: u32,
label: &'static str,
target_mask_bits: u8,
parameter_family: &'static str,
executable_in_runtime: bool,
}
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 8] = [
RealGroupedEffectDescriptorMetadata {
descriptor_id: 1,
label: "Player Cash",
target_mask_bits: 0x02,
parameter_family: "player_finance_scalar",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 2,
label: "Company Cash",
target_mask_bits: 0x01,
parameter_family: "company_finance_scalar",
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 3,
label: "Territory - Allow All",
target_mask_bits: 0x05,
parameter_family: "territory_access_toggle",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 8,
label: "Economic Status",
target_mask_bits: 0x08,
parameter_family: "whole_game_state_enum",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 9,
label: "Confiscate All",
target_mask_bits: 0x01,
parameter_family: "company_confiscation_variant",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 13,
label: "Deactivate Company",
target_mask_bits: 0x01,
parameter_family: "company_lifecycle_toggle",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 15,
label: "Retire Train",
target_mask_bits: 0x0d,
parameter_family: "company_or_territory_asset_toggle",
executable_in_runtime: false,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 16,
label: "Company Track Pieces Buildable",
target_mask_bits: 0x01,
parameter_family: "company_build_limit_scalar",
executable_in_runtime: false,
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct KnownSpecialConditionDefinition { struct KnownSpecialConditionDefinition {
slot_index: u8, slot_index: u8,
@ -1295,6 +1363,12 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub group_index: usize, pub group_index: usize,
pub row_index: usize, pub row_index: usize,
pub descriptor_id: u32, pub descriptor_id: u32,
#[serde(default)]
pub descriptor_label: Option<String>,
#[serde(default)]
pub target_mask_bits: Option<u8>,
#[serde(default)]
pub parameter_family: Option<String>,
pub opcode: u8, pub opcode: u8,
pub raw_scalar_value: i32, pub raw_scalar_value: i32,
pub value_byte_0x09: u8, pub value_byte_0x09: u8,
@ -1305,6 +1379,10 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub value_word_0x16: u16, pub value_word_0x16: u16,
pub row_shape: String, pub row_shape: String,
#[serde(default)] #[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub locomotive_name: Option<String>, pub locomotive_name: Option<String>,
#[serde(default)] #[serde(default)]
pub notes: Vec<String>, pub notes: Vec<String>,
@ -1852,8 +1930,7 @@ fn parse_real_event_runtime_record_summary(
let row_bytes = let row_bytes =
record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?; record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?;
cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN; cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN;
let locomotive_name = let locomotive_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
grouped_effect_rows.push(parse_real_grouped_effect_row_summary( grouped_effect_rows.push(parse_real_grouped_effect_row_summary(
row_bytes, row_bytes,
group_index, group_index,
@ -1863,6 +1940,14 @@ fn parse_real_event_runtime_record_summary(
} }
} }
let decoded_actions = compact_control
.as_ref()
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
.unwrap_or_default();
let executable_import_ready = !decoded_actions.is_empty()
&& decoded_actions
.iter()
.all(runtime_effect_supported_for_save_import);
let consumed_len = cursor; let consumed_len = cursor;
Some(( Some((
SmpLoadedPackedEventRecordSummary { SmpLoadedPackedEventRecordSummary {
@ -1884,9 +1969,12 @@ fn parse_real_event_runtime_record_summary(
standalone_condition_rows, standalone_condition_rows,
grouped_effect_row_counts, grouped_effect_row_counts,
grouped_effect_rows, grouped_effect_rows,
decoded_actions: Vec::new(), decoded_actions,
executable_import_ready: false, executable_import_ready,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], notes: vec![
"decoded from grounded real 0x4e9a row framing".to_string(),
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(),
],
}, },
consumed_len, consumed_len,
)) ))
@ -1931,8 +2019,7 @@ fn parse_optional_real_compact_control_summary(
let summary_toggle_0x800 = read_u8_at(bytes, local)?; let summary_toggle_0x800 = read_u8_at(bytes, local)?;
local += 1; local += 1;
let mut grouped_territory_selectors_0x80f = let mut grouped_territory_selectors_0x80f = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?); grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?);
local += 4; local += 4;
@ -1978,7 +2065,9 @@ fn parse_real_condition_row_summary(
row_index, row_index,
raw_condition_id, raw_condition_id,
subtype, subtype,
flag_bytes: row_bytes.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?.to_vec(), flag_bytes: row_bytes
.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?
.to_vec(),
candidate_name, candidate_name,
notes, notes,
}) })
@ -2008,16 +2097,32 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x16, value_word_0x16,
) )
.to_string(); .to_string();
let descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id);
let semantic_family = classify_real_grouped_effect_semantic_family(
opcode,
raw_scalar_value,
value_byte_0x11,
value_byte_0x12,
value_word_0x14,
value_word_0x16,
)
.to_string();
let mut notes = Vec::new(); let mut notes = Vec::new();
if locomotive_name.is_some() { if locomotive_name.is_some() {
notes.push("grouped effect row carries locomotive-name side string".to_string()); notes.push("grouped effect row carries locomotive-name side string".to_string());
} }
if descriptor_metadata.is_none() {
notes.push("descriptor id not yet recovered in the checked-in effect table".to_string());
}
Some(SmpLoadedPackedEventGroupedEffectRowSummary { Some(SmpLoadedPackedEventGroupedEffectRowSummary {
group_index, group_index,
row_index, row_index,
descriptor_id, descriptor_id,
descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()),
target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits),
parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()),
opcode, opcode,
raw_scalar_value, raw_scalar_value,
value_byte_0x09, value_byte_0x09,
@ -2027,11 +2132,51 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x14, value_word_0x14,
value_word_0x16, value_word_0x16,
row_shape, row_shape,
semantic_family: Some(semantic_family.clone()),
semantic_preview: Some(build_real_grouped_effect_semantic_preview(
descriptor_metadata.map(|metadata| metadata.label),
&semantic_family,
raw_scalar_value,
value_byte_0x11,
value_byte_0x12,
value_word_0x14,
value_word_0x16,
)),
locomotive_name, locomotive_name,
notes, notes,
}) })
} }
fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> {
REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA
.iter()
.copied()
.find(|metadata| metadata.descriptor_id == descriptor_id)
}
fn classify_real_grouped_effect_semantic_family(
opcode: u8,
raw_scalar_value: i32,
value_byte_0x11: u8,
value_byte_0x12: u8,
value_word_0x14: u16,
value_word_0x16: u16,
) -> &'static str {
if opcode == 8 {
return "multivalue_scalar";
}
if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0
{
return "timed_duration";
}
if raw_scalar_value == 0 || raw_scalar_value == 1 {
return "bool_toggle";
}
"scalar_assignment"
}
fn classify_real_grouped_effect_row_shape( fn classify_real_grouped_effect_row_shape(
opcode: u8, opcode: u8,
raw_scalar_value: i32, raw_scalar_value: i32,
@ -2050,7 +2195,77 @@ fn classify_real_grouped_effect_row_shape(
if raw_scalar_value == 0 || raw_scalar_value == 1 { if raw_scalar_value == 0 || raw_scalar_value == 1 {
return "bool_toggle"; return "bool_toggle";
} }
"raw_other" "scalar_assignment"
}
fn build_real_grouped_effect_semantic_preview(
descriptor_label: Option<&str>,
semantic_family: &str,
raw_scalar_value: i32,
value_byte_0x11: u8,
value_byte_0x12: u8,
value_word_0x14: u16,
value_word_0x16: u16,
) -> String {
let label = descriptor_label.unwrap_or("descriptor");
match semantic_family {
"bool_toggle" => {
let state = if raw_scalar_value == 0 {
"FALSE"
} else {
"TRUE"
};
format!("Set {label} to {state}")
}
"timed_duration" => format!(
"Set {label} to {raw_scalar_value} for {value_word_0x14} years {value_word_0x16} months"
),
"multivalue_scalar" => format!(
"Set {label} to {raw_scalar_value} with aux [{value_byte_0x11}, {value_byte_0x12}, {value_word_0x14}, {value_word_0x16}]"
),
_ => format!("Set {label} to {raw_scalar_value}"),
}
}
fn decode_real_grouped_effect_actions(
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
compact_control: &SmpLoadedPackedEventCompactControlSummary,
) -> Vec<RuntimeEffect> {
grouped_effect_rows
.iter()
.filter_map(|row| decode_real_grouped_effect_action(row, compact_control))
.collect()
}
fn decode_real_grouped_effect_action(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
compact_control: &SmpLoadedPackedEventCompactControlSummary,
) -> Option<RuntimeEffect> {
let descriptor_metadata = real_grouped_effect_descriptor_metadata(row.descriptor_id)?;
let target_scope_ordinal = compact_control
.grouped_target_scope_ordinals_0x7fb
.get(row.group_index)
.copied()?;
let target = match target_scope_ordinal {
0 => RuntimeCompanyTarget::ConditionTrueCompany,
1 => RuntimeCompanyTarget::SelectedCompany,
2 => RuntimeCompanyTarget::HumanCompanies,
3 => RuntimeCompanyTarget::AiCompanies,
_ => return None,
};
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 2
&& row.opcode == 8
&& row.row_shape == "multivalue_scalar"
{
return Some(RuntimeEffect::SetCompanyCash {
target,
value: i64::from(row.raw_scalar_value),
});
}
None
} }
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> { fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
@ -2183,7 +2398,10 @@ fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<String>
Some(String::from_utf8_lossy(text_bytes).into_owned()) Some(String::from_utf8_lossy(text_bytes).into_owned())
} }
fn parse_optional_u16_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<Option<String>> { fn parse_optional_u16_len_prefixed_string(
bytes: &[u8],
cursor: &mut usize,
) -> Option<Option<String>> {
let len = usize::from(read_u16_at(bytes, *cursor)?); let len = usize::from(read_u16_at(bytes, *cursor)?);
*cursor += 2; *cursor += 2;
if len == 0 { if len == 0 {
@ -2202,7 +2420,8 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => true, | RuntimeEffect::RemoveEventRecord { .. } => true,
RuntimeEffect::AdjustCompanyCash { target, .. } RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => { | RuntimeEffect::AdjustCompanyDebt { target, .. } => {
matches!(target, RuntimeCompanyTarget::AllActive) matches!(target, RuntimeCompanyTarget::AllActive)
} }
@ -7356,7 +7575,10 @@ mod tests {
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
assert_eq!(summary.records[0].standalone_condition_row_count, 0); assert_eq!(summary.records[0].standalone_condition_row_count, 0);
assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); assert_eq!(summary.records[0].standalone_condition_rows.len(), 0);
assert_eq!(summary.records[0].grouped_effect_row_counts, vec![0, 0, 0, 0]); assert_eq!(
summary.records[0].grouped_effect_row_counts,
vec![0, 0, 0, 0]
);
assert_eq!(summary.records[0].grouped_effect_rows.len(), 0); assert_eq!(summary.records[0].grouped_effect_rows.len(), 0);
} }
@ -7422,7 +7644,10 @@ mod tests {
.grouped_target_scope_ordinals_0x7fb, .grouped_target_scope_ordinals_0x7fb,
vec![1, 4, 7, 8] vec![1, 4, 7, 8]
); );
assert_eq!(summary.records[0].standalone_condition_rows[0].raw_condition_id, -1); assert_eq!(
summary.records[0].standalone_condition_rows[0].raw_condition_id,
-1
);
assert_eq!( assert_eq!(
summary.records[0].standalone_condition_rows[0] summary.records[0].standalone_condition_rows[0]
.candidate_name .candidate_name
@ -7431,16 +7656,173 @@ mod tests {
); );
assert_eq!(summary.records[0].grouped_effect_rows.len(), 1); assert_eq!(summary.records[0].grouped_effect_rows.len(), 1);
assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8); assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.descriptor_label
.as_deref(),
Some("Company Cash")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0].target_mask_bits,
Some(0x01)
);
assert_eq!( assert_eq!(
summary.records[0].grouped_effect_rows[0].row_shape, summary.records[0].grouped_effect_rows[0].row_shape,
"multivalue_scalar" "multivalue_scalar"
); );
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.semantic_family
.as_deref(),
Some("multivalue_scalar")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.semantic_preview
.as_deref(),
Some("Set Company Cash to 7 with aux [2, 3, 24, 36]")
);
assert_eq!( assert_eq!(
summary.records[0].grouped_effect_rows[0] summary.records[0].grouped_effect_rows[0]
.locomotive_name .locomotive_name
.as_deref(), .as_deref(),
Some("Mikado") Some("Mikado")
); );
assert_eq!(
summary.records[0].decoded_actions,
vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
value: 7,
}]
);
}
#[test]
fn classifies_real_grouped_row_semantic_families() {
let grouped_rows = vec![
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 2,
opcode: 1,
raw_scalar_value: 1,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
locomotive_name: None,
}),
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 2,
opcode: 4,
raw_scalar_value: 25,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 2,
value_word_0x16: 6,
locomotive_name: None,
}),
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 2,
opcode: 3,
raw_scalar_value: 250,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
locomotive_name: None,
}),
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 2,
opcode: 8,
raw_scalar_value: 7,
value_byte_0x09: 1,
value_dword_0x0d: 12,
value_byte_0x11: 2,
value_byte_0x12: 3,
value_word_0x14: 24,
value_word_0x16: 36,
locomotive_name: Some("Mikado"),
}),
];
let record_body = build_real_event_record(
[b"Semantic", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0x63,
grouped_mode_0x7f4: 1,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [1, 1, 1, 1],
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
summary_toggle_0x800: 0,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[],
[&grouped_rows, &[], &[], &[]],
);
let mut bytes = Vec::new();
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for word in header_words {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(&[0x00, 0x00]);
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
bytes.extend_from_slice(&record_body);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
let report = inspect_smp_bytes(&bytes);
let summary = report
.event_runtime_collection_summary
.as_ref()
.expect("event runtime collection summary should parse");
let families = summary.records[0]
.grouped_effect_rows
.iter()
.map(|row| row.semantic_family.as_deref().unwrap_or(""))
.collect::<Vec<_>>();
assert_eq!(
families,
vec![
"bool_toggle",
"timed_duration",
"scalar_assignment",
"multivalue_scalar",
]
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.semantic_preview
.as_deref(),
Some("Set Company Cash to TRUE")
);
assert_eq!(
summary.records[0].grouped_effect_rows[1]
.semantic_preview
.as_deref(),
Some("Set Company Cash to 25 for 2 years 6 months")
);
assert_eq!(
summary.records[0].grouped_effect_rows[2]
.semantic_preview
.as_deref(),
Some("Set Company Cash to 250")
);
assert_eq!(
summary.records[0].grouped_effect_rows[3]
.semantic_preview
.as_deref(),
Some("Set Company Cash to 7 with aux [2, 3, 24, 36]")
);
} }
#[test] #[test]

View file

@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary, RuntimeState, RuntimeSummary, calendar::BoundaryEventKind,
calendar::BoundaryEventKind,
}; };
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4]; const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -286,6 +285,20 @@ fn apply_runtime_effects(
RuntimeEffect::SetWorldFlag { key, value } => { RuntimeEffect::SetWorldFlag { key, value } => {
state.world_flags.insert(key.clone(), *value); state.world_flags.insert(key.clone(), *value);
} }
RuntimeEffect::SetCompanyCash { target, value } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!("missing company_id {company_id} while applying cash effect")
})?;
company.current_cash = *value;
mutated_company_ids.insert(company_id);
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => { RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target)?; let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids { for company_id in company_ids {

View file

@ -77,11 +77,11 @@ The highest-value next passes are now:
avoid shell-first implementation bets avoid shell-first implementation bets
- keep using overlay imports as the context bridge when selectively executable packed rows still - keep using overlay imports as the context bridge when selectively executable packed rows still
need live company state that save slices do not persist need live company state that save slices do not persist
- treat normalized symbolic company targets as the active packed-event frontier now that - treat broader real grouped-descriptor recovery as the active packed-event frontier now that
`selected_company`, `human_companies`, and `ai_companies` import and execute through the runtime descriptor `2` `Company Cash` already parses, summarizes, and executes through the ordinary
service path runtime path when overlay context resolves its symbolic company scope
- widen real packed-event executable coverage only after the compact-control, symbolic target, and - widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
descriptor frontier is stable, not just after row framing is parsed and normalized effect semantics are all grounded, not just after row framing is parsed
- leave condition-relative company scopes explicit and blocked until condition evaluation has - leave condition-relative company scopes explicit and blocked until condition evaluation has
grounded runtime semantics grounded runtime semantics
- keep in mind that the current local `.gms` corpus still exports with no packed event collection, - keep in mind that the current local `.gms` corpus still exports with no packed event collection,

View file

@ -28,10 +28,12 @@ Implemented today:
company-target model can execute `selected_company`, `human_companies`, and `ai_companies` company-target model can execute `selected_company`, `human_companies`, and `ai_companies`
symbolic scopes through the ordinary runtime service path while keeping condition-relative symbolic scopes through the ordinary runtime service path while keeping condition-relative
company scopes explicitly blocked company scopes explicitly blocked
- real `0x4e9a` grouped rows now carry checked-in descriptor metadata, semantic family/preview
summaries, and one recovered executable family: descriptor `2` = `Company Cash`
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 grouped-descriptor semantic mapping on top of the now-stable compact-control and symbolic broader real grouped-descriptor coverage beyond `Company Cash`, plus condition-relative execution
target frontier, not another persistence scaffold pass. for the still-blocked symbolic scopes, not another persistence scaffold pass.
## Why This Boundary ## Why This Boundary
@ -372,45 +374,44 @@ Checked-in fixture families already include:
## Next Slice ## Next Slice
The recommended next implementation slice is real `0x4e9a` compact-control decode on top of the The recommended next implementation slice is broader real grouped-descriptor coverage on top of the
existing real-row structural parse. now-stable compact-control, symbolic-target, and first recovered real-family path.
Target behavior: Target behavior:
- decode the compact control block that sits above the real standalone-condition and grouped-effect - keep descriptor `2` `Company Cash` as the proof that real grouped rows can cross the whole path:
row families, carrying through raw grounded lanes such as mode byte `0x7ef`, primary selector parse, semantic summary, overlay-backed import, and ordinary trigger execution
`0x7f0`, grouped mode `0x7f4`, one-shot header `0x7f5`, modifier bytes `0x7f9/0x7fa`, grouped - recover more real descriptor identities from the checked-in effect table and expose their target
target-scope ordinals `0x7fb`, grouped scope checkboxes `0x7ff`, summary toggle `0x800`, and masks and conservative semantic previews without guessing unsupported behavior
grouped territory selectors `0x80f` - widen executable real import only when both descriptor identity and runtime effect semantics are
- keep real rows parity-only in runtime import, but replace the coarse `blocked_structural_only` grounded enough to map into the normalized runtime path honestly
frontier with narrower outcomes such as `blocked_missing_compact_control` and - keep condition-relative company scopes explicit until a real condition evaluator exists, instead
`blocked_unmapped_real_descriptor` of silently degrading or inventing target resolution
- keep the existing synthetic harness and overlay-backed executable import path working unchanged
- reserve the first real descriptor-to-effect mapping for a later slice once captured evidence is
tighter
Public-model additions for that slice: Public-model expectations for that slice:
- compact-control summaries on packed-event records in both the save-side and runtime-side models - additional checked-in grouped-descriptor metadata entries keyed by recovered descriptor id
- runtime summary counts for compact-control-missing and unmapped-real-descriptor blockers - more parity summaries with real descriptor labels, target masks, parameter families, and semantic
- trigger-kind and one-shot derivation only where the compact-control mapping is already grounded previews
- more selective real-row `decoded_actions` only where the descriptor-to-runtime mapping is
supported end to end
Fixture work for that slice: Fixture work for that slice:
- update the parity-heavy tracked sample so the real row includes compact-control state - preserve the parity-heavy tracked sample as the condition-relative blocked frontier while it now
- regression fixtures that keep synthetic executable import and overlay-backed company-context carries recovered `Company Cash` semantics
upgrade behavior green - add overlay-backed captured fixtures whenever a new real descriptor family becomes executable
- state-fragment assertions that lock the new compact-control summary and narrower import blockers - keep synthetic harness, save-slice, and overlay paths green as the real descriptor surface widens
Current local constraint: Current local constraint:
- the local checked-in and on-disk `.gms` corpus currently exports with - the local checked-in and on-disk `.gms` corpus still does not provide a richer captured packed
`packed_event_collection_present = false`, so this slice must not depend on a newly captured real event save set, so descriptor recovery must continue to rely on the grounded static tables and
packed-event-bearing save for acceptance tracked JSON artifacts until new captures exist
Do not mix this slice with: Do not mix this slice with:
- territory-access or selected-profile parity
- placed-structure batch placement parity
- shell queue/modal behavior - shell queue/modal behavior
- broad speculative translation of real packed RT3 event rows into executable normalized effects - territory-access or selected-profile parity
- broad condition evaluation without grounded runtime ownership
- speculative executable import for real rows whose descriptor meaning is still weak

View file

@ -70,9 +70,23 @@
"candidate_name": "AutoPlant" "candidate_name": "AutoPlant"
} }
], ],
"decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 7
}
],
"grouped_effect_rows": [ "grouped_effect_rows": [
{ {
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"row_shape": "multivalue_scalar", "row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 7 with aux [2, 3, 24, 36]",
"locomotive_name": "Mikado" "locomotive_name": "Mikado"
} }
] ]

View file

@ -7,7 +7,7 @@
"original_save_sha256": "parity-sample-sha256", "original_save_sha256": "parity-sample-sha256",
"notes": [ "notes": [
"tracked as JSON save-slice document rather than raw .smp", "tracked as JSON save-slice document rather than raw .smp",
"preserves one unsupported row and one decoded-but-parity-only row" "preserves one unsupported row and one semantically decoded-but-parity-only row"
] ]
}, },
"save_slice": { "save_slice": {
@ -133,6 +133,9 @@
"group_index": 0, "group_index": 0,
"row_index": 0, "row_index": 0,
"descriptor_id": 2, "descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8, "opcode": 8,
"raw_scalar_value": 7, "raw_scalar_value": 7,
"value_byte_0x09": 1, "value_byte_0x09": 1,
@ -142,16 +145,27 @@
"value_word_0x14": 24, "value_word_0x14": 24,
"value_word_0x16": 36, "value_word_0x16": 36,
"row_shape": "multivalue_scalar", "row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 7 with aux [2, 3, 24, 36]",
"locomotive_name": "Mikado", "locomotive_name": "Mikado",
"notes": [ "notes": [
"grouped effect row carries locomotive-name side string" "grouped effect row carries locomotive-name side string"
] ]
} }
], ],
"decoded_actions": [], "decoded_actions": [
{
"kind": "set_company_cash",
"target": {
"kind": "condition_true_company"
},
"value": 7
}
],
"executable_import_ready": false, "executable_import_ready": false,
"notes": [ "notes": [
"decoded from grounded real 0x4e9a row framing with compact control" "decoded from grounded real 0x4e9a row framing",
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0"
] ]
} }
] ]