Import locomotive availability descriptors with overlay context

This commit is contained in:
Jan Petykiewicz 2026-04-16 10:50:13 -07:00
commit 87108f357b
21 changed files with 1154 additions and 27 deletions

View file

@ -51,15 +51,18 @@ recovered locomotives-page `real_packed_v1` record that lands in the explicit
`blocked_unmapped_world_descriptor` bucket. The next recovered descriptor band is now partially `blocked_unmapped_world_descriptor` bucket. The next recovered descriptor band is now partially
executable too: descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower executable too: descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower
through checked-in metadata into keyed `world_flags`, while the wider locomotive availability/cost through checked-in metadata into keyed `world_flags`, while the wider locomotive availability/cost
scalar bands remain recovered-but-parity-only until per-locomotive identity is grounded. The scalar bands are now split more cleanly: the boolean `0/1` availability subset can import through
runtime now carries the save-owned named locomotive availability table directly too: checked-in an overlay-backed `RuntimeState.locomotive_catalog` into
save-slice documents can populate `RuntimeState.named_locomotive_availability`, and imported `RuntimeState.named_locomotive_availability`, while non-boolean availability payloads plus the
runtime effects can mutate that map through the ordinary event-service path without needing full locomotive-cost/cargo-production/territory-access-cost families remain recovered-but-parity-only.
Trainbuy or live-locomotive parity. Explicit unmapped world-condition and world-descriptor The runtime still carries the save-owned named locomotive availability table directly too:
frontier buckets still remain where current checked-in metadata stops. Shell purchase-flow and checked-in save-slice documents can populate `RuntimeState.named_locomotive_availability`, and
selected-profile parity remain out of scope. Mixed supported/unsupported real rows still stay imported runtime effects can mutate that map through the ordinary event-service path without
parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer needing full Trainbuy or live-locomotive parity. Explicit unmapped world-condition and
the main execution milestone. world-descriptor frontier buckets still remain where current checked-in metadata stops. Shell
purchase-flow and selected-profile parity remain out of scope. Mixed supported/unsupported real
rows still stay parity-only. The PE32 hook remains useful as capture and integration tooling, but
it is no longer the main execution milestone.
## Project Docs ## Project Docs

View file

@ -4457,6 +4457,12 @@ mod tests {
let named_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( let named_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-named-locomotive-availability-save-slice-fixture.json", "../../fixtures/runtime/packed-event-named-locomotive-availability-save-slice-fixture.json",
); );
let missing_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-missing-catalog-save-slice-fixture.json",
);
let overlay_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture) run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize"); .expect("save-slice-backed parity fixture should summarize");
@ -4476,6 +4482,11 @@ mod tests {
.expect("overlay-backed mixed real-row fixture should summarize"); .expect("overlay-backed mixed real-row fixture should summarize");
run_runtime_summarize_fixture(&named_locomotive_fixture) run_runtime_summarize_fixture(&named_locomotive_fixture)
.expect("save-slice-backed named locomotive availability fixture should summarize"); .expect("save-slice-backed named locomotive availability fixture should summarize");
run_runtime_summarize_fixture(&missing_catalog_fixture).expect(
"save-slice-backed locomotive availability missing-catalog fixture should summarize",
);
run_runtime_summarize_fixture(&overlay_locomotive_fixture)
.expect("overlay-backed locomotive availability fixture should summarize");
} }
#[test] #[test]

View file

@ -177,6 +177,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -348,6 +349,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),

View file

