Skip to main content

naia_shared/
game_time.rs

1use naia_serde::{BitReader, BitWrite, ConstBitLength, Serde, SerdeErr, UnsignedInteger};
2use naia_socket_shared::Instant;
3
4const GAME_INSANT_BITS: u8 = 22;
5/// Wrapping period of [`GameInstant`] in milliseconds (2^22 ≈ 70 minutes).
6pub const GAME_TIME_LIMIT: u32 = 4194304; // 2^22
7const GAME_TIME_LIMIT_U128: u128 = 4194304;
8const GAME_TIME_MAX: u32 = 4194303; // 2^22 - 1
9const TIME_OFFSET_MAX: i32 = 2097151; // 2^21 - 1
10const TIME_OFFSET_MIN: i32 = -2097152; // 2^21 * -1
11
12/// Server-relative millisecond timestamp that wraps at 2^22 ms (~70 minutes).
13#[derive(PartialEq, Debug, Clone, Copy)]
14pub struct GameInstant {
15    millis: u32,
16}
17
18impl GameInstant {
19    /// Creates a `GameInstant` representing the current time relative to `start_instant`.
20    pub fn new(start_instant: &Instant) -> Self {
21        let now = Instant::now();
22        let millis = (start_instant.elapsed(&now).as_millis() % GAME_TIME_LIMIT_U128) as u32;
23
24        // start_instant should mark the initialization of the Server's TimeManager
25        Self { millis }
26    }
27
28    /// Returns the duration elapsed since `previous_instant` (assumed to be in the past).
29    pub fn time_since(&self, previous_instant: &GameInstant) -> GameDuration {
30        let previous_millis = previous_instant.millis;
31        let current_millis = self.millis;
32
33        if previous_millis == current_millis {
34            return GameDuration { millis: 0 };
35        }
36
37        if previous_millis < current_millis {
38            GameDuration::from_millis(current_millis - previous_millis)
39        } else {
40            GameDuration::from_millis(GAME_TIME_MAX - previous_millis + current_millis)
41        }
42    }
43
44    /// Signed millisecond offset to `other` (positive = `other` is later). Wraps correctly at 2^22.
45    pub fn offset_from(&self, other: &GameInstant) -> i32 {
46        const MAX: i32 = TIME_OFFSET_MAX;
47        const MIN: i32 = TIME_OFFSET_MIN;
48        const ADJUST: i32 = GAME_TIME_LIMIT as i32;
49
50        let a: i32 = self.millis as i32;
51        let b: i32 = other.millis as i32;
52
53        let mut result = b - a;
54        if (MIN..=MAX).contains(&result) {
55            result
56        } else if b > a {
57            result = b - (a + ADJUST);
58            if (MIN..=MAX).contains(&result) {
59                result
60            } else {
61                panic!("integer overflow, this shouldn't happen");
62            }
63        } else {
64            result = (b + ADJUST) - a;
65            if (MIN..=MAX).contains(&result) {
66                result
67            } else {
68                panic!("integer overflow, this shouldn't happen");
69            }
70        }
71    }
72
73    /// Returns `true` if `self` is strictly later than `other` (wrapping-aware).
74    /// Returns `true` if `self` is strictly later than `other` (wrapping-aware).
75    pub fn is_more_than(&self, other: &GameInstant) -> bool {
76        self.offset_from(other) < 0
77    }
78
79    /// Returns the raw millisecond value (in `[0, GAME_TIME_LIMIT)`).
80    pub fn as_millis(&self) -> u32 {
81        self.millis
82    }
83
84    /// Returns a new `GameInstant` `millis` milliseconds in the future (wrapping).
85    pub fn add_millis(&self, millis: u32) -> Self {
86        Self {
87            millis: (self.millis + millis) % GAME_TIME_LIMIT,
88        }
89    }
90
91    /// Returns a new `GameInstant` `millis` milliseconds in the past (wrapping).
92    pub fn sub_millis(&self, millis: u32) -> Self {
93        let millis = millis % GAME_TIME_LIMIT;
94        if self.millis >= millis {
95            Self {
96                millis: self.millis - millis,
97            }
98        } else {
99            // my millis is less than your millis
100            let delta = millis - self.millis;
101            Self {
102                millis: GAME_TIME_LIMIT - delta,
103            }
104        }
105    }
106
107    /// Returns a new `GameInstant` offset by `millis` (positive = future, negative = past).
108    pub fn add_signed_millis(&self, millis: i32) -> Self {
109        if millis >= 0 {
110            self.add_millis(millis as u32)
111        } else {
112            self.sub_millis(-millis as u32)
113        }
114    }
115}
116
117impl Serde for GameInstant {
118    fn ser(&self, writer: &mut dyn BitWrite) {
119        let integer = UnsignedInteger::<GAME_INSANT_BITS>::new(self.millis as u64);
120        integer.ser(writer);
121    }
122
123    fn de(reader: &mut BitReader) -> Result<Self, SerdeErr> {
124        let integer = UnsignedInteger::<GAME_INSANT_BITS>::de(reader)?;
125        let millis = integer.get() as u32;
126        Ok(Self { millis })
127    }
128
129    fn bit_length(&self) -> u32 {
130        <Self as ConstBitLength>::const_bit_length()
131    }
132}
133
134impl ConstBitLength for GameInstant {
135    fn const_bit_length() -> u32 {
136        <UnsignedInteger<GAME_INSANT_BITS> as ConstBitLength>::const_bit_length()
137    }
138}
139
140/// Unsigned millisecond duration between two [`GameInstant`] values.
141#[derive(PartialEq, PartialOrd, Eq, Clone)]
142pub struct GameDuration {
143    millis: u32,
144}
145
146impl GameDuration {
147    /// Creates a `GameDuration` of `millis` milliseconds.
148    pub fn from_millis(millis: u32) -> Self {
149        Self { millis }
150    }
151
152    /// Returns the duration in milliseconds.
153    pub fn as_millis(&self) -> u32 {
154        self.millis
155    }
156
157    /// Returns a new duration extended by `millis` milliseconds.
158    pub fn add_millis(&self, millis: u32) -> Self {
159        Self {
160            millis: self.millis + millis,
161        }
162    }
163
164    /// Returns a new duration reduced by `millis` milliseconds.
165    pub fn sub_millis(&self, millis: u32) -> Self {
166        Self {
167            millis: self.millis - millis,
168        }
169    }
170}
171
172// Tests
173#[cfg(test)]
174mod wrapping_diff_tests {
175    use super::GameInstant;
176    use crate::game_time::{GAME_TIME_LIMIT, GAME_TIME_MAX};
177
178    #[test]
179    fn simple() {
180        let a = GameInstant { millis: 10 };
181        let b = GameInstant { millis: 12 };
182
183        let result = a.offset_from(&b);
184
185        assert_eq!(result, 2);
186    }
187
188    #[test]
189    fn simple_backwards() {
190        let a = GameInstant { millis: 10 };
191        let b = GameInstant { millis: 12 };
192
193        let result = b.offset_from(&a);
194
195        assert_eq!(result, -2);
196    }
197
198    #[test]
199    fn max_wrap() {
200        let a = GameInstant {
201            millis: GAME_TIME_MAX,
202        };
203        let b = a.add_millis(2);
204
205        let result = a.offset_from(&b);
206
207        assert_eq!(result, 2);
208    }
209
210    #[test]
211    fn min_wrap() {
212        let a = GameInstant { millis: 0 };
213        let b = a.sub_millis(2);
214
215        let result = a.offset_from(&b);
216
217        assert_eq!(result, -2);
218    }
219
220    #[test]
221    fn max_wrap_backwards() {
222        let a = GameInstant {
223            millis: GAME_TIME_MAX,
224        };
225        let b = a.add_millis(2);
226
227        let result = b.offset_from(&a);
228
229        assert_eq!(result, -2);
230    }
231
232    #[test]
233    fn min_wrap_backwards() {
234        let a = GameInstant { millis: 0 };
235        let b = a.sub_millis(2);
236
237        let result = b.offset_from(&a);
238
239        assert_eq!(result, 2);
240    }
241
242    #[test]
243    fn medium_min_wrap() {
244        let diff = GAME_TIME_LIMIT / 2;
245        let a = GameInstant { millis: 0 };
246        let b = a.sub_millis(diff);
247
248        let result = a.offset_from(&b);
249
250        assert_eq!(result as i64, -i64::from(diff));
251    }
252
253    #[test]
254    fn medium_min_wrap_backwards() {
255        let diff = (GAME_TIME_LIMIT / 2) - 1;
256        let a = GameInstant { millis: 0 };
257        let b = a.sub_millis(diff);
258
259        let result = b.offset_from(&a);
260
261        assert_eq!(result as i64, i64::from(diff));
262    }
263
264    #[test]
265    fn medium_max_wrap() {
266        let diff = (GAME_TIME_LIMIT / 2) - 1;
267        let a = GameInstant {
268            millis: GAME_TIME_MAX,
269        };
270        let b = a.add_millis(diff);
271
272        let result = a.offset_from(&b);
273
274        assert_eq!(result as i64, i64::from(diff));
275    }
276
277    #[test]
278    fn medium_max_wrap_backwards() {
279        let diff = GAME_TIME_LIMIT / 2;
280        let a = GameInstant {
281            millis: GAME_TIME_MAX,
282        };
283        let b = a.add_millis(diff);
284
285        let result = b.offset_from(&a);
286
287        assert_eq!(result as i64, -i64::from(diff));
288    }
289}