Skip to main content

opsis_core/
clock.rs

1use std::fmt;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Monotonically increasing tick counter for the world clock.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
8pub struct WorldTick(pub u64);
9
10impl WorldTick {
11    /// The zero tick (epoch start).
12    pub fn zero() -> Self {
13        Self(0)
14    }
15
16    /// Return the next tick value.
17    pub fn next(self) -> Self {
18        Self(self.0 + 1)
19    }
20}
21
22impl fmt::Display for WorldTick {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "tick:{}", self.0)
25    }
26}
27
28/// World clock — drives the simulation cadence.
29///
30/// Each call to [`advance`](WorldClock::advance) increments the tick counter and
31/// updates wall-time.  The `hz` field controls the nominal tick rate, while
32/// `time_scale` allows simulated time to run faster or slower than real time.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct WorldClock {
35    /// Current tick number.
36    pub tick: WorldTick,
37    /// UTC timestamp of tick zero.
38    pub epoch: DateTime<Utc>,
39    /// Wall-clock time of the most recent tick.
40    pub wall_time: DateTime<Utc>,
41    /// Nominal ticks per second.
42    pub hz: f64,
43    /// Multiplier for simulated time (1.0 = real-time).
44    pub time_scale: f64,
45}
46
47impl WorldClock {
48    /// Create a new clock at tick zero running at the given frequency.
49    pub fn new(hz: f64) -> Self {
50        let now = Utc::now();
51        Self {
52            tick: WorldTick::zero(),
53            epoch: now,
54            wall_time: now,
55            hz,
56            time_scale: 1.0,
57        }
58    }
59
60    /// Advance the clock by one tick, updating wall-time to now.
61    pub fn advance(&mut self) {
62        self.tick = self.tick.next();
63        self.wall_time = Utc::now();
64    }
65
66    /// Real seconds elapsed since epoch.
67    pub fn elapsed_seconds(&self) -> f64 {
68        let dur = self.wall_time.signed_duration_since(self.epoch);
69        dur.num_milliseconds() as f64 / 1000.0
70    }
71
72    /// Simulated seconds = `ticks / hz * time_scale`.
73    pub fn simulated_seconds(&self) -> f64 {
74        (self.tick.0 as f64 / self.hz) * self.time_scale
75    }
76}
77
78impl Default for WorldClock {
79    fn default() -> Self {
80        Self::new(1.0)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn starts_at_zero() {
90        let clock = WorldClock::default();
91        assert_eq!(clock.tick, WorldTick::zero());
92    }
93
94    #[test]
95    fn advance_increments() {
96        let mut clock = WorldClock::default();
97        clock.advance();
98        assert_eq!(clock.tick, WorldTick(1));
99        clock.advance();
100        assert_eq!(clock.tick, WorldTick(2));
101    }
102
103    #[test]
104    fn simulated_seconds_at_1hz() {
105        let mut clock = WorldClock::new(1.0);
106        clock.tick = WorldTick(10);
107        assert!((clock.simulated_seconds() - 10.0).abs() < 1e-9);
108    }
109
110    #[test]
111    fn world_tick_ordering() {
112        assert!(WorldTick(0) < WorldTick(1));
113        assert!(WorldTick(100) > WorldTick(42));
114        assert_eq!(WorldTick(7), WorldTick(7));
115    }
116}