@ -76,6 +76,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub retired_train_count: Option<usize>, pub retired_train_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub locomotive_catalog_count: Option<usize>,
#[serde(default)]
pub territory_count: Option<usize>, pub territory_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub company_territory_track_count: Option<usize>, pub company_territory_track_count: Option<usize>,
@ -136,6 +138,8 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_train_territory_context_count: Option<usize>, pub packed_event_blocked_missing_train_territory_context_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_missing_locomotive_catalog_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_confiscation_variant_count: Option<usize>, pub packed_event_blocked_confiscation_variant_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub packed_event_blocked_retire_train_variant_count: Option<usize>, pub packed_event_blocked_retire_train_variant_count: Option<usize>,
@ -443,6 +447,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.locomotive_catalog_count {
if actual.locomotive_catalog_count != count {
mismatches.push(format!(
"locomotive_catalog_count mismatch: expected {count}, got {}",
actual.locomotive_catalog_count
));
}
}
if let Some(count) = self.territory_count { if let Some(count) = self.territory_count {
if actual.territory_count != count { if actual.territory_count != count {
mismatches.push(format!( mismatches.push(format!(
@ -683,6 +695,14 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.packed_event_blocked_missing_locomotive_catalog_context_count {
if actual.packed_event_blocked_missing_locomotive_catalog_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_locomotive_catalog_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_locomotive_catalog_context_count
));
}
}
if let Some(count) = self.packed_event_blocked_confiscation_variant_count { if let Some(count) = self.packed_event_blocked_confiscation_variant_count {
if actual.packed_event_blocked_confiscation_variant_count != count { if actual.packed_event_blocked_confiscation_variant_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -117,6 +117,7 @@ struct ImportRuntimeContext {
territory_name_to_id: BTreeMap<String, u32>, territory_name_to_id: BTreeMap<String, u32>,
has_train_context: bool, has_train_context: bool,
has_train_territory_context: bool, has_train_territory_context: bool,
locomotive_catalog_names_by_id: BTreeMap<u32, String>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -136,6 +137,7 @@ enum ImportBlocker {
UnmappedWorldCondition, UnmappedWorldCondition,
MissingTrainContext, MissingTrainContext,
MissingTrainTerritoryContext, MissingTrainTerritoryContext,
MissingLocomotiveCatalogContext,
} }
impl ImportRuntimeContext { impl ImportRuntimeContext {
@ -152,6 +154,7 @@ impl ImportRuntimeContext {
territory_name_to_id: BTreeMap::new(), territory_name_to_id: BTreeMap::new(),
has_train_context: false, has_train_context: false,
has_train_territory_context: false, has_train_territory_context: false,
locomotive_catalog_names_by_id: BTreeMap::new(),
} }
} }
@ -199,6 +202,11 @@ impl ImportRuntimeContext {
.trains .trains
.iter() .iter()
.any(|train| train.territory_id.is_some()), .any(|train| train.territory_id.is_some()),
locomotive_catalog_names_by_id: state
.locomotive_catalog
.iter()
.map(|entry| (entry.locomotive_id, entry.name.clone()))
.collect(),
} }
} }
} }
@ -233,6 +241,7 @@ pub fn project_save_slice_to_runtime_state_import(
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -289,6 +298,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
players: base_state.players.clone(), players: base_state.players.clone(),
selected_player_id: base_state.selected_player_id, selected_player_id: base_state.selected_player_id,
trains: base_state.trains.clone(), trains: base_state.trains.clone(),
locomotive_catalog: base_state.locomotive_catalog.clone(),
territories: base_state.territories.clone(), territories: base_state.territories.clone(),
company_territory_track_piece_counts: base_state company_territory_track_piece_counts: base_state
.company_territory_track_piece_counts .company_territory_track_piece_counts
@ -839,6 +849,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
row_shape: row.row_shape.clone(), row_shape: row.row_shape.clone(),
semantic_family: row.semantic_family.clone(), semantic_family: row.semantic_family.clone(),
semantic_preview: row.semantic_preview.clone(), semantic_preview: row.semantic_preview.clone(),
recovered_locomotive_id: row.recovered_locomotive_id,
locomotive_name: row.locomotive_name.clone(), locomotive_name: row.locomotive_name.clone(),
notes: row.notes.clone(), notes: row.notes.clone(),
} }
@ -851,10 +862,8 @@ fn smp_packed_record_to_runtime_event_record(
if record.decode_status == "unsupported_framing" { if record.decode_status == "unsupported_framing" {
return None; return None;
} }
if record.payload_family == "real_packed_v1" { if record.payload_family == "real_packed_v1" && record.compact_control.is_none() {
if record.compact_control.is_none() || !record.executable_import_ready { return None;
return None;
}
} }
let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) { let lowered_conditions = match lowered_record_decoded_conditions(record, company_context) {
@ -934,8 +943,14 @@ fn lowered_record_decoded_actions(
let lowered_company_target = lowered_condition_true_company_target(record)?; let lowered_company_target = lowered_condition_true_company_target(record)?;
let lowered_player_target = lowered_condition_true_player_target(record)?; let lowered_player_target = lowered_condition_true_player_target(record)?;
record let base_effects = if record.payload_family != "real_packed_v1"
.decoded_actions || record.decoded_actions.len() == record.grouped_effect_rows.len()
{
record.decoded_actions.clone()
} else {
lower_contextual_real_grouped_effects(record, company_context)?
};
base_effects
.iter() .iter()
.map(|effect| { .map(|effect| {
lower_condition_targets_in_effect( lower_condition_targets_in_effect(
@ -947,6 +962,66 @@ fn lowered_record_decoded_actions(
.collect() .collect()
} }
fn lower_contextual_real_grouped_effects(
record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportRuntimeContext,
) -> Result<Vec<RuntimeEffect>, ImportBlocker> {
if record.payload_family != "real_packed_v1" || record.compact_control.is_none() {
return Err(ImportBlocker::UnmappedWorldCondition);
}
let mut effects = Vec::with_capacity(record.grouped_effect_rows.len());
for row in &record.grouped_effect_rows {
if let Some(effect) = lower_contextual_locomotive_availability_effect(row, company_context)?
{
effects.push(effect);
continue;
}
return Err(if real_grouped_row_is_world_state_family(row) {
ImportBlocker::UnmappedWorldCondition
} else {
ImportBlocker::UnmappedOrdinaryCondition
});
}
if effects.is_empty() {
return Err(ImportBlocker::UnmappedWorldCondition);
}
Ok(effects)
}
fn lower_contextual_locomotive_availability_effect(
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
company_context: &ImportRuntimeContext,
) -> Result<Option<RuntimeEffect>, ImportBlocker> {
if row.parameter_family.as_deref() != Some("locomotive_availability_scalar") {
return Ok(None);
}
if row.row_shape != "scalar_assignment" {
return Ok(None);
}
let value = match row.raw_scalar_value {
0 => false,
1 => true,
_ => return Ok(None),
};
let Some(locomotive_id) = row.recovered_locomotive_id else {
return Ok(None);
};
let Some(name) = company_context
.locomotive_catalog_names_by_id
.get(&locomotive_id)
.cloned()
else {
return Err(ImportBlocker::MissingLocomotiveCatalogContext);
};
Ok(Some(RuntimeEffect::SetNamedLocomotiveAvailability {
name,
value,
}))
}
fn packed_record_condition_scope_import_blocker( fn packed_record_condition_scope_import_blocker(
record: &SmpLoadedPackedEventRecordSummary, record: &SmpLoadedPackedEventRecordSummary,
company_context: &ImportRuntimeContext, company_context: &ImportRuntimeContext,
@ -1781,6 +1856,9 @@ fn company_target_import_error_message(
Some(ImportBlocker::MissingTrainTerritoryContext) => { Some(ImportBlocker::MissingTrainTerritoryContext) => {
"packed train effect requires runtime train territory context".to_string() "packed train effect requires runtime train territory context".to_string()
} }
Some(ImportBlocker::MissingLocomotiveCatalogContext) => {
"packed locomotive availability row requires locomotive catalog context".to_string()
}
Some(ImportBlocker::MissingPlayerContext) Some(ImportBlocker::MissingPlayerContext)
| Some(ImportBlocker::MissingPlayerSelectionContext) | Some(ImportBlocker::MissingPlayerSelectionContext)
| Some(ImportBlocker::MissingPlayerRoleContext) | Some(ImportBlocker::MissingPlayerRoleContext)
@ -1871,6 +1949,11 @@ fn determine_packed_event_import_outcome(
return "blocked_missing_compact_control".to_string(); return "blocked_missing_compact_control".to_string();
} }
if !record.executable_import_ready { if !record.executable_import_ready {
if let Err(blocker) = lowered_record_decoded_actions(record, company_context) {
if blocker == ImportBlocker::MissingLocomotiveCatalogContext {
return company_target_import_outcome(blocker).to_string();
}
}
if record if record
.grouped_effect_rows .grouped_effect_rows
.iter() .iter()
@ -2085,6 +2168,9 @@ fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str {
ImportBlocker::UnmappedWorldCondition => "blocked_unmapped_world_condition", ImportBlocker::UnmappedWorldCondition => "blocked_unmapped_world_condition",
ImportBlocker::MissingTrainContext => "blocked_missing_train_context", ImportBlocker::MissingTrainContext => "blocked_missing_train_context",
ImportBlocker::MissingTrainTerritoryContext => "blocked_missing_train_territory_context", ImportBlocker::MissingTrainTerritoryContext => "blocked_missing_train_territory_context",
ImportBlocker::MissingLocomotiveCatalogContext => {
"blocked_missing_locomotive_catalog_context"
}
} }
} }
@ -2571,6 +2657,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -2719,6 +2806,7 @@ mod tests {
row_shape: "multivalue_scalar".to_string(), row_shape: "multivalue_scalar".to_string(),
semantic_family: Some("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()), semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()),
recovered_locomotive_id: None,
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()],
}] }]
@ -2748,6 +2836,7 @@ mod tests {
"Set Deactivate Company to {}", "Set Deactivate Company to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2772,6 +2861,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")), semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2801,6 +2891,7 @@ mod tests {
"Set Deactivate Player to {}", "Set Deactivate Player to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2831,6 +2922,7 @@ mod tests {
"Set Territory - Allow All to {}", "Set Territory - Allow All to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes, notes,
} }
@ -2855,6 +2947,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Economic Status to {value}")), semantic_preview: Some(format!("Set Economic Status to {value}")),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2881,6 +2974,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")), semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2907,6 +3001,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")), semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2933,6 +3028,71 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")), semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")),
recovered_locomotive_id: None,
locomotive_name: None,
notes: vec![],
}
}
fn real_locomotive_availability_row(
descriptor_id: u32,
value: i32,
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id,
descriptor_label: Some("Unknown Loco Available".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_availability_scalar".to_string()),
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Unknown Loco Available to {value}")),
recovered_locomotive_id: match descriptor_id {
241..=351 => Some(descriptor_id - 240),
457..=474 => Some(descriptor_id - 345),
_ => None,
},
locomotive_name: None,
notes: vec![],
}
}
fn real_locomotive_cost_row(
descriptor_id: u32,
value: i32,
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id,
descriptor_label: Some("Unknown Loco Cost".to_string()),
target_mask_bits: Some(0x08),
parameter_family: Some("locomotive_cost_scalar".to_string()),
opcode: 3,
raw_scalar_value: value,
value_byte_0x09: 0,
value_dword_0x0d: 0,
value_byte_0x11: 0,
value_byte_0x12: 0,
value_word_0x14: 0,
value_word_0x16: 0,
row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Unknown Loco Cost to {value}")),
recovered_locomotive_id: match descriptor_id {
352..=451 => Some(descriptor_id - 351),
475..=500 => Some(descriptor_id - 374),
_ => None,
},
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2964,6 +3124,7 @@ mod tests {
"Set {label} to {}", "Set {label} to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -2993,6 +3154,7 @@ mod tests {
"Set Confiscate All to {}", "Set Confiscate All to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -3024,6 +3186,7 @@ mod tests {
"Set Retire Train to {}", "Set Retire Train to {}",
if enabled { "TRUE" } else { "FALSE" } if enabled { "TRUE" } else { "FALSE" }
)), )),
recovered_locomotive_id: None,
locomotive_name: locomotive_name.map(ToString::to_string), locomotive_name: locomotive_name.map(ToString::to_string),
notes, notes,
} }
@ -3048,6 +3211,7 @@ mod tests {
row_shape: "bool_toggle".to_string(), row_shape: "bool_toggle".to_string(),
semantic_family: Some("bool_toggle".to_string()), semantic_family: Some("bool_toggle".to_string()),
semantic_preview: Some("Set Confiscate All to FALSE".to_string()), semantic_preview: Some("Set Confiscate All to FALSE".to_string()),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
} }
@ -4667,6 +4831,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some("Set Unknown Loco Available to 42".to_string()), semantic_preview: Some("Set Unknown Loco Available to 42".to_string()),
recovered_locomotive_id: Some(10),
locomotive_name: None, locomotive_name: None,
notes: vec![], notes: vec![],
}], }],
@ -4701,6 +4866,295 @@ mod tests {
); );
} }
#[test]
fn blocks_boolean_locomotive_availability_rows_without_catalog_context() {
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,
named_locomotive_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: 32,
live_record_count: 1,
live_entry_ids: vec![32],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 32,
payload_offset: Some(0x7202),
payload_len: Some(96),
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(real_compact_control()),
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_locomotive_availability_row(250, 1)],
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec![
"boolean locomotive availability row still needs catalog context"
.to_string(),
],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-locomotive-availability-missing-catalog",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_missing_locomotive_catalog_context")
);
}
#[test]
fn overlays_boolean_locomotive_availability_rows_into_named_availability_effects() {
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::new(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 10,
name: "Locomotive 10".to_string(),
},
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 112,
name: "Locomotive 112".to_string(),
},
],
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::from([
("Locomotive 10".to_string(), 0),
("Locomotive 112".to_string(), 1),
]),
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,
named_locomotive_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: 33,
live_record_count: 1,
live_entry_ids: vec![33],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 33,
payload_offset: Some(0x7202),
payload_len: Some(120),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![2, 0, 0, 0],
grouped_effect_rows: vec![
real_locomotive_availability_row(250, 1),
real_locomotive_availability_row(457, 0),
],
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec![
"boolean locomotive availability rows use overlay catalog context"
.to_string(),
],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"overlay-locomotive-availability",
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("overlay-imported locomotive availability record should run");
assert_eq!(
import
.state
.named_locomotive_availability
.get("Locomotive 10"),
Some(&1)
);
assert_eq!(
import
.state
.named_locomotive_availability
.get("Locomotive 112"),
Some(&0)
);
}
#[test]
fn keeps_recovered_locomotive_cost_rows_parity_only() {
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_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: 34,
live_record_count: 1,
live_entry_ids: vec![34],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 34,
payload_offset: Some(0x7202),
payload_len: Some(96),
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(real_compact_control()),
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_locomotive_cost_row(352, 250000)],
decoded_conditions: Vec::new(),
decoded_actions: vec![],
executable_import_ready: false,
notes: vec!["locomotive cost rows remain metadata-only".to_string()],
}],
}),
notes: vec![],
};
let import = project_save_slice_to_runtime_state_import(
&save_slice,
"packed-events-locomotive-cost-frontier",
None,
)
.expect("save slice should project");
assert!(import.state.event_runtime_records.is_empty());
assert_eq!(
import
.state
.packed_event_collection
.as_ref()
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
Some("blocked_unmapped_world_descriptor")
);
}
#[test] #[test]
fn overlays_real_company_cash_descriptor_into_executable_runtime_record() { fn overlays_real_company_cash_descriptor_into_executable_runtime_record() {
let base_state = RuntimeState { let base_state = RuntimeState {
@ -4729,6 +5183,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -4813,6 +5268,7 @@ mod tests {
semantic_preview: Some( semantic_preview: Some(
"Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(), "Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(),
), ),
recovered_locomotive_id: None,
locomotive_name: Some("Mikado".to_string()), locomotive_name: Some("Mikado".to_string()),
notes: vec![ notes: vec![
"grouped effect row carries locomotive-name side string".to_string(), "grouped effect row carries locomotive-name side string".to_string(),
@ -6353,6 +6809,7 @@ mod tests {
row_shape: "scalar_assignment".to_string(), row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()), semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()), semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()),
recovered_locomotive_id: None,
locomotive_name: None, locomotive_name: None,
notes: vec!["checked-in whole-game grouped-effect sample".to_string()], notes: vec!["checked-in whole-game grouped-effect sample".to_string()],
}], }],
@ -7575,6 +8032,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -7753,6 +8211,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),

