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
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 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
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.
selected-company and controller-role context through overlay imports, real descriptor `2`
`Company Cash` now parses and executes through the ordinary runtime path, and synthetic packed
records still exercise the same service engine without a parallel packed executor. Condition-
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

View file

@ -6,10 +6,9 @@ use serde::{Deserialize, Serialize};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::{
CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
@ -133,10 +132,9 @@ impl ImportCompanyContext {
.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),
&& state.companies.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_record_ids = BTreeSet::new();
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?;
imported_record_ids.insert(record.live_entry_id);
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,
row_index: row.row_index,
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,
raw_scalar_value: row.raw_scalar_value,
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_0x16: row.value_word_0x16,
row_shape: row.row_shape.clone(),
semantic_family: row.semantic_family.clone(),
semantic_preview: row.semantic_preview.clone(),
locomotive_name: row.locomotive_name.clone(),
notes: row.notes.clone(),
}
@ -702,14 +707,18 @@ fn smp_packed_record_to_runtime_event_record(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportCompanyContext,
) -> 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;
}
let effects = match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
let effects =
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
Ok(effects) => effects,
Err(_) => return None,
};
Some((|| {
let trigger_kind = record.trigger_kind.ok_or_else(|| {
@ -718,24 +727,9 @@ fn smp_packed_record_to_runtime_event_record(
record.live_entry_id
)
})?;
let active = record.active.ok_or_else(|| {
format!(
"packed event record {} is missing active flag",
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
)
})?;
let active = record.active.unwrap_or(true);
let marks_collection_dirty = record.marks_collection_dirty.unwrap_or(false);
let one_shot = record.one_shot.unwrap_or(false);
Ok(RuntimeEventRecordTemplate {
record_id: record.live_entry_id,
trigger_kind,
@ -767,6 +761,16 @@ fn smp_runtime_effect_to_runtime_effect(
key: key.clone(),
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 } => {
if company_target_import_blocker(target, company_context).is_none() {
Ok(RuntimeEffect::AdjustCompanyCash {
@ -908,6 +912,13 @@ fn determine_packed_event_import_outcome(
if record.compact_control.is_none() {
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) {
return company_target_import_outcome(blocker).to_string();
}
@ -934,14 +945,14 @@ fn runtime_effect_company_target_import_blocker(
company_context: &ImportCompanyContext,
) -> Option<CompanyTargetImportBlocker> {
match effect {
RuntimeEffect::AdjustCompanyCash { target, .. }
RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
company_target_import_blocker(target, company_context)
}
RuntimeEffect::AppendEventRecord { record } => record
.effects
.iter()
.find_map(|nested| runtime_effect_company_target_import_blocker(nested, company_context)),
RuntimeEffect::AppendEventRecord { record } => record.effects.iter().find_map(|nested| {
runtime_effect_company_target_import_blocker(nested, company_context)
}),
RuntimeEffect::SetWorldFlag { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
@ -998,15 +1009,11 @@ fn real_record_company_target_import_blocker(
fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str {
match blocker {
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
CompanyTargetImportBlocker::MissingSelectionContext => {
"blocked_missing_selection_context"
}
CompanyTargetImportBlocker::MissingSelectionContext => "blocked_missing_selection_context",
CompanyTargetImportBlocker::MissingCompanyRoleContext => {
"blocked_missing_company_role_context"
}
CompanyTargetImportBlocker::MissingConditionContext => {
"blocked_missing_condition_context"
}
CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
}
}
@ -1391,6 +1398,9 @@ mod tests {
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: 7,
value_byte_0x09: 1,
@ -1400,6 +1410,8 @@ mod tests {
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 7 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()],
}]
@ -1420,8 +1432,8 @@ mod tests {
}
}
fn real_compact_control_without_symbolic_company_scope(
) -> crate::SmpLoadedPackedEventCompactControlSummary {
fn real_compact_control_without_symbolic_company_scope()
-> crate::SmpLoadedPackedEventCompactControlSummary {
crate::SmpLoadedPackedEventCompactControlSummary {
mode_byte_0x7ef: 6,
primary_selector_0x7f0: 0x63,
@ -2128,12 +2140,9 @@ mod tests {
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"symbolic-blockers",
None,
)
.expect("standalone projection should succeed");
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
@ -2299,7 +2308,10 @@ mod tests {
standalone_condition_rows: real_condition_rows(),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: real_grouped_rows(),
decoded_actions: vec![],
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
value: 7,
}],
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
}],
@ -2342,7 +2354,8 @@ mod tests {
}
#[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 {
file_extension_hint: Some("gms".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]
fn overlays_save_slice_events_onto_base_company_context() {
let base_state = RuntimeState {

View file

@ -40,6 +40,10 @@ pub enum RuntimeEffect {
key: String,
value: bool,
},
SetCompanyCash {
target: RuntimeCompanyTarget,
value: i64,
},
AdjustCompanyCash {
target: RuntimeCompanyTarget,
delta: i64,
@ -206,6 +210,12 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub group_index: usize,
pub row_index: usize,
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 raw_scalar_value: i32,
pub value_byte_0x09: u8,
@ -216,6 +226,10 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
pub value_word_0x16: u16,
pub row_shape: String,
#[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
@ -492,7 +506,8 @@ impl RuntimeState {
));
}
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!(
"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());
}
}
RuntimeEffect::AdjustCompanyCash { target, .. }
RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
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,
];
#[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)]
struct KnownSpecialConditionDefinition {
slot_index: u8,
@ -1295,6 +1363,12 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub group_index: usize,
pub row_index: usize,
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 raw_scalar_value: i32,
pub value_byte_0x09: u8,
@ -1305,6 +1379,10 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
pub value_word_0x16: u16,
pub row_shape: String,
#[serde(default)]
pub semantic_family: Option<String>,
#[serde(default)]
pub semantic_preview: Option<String>,
#[serde(default)]
pub locomotive_name: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
@ -1852,8 +1930,7 @@ fn parse_real_event_runtime_record_summary(
let row_bytes =
record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?;
cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN;
let locomotive_name =
parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
let locomotive_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
grouped_effect_rows.push(parse_real_grouped_effect_row_summary(
row_bytes,
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;
Some((
SmpLoadedPackedEventRecordSummary {
@ -1884,9 +1969,12 @@ fn parse_real_event_runtime_record_summary(
standalone_condition_rows,
grouped_effect_row_counts,
grouped_effect_rows,
decoded_actions: Vec::new(),
executable_import_ready: false,
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
decoded_actions,
executable_import_ready,
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,
))
@ -1931,8 +2019,7 @@ fn parse_optional_real_compact_control_summary(
let summary_toggle_0x800 = read_u8_at(bytes, local)?;
local += 1;
let mut grouped_territory_selectors_0x80f =
Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
let mut grouped_territory_selectors_0x80f = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?);
local += 4;
@ -1978,7 +2065,9 @@ fn parse_real_condition_row_summary(
row_index,
raw_condition_id,
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,
notes,
})
@ -2008,16 +2097,32 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x16,
)
.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();
if locomotive_name.is_some() {
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 {
group_index,
row_index,
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,
raw_scalar_value,
value_byte_0x09,
@ -2027,11 +2132,51 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x14,
value_word_0x16,
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,
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(
opcode: u8,
raw_scalar_value: i32,
@ -2050,7 +2195,77 @@ fn classify_real_grouped_effect_row_shape(
if raw_scalar_value == 0 || raw_scalar_value == 1 {
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> {
@ -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())
}
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)?);
*cursor += 2;
if len == 0 {
@ -2202,7 +2420,8 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
| RuntimeEffect::RemoveEventRecord { .. } => true,
RuntimeEffect::AdjustCompanyCash { target, .. }
RuntimeEffect::SetCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyCash { target, .. }
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
matches!(target, RuntimeCompanyTarget::AllActive)
}
@ -2228,14 +2447,14 @@ fn build_unsupported_event_runtime_record_summaries(
payload_offset: None,
payload_len: None,
decode_status: "unsupported_framing".to_string(),
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
compact_control: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
payload_family: "unsupported_framing".to_string(),
trigger_kind: None,
active: None,
marks_collection_dirty: None,
one_shot: None,
compact_control: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: Vec::new(),
@ -7356,7 +7575,10 @@ mod tests {
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_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);
}
@ -7422,7 +7644,10 @@ mod tests {
.grouped_target_scope_ordinals_0x7fb,
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!(
summary.records[0].standalone_condition_rows[0]
.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[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!(
summary.records[0].grouped_effect_rows[0].row_shape,
"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!(
summary.records[0].grouped_effect_rows[0]
.locomotive_name
.as_deref(),
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]

View file

@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary,
calendar::BoundaryEventKind,
RuntimeState, RuntimeSummary, calendar::BoundaryEventKind,
};
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 } => {
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 } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {

View file

@ -77,11 +77,11 @@ The highest-value next passes are now:
avoid shell-first implementation bets
- 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
- treat broader real grouped-descriptor recovery as the active packed-event frontier now that
descriptor `2` `Company Cash` already parses, summarizes, and executes through the ordinary
runtime path when overlay context resolves its symbolic company scope
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
and normalized effect semantics are all grounded, not just after row framing is parsed
- 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,

View file

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

View file

@ -7,7 +7,7 @@
"original_save_sha256": "parity-sample-sha256",
"notes": [
"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": {
@ -133,6 +133,9 @@
"group_index": 0,
"row_index": 0,
"descriptor_id": 2,
"descriptor_label": "Company Cash",
"target_mask_bits": 1,
"parameter_family": "company_finance_scalar",
"opcode": 8,
"raw_scalar_value": 7,
"value_byte_0x09": 1,
@ -142,16 +145,27 @@
"value_word_0x14": 24,
"value_word_0x16": 36,
"row_shape": "multivalue_scalar",
"semantic_family": "multivalue_scalar",
"semantic_preview": "Set Company Cash to 7 with aux [2, 3, 24, 36]",
"locomotive_name": "Mikado",
"notes": [
"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,
"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"
]
}
]