diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 38988c3..29ac8c4 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -2395,13 +2395,14 @@ impl RuntimeState { } } + let known_company_ids = self + .companies + .iter() + .map(|company| company.company_id) + .collect::>(); self.service_state .company_periodic_side_latch_state - .retain(|company_id, _| { - self.service_state - .company_market_state - .contains_key(company_id) - }); + .retain(|company_id, _| known_company_ids.contains(company_id)); for (company_id, market_state) in &self.service_state.company_market_state { self.service_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] fn reads_grounded_company_stat_family_slots_from_runtime_state() { let mut year_stat_family_qword_bits = vec![ diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index c5ca5ac..3ebdf55 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -247,6 +247,7 @@ fn service_periodic_boundary( service_events: &mut Vec, ) -> Result<(), String> { state.service_state.periodic_boundary_calls += 1; + service_refresh_company_periodic_side_latch_state(state); for trigger_kind in PERIODIC_TRIGGER_KIND_ORDER { service_trigger_kind(state, trigger_kind, service_events)?; @@ -256,6 +257,52 @@ fn service_periodic_boundary( 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::>(); + 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 { let value = f64::from_bits(raw_bits); value.is_finite().then_some(value) @@ -2884,11 +2931,11 @@ mod tests { use crate::{ CalendarPoint, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, - RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition, - RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, - RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, - RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, - RuntimeWorldRestoreState, + RuntimeCompanyControllerKind, RuntimeCompanyPeriodicSideLatchState, RuntimeCompanyTarget, + RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, + RuntimeEventRecordTemplate, RuntimePlayer, RuntimePlayerTarget, RuntimeSaveProfileState, + RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeTrackPieceCounts, + RuntimeTrain, RuntimeWorldRestoreState, }; 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] fn periodic_boundary_applies_dividend_adjustment_from_annual_finance_policy() { let mut year_stat_family_qword_bits = vec![ diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 8eff935..ba7ee47 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -13,9 +13,9 @@ Working rule: `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 that pass split across annual-finance readers and atlas notes. -- Move the periodic-boundary owner from passive imported side-latch state toward same-cycle - service-owned refresh or reset behavior, so earlier periodic branches can eventually set those - lanes before annual finance consumes them. +- Rehost the world-side `[world+0x4c74]` post-text owner lane so the temporary route-preference + override fed by company-side `0x0d17` can eventually move through a real runtime seam instead + of staying atlas-only. - 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. @@ -63,6 +63,9 @@ Working rule: - 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 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 drifting from market/cache readers. - Company credit rating, prime rate, book value per share, investor confidence, and management