View file

@ -38,7 +38,7 @@ pub use runtime::{
RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompany, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess,
RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,

View file

@ -97,6 +97,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),

View file

@ -107,6 +107,12 @@ pub struct RuntimeTrain {
pub retired: bool, pub retired: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeLocomotiveCatalogEntry {
pub locomotive_id: u32,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeCompanyTarget { pub enum RuntimeCompanyTarget {
@ -512,6 +518,8 @@ pub struct RuntimePackedEventGroupedEffectRowSummary {
#[serde(default)] #[serde(default)]
pub semantic_preview: Option<String>, pub semantic_preview: Option<String>,
#[serde(default)] #[serde(default)]
pub recovered_locomotive_id: Option<u32>,
#[serde(default)]
pub locomotive_name: Option<String>, pub locomotive_name: Option<String>,
#[serde(default)] #[serde(default)]
pub notes: Vec<String>, pub notes: Vec<String>,
@ -629,6 +637,8 @@ pub struct RuntimeState {
#[serde(default)] #[serde(default)]
pub trains: Vec<RuntimeTrain>, pub trains: Vec<RuntimeTrain>,
#[serde(default)] #[serde(default)]
pub locomotive_catalog: Vec<RuntimeLocomotiveCatalogEntry>,
#[serde(default)]
pub territories: Vec<RuntimeTerritory>, pub territories: Vec<RuntimeTerritory>,
#[serde(default)] #[serde(default)]
pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>, pub company_territory_track_piece_counts: Vec<RuntimeCompanyTerritoryTrackPieceCount>,
@ -756,6 +766,28 @@ impl RuntimeState {
)); ));
} }
} }
let mut seen_locomotive_ids = BTreeSet::new();
let mut seen_locomotive_names = BTreeSet::new();
for entry in &self.locomotive_catalog {
if !seen_locomotive_ids.insert(entry.locomotive_id) {
return Err(format!(
"duplicate locomotive_catalog.locomotive_id {}",
entry.locomotive_id
));
}
if entry.name.trim().is_empty() {
return Err(format!(
"locomotive_catalog entry {} has an empty name",
entry.locomotive_id
));
}
if !seen_locomotive_names.insert(entry.name.clone()) {
return Err(format!(
"duplicate locomotive_catalog.name {:?}",
entry.name
));
}
}
for entry in &self.company_territory_track_piece_counts { for entry in &self.company_territory_track_piece_counts {
if !seen_company_ids.contains(&entry.company_id) { if !seen_company_ids.contains(&entry.company_id) {
return Err(format!( return Err(format!(
@ -1419,6 +1451,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1475,6 +1508,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1517,6 +1551,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1572,6 +1607,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1627,6 +1663,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1733,6 +1770,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1775,6 +1813,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1834,6 +1873,7 @@ mod tests {
retired: false, retired: false,
}, },
], ],
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1883,6 +1923,7 @@ mod tests {
active: true, active: true,
retired: false, retired: false,
}], }],
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1932,6 +1973,7 @@ mod tests {
active: true, active: true,
retired: false, retired: false,
}], }],
locomotive_catalog: Vec::new(),
territories: vec![RuntimeTerritory { territories: vec![RuntimeTerritory {
territory_id: 1, territory_id: 1,
name: Some("Appalachia".to_string()), name: Some("Appalachia".to_string()),
@ -1985,6 +2027,7 @@ mod tests {
active: true, active: true,
retired: true, retired: true,
}], }],
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -2027,6 +2070,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: vec![RuntimeTerritory { territories: vec![RuntimeTerritory {
territory_id: 7, territory_id: 7,
name: Some("Appalachia".to_string()), name: Some("Appalachia".to_string()),
@ -2082,6 +2126,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: vec![RuntimeTerritory { territories: vec![RuntimeTerritory {
territory_id: 7, territory_id: 7,
name: Some("Appalachia".to_string()), name: Some("Appalachia".to_string()),
@ -2131,6 +2176,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: vec![RuntimeTerritory { territories: vec![RuntimeTerritory {
territory_id: 7, territory_id: 7,
name: Some("Appalachia".to_string()), name: Some("Appalachia".to_string()),

View file

@ -1674,6 +1674,8 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
#[serde(default)] #[serde(default)]
pub semantic_preview: Option<String>, pub semantic_preview: Option<String>,
#[serde(default)] #[serde(default)]
pub recovered_locomotive_id: Option<u32>,
#[serde(default)]
pub locomotive_name: Option<String>, pub locomotive_name: Option<String>,
#[serde(default)] #[serde(default)]
pub notes: Vec<String>, pub notes: Vec<String>,
@ -2709,6 +2711,7 @@ fn parse_real_grouped_effect_row_summary(
value_word_0x14, value_word_0x14,
value_word_0x16, value_word_0x16,
)), )),
recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id),
locomotive_name, locomotive_name,
notes, notes,
}) })
@ -9252,6 +9255,43 @@ mod tests {
assert!(!metadata.executable_in_runtime); assert!(!metadata.executable_in_runtime);
} }
#[test]
fn looks_up_upper_band_recovered_locomotive_availability_descriptor_metadata() {
let metadata =
real_grouped_effect_descriptor_metadata(457).expect("descriptor metadata should exist");
assert_eq!(metadata.label, "Unknown Loco Available");
assert_eq!(metadata.target_mask_bits, 0x08);
assert_eq!(metadata.parameter_family, "locomotive_availability_scalar");
assert_eq!(recovered_locomotive_availability_loco_id(457), Some(112));
}
#[test]
fn parses_recovered_locomotive_availability_row_with_structured_locomotive_id() {
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 250,
raw_scalar_value: 1,
opcode: 3,
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,
});
let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None)
.expect("row should parse");
assert_eq!(row.descriptor_id, 250);
assert_eq!(row.recovered_locomotive_id, Some(10));
assert_eq!(
row.parameter_family.as_deref(),
Some("locomotive_availability_scalar")
);
}
#[test] #[test]
fn looks_up_recovered_locomotive_policy_descriptor_metadata() { fn looks_up_recovered_locomotive_policy_descriptor_metadata() {
let metadata = let metadata =

View file

@ -1157,6 +1157,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),

View file

@ -35,6 +35,7 @@ pub struct RuntimeSummary {
pub train_count: usize, pub train_count: usize,
pub active_train_count: usize, pub active_train_count: usize,
pub retired_train_count: usize, pub retired_train_count: usize,
pub locomotive_catalog_count: usize,
pub territory_count: usize, pub territory_count: usize,
pub company_territory_track_count: usize, pub company_territory_track_count: usize,
pub packed_event_collection_present: bool, pub packed_event_collection_present: bool,
@ -65,6 +66,7 @@ pub struct RuntimeSummary {
pub packed_event_blocked_territory_access_scope_count: usize, pub packed_event_blocked_territory_access_scope_count: usize,
pub packed_event_blocked_missing_train_context_count: usize, pub packed_event_blocked_missing_train_context_count: usize,
pub packed_event_blocked_missing_train_territory_context_count: usize, pub packed_event_blocked_missing_train_territory_context_count: usize,
pub packed_event_blocked_missing_locomotive_catalog_context_count: usize,
pub packed_event_blocked_confiscation_variant_count: usize, pub packed_event_blocked_confiscation_variant_count: usize,
pub packed_event_blocked_retire_train_variant_count: usize, pub packed_event_blocked_retire_train_variant_count: usize,
pub packed_event_blocked_retire_train_scope_count: usize, pub packed_event_blocked_retire_train_scope_count: usize,
@ -165,6 +167,7 @@ impl RuntimeSummary {
train_count: state.trains.len(), train_count: state.trains.len(),
active_train_count: state.trains.iter().filter(|train| train.active).count(), active_train_count: state.trains.iter().filter(|train| train.active).count(),
retired_train_count: state.trains.iter().filter(|train| train.retired).count(), retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
locomotive_catalog_count: state.locomotive_catalog.len(),
territory_count: state.territories.len(), territory_count: state.territories.len(),
company_territory_track_count: state.company_territory_track_piece_counts.len(), company_territory_track_count: state.company_territory_track_piece_counts.len(),
packed_event_collection_present: state.packed_event_collection.is_some(), packed_event_collection_present: state.packed_event_collection.is_some(),
@ -513,6 +516,20 @@ impl RuntimeSummary {
.count() .count()
}) })
.unwrap_or(0), .unwrap_or(0),
packed_event_blocked_missing_locomotive_catalog_context_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| {
record.import_outcome.as_deref()
== Some("blocked_missing_locomotive_catalog_context")
})
.count()
})
.unwrap_or(0),
packed_event_blocked_confiscation_variant_count: state packed_event_blocked_confiscation_variant_count: state
.packed_event_collection .packed_event_collection
.as_ref() .as_ref()
@ -642,6 +659,16 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 10,
name: "Locomotive 10".to_string(),
},
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 112,
name: "Locomotive 112".to_string(),
},
],
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -872,6 +899,16 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 10,
name: "Locomotive 10".to_string(),
},
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 112,
name: "Locomotive 112".to_string(),
},
],
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -906,6 +943,16 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: vec![
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 10,
name: "Locomotive 10".to_string(),
},
crate::RuntimeLocomotiveCatalogEntry {
locomotive_id: 112,
name: "Locomotive 112".to_string(),
},
],
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -922,6 +969,7 @@ mod tests {
}; };
let summary = RuntimeSummary::from_state(&state); let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.locomotive_catalog_count, 2);
assert_eq!(summary.named_locomotive_availability_count, 3); assert_eq!(summary.named_locomotive_availability_count, 3);
assert_eq!(summary.zero_named_locomotive_availability_count, 2); assert_eq!(summary.zero_named_locomotive_availability_count, 2);
} }
@ -944,6 +992,7 @@ mod tests {
players: Vec::new(), players: Vec::new(),
selected_player_id: None, selected_player_id: None,
trains: Vec::new(), trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(), territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(), company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(), company_territory_access: Vec::new(),
@ -1029,4 +1078,78 @@ mod tests {
1 1
); );
} }
#[test]
fn counts_missing_locomotive_catalog_context_frontier() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: Some(RuntimePackedEventCollectionSummary {
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()),
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 1,
live_record_count: 1,
live_entry_ids: vec![1],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![RuntimePackedEventRecordSummary {
record_index: 0,
live_entry_id: 1,
payload_offset: Some(0x7202),
payload_len: Some(96),
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: None,
compact_control: None,
text_bands: Vec::new(),
standalone_condition_row_count: 0,
standalone_condition_rows: Vec::new(),
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: Vec::new(),
grouped_company_targets: Vec::new(),
decoded_conditions: Vec::new(),
decoded_actions: Vec::new(),
executable_import_ready: false,
import_outcome: Some("blocked_missing_locomotive_catalog_context".to_string()),
notes: Vec::new(),
}],
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(
summary.packed_event_blocked_missing_locomotive_catalog_context_count,
1
);
}
} }

