diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index be2c09b..e1cbf59 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -4133,6 +4133,45 @@ pub fn runtime_decode_packed_calendar_tuple( } } +pub fn runtime_encode_packed_calendar_tuple(tuple: RuntimePackedCalendarTuple) -> (u32, u32) { + let year_bytes = tuple.year_word.to_le_bytes(); + let word_0 = u32::from_le_bytes([ + year_bytes[0], + year_bytes[1], + tuple.month_1_based, + tuple.week_1_based, + ]); + let word_1 = u32::from_le_bytes([ + tuple.day_1_based, + tuple.hour_0_based, + tuple.quarter_day_1_based, + tuple.minute_0_based, + ]); + (word_0, word_1) +} + +pub fn runtime_derive_packed_calendar_tuple_from_calendar_point( + calendar: CalendarPoint, +) -> Option { + let year_word = u16::try_from(calendar.year).ok()?; + let month_1_based = u8::try_from(calendar.month_slot.checked_add(1)?).ok()?; + let day_1_based = u8::try_from(calendar.phase_slot.checked_add(1)?).ok()?; + let week_1_based = u8::try_from(calendar.phase_slot / 7 + 1).ok()?; + let total_minutes = calendar.tick_slot.checked_mul(1440)? / crate::TICKS_PER_PHASE; + let hour_0_based = u8::try_from(total_minutes / 60).ok()?; + let minute_0_based = u8::try_from(total_minutes % 60).ok()?; + let quarter_day_1_based = u8::try_from((u32::from(hour_0_based) / 6) + 1).ok()?; + Some(RuntimePackedCalendarTuple { + year_word, + month_1_based, + week_1_based, + day_1_based, + hour_0_based, + quarter_day_1_based, + minute_0_based, + }) +} + pub fn runtime_pack_packed_calendar_tuple_to_absolute_counter( tuple: RuntimePackedCalendarTuple, ) -> Option { @@ -7888,6 +7927,33 @@ mod tests { ); } + #[test] + fn derives_and_encodes_packed_calendar_tuple_from_runtime_calendar() { + let tuple = runtime_derive_packed_calendar_tuple_from_calendar_point(CalendarPoint { + year: 1830, + month_slot: 11, + phase_slot: 27, + tick_slot: 179, + }) + .expect("runtime calendar tuple"); + assert_eq!( + tuple, + RuntimePackedCalendarTuple { + year_word: 1830, + month_1_based: 12, + week_1_based: 4, + day_1_based: 28, + hour_0_based: 23, + quarter_day_1_based: 4, + minute_0_based: 52, + } + ); + assert_eq!( + runtime_encode_packed_calendar_tuple(tuple), + (0x040c_0726, 0x3404_171c) + ); + } + #[test] fn derives_company_unassigned_share_pool_from_market_state_and_holdings() { let state = RuntimeState { diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index b427df4..7eba9e1 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -9,7 +9,7 @@ use crate::runtime::{ runtime_round_f64_to_i64, }; use crate::{ - RUNTIME_COMPANY_STAT_SLOT_COUNT, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + CalendarPoint, RUNTIME_COMPANY_STAT_SLOT_COUNT, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, @@ -191,6 +191,7 @@ fn step_once( boundary_events: &mut Vec, service_events: &mut Vec, ) -> Result<(), String> { + let prior_calendar = state.calendar; let boundary = state.calendar.step_forward(); if boundary != BoundaryEventKind::Tick { boundary_events.push(BoundaryEvent { @@ -199,8 +200,10 @@ fn step_once( }); } if boundary == BoundaryEventKind::YearRollover { + service_sync_world_restore_time_from_calendar(state, prior_calendar); service_periodic_boundary(state, service_events)?; } + service_sync_world_restore_time_from_calendar(state, state.calendar); Ok(()) } @@ -213,6 +216,31 @@ fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { } } +fn service_sync_world_restore_time_from_calendar( + state: &mut RuntimeState, + calendar: CalendarPoint, +) { + if let Ok(year_word) = u16::try_from(calendar.year) { + state.world_restore.packed_year_word_raw_u16 = Some(year_word); + } + if let Ok(partial_year_progress) = u8::try_from(calendar.month_slot.saturating_add(1)) { + state.world_restore.partial_year_progress_raw_u8 = Some(partial_year_progress); + } + if let Some(tuple) = + crate::runtime::runtime_derive_packed_calendar_tuple_from_calendar_point(calendar) + { + let (word_0, word_1) = crate::runtime::runtime_encode_packed_calendar_tuple(tuple); + state.world_restore.current_calendar_tuple_word_raw_u32 = Some(word_0); + state.world_restore.current_calendar_tuple_word_2_raw_u32 = Some(word_1); + if let Some(absolute_counter) = + crate::runtime::runtime_pack_packed_calendar_tuple_to_absolute_counter(tuple) + { + state.world_restore.absolute_counter_raw_u32 = Some(absolute_counter); + state.world_restore.absolute_counter_mirror_raw_u32 = Some(absolute_counter); + } + } +} + fn service_periodic_boundary( state: &mut RuntimeState, service_events: &mut Vec, @@ -2922,6 +2950,24 @@ mod tests { assert_eq!(result.steps_executed, 5); assert_eq!(state.calendar.tick_slot, 5); + assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1830)); + assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); + assert_eq!( + state.world_restore.current_calendar_tuple_word_raw_u32, + Some(0x0101_0726) + ); + assert_eq!( + state.world_restore.current_calendar_tuple_word_2_raw_u32, + Some(0x2801_0001) + ); + assert_eq!( + state.world_restore.absolute_counter_raw_u32, + Some(885_427_240) + ); + assert_eq!( + state.world_restore.absolute_counter_mirror_raw_u32, + Some(885_427_240) + ); } #[test] @@ -2974,6 +3020,24 @@ mod tests { .get("world.periodic_rollover_service_fired"), Some(&true) ); + assert_eq!(state.world_restore.packed_year_word_raw_u16, Some(1831)); + assert_eq!(state.world_restore.partial_year_progress_raw_u8, Some(1)); + assert_eq!( + state.world_restore.current_calendar_tuple_word_raw_u32, + Some(0x0101_0727) + ); + assert_eq!( + state.world_restore.current_calendar_tuple_word_2_raw_u32, + Some(0x0001_0001) + ); + assert_eq!( + state.world_restore.absolute_counter_raw_u32, + Some(885_911_040) + ); + assert_eq!( + state.world_restore.absolute_counter_mirror_raw_u32, + Some(885_911_040) + ); assert!( result .service_events diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md new file mode 100644 index 0000000..18a3551 --- /dev/null +++ b/docs/rehost-queue.md @@ -0,0 +1,51 @@ +# Rehost Queue + +Working rule: + +- Do not stop after commits. +- After each commit, check this queue and continue. +- Only stop if the queue is empty, you hit a real blocker, or you need approval. +- Before any final response, state which stop condition is true. If none is true, continue. + +## Next + +- Rehost world time owner-state progression from `CalendarPoint` into `world_restore`: + sync `packed_year_word_raw_u16`, `partial_year_progress_raw_u8`, packed calendar tuple words, + and absolute counter so shellless stepping advances the same finance/economy reader inputs used + by periodic service. +- Make automatic year-rollover periodic service run against a consistent end-of-year world-time + snapshot, then refresh into the new-year snapshot after service commits. +- Rehost the next shellless periodic-boundary world work under + `simulation_service_periodic_boundary_work`, starting with any bounded non-dialog queue/state + family that can execute without shell ownership. + +## In Progress + +- Widen shellless simulation from explicit service commands toward “advance the runtime clock and + the simulation-owned services advance with it.” + +## Queued + +- Rehost additional periodic finance/service branches that still depend on frozen world restore + fields instead of advanced runtime-owned time state. +- Reduce remaining company/chairman save-native gaps that still block standalone simulation + quality, especially controller-kind closure and any deeper finance/state fields that still rely + on conservative defaults. +- Rehost bounded live economy owner state beyond selector/catalog/override surfaces when a + concrete non-shell-owned seam is grounded. +- Keep tightening shell-owned parity families only when that directly supports later rehosting. + +## Blocked + +- Full shell/dialog ownership remains intentionally out of scope. +- Any candidate slice that requires guessing rather than rehosting owning state or real + reader/setter families stays blocked until a better owner seam is grounded. + +## Recently Done + +- Automatic year-rollover calendar stepping now invokes periodic-boundary service. +- 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 + attitude now refresh from grounded owner-state readers. +- Annual finance service persists structured news events and grounded debt/share flow totals.