Add named locomotive availability runtime surface

This commit is contained in:
Jan Petykiewicz 2026-04-16 10:23:29 -07:00
commit 8c7ff335cb
16 changed files with 542 additions and 13 deletions

View file

@ -51,11 +51,15 @@ 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
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
scalar bands remain recovered-but-parity-only until per-locomotive identity is grounded. Explicit
unmapped world-condition and 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.
scalar bands remain recovered-but-parity-only until per-locomotive identity is grounded. The
runtime now carries the save-owned named locomotive availability table directly too: checked-in
save-slice documents can populate `RuntimeState.named_locomotive_availability`, and imported
runtime effects can mutate that map through the ordinary event-service path without needing full
Trainbuy or live-locomotive parity. Explicit unmapped world-condition and 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

View file

@ -4454,6 +4454,9 @@ mod tests {
let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json",
);
let named_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-named-locomotive-availability-save-slice-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize");
@ -4471,6 +4474,8 @@ mod tests {
.expect("overlay-backed track-capacity fixture should summarize");
run_runtime_summarize_fixture(&mixed_overlay_fixture)
.expect("overlay-backed mixed real-row fixture should summarize");
run_runtime_summarize_fixture(&named_locomotive_fixture)
.expect("save-slice-backed named locomotive availability fixture should summarize");
}
#[test]
@ -4495,6 +4500,7 @@ mod tests {
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec!["exported for test".to_string()],

View file

@ -183,6 +183,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
@ -259,6 +260,7 @@ mod tests {
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
@ -352,6 +354,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
@ -371,6 +374,7 @@ mod tests {
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None,
event_runtime_collection: Some(
rrt_runtime::SmpLoadedEventRuntimeCollectionSummary {

View file

@ -150,6 +150,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub zero_candidate_availability_count: Option<usize>,
#[serde(default)]
pub named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub zero_named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub special_condition_count: Option<usize>,
#[serde(default)]
pub enabled_special_condition_count: Option<usize>,
@ -735,6 +739,22 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.named_locomotive_availability_count {
if actual.named_locomotive_availability_count != count {
mismatches.push(format!(
"named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.named_locomotive_availability_count
));
}
}
if let Some(count) = self.zero_named_locomotive_availability_count {
if actual.zero_named_locomotive_availability_count != count {
mismatches.push(format!(
"zero_named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.zero_named_locomotive_availability_count
));
}
}
if let Some(count) = self.special_condition_count {
if actual.special_condition_count != count {
mismatches.push(format!(

View file

@ -94,6 +94,7 @@ struct SaveSliceProjection {
packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
event_runtime_records: Vec<RuntimeEventRecord>,
candidate_availability: BTreeMap<String, u32>,
named_locomotive_availability: BTreeMap<String, u32>,
special_conditions: BTreeMap<String, u32>,
}
@ -238,6 +239,7 @@ pub fn project_save_slice_to_runtime_state_import(
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
special_conditions: projection.special_conditions,
service_state: RuntimeServiceState::default(),
};
@ -295,6 +297,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
special_conditions: projection.special_conditions,
service_state: base_state.service_state.clone(),
};
@ -325,6 +328,10 @@ fn project_save_slice_components(
"save_slice.special_conditions_present".to_string(),
save_slice.special_conditions_table.is_some(),
);
world_flags.insert(
"save_slice.named_locomotive_availability_present".to_string(),
save_slice.named_locomotive_availability_table.is_some(),
);
world_flags.insert(
"save_slice.event_runtime_collection_present".to_string(),
save_slice.event_runtime_collection.is_some(),
@ -590,6 +597,35 @@ fn project_save_slice_components(
}
}
let mut named_locomotive_availability = BTreeMap::new();
if let Some(table) = &save_slice.named_locomotive_availability_table {
metadata.insert(
"save_slice.named_locomotive_availability_source_kind".to_string(),
table.source_kind.clone(),
);
metadata.insert(
"save_slice.named_locomotive_availability_semantic_family".to_string(),
table.semantic_family.clone(),
);
metadata.insert(
"save_slice.named_locomotive_availability_entry_count".to_string(),
table.observed_entry_count.to_string(),
);
metadata.insert(
"save_slice.named_locomotive_availability_zero_count".to_string(),
table.zero_availability_count.to_string(),
);
if let Some(header_offset) = table.header_offset {
metadata.insert(
"save_slice.named_locomotive_availability_header_offset".to_string(),
header_offset.to_string(),
);
}
for entry in &table.entries {
named_locomotive_availability.insert(entry.text.clone(), entry.availability_dword);
}
}
for (index, note) in save_slice.notes.iter().enumerate() {
metadata.insert(format!("save_slice.note.{index}"), note.clone());
}
@ -602,6 +638,7 @@ fn project_save_slice_components(
packed_event_collection,
event_runtime_records,
candidate_availability,
named_locomotive_availability,
special_conditions,
})
}
@ -1133,6 +1170,12 @@ fn lower_condition_targets_in_effect(
value: *value,
}
}
RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => {
RuntimeEffect::SetNamedLocomotiveAvailability {
name: name.clone(),
value: *value,
}
}
RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition {
label: label.clone(),
value: *value,
@ -1562,6 +1605,12 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value,
})
}
RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => {
Ok(RuntimeEffect::SetNamedLocomotiveAvailability {
name: name.clone(),
value: *value,
})
}
RuntimeEffect::SetSpecialCondition { label, value } => {
Ok(RuntimeEffect::SetSpecialCondition {
label: label.clone(),
@ -2111,6 +2160,7 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetPlayerCash { .. }
| RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
@ -2188,6 +2238,7 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. }
@ -2526,6 +2577,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
}
@ -3080,6 +3132,7 @@ mod tests {
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
@ -3118,6 +3171,7 @@ mod tests {
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
@ -3199,6 +3253,38 @@ mod tests {
},
],
}),
named_locomotive_availability_table: Some(
crate::SmpLoadedNamedLocomotiveAvailabilityTable {
source_kind: "runtime-save-direct-serializer".to_string(),
semantic_family: "scenario-named-locomotive-availability-table".to_string(),
header_offset: None,
entries_offset: None,
entries_end_offset: None,
observed_entry_count: 2,
zero_availability_count: 1,
zero_availability_names: vec!["Big Boy".to_string()],
entries: vec![
crate::SmpRt3105SaveNameTableEntry {
index: 0,
offset: 0,
text: "Big Boy".to_string(),
availability_dword: 0,
availability_dword_hex: "0x00000000".to_string(),
trailer_word: 0,
trailer_word_hex: "0x00000000".to_string(),
},
crate::SmpRt3105SaveNameTableEntry {
index: 1,
offset: 0x41,
text: "GP7".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
],
},
),
special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable {
source_kind: "save-fixed-special-conditions-range".to_string(),
table_offset: 0x0d64,
@ -3444,10 +3530,34 @@ mod tests {
import.state.candidate_availability.get("Uranium Mine"),
Some(&0)
);
assert_eq!(
import.state.named_locomotive_availability.get("Big Boy"),
Some(&0)
);
assert_eq!(
import.state.named_locomotive_availability.get("GP7"),
Some(&1)
);
assert_eq!(
import.state.special_conditions.get("Disable Cargo Economy"),
Some(&0)
);
assert_eq!(
import
.state
.metadata
.get("save_slice.named_locomotive_availability_source_kind")
.map(String::as_str),
Some("runtime-save-direct-serializer")
);
assert_eq!(
import
.state
.metadata
.get("save_slice.named_locomotive_availability_entry_count")
.map(String::as_str),
Some("2")
);
assert_eq!(
import
.state
@ -3485,6 +3595,7 @@ mod tests {
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(),
@ -3598,6 +3709,7 @@ mod tests {
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(),
@ -3689,6 +3801,7 @@ mod tests {
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(),
@ -3798,6 +3911,7 @@ mod tests {
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(),
@ -3880,6 +3994,7 @@ mod tests {
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(),
@ -4012,6 +4127,7 @@ mod tests {
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(),
@ -4252,6 +4368,7 @@ mod tests {
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(),
@ -4327,6 +4444,7 @@ mod tests {
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(),
@ -4424,6 +4542,7 @@ mod tests {
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(),
@ -4496,6 +4615,7 @@ mod tests {
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(),
@ -4615,6 +4735,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -4627,6 +4748,7 @@ mod tests {
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(),
@ -4769,6 +4891,7 @@ mod tests {
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(),
@ -4871,6 +4994,7 @@ mod tests {
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(),
@ -4954,6 +5078,7 @@ mod tests {
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(),
@ -5058,6 +5183,7 @@ mod tests {
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(),
@ -5162,6 +5288,7 @@ mod tests {
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(),
@ -5256,6 +5383,7 @@ mod tests {
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(),
@ -5346,6 +5474,7 @@ mod tests {
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(),
@ -5444,6 +5573,7 @@ mod tests {
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(),
@ -5533,6 +5663,7 @@ mod tests {
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(),
@ -5604,6 +5735,7 @@ mod tests {
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(),
@ -5680,6 +5812,7 @@ mod tests {
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(),
@ -5761,6 +5894,7 @@ mod tests {
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(),
@ -5842,6 +5976,7 @@ mod tests {
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(),
@ -5939,6 +6074,7 @@ mod tests {
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(),
@ -6027,6 +6163,7 @@ mod tests {
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(),
@ -6141,6 +6278,7 @@ mod tests {
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(),
@ -6264,6 +6402,7 @@ mod tests {
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(),
@ -6372,6 +6511,7 @@ mod tests {
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(),
@ -6543,6 +6683,7 @@ mod tests {
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(),
@ -6634,6 +6775,7 @@ mod tests {
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(),
@ -6721,6 +6863,7 @@ mod tests {
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(),
@ -6873,6 +7016,7 @@ mod tests {
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(),
@ -7014,6 +7158,7 @@ mod tests {
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(),
@ -7102,6 +7247,7 @@ mod tests {
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(),
@ -7217,6 +7363,7 @@ mod tests {
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(),
@ -7320,6 +7467,7 @@ mod tests {
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(),
@ -7443,6 +7591,7 @@ mod tests {
effects: vec![],
}],
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
periodic_boundary_calls: 9,
@ -7460,6 +7609,7 @@ mod tests {
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(),
@ -7609,6 +7759,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
@ -7629,6 +7780,7 @@ mod tests {
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(),

View file

@ -53,10 +53,11 @@ pub use smp::{
SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe,
SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit,
SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary,
SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventConditionRowSummary,
SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary,
SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile,
SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
SmpLoadedNamedLocomotiveAvailabilityTable, SmpLoadedPackedEventCompactControlSummary,
SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary,
SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary,
SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedSaveSlice,
SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation,
SmpLocomotivePolicyFloatAlignmentCandidate, SmpLocomotivePolicyNeighborhoodProbe,
SmpPackedProfileWordLane, SmpPostSpecialConditionsScalarLane,
SmpPostSpecialConditionsScalarProbe, SmpPostTextFieldNeighborhoodProbe,

View file

@ -103,6 +103,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},

View file

@ -307,6 +307,10 @@ pub enum RuntimeEffect {
name: String,
value: u32,
},
SetNamedLocomotiveAvailability {
name: String,
value: bool,
},
SetSpecialCondition {
label: String,
value: u32,
@ -637,6 +641,8 @@ pub struct RuntimeState {
#[serde(default)]
pub candidate_availability: BTreeMap<String, u32>,
#[serde(default)]
pub named_locomotive_availability: BTreeMap<String, u32>,
#[serde(default)]
pub special_conditions: BTreeMap<String, u32>,
#[serde(default)]
pub service_state: RuntimeServiceState,
@ -1119,6 +1125,11 @@ impl RuntimeState {
return Err("candidate_availability contains an empty key".to_string());
}
}
for key in self.named_locomotive_availability.keys() {
if key.trim().is_empty() {
return Err("named_locomotive_availability contains an empty key".to_string());
}
}
for key in self.special_conditions.keys() {
if key.trim().is_empty() {
return Err("special_conditions contains an empty key".to_string());
@ -1190,6 +1201,11 @@ fn validate_runtime_effect(
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetNamedLocomotiveAvailability { name, .. } => {
if name.trim().is_empty() {
return Err("name must not be empty".to_string());
}
}
RuntimeEffect::SetSpecialCondition { label, .. } => {
if label.trim().is_empty() {
return Err("label must not be empty".to_string());
@ -1409,6 +1425,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1464,6 +1481,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1518,6 +1536,7 @@ mod tests {
}],
}],
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1582,6 +1601,7 @@ mod tests {
}],
}],
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1677,6 +1697,7 @@ mod tests {
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1718,6 +1739,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1759,6 +1781,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1817,6 +1840,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1865,6 +1889,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1917,6 +1942,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -1965,6 +1991,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2019,6 +2046,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2067,6 +2095,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -2115,6 +2144,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};

View file

@ -1503,6 +1503,22 @@ pub struct SmpLoadedCandidateAvailabilityTable {
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedNamedLocomotiveAvailabilityTable {
pub source_kind: String,
pub semantic_family: String,
#[serde(default)]
pub header_offset: Option<usize>,
#[serde(default)]
pub entries_offset: Option<usize>,
#[serde(default)]
pub entries_end_offset: Option<usize>,
pub observed_entry_count: usize,
pub zero_availability_count: usize,
pub zero_availability_names: Vec<String>,
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSpecialConditionsTable {
pub source_kind: String,
@ -1673,6 +1689,7 @@ pub struct SmpLoadedSaveSlice {
pub bridge_family: Option<String>,
pub profile: Option<SmpLoadedProfile>,
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
pub named_locomotive_availability_table: Option<SmpLoadedNamedLocomotiveAvailabilityTable>,
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
pub notes: Vec<String>,
@ -1840,6 +1857,7 @@ pub fn load_save_slice_from_report(
bridge_family: summary.bridge_family.clone(),
profile,
candidate_availability_table,
named_locomotive_availability_table: None,
special_conditions_table,
event_runtime_collection: report.event_runtime_collection_summary.clone(),
notes: summary.notes.clone(),
@ -2659,6 +2677,11 @@ fn parse_real_grouped_effect_row_summary(
if descriptor_metadata.is_none() {
notes.push("descriptor id not yet recovered in the checked-in effect table".to_string());
}
if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) {
notes.push(format!(
"locomotive availability descriptor maps to live locomotive id {loco_id}"
));
}
Some(SmpLoadedPackedEventGroupedEffectRowSummary {
group_index,
@ -2836,6 +2859,16 @@ fn recovered_locomotive_availability_descriptor_metadata(
})
}
fn recovered_locomotive_availability_loco_id(descriptor_id: u32) -> Option<u32> {
if (241..=351).contains(&descriptor_id) {
return Some(descriptor_id - 240);
}
if (457..=474).contains(&descriptor_id) {
return Some(descriptor_id - 345);
}
None
}
fn recovered_locomotive_cost_descriptor_metadata(
descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -3409,6 +3442,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ConfiscateCompanyAssets { .. }
| RuntimeEffect::DeactivateCompany { .. }

View file

@ -490,6 +490,11 @@ fn apply_runtime_effects(
RuntimeEffect::SetCandidateAvailability { name, value } => {
state.candidate_availability.insert(name.clone(), *value);
}
RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => {
state
.named_locomotive_availability
.insert(name.clone(), u32::from(*value));
}
RuntimeEffect::SetSpecialCondition { label, value } => {
state.special_conditions.insert(label.clone(), *value);
}
@ -1158,6 +1163,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
}
@ -1372,6 +1378,43 @@ mod tests {
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
}
#[test]
fn applies_named_locomotive_availability_effects() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 10,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::SetNamedLocomotiveAvailability {
name: "Big Boy".to_string(),
value: false,
},
RuntimeEffect::SetNamedLocomotiveAvailability {
name: "GP7".to_string(),
value: true,
},
],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("named locomotive availability effects should succeed");
assert_eq!(state.named_locomotive_availability.get("Big Boy"), Some(&0));
assert_eq!(state.named_locomotive_availability.get("GP7"), Some(&1));
assert_eq!(result.service_events[0].applied_effect_count, 2);
}
#[test]
fn resolves_symbolic_company_targets() {
let mut state = RuntimeState {

View file

@ -72,6 +72,8 @@ pub struct RuntimeSummary {
pub event_runtime_record_count: usize,
pub candidate_availability_count: usize,
pub zero_candidate_availability_count: usize,
pub named_locomotive_availability_count: usize,
pub zero_named_locomotive_availability_count: usize,
pub special_condition_count: usize,
pub enabled_special_condition_count: usize,
pub save_profile_kind: Option<String>,
@ -570,6 +572,12 @@ impl RuntimeSummary {
.values()
.filter(|value| **value == 0)
.count(),
named_locomotive_availability_count: state.named_locomotive_availability.len(),
zero_named_locomotive_availability_count: state
.named_locomotive_availability
.values()
.filter(|value| **value == 0)
.count(),
special_condition_count: state.special_conditions.len(),
enabled_special_condition_count: state
.special_conditions
@ -781,6 +789,7 @@ mod tests {
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -869,6 +878,7 @@ mod tests {
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
@ -878,6 +888,44 @@ mod tests {
assert_eq!(summary.active_company_count, 1);
}
#[test]
fn counts_named_locomotive_availability_entries_and_zero_values() {
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(),
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([
("Big Boy".to_string(), 0),
("GP7".to_string(), 1),
("Mikado".to_string(), 0),
]),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.named_locomotive_availability_count, 3);
assert_eq!(summary.zero_named_locomotive_availability_count, 2);
}
#[test]
fn counts_world_frontier_buckets_separately() {
let state = RuntimeState {
@ -966,6 +1014,7 @@ mod tests {
}),
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};

View file

@ -127,6 +127,10 @@ The highest-value next passes are now:
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
remain recovered-but-parity-only until per-locomotive identity is grounded
- the runtime now also carries the save-owned named locomotive availability table directly:
checked-in save-slice documents can populate `RuntimeState.named_locomotive_availability`, and
imported runtime effects can mutate that map through the ordinary event-service path without
needing full live locomotive-pool parity
- keep in mind that the current local `.gms` corpus still exports with no packed event collection,
so real descriptor mapping needs to stay plumbing-first until better captures exist
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution

View file

@ -82,12 +82,16 @@ Implemented today:
descriptors `230..240`, `241..351`, `352..451`, `453`, and `457..500` now carry recovered
world-side scalar metadata, while descriptors `454..456` (`All Steam/Diesel/Electric Locos
Avail.`) now execute as keyed `world_flags`
- a first-class named locomotive availability runtime surface now exists too:
save-slice documents can carry the persisted `[world+0x66b6]` name table into
`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
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,
whole-game toggle, train, player, and numeric-threshold batches. Richer runtime ownership should
still be added only where a later descriptor or condition family needs more than the current
event-owned roster.
whole-game toggle, train, player, numeric-threshold, and named locomotive availability batches.
Richer runtime ownership should still be added only where a later descriptor or condition family
needs more than the current event-owned roster.
## Why This Boundary

View file

@ -0,0 +1,60 @@
{
"format_version": 1,
"fixture_id": "packed-event-named-locomotive-availability-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by a tracked save-slice document that imports the named locomotive availability table and mutates it through an imported packed-event record."
},
"state_save_slice_path": "packed-event-named-locomotive-availability-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"named_locomotive_availability_count": 3,
"zero_named_locomotive_availability_count": 2,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1,
"dirty_rerun_count": 0
},
"expected_state_fragment": {
"world_flags": {
"save_slice.named_locomotive_availability_present": true
},
"metadata": {
"save_slice.named_locomotive_availability_source_kind": "runtime-save-direct-serializer",
"save_slice.named_locomotive_availability_entry_count": "2",
"save_slice.named_locomotive_availability_zero_count": "1"
},
"named_locomotive_availability": {
"Big Boy": 0,
"GP7": 0,
"Mikado": 1
},
"packed_event_collection": {
"live_entry_ids": [
17
],
"records": [
{
"decode_status": "executable",
"import_outcome": "imported"
}
]
},
"event_runtime_records": [
{
"record_id": 17,
"service_count": 1
}
]
}
}

View file

@ -0,0 +1,117 @@
{
"format_version": 1,
"save_slice_id": "packed-event-named-locomotive-availability-save-slice",
"source": {
"description": "Tracked save-slice document proving named locomotive availability survives save import and can be mutated by imported packed events.",
"original_save_filename": "captured-named-locomotive-availability.gms",
"original_save_sha256": "named-locomotive-availability-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks the named locomotive availability import surface without guessing per-descriptor locomotive names"
]
},
"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": {
"source_kind": "runtime-save-direct-serializer",
"semantic_family": "scenario-named-locomotive-availability-table",
"header_offset": null,
"entries_offset": null,
"entries_end_offset": null,
"observed_entry_count": 2,
"zero_availability_count": 1,
"zero_availability_names": [
"GP7"
],
"entries": [
{
"index": 0,
"offset": 0,
"text": "Big Boy",
"availability_dword": 1,
"availability_dword_hex": "0x00000001",
"trailer_word": 1,
"trailer_word_hex": "0x00000001"
},
{
"index": 1,
"offset": 65,
"text": "GP7",
"availability_dword": 0,
"availability_dword_hex": "0x00000000",
"trailer_word": 0,
"trailer_word_hex": "0x00000000"
}
]
},
"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": 29440,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 17,
"live_record_count": 1,
"live_entry_ids": [
17
],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 17,
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [],
"standalone_condition_row_count": 0,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [
0,
0,
0,
0
],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_named_locomotive_availability",
"name": "Big Boy",
"value": false
},
{
"kind": "set_named_locomotive_availability",
"name": "Mikado",
"value": true
}
],
"executable_import_ready": true,
"notes": [
"fixture packed-event record"
]
}
]
},
"notes": [
"named locomotive availability import surface"
]
}
}

View file

@ -21,7 +21,7 @@
},
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"world_flag_count": 7,
"world_flag_count": 8,
"company_count": 1,
"packed_event_collection_present": true,
"packed_event_record_count": 2,