View file

@ -127,10 +127,10 @@ The highest-value next passes are now:
descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower through checked-in descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower through checked-in
metadata into keyed `world_flags`, while the wider locomotive availability/cost scalar bands metadata into keyed `world_flags`, while the wider locomotive availability/cost scalar bands
remain recovered-but-parity-only until per-locomotive identity is grounded remain recovered-but-parity-only until per-locomotive identity is grounded
- the runtime now also carries the save-owned named locomotive availability table directly: - the runtime now also carries both the save-owned named locomotive availability table and an
checked-in save-slice documents can populate `RuntimeState.named_locomotive_availability`, and overlay-backed locomotive catalog context: checked-in save-slice documents can populate
imported runtime effects can mutate that map through the ordinary event-service path without `RuntimeState.named_locomotive_availability`, and boolean `0/1` availability descriptors can
needing full live locomotive-pool parity lower through `RuntimeState.locomotive_catalog` into the same ordinary event-service path
- keep in mind that the current local `.gms` corpus still exports with no packed event collection, - keep in mind that the current local `.gms` corpus still exports with no packed event collection,
so real descriptor mapping needs to stay plumbing-first until better captures exist so real descriptor mapping needs to stay plumbing-first until better captures exist
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution

View file

@ -86,10 +86,14 @@ Implemented today:
save-slice documents can carry the persisted `[world+0x66b6]` name table into save-slice documents can carry the persisted `[world+0x66b6]` name table into
`RuntimeState.named_locomotive_availability`, and imported runtime effects can mutate that map `RuntimeState.named_locomotive_availability`, and imported runtime effects can mutate that map
through the ordinary event-service path without requiring Trainbuy or live locomotive-pool parity through the ordinary event-service path without requiring Trainbuy or live locomotive-pool parity
- the boolean `0/1` subset of the recovered locomotives-page availability bands can now import
through an overlay-backed `RuntimeState.locomotive_catalog`; non-boolean availability payloads
and the adjacent locomotive-cost/cargo-production/access-cost families remain parity-only
That means the next implementation work is breadth, not bootstrap. The recommended next slice is That means the next implementation work is breadth, not bootstrap. The recommended next slice is
broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, broader real grouped-descriptor and ordinary condition-id coverage beyond the current access,
whole-game toggle, train, player, numeric-threshold, and named locomotive availability batches. whole-game toggle, train, player, numeric-threshold, named locomotive availability, and
overlay-resolved locomotive availability batches.
Richer runtime ownership should still be added only where a later descriptor or condition family Richer runtime ownership should still be added only where a later descriptor or condition family
needs more than the current event-owned roster. needs more than the current event-owned roster.

View file

@ -0,0 +1,52 @@
{
"format_version": 1,
"fixture_id": "packed-event-locomotive-availability-missing-catalog-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by a tracked save-slice document that leaves a boolean locomotive availability row blocked until overlay-backed catalog context is supplied."
},
"state_save_slice_path": "packed-event-locomotive-availability-missing-catalog-save-slice.json",
"commands": [
{
"kind": "step_count",
"steps": 1
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"calendar_projection_is_placeholder": true,
"locomotive_catalog_count": 0,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1,
"packed_event_blocked_missing_locomotive_catalog_context_count": 1,
"event_runtime_record_count": 0,
"named_locomotive_availability_count": 0
},
"expected_state_fragment": {
"packed_event_collection": {
"live_entry_ids": [32],
"records": [
{
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"import_outcome": "blocked_missing_locomotive_catalog_context",
"grouped_effect_rows": [
{
"descriptor_id": 250,
"recovered_locomotive_id": 10,
"semantic_preview": "Set Unknown Loco Available to 1"
}
]
}
]
}
}
}

View file

@ -0,0 +1,101 @@
{
"format_version": 1,
"save_slice_id": "packed-event-locomotive-availability-missing-catalog-save-slice",
"source": {
"description": "Tracked save-slice document proving boolean locomotive availability rows stay parity-only without overlay-backed locomotive catalog context.",
"original_save_filename": "captured-locomotive-availability-missing-catalog.gms",
"original_save_sha256": "locomotive-availability-missing-catalog-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks the explicit missing locomotive catalog frontier for boolean availability rows"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"named_locomotive_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 32,
"live_record_count": 1,
"live_entry_ids": [32],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 32,
"payload_offset": 29186,
"payload_len": 96,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 0,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 0,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 0, 0, 0],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [1, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 250,
"descriptor_label": "Unknown Loco Available",
"target_mask_bits": 8,
"parameter_family": "locomotive_availability_scalar",
"opcode": 3,
"raw_scalar_value": 1,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "scalar_assignment",
"semantic_family": "scalar_assignment",
"semantic_preview": "Set Unknown Loco Available to 1",
"recovered_locomotive_id": 10,
"locomotive_name": null,
"notes": []
}
],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"boolean locomotive availability row still requires overlay-backed locomotive catalog context"
]
}
]
},
"notes": [
"locomotive availability catalog blocker sample"
]
}
}

