Skip to main content

wowlab_types/types/sim/
time.rs

1use serde::{Deserialize, Serialize};
2
3/// Simulation time in milliseconds.
4#[derive(
5    Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
6)]
7#[serde(transparent)]
8#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
9#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
10// docref:start des-sim-time-struct
11// #t(rust_newtype_pub_field) transparent simulation-time wire type with established tuple construction
12pub struct SimTime(pub u32);
13// docref:end des-sim-time-struct
14
15impl SimTime {
16    pub const ZERO: SimTime = SimTime(0);
17    pub const MAX: SimTime = SimTime(u32::MAX);
18
19    const MS_PER_SEC: u32 = 1000;
20    const MS_PER_SEC_F32: f32 = 1000.0;
21
22    #[inline]
23    #[must_use]
24    pub const fn from_millis(ms: u32) -> Self {
25        SimTime(ms)
26    }
27
28    #[inline]
29    #[must_use]
30    pub const fn from_secs(secs: u32) -> Self {
31        SimTime(secs * Self::MS_PER_SEC)
32    }
33
34    #[inline]
35    #[must_use]
36    #[expect(
37        clippy::cast_possible_truncation,
38        clippy::cast_sign_loss,
39        reason = "Rust float-to-int saturation is the established behavior for out-of-range times"
40    )]
41    pub fn from_secs_f32(secs: f32) -> Self {
42        // Round to nearest: truncation made ms→secs→SimTime round-trips land 1ms
43        // early (16.194s * 1000 = 16193.999…), which permanently stalled events
44        // scheduled off f64-second slots (auto-attack swings, GCD ends).
45        SimTime((secs * Self::MS_PER_SEC_F32).round() as u32)
46    }
47
48    #[inline]
49    #[must_use]
50    #[expect(
51        clippy::cast_possible_truncation,
52        clippy::cast_sign_loss,
53        reason = "Rust float-to-int saturation is the established behavior for out-of-range times"
54    )]
55    pub fn from_secs_f64(secs: f64) -> Self {
56        SimTime((secs * crate::constants::MS_PER_SECOND).round() as u32)
57    }
58
59    #[inline]
60    #[must_use]
61    pub const fn as_millis(&self) -> u32 {
62        self.0
63    }
64
65    #[inline]
66    #[must_use]
67    #[expect(
68        clippy::cast_precision_loss,
69        reason = "f32 seconds are an explicitly lower-precision display and scheduling API"
70    )]
71    pub fn as_secs_f32(&self) -> f32 {
72        // #t(rust_lossy_cast) millis u32 -> f32 is fine for display, max precision loss ~1ms at high values
73        self.0 as f32 / Self::MS_PER_SEC_F32
74    }
75
76    #[inline]
77    #[must_use]
78    pub fn as_secs_f64(&self) -> f64 {
79        f64::from(self.0) / crate::constants::MS_PER_SECOND
80    }
81
82    #[inline]
83    #[must_use]
84    pub const fn saturating_sub(self, other: SimTime) -> SimTime {
85        SimTime(self.0.saturating_sub(other.0))
86    }
87
88    #[inline]
89    #[must_use]
90    pub const fn saturating_add(self, other: SimTime) -> SimTime {
91        SimTime(self.0.saturating_add(other.0))
92    }
93}
94
95impl std::ops::Add for SimTime {
96    type Output = SimTime;
97    #[inline]
98    fn add(self, rhs: SimTime) -> SimTime {
99        SimTime(self.0 + rhs.0)
100    }
101}
102
103impl std::ops::Sub for SimTime {
104    type Output = SimTime;
105    #[inline]
106    fn sub(self, rhs: SimTime) -> SimTime {
107        SimTime(self.0 - rhs.0)
108    }
109}
110
111impl std::ops::AddAssign for SimTime {
112    #[inline]
113    fn add_assign(&mut self, rhs: SimTime) {
114        self.0 += rhs.0;
115    }
116}
117
118impl std::ops::SubAssign for SimTime {
119    #[inline]
120    fn sub_assign(&mut self, rhs: SimTime) {
121        self.0 -= rhs.0;
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use googletest::prelude::*;
128    use rstest::rstest;
129
130    use super::*;
131
132    #[gtest]
133    #[rstest]
134    #[case::zero(0, SimTime(0))]
135    #[case::five(5, SimTime(5000))]
136    fn from_secs(#[case] input: u32, #[case] expected: SimTime) -> Result<()> {
137        verify_that!(SimTime::from_secs(input), eq(expected))
138    }
139
140    #[gtest]
141    #[rstest]
142    #[case::whole(1.5, SimTime(1500))]
143    #[case::sub_milli(0.001, SimTime(1))]
144    #[case::zero(0.0, SimTime(0))]
145    fn from_secs_f64(#[case] input: f64, #[case] expected: SimTime) -> Result<()> {
146        verify_that!(SimTime::from_secs_f64(input), eq(expected))
147    }
148
149    #[gtest]
150    #[rstest]
151    #[case::whole(1.5, SimTime(1500))]
152    #[case::sub_milli(0.001, SimTime(1))]
153    #[case::zero(0.0, SimTime(0))]
154    fn from_secs_f32(#[case] input: f32, #[case] expected: SimTime) -> Result<()> {
155        verify_that!(SimTime::from_secs_f32(input), eq(expected))
156    }
157
158    #[gtest]
159    #[rstest]
160    #[case::whole(SimTime(1500), 1.5)]
161    #[case::zero(SimTime(0), 0.0)]
162    fn as_secs_f64(#[case] input: SimTime, #[case] expected: f64) -> Result<()> {
163        verify_that!(input.as_secs_f64(), near(expected, 1e-9))
164    }
165
166    #[gtest]
167    #[rstest]
168    #[case::whole(SimTime(1500), 1.5)]
169    #[case::zero(SimTime(0), 0.0)]
170    fn as_secs_f32(#[case] input: SimTime, #[case] expected: f32) -> Result<()> {
171        verify_that!(input.as_secs_f32(), near(expected, 1e-4))
172    }
173
174    #[gtest]
175    #[rstest]
176    #[case::saturates(SimTime(u32::MAX), SimTime(1), SimTime(u32::MAX))]
177    #[case::normal(SimTime(100), SimTime(50), SimTime(150))]
178    fn saturating_add(
179        #[case] a: SimTime,
180        #[case] b: SimTime,
181        #[case] expected: SimTime,
182    ) -> Result<()> {
183        verify_that!(a.saturating_add(b), eq(expected))
184    }
185
186    #[gtest]
187    #[rstest]
188    #[case::underflow_floor(SimTime(3), SimTime(10), SimTime(0))]
189    #[case::normal(SimTime(10), SimTime(4), SimTime(6))]
190    fn saturating_sub(
191        #[case] a: SimTime,
192        #[case] b: SimTime,
193        #[case] expected: SimTime,
194    ) -> Result<()> {
195        verify_that!(a.saturating_sub(b), eq(expected))
196    }
197
198    #[gtest]
199    fn operator_overloads() -> Result<()> {
200        verify_that!(SimTime(10) + SimTime(5), eq(SimTime(15)))?;
201        verify_that!(SimTime(10) - SimTime(4), eq(SimTime(6)))?;
202        let mut t = SimTime(10);
203
204        t += SimTime(5);
205        verify_that!(t, eq(SimTime(15)))?;
206        t -= SimTime(3);
207
208        verify_that!(t, eq(SimTime(12)))
209    }
210}