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 `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. Explicit scalar bands remain recovered-but-parity-only until per-locomotive identity is grounded. The
unmapped world-condition and world-descriptor frontier buckets still remain where current runtime now carries the save-owned named locomotive availability table directly too: checked-in
checked-in metadata stops. Shell purchase-flow and selected-profile parity remain out of scope. save-slice documents can populate `RuntimeState.named_locomotive_availability`, and imported
Mixed supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as runtime effects can mutate that map through the ordinary event-service path without needing full
capture and integration tooling, but it is no longer the main execution milestone. 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 ## Project Docs

View file

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

View file

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

View file

@ -150,6 +150,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub zero_candidate_availability_count: Option<usize>, pub zero_candidate_availability_count: Option<usize>,
#[serde(default)] #[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>, pub special_condition_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub enabled_special_condition_count: Option<usize>, 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 let Some(count) = self.special_condition_count {
if actual.special_condition_count != count { if actual.special_condition_count != count {
mismatches.push(format!( mismatches.push(format!(

View file

@ -94,6 +94,7 @@ struct SaveSliceProjection {
packed_event_collection: Option<RuntimePackedEventCollectionSummary>, packed_event_collection: Option<RuntimePackedEventCollectionSummary>,
event_runtime_records: Vec<RuntimeEventRecord>, event_runtime_records: Vec<RuntimeEventRecord>,
candidate_availability: BTreeMap<String, u32>, candidate_availability: BTreeMap<String, u32>,
named_locomotive_availability: BTreeMap<String, u32>,
special_conditions: 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, packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records, event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability, candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
special_conditions: projection.special_conditions, special_conditions: projection.special_conditions,
service_state: RuntimeServiceState::default(), 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, packed_event_collection: projection.packed_event_collection,
event_runtime_records: projection.event_runtime_records, event_runtime_records: projection.event_runtime_records,
candidate_availability: projection.candidate_availability, candidate_availability: projection.candidate_availability,
named_locomotive_availability: projection.named_locomotive_availability,
special_conditions: projection.special_conditions, special_conditions: projection.special_conditions,
service_state: base_state.service_state.clone(), 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_present".to_string(),
save_slice.special_conditions_table.is_some(), 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( world_flags.insert(
"save_slice.event_runtime_collection_present".to_string(), "save_slice.event_runtime_collection_present".to_string(),
save_slice.event_runtime_collection.is_some(), 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() { for (index, note) in save_slice.notes.iter().enumerate() {
metadata.insert(format!("save_slice.note.{index}"), note.clone()); metadata.insert(format!("save_slice.note.{index}"), note.clone());
} }
@ -602,6 +638,7 @@ fn project_save_slice_components(
packed_event_collection, packed_event_collection,
event_runtime_records, event_runtime_records,
candidate_availability, candidate_availability,
named_locomotive_availability,
special_conditions, special_conditions,
}) })
} }
@ -1133,6 +1170,12 @@ fn lower_condition_targets_in_effect(
value: *value, value: *value,
} }
} }
RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => {
RuntimeEffect::SetNamedLocomotiveAvailability {
name: name.clone(),
value: *value,
}
}
RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition {
label: label.clone(), label: label.clone(),
value: *value, value: *value,
@ -1562,6 +1605,12 @@ fn smp_runtime_effect_to_runtime_effect(
value: *value, value: *value,
}) })
} }
RuntimeEffect::SetNamedLocomotiveAvailability { name, value } => {
Ok(RuntimeEffect::SetNamedLocomotiveAvailability {
name: name.clone(),
value: *value,
})
}
RuntimeEffect::SetSpecialCondition { label, value } => { RuntimeEffect::SetSpecialCondition { label, value } => {
Ok(RuntimeEffect::SetSpecialCondition { Ok(RuntimeEffect::SetSpecialCondition {
label: label.clone(), label: label.clone(),
@ -2111,6 +2160,7 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetPlayerCash { .. } | RuntimeEffect::SetPlayerCash { .. }
| RuntimeEffect::DeactivatePlayer { .. } | RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
@ -2188,6 +2238,7 @@ fn runtime_effect_company_target_import_blocker(
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::ActivateEventRecord { .. }
| RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. }
@ -2526,6 +2577,7 @@ mod tests {
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(), special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
} }
@ -3080,6 +3132,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: None, event_runtime_collection: None,
notes: vec![], notes: vec![],
@ -3118,6 +3171,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: None, event_runtime_collection: None,
notes: vec![], 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 { special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable {
source_kind: "save-fixed-special-conditions-range".to_string(), source_kind: "save-fixed-special-conditions-range".to_string(),
table_offset: 0x0d64, table_offset: 0x0d64,
@ -3444,10 +3530,34 @@ mod tests {
import.state.candidate_availability.get("Uranium Mine"), import.state.candidate_availability.get("Uranium Mine"),
Some(&0) 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!( assert_eq!(
import.state.special_conditions.get("Disable Cargo Economy"), import.state.special_conditions.get("Disable Cargo Economy"),
Some(&0) 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!( assert_eq!(
import import
.state .state
@ -3485,6 +3595,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -3598,6 +3709,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -3689,6 +3801,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -3798,6 +3911,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -3880,6 +3994,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4012,6 +4127,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4252,6 +4368,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4327,6 +4444,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4424,6 +4542,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4496,6 +4615,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4615,6 +4735,7 @@ mod tests {
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(), special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
}; };
@ -4627,6 +4748,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4769,6 +4891,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4871,6 +4994,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -4954,6 +5078,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5058,6 +5183,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5162,6 +5288,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5256,6 +5383,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5346,6 +5474,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5444,6 +5573,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5533,6 +5663,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5604,6 +5735,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5680,6 +5812,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5761,6 +5894,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5842,6 +5976,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -5939,6 +6074,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6027,6 +6163,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6141,6 +6278,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6264,6 +6402,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6372,6 +6511,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6543,6 +6683,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6634,6 +6775,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6721,6 +6863,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -6873,6 +7016,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7014,6 +7158,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7102,6 +7247,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7217,6 +7363,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7320,6 +7467,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7443,6 +7591,7 @@ mod tests {
effects: vec![], effects: vec![],
}], }],
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(), special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState { service_state: RuntimeServiceState {
periodic_boundary_calls: 9, periodic_boundary_calls: 9,
@ -7460,6 +7609,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),
@ -7609,6 +7759,7 @@ mod tests {
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(), special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
}, },
@ -7629,6 +7780,7 @@ mod tests {
bridge_family: None, bridge_family: None,
profile: None, profile: None,
candidate_availability_table: None, candidate_availability_table: None,
named_locomotive_availability_table: None,
special_conditions_table: None, special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(), source_kind: "packed-event-runtime-collection".to_string(),

View file

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

View file

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

View file

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

View file

@ -1503,6 +1503,22 @@ pub struct SmpLoadedCandidateAvailabilityTable {
pub entries: Vec<SmpRt3105SaveNameTableEntry>, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpLoadedSpecialConditionsTable { pub struct SmpLoadedSpecialConditionsTable {
pub source_kind: String, pub source_kind: String,
@ -1673,6 +1689,7 @@ pub struct SmpLoadedSaveSlice {
pub bridge_family: Option<String>, pub bridge_family: Option<String>,
pub profile: Option<SmpLoadedProfile>, pub profile: Option<SmpLoadedProfile>,
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>, pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
pub named_locomotive_availability_table: Option<SmpLoadedNamedLocomotiveAvailabilityTable>,
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>, pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>, pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
pub notes: Vec<String>, pub notes: Vec<String>,
@ -1840,6 +1857,7 @@ pub fn load_save_slice_from_report(
bridge_family: summary.bridge_family.clone(), bridge_family: summary.bridge_family.clone(),
profile, profile,
candidate_availability_table, candidate_availability_table,
named_locomotive_availability_table: None,
special_conditions_table, special_conditions_table,
event_runtime_collection: report.event_runtime_collection_summary.clone(), event_runtime_collection: report.event_runtime_collection_summary.clone(),
notes: summary.notes.clone(), notes: summary.notes.clone(),
@ -2659,6 +2677,11 @@ fn parse_real_grouped_effect_row_summary(
if descriptor_metadata.is_none() { if descriptor_metadata.is_none() {
notes.push("descriptor id not yet recovered in the checked-in effect table".to_string()); 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 { Some(SmpLoadedPackedEventGroupedEffectRowSummary {
group_index, 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( fn recovered_locomotive_cost_descriptor_metadata(
descriptor_id: u32, descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> { ) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -3409,6 +3442,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. } | RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
| RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetEconomicStatusCode { .. }
| RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetCandidateAvailability { .. }
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
| RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ConfiscateCompanyAssets { .. } | RuntimeEffect::ConfiscateCompanyAssets { .. }
| RuntimeEffect::DeactivateCompany { .. } | RuntimeEffect::DeactivateCompany { .. }

View file

@ -490,6 +490,11 @@ fn apply_runtime_effects(
RuntimeEffect::SetCandidateAvailability { name, value } => { RuntimeEffect::SetCandidateAvailability { name, value } => {
state.candidate_availability.insert(name.clone(), *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 } => { RuntimeEffect::SetSpecialCondition { label, value } => {
state.special_conditions.insert(label.clone(), *value); state.special_conditions.insert(label.clone(), *value);
} }
@ -1158,6 +1163,7 @@ mod tests {
packed_event_collection: None, packed_event_collection: None,
event_runtime_records: Vec::new(), event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(), candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
special_conditions: BTreeMap::new(), special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(), service_state: RuntimeServiceState::default(),
} }
@ -1372,6 +1378,43 @@ mod tests {
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]); 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] #[test]
fn resolves_symbolic_company_targets() { fn resolves_symbolic_company_targets() {
let mut state = RuntimeState { let mut state = RuntimeState {

View file

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

@ -82,12 +82,16 @@ Implemented today:
descriptors `230..240`, `241..351`, `352..451`, `453`, and `457..500` now carry recovered 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 world-side scalar metadata, while descriptors `454..456` (`All Steam/Diesel/Electric Locos
Avail.`) now execute as keyed `world_flags` 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 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, and numeric-threshold batches. Richer runtime ownership should whole-game toggle, train, player, numeric-threshold, and named locomotive availability batches.
still be added only where a later descriptor or condition family needs more than the current Richer runtime ownership should still be added only where a later descriptor or condition family
event-owned roster. needs more than the current event-owned roster.
## Why This Boundary ## 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_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false, "calendar_projection_is_placeholder": false,
"world_flag_count": 7, "world_flag_count": 8,
"company_count": 1, "company_count": 1,
"packed_event_collection_present": true, "packed_event_collection_present": true,
"packed_event_record_count": 2, "packed_event_record_count": 2,