View file

@ -0,0 +1,44 @@
{
"format_version": 1,
"snapshot_id": "packed-event-locomotive-availability-overlay-base-snapshot",
"source": {
"description": "Base runtime snapshot supplying locomotive catalog context for descriptor-driven named locomotive availability import."
},
"state": {
"calendar": {
"year": 1835,
"month_slot": 1,
"phase_slot": 2,
"tick_slot": 4
},
"world_flags": {
"base.only": true
},
"metadata": {
"base.note": "preserve locomotive catalog context"
},
"locomotive_catalog": [
{
"locomotive_id": 10,
"name": "Locomotive 10"
},
{
"locomotive_id": 112,
"name": "Locomotive 112"
}
],
"named_locomotive_availability": {
"Locomotive 10": 0,
"Locomotive 112": 1
},
"event_runtime_records": [],
"candidate_availability": {},
"special_conditions": {},
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
}
}

View file

@ -0,0 +1,83 @@
{
"format_version": 1,
"fixture_id": "packed-event-locomotive-availability-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by an overlay import document so boolean locomotive availability descriptors execute against captured catalog context."
},
"state_import_path": "packed-event-locomotive-availability-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar": {
"year": 1835,
"month_slot": 1,
"phase_slot": 2,
"tick_slot": 4
},
"calendar_projection_is_placeholder": false,
"locomotive_catalog_count": 2,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"event_runtime_record_count": 1,
"named_locomotive_availability_count": 2,
"zero_named_locomotive_availability_count": 1,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"metadata": {
"base.note": "preserve locomotive catalog context",
"save_slice.import_projection": "overlay-runtime-restore-v1"
},
"named_locomotive_availability": {
"Locomotive 10": 1,
"Locomotive 112": 0
},
"packed_event_collection": {
"live_entry_ids": [33],
"records": [
{
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"import_outcome": "imported",
"grouped_effect_rows": [
{
"descriptor_id": 250,
"recovered_locomotive_id": 10
},
{
"descriptor_id": 457,
"recovered_locomotive_id": 112
}
]
}
]
},
"event_runtime_records": [
{
"record_id": 33,
"service_count": 1,
"effects": [
{
"kind": "set_named_locomotive_availability",
"name": "Locomotive 10",
"value": true
},
{
"kind": "set_named_locomotive_availability",
"name": "Locomotive 112",
"value": false
}
]
}
]
}
}

