Refresh company periodic side latches at boundary

This commit is contained in:
Jan Petykiewicz 2026-04-18 07:26:39 -07:00
commit 6fc8da2153
3 changed files with 215 additions and 13 deletions

View file

@ -2395,13 +2395,14 @@ impl RuntimeState {
} }
} }
let known_company_ids = self
.companies
.iter()
.map(|company| company.company_id)
.collect::<BTreeSet<_>>();
self.service_state self.service_state
.company_periodic_side_latch_state .company_periodic_side_latch_state
.retain(|company_id, _| { .retain(|company_id, _| known_company_ids.contains(company_id));
self.service_state
.company_market_state
.contains_key(company_id)
});
for (company_id, market_state) in &self.service_state.company_market_state { for (company_id, market_state) in &self.service_state.company_market_state {
self.service_state self.service_state
.company_periodic_side_latch_state .company_periodic_side_latch_state
@ -6818,6 +6819,93 @@ mod tests {
); );
} }
#[test]
fn preserves_company_periodic_side_latch_state_without_market_projection() {
let mut 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![RuntimeCompany {
company_id: 1,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
controller_kind: RuntimeCompanyControllerKind::Unknown,
}],
selected_company_id: Some(1),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: 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::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
company_periodic_side_latch_state: BTreeMap::from([(
1,
RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: Some(2),
city_connection_latch: true,
linked_transit_latch: false,
},
)]),
..RuntimeServiceState::default()
},
};
state.refresh_derived_market_state();
assert_eq!(
state
.service_state
.company_periodic_side_latch_state
.get(&1),
Some(&RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: Some(2),
city_connection_latch: true,
linked_transit_latch: false,
})
);
}
#[test] #[test]
fn reads_grounded_company_stat_family_slots_from_runtime_state() { fn reads_grounded_company_stat_family_slots_from_runtime_state() {
let mut year_stat_family_qword_bits = vec![ let mut year_stat_family_qword_bits = vec![

View file

@ -247,6 +247,7 @@ fn service_periodic_boundary(
service_events: &mut Vec<ServiceEvent>, service_events: &mut Vec<ServiceEvent>,
) -> Result<(), String> { ) -> Result<(), String> {
state.service_state.periodic_boundary_calls += 1; state.service_state.periodic_boundary_calls += 1;
service_refresh_company_periodic_side_latch_state(state);
for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER { for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER {
service_trigger_kind(state, trigger_kind, service_events)?; service_trigger_kind(state, trigger_kind, service_events)?;
@ -256,6 +257,52 @@ fn service_periodic_boundary(
Ok(()) Ok(())
} }
fn service_refresh_company_periodic_side_latch_state(state: &mut RuntimeState) {
let active_company_ids = state
.companies
.iter()
.filter(|company| company.active)
.map(|company| company.company_id)
.collect::<BTreeSet<_>>();
state
.service_state
.company_periodic_side_latch_state
.retain(|company_id, _| active_company_ids.contains(company_id));
for company_id in active_company_ids {
match (
state
.service_state
.company_periodic_side_latch_state
.get_mut(&company_id),
state.service_state.company_market_state.get(&company_id),
) {
(Some(latch_state), Some(market_state)) => {
latch_state.preferred_locomotive_engine_type_raw_u8 = None;
latch_state.city_connection_latch = market_state.city_connection_latch;
latch_state.linked_transit_latch = market_state.linked_transit_latch;
}
(Some(latch_state), None) => {
latch_state.preferred_locomotive_engine_type_raw_u8 = None;
}
(None, Some(market_state)) => {
state
.service_state
.company_periodic_side_latch_state
.insert(
company_id,
crate::RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: None,
city_connection_latch: market_state.city_connection_latch,
linked_transit_latch: market_state.linked_transit_latch,
},
);
}
(None, None) => {}
}
}
}
fn service_decode_saved_f64_bits(raw_bits: u64) -> Option<f64> { fn service_decode_saved_f64_bits(raw_bits: u64) -> Option<f64> {
let value = f64::from_bits(raw_bits); let value = f64::from_bits(raw_bits);
value.is_finite().then_some(value) value.is_finite().then_some(value)
@ -2884,11 +2931,11 @@ mod tests {
use crate::{ use crate::{
CalendarPoint, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, CalendarPoint, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget,
RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, RuntimeCompanyControllerKind, RuntimeCompanyPeriodicSideLatchState, RuntimeCompanyTarget,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord,
RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeEventRecordTemplate, RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts,
RuntimeWorldRestoreState, RuntimeTrain, RuntimeWorldRestoreState,
}; };
fn state() -> RuntimeState { fn state() -> RuntimeState {
@ -3227,6 +3274,70 @@ mod tests {
); );
} }
#[test]
fn periodic_boundary_clears_transient_preferred_locomotive_side_latch() {
let mut state = state();
state.service_state.company_periodic_side_latch_state = BTreeMap::from([(
1,
RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: Some(2),
city_connection_latch: true,
linked_transit_latch: false,
},
)]);
execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should refresh periodic side latches");
assert_eq!(
state
.service_state
.company_periodic_side_latch_state
.get(&1),
Some(&RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: None,
city_connection_latch: true,
linked_transit_latch: false,
})
);
}
#[test]
fn periodic_boundary_reseeds_finance_side_latches_from_market_state() {
let mut state = state();
state.service_state.company_market_state = BTreeMap::from([(
1,
crate::RuntimeCompanyMarketState {
city_connection_latch: false,
linked_transit_latch: true,
..crate::RuntimeCompanyMarketState::default()
},
)]);
state.service_state.company_periodic_side_latch_state = BTreeMap::from([(
1,
RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: Some(2),
city_connection_latch: true,
linked_transit_latch: false,
},
)]);
execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should reseed finance latches from market state");
assert_eq!(
state
.service_state
.company_periodic_side_latch_state
.get(&1),
Some(&RuntimeCompanyPeriodicSideLatchState {
preferred_locomotive_engine_type_raw_u8: None,
city_connection_latch: false,
linked_transit_latch: true,
})
);
}
#[test] #[test]
fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() { fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() {
let mut year_stat_family_qword_bits = vec![ let mut year_stat_family_qword_bits = vec![

View file

@ -13,9 +13,9 @@ Working rule:
`company_service_periodic_city_connection_finance_and_linked_transit_lanes`, using the now `company_service_periodic_city_connection_finance_and_linked_transit_lanes`, using the now
save-native side-latch trio `0x0d17/0x0d18/0x0d56` as owned runtime state instead of leaving save-native side-latch trio `0x0d17/0x0d18/0x0d56` as owned runtime state instead of leaving
that pass split across annual-finance readers and atlas notes. that pass split across annual-finance readers and atlas notes.
- Move the periodic-boundary owner from passive imported side-latch state toward same-cycle - Rehost the world-side `[world+0x4c74]` post-text owner lane so the temporary route-preference
service-owned refresh or reset behavior, so earlier periodic branches can eventually set those override fed by company-side `0x0d17` can eventually move through a real runtime seam instead
lanes before annual finance consumes them. of staying atlas-only.
- Keep widening selected-year world-owner state only when a full owning reader/rebuild family is - Keep widening selected-year world-owner state only when a full owning reader/rebuild family is
grounded strongly enough to avoid one-off leaf guesses. grounded strongly enough to avoid one-off leaf guesses.
@ -63,6 +63,9 @@ Working rule:
- That same side-latch trio now also has a runtime-owned service-state map and summary surface, - That same side-latch trio now also has a runtime-owned service-state map and summary surface,
so later periodic company-service work can stop reading those lanes directly from imported so later periodic company-service work can stop reading those lanes directly from imported
market/cache residue. market/cache residue.
- The periodic-boundary owner now also clears the transient preferred-locomotive side latch every
cycle and reseeds the finance latches from market state where present, while preserving
side-latch-only company context when no market projection exists.
- Company cash, confiscation, and major governance effects now write through owner state instead of - Company cash, confiscation, and major governance effects now write through owner state instead of
drifting from market/cache readers. drifting from market/cache readers.
- Company credit rating, prime rate, book value per share, investor confidence, and management - Company credit rating, prime rate, book value per share, investor confidence, and management