View file

@ -0,0 +1,123 @@
{
"format_version": 1,
"save_slice_id": "packed-event-locomotive-availability-overlay-save-slice",
"source": {
"description": "Tracked save-slice document proving boolean locomotive availability descriptors can import through overlay-backed catalog context.",
"original_save_filename": "captured-locomotive-availability-overlay.gms",
"original_save_sha256": "locomotive-availability-overlay-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"uses synthetic catalog names to prove descriptor-to-id lowering plus overlay-backed resolution"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"named_locomotive_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 33,
"live_record_count": 1,
"live_entry_ids": [33],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 33,
"payload_offset": 29186,
"payload_len": 120,
"decode_status": "parity_only",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 7,
"primary_selector_0x7f0": 0,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 0,
"modifier_flag_0x7f9": 0,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [0, 0, 0, 0],
"grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [-1, -1, -1, -1]
},
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [2, 0, 0, 0],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 250,
"descriptor_label": "Unknown Loco Available",
"target_mask_bits": 8,
"parameter_family": "locomotive_availability_scalar",
"opcode": 3,
"raw_scalar_value": 1,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "scalar_assignment",
"semantic_family": "scalar_assignment",
"semantic_preview": "Set Unknown Loco Available to 1",
"recovered_locomotive_id": 10,
"locomotive_name": null,
"notes": []
},
{
"group_index": 0,
"row_index": 1,
"descriptor_id": 457,
"descriptor_label": "Unknown Loco Available",
"target_mask_bits": 8,
"parameter_family": "locomotive_availability_scalar",
"opcode": 3,
"raw_scalar_value": 0,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "scalar_assignment",
"semantic_family": "scalar_assignment",
"semantic_preview": "Set Unknown Loco Available to 0",
"recovered_locomotive_id": 112,
"locomotive_name": null,
"notes": []
}
],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"boolean locomotive availability rows use overlay-backed catalog context"
]
}
]
},
"notes": [
"overlay-backed descriptor-driven named locomotive availability sample"
]
}
}

View file

@ -0,0 +1,12 @@
{
"format_version": 1,
"import_id": "packed-event-locomotive-availability-overlay",
"source": {
"description": "Overlay import that combines a captured base snapshot with boolean locomotive availability descriptors.",
"notes": [
"used to upgrade descriptor-driven named locomotive availability rows through overlay-backed catalog context"
]
},
"base_snapshot_path": "packed-event-locomotive-availability-overlay-base-snapshot.json",
"save_slice_path": "packed-event-locomotive-availability-overlay-save-slice.json"
}

View file

@ -57,6 +57,7 @@
"parameter_family": "locomotive_availability_scalar", "parameter_family": "locomotive_availability_scalar",
"semantic_family": "scalar_assignment", "semantic_family": "scalar_assignment",
"semantic_preview": "Set Unknown Loco Available to 42", "semantic_preview": "Set Unknown Loco Available to 42",
"recovered_locomotive_id": 10,
"row_shape": "scalar_assignment" "row_shape": "scalar_assignment"
} }
] ]

View file

@ -5,10 +5,10 @@
"description": "Tracked save-slice document representing a parity-heavy captured packed-event collection.", "description": "Tracked save-slice document representing a parity-heavy captured packed-event collection.",
"original_save_filename": "captured-parity.gms", "original_save_filename": "captured-parity.gms",
"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 recovered-but-unmapped locomotive policy row and one semantically decoded-but-parity-only row" "preserves one recovered-but-unmapped locomotive availability row and one semantically decoded-but-parity-only row"
] ]
}, },
"save_slice": { "save_slice": {
"file_extension_hint": "gms", "file_extension_hint": "gms",
@ -80,9 +80,10 @@
"row_shape": "scalar_assignment", "row_shape": "scalar_assignment",
"semantic_family": "scalar_assignment", "semantic_family": "scalar_assignment",
"semantic_preview": "Set Unknown Loco Available to 42", "semantic_preview": "Set Unknown Loco Available to 42",
"recovered_locomotive_id": 10,
"locomotive_name": null, "locomotive_name": null,
"notes": [ "notes": [
"recovered locomotive availability descriptor family remains parity-only until per-locomotive identity is grounded" "recovered locomotive availability descriptor family remains parity-only until the scalar payload is in the grounded boolean subset"
] ]
} }
], ],
@ -90,7 +91,7 @@
"executable_import_ready": false, "executable_import_ready": false,
"notes": [ "notes": [
"decoded from grounded real 0x4e9a row framing", "decoded from grounded real 0x4e9a row framing",
"recovered locomotives-page descriptor band is now checked in, but this scalar family still has no executable runtime landing surface" "recovered locomotives-page descriptor band is now checked in, but this scalar family still needs overlay-backed locomotive catalog context and a grounded boolean scalar payload"
] ]
}, },
{ {