Skip to main content

steel_core/world/
clock.rs

1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use steel_registry::{REGISTRY, world_clock::WorldClockRef};
4use steel_utils::Identifier;
5use thiserror::Error;
6
7/// Game-time synchronization interval, measured in simulation ticks.
8const GAME_TIME_SYNC_INTERVAL_TICKS: i64 = 20;
9
10/// One persisted instance of a registered world clock.
11#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
12#[serde(default)]
13struct ClockState {
14    total_ticks: i64,
15    partial_tick: f32,
16    rate: f32,
17    paused: bool,
18}
19
20impl Default for ClockState {
21    fn default() -> Self {
22        Self {
23            total_ticks: 0,
24            partial_tick: 0.0,
25            rate: 1.0,
26            paused: false,
27        }
28    }
29}
30
31/// Invalid persisted state for a world's clock manager.
32#[derive(Debug, Error, PartialEq)]
33pub(crate) enum WorldClockLoadError {
34    #[error("saved state references unknown world clock {0}")]
35    UnknownClock(Identifier),
36    #[error("world clock {clock} has invalid partial tick {partial_tick}")]
37    InvalidPartialTick {
38        clock: Identifier,
39        partial_tick: f32,
40    },
41    #[error("world clock {clock} has invalid rate {rate}")]
42    InvalidRate { clock: Identifier, rate: f32 },
43}
44
45/// Per-world clock instances.
46///
47/// Vanilla 26.2 owns one clock manager at server scope and every loaded level
48/// delegates to it. Steel intentionally persists one manager in each world's
49/// level data instead: equal clock keys are not shared, so two overworlds in the
50/// same domain can advance and be configured independently.
51#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
52#[serde(transparent)]
53pub(crate) struct WorldClockManager {
54    clocks: FxHashMap<Identifier, ClockState>,
55}
56
57impl WorldClockManager {
58    #[must_use]
59    pub(crate) fn new() -> Self {
60        let mut manager = Self::default();
61        for (_, clock) in REGISTRY.world_clocks.iter() {
62            manager
63                .clocks
64                .insert(clock.key.clone(), ClockState::default());
65        }
66        manager
67    }
68
69    /// Validates loaded states and creates default instances for newly registered clocks.
70    pub(crate) fn initialize_registered_clocks(&mut self) -> Result<bool, WorldClockLoadError> {
71        for (key, state) in &self.clocks {
72            if REGISTRY.world_clocks.by_key(key).is_none() {
73                return Err(WorldClockLoadError::UnknownClock(key.clone()));
74            }
75            if !state.partial_tick.is_finite() {
76                return Err(WorldClockLoadError::InvalidPartialTick {
77                    clock: key.clone(),
78                    partial_tick: state.partial_tick,
79                });
80            }
81            if !state.rate.is_finite() || state.rate <= 0.0 {
82                return Err(WorldClockLoadError::InvalidRate {
83                    clock: key.clone(),
84                    rate: state.rate,
85                });
86            }
87        }
88
89        let previous_len = self.clocks.len();
90        for (_, clock) in REGISTRY.world_clocks.iter() {
91            self.clocks.entry(clock.key.clone()).or_default();
92        }
93        Ok(self.clocks.len() != previous_len)
94    }
95
96    #[must_use]
97    pub(crate) fn total_ticks(&self, clock: WorldClockRef) -> Option<i64> {
98        self.clocks.get(&clock.key).map(|state| state.total_ticks)
99    }
100
101    pub(crate) fn set_total_ticks(&mut self, clock: WorldClockRef, total_ticks: i64) -> Option<()> {
102        let state = self.clocks.get_mut(&clock.key)?;
103        state.total_ticks = total_ticks;
104        state.partial_tick = 0.0;
105        Some(())
106    }
107
108    pub(crate) fn add_ticks(&mut self, clock: WorldClockRef, ticks: i32) -> Option<i64> {
109        let state = self.clocks.get_mut(&clock.key)?;
110        state.total_ticks = state.total_ticks.wrapping_add(i64::from(ticks)).max(0);
111        Some(state.total_ticks)
112    }
113
114    pub(crate) fn set_paused(&mut self, clock: WorldClockRef, paused: bool) -> Option<()> {
115        let state = self.clocks.get_mut(&clock.key)?;
116        state.paused = paused;
117        Some(())
118    }
119
120    pub(crate) fn set_rate(&mut self, clock: WorldClockRef, rate: f32) -> Option<()> {
121        if !rate.is_finite() || rate <= 0.0 {
122            return None;
123        }
124        let state = self.clocks.get_mut(&clock.key)?;
125        state.rate = rate;
126        Some(())
127    }
128
129    /// Moves a clock to a marker, returning `false` when that marker is not defined for it.
130    pub(crate) fn move_to_time_marker(
131        &mut self,
132        clock: WorldClockRef,
133        marker_key: &Identifier,
134    ) -> Option<bool> {
135        let total_ticks = self.total_ticks(clock)?;
136        let marker = REGISTRY
137            .timelines
138            .iter()
139            .filter(|(_, timeline)| timeline.clock == clock)
140            .find_map(|(_, timeline)| {
141                timeline
142                    .time_markers
143                    .iter()
144                    .find(|marker| &marker.key == marker_key)
145                    .map(|marker| (marker, timeline.period_ticks))
146            });
147        let Some((marker, period_ticks)) = marker else {
148            return Some(false);
149        };
150        let state = self.clocks.get_mut(&clock.key)?;
151        state.total_ticks = marker.resolve_time_to_move_to(total_ticks, period_ticks);
152        state.partial_tick = 0.0;
153        Some(true)
154    }
155
156    /// Advances all unpaused clocks once when the world's `advance_time` rule allows it.
157    pub(crate) fn tick(&mut self, advance_time: bool) -> bool {
158        if !advance_time {
159            return false;
160        }
161        let mut changed = false;
162        for state in self.clocks.values_mut() {
163            if state.paused {
164                continue;
165            }
166            state.partial_tick += state.rate;
167            let full_ticks = state.partial_tick.floor() as i32;
168            state.partial_tick -= full_ticks as f32;
169            state.total_ticks = state.total_ticks.wrapping_add(i64::from(full_ticks));
170            changed = true;
171        }
172        changed
173    }
174
175    #[must_use]
176    #[expect(
177        clippy::cast_possible_truncation,
178        reason = "registry IDs are bounded by the protocol's signed VarInt index space"
179    )]
180    pub(crate) fn network_updates(&self, advance_time: bool) -> Vec<(i32, i64, f32, f32)> {
181        REGISTRY
182            .world_clocks
183            .iter()
184            .filter_map(|(id, clock)| {
185                self.clocks.get(&clock.key).map(|state| {
186                    let rate = if state.paused || !advance_time {
187                        0.0
188                    } else {
189                        state.rate
190                    };
191                    (id as i32, state.total_ticks, state.partial_tick, rate)
192                })
193            })
194            .collect()
195    }
196
197    #[must_use]
198    #[expect(
199        clippy::cast_possible_truncation,
200        reason = "registry IDs are bounded by the protocol's signed VarInt index space"
201    )]
202    pub(crate) fn network_update(
203        &self,
204        clock: WorldClockRef,
205        advance_time: bool,
206    ) -> Option<(i32, i64, f32, f32)> {
207        let id = REGISTRY.world_clocks.id_from_key(&clock.key)?;
208        let state = self.clocks.get(&clock.key)?;
209        let rate = if state.paused || !advance_time {
210            0.0
211        } else {
212            state.rate
213        };
214        Some((id as i32, state.total_ticks, state.partial_tick, rate))
215    }
216}
217
218use super::{CSetTime, RegistryExt, World, clock};
219
220impl World {
221    /// Returns vanilla level game time.
222    pub fn game_time(&self) -> i64 {
223        self.game_time.ticks()
224    }
225
226    /// Returns the total ticks of one clock in this world.
227    pub(crate) fn clock_total_ticks(&self, clock: WorldClockRef) -> Option<i64> {
228        self.level_data.read().world_clocks().total_ticks(clock)
229    }
230
231    /// Creates a full per-world time synchronization packet.
232    pub(crate) fn time_sync_packet(&self) -> CSetTime {
233        let level_data = self.level_data.read();
234        let advance_time = self.advance_time_with_guard(&level_data);
235        CSetTime::new(
236            self.game_time(),
237            level_data.world_clocks().network_updates(advance_time),
238        )
239    }
240
241    /// Broadcasts all clock states to players in this world.
242    pub(crate) fn broadcast_time_sync(&self) {
243        self.broadcast_to_all(self.time_sync_packet());
244    }
245
246    pub(crate) fn set_clock_total_ticks(
247        &self,
248        clock: WorldClockRef,
249        total_ticks: i64,
250    ) -> Option<()> {
251        self.modify_clock(clock, |manager| manager.set_total_ticks(clock, total_ticks))
252    }
253
254    pub(crate) fn add_clock_ticks(&self, clock: WorldClockRef, ticks: i32) -> Option<i64> {
255        self.modify_clock(clock, |manager| manager.add_ticks(clock, ticks))
256    }
257
258    pub(crate) fn set_clock_paused(&self, clock: WorldClockRef, paused: bool) -> Option<()> {
259        self.modify_clock(clock, |manager| manager.set_paused(clock, paused))
260    }
261
262    pub(crate) fn set_clock_rate(&self, clock: WorldClockRef, rate: f32) -> Option<()> {
263        self.modify_clock(clock, |manager| manager.set_rate(clock, rate))
264    }
265
266    pub(crate) fn move_clock_to_time_marker(
267        &self,
268        clock: WorldClockRef,
269        marker: &Identifier,
270    ) -> Option<bool> {
271        self.modify_clock(clock, |manager| manager.move_to_time_marker(clock, marker))
272    }
273
274    pub(super) fn modify_clock<R>(
275        &self,
276        clock: WorldClockRef,
277        action: impl FnOnce(&mut clock::WorldClockManager) -> Option<R>,
278    ) -> Option<R> {
279        let (result, packet) = {
280            let mut level_data = self.level_data.write();
281            let result = action(level_data.world_clocks_mut())?;
282            let advance_time = self.advance_time_with_guard(&level_data);
283            let update = level_data
284                .world_clocks()
285                .network_update(clock, advance_time)?;
286            (result, CSetTime::new(self.game_time(), vec![update]))
287        };
288        self.broadcast_to_all(packet);
289        Some(result)
290    }
291
292    /// Advances this world's clocks and periodically synchronizes the shared game time.
293    pub(super) fn tick_time(&self) {
294        let game_time = {
295            let mut lock = self.level_data.write();
296            let advance_time = self.advance_time_with_guard(&lock);
297            lock.world_clocks_mut().tick(advance_time);
298            self.game_time()
299        };
300
301        if game_time % GAME_TIME_SYNC_INTERVAL_TICKS == 0 {
302            self.broadcast_to_all(CSetTime::new(game_time, Vec::new()));
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use steel_registry::{init_vanilla_registry, vanilla_world_clocks};
310
311    use super::*;
312
313    #[test]
314    fn initializes_every_registered_clock() {
315        init_vanilla_registry();
316        let manager = WorldClockManager::new();
317
318        assert_eq!(
319            manager.total_ticks(&vanilla_world_clocks::OVERWORLD),
320            Some(0)
321        );
322        assert_eq!(manager.total_ticks(&vanilla_world_clocks::THE_END), Some(0));
323        assert_eq!(manager.network_updates(true).len(), 2);
324    }
325
326    #[test]
327    fn separate_worlds_keep_independent_clock_state() {
328        init_vanilla_registry();
329        let mut first_world = WorldClockManager::new();
330        let second_world = WorldClockManager::new();
331
332        assert_eq!(
333            first_world.set_total_ticks(&vanilla_world_clocks::OVERWORLD, 6_000),
334            Some(())
335        );
336        assert_eq!(
337            first_world.total_ticks(&vanilla_world_clocks::OVERWORLD),
338            Some(6_000)
339        );
340        assert_eq!(
341            second_world.total_ticks(&vanilla_world_clocks::OVERWORLD),
342            Some(0)
343        );
344    }
345
346    #[test]
347    fn rate_accumulates_partial_ticks_like_vanilla() {
348        init_vanilla_registry();
349        let mut manager = WorldClockManager::new();
350        assert_eq!(
351            manager.set_rate(&vanilla_world_clocks::OVERWORLD, 0.25),
352            Some(())
353        );
354
355        for _ in 0..3 {
356            assert!(manager.tick(true));
357        }
358        assert_eq!(
359            manager.total_ticks(&vanilla_world_clocks::OVERWORLD),
360            Some(0)
361        );
362        assert!(manager.tick(true));
363        assert_eq!(
364            manager.total_ticks(&vanilla_world_clocks::OVERWORLD),
365            Some(1)
366        );
367    }
368
369    #[test]
370    fn pause_and_advance_time_gate_network_rate_and_ticks() {
371        init_vanilla_registry();
372        let mut manager = WorldClockManager::new();
373        assert_eq!(
374            manager.set_paused(&vanilla_world_clocks::OVERWORLD, true),
375            Some(())
376        );
377
378        assert!(manager.tick(true));
379        assert_eq!(
380            manager.total_ticks(&vanilla_world_clocks::OVERWORLD),
381            Some(0)
382        );
383        let Some(update) = manager.network_update(&vanilla_world_clocks::OVERWORLD, true) else {
384            panic!("overworld clock update should exist");
385        };
386        assert_eq!(update.3, 0.0);
387
388        let Some(update) = manager.network_update(&vanilla_world_clocks::THE_END, false) else {
389            panic!("end clock update should exist");
390        };
391        assert_eq!(update.3, 0.0);
392    }
393
394    #[test]
395    fn repeating_time_marker_moves_strictly_forward() {
396        init_vanilla_registry();
397        let mut manager = WorldClockManager::new();
398        assert_eq!(
399            manager.set_total_ticks(&vanilla_world_clocks::OVERWORLD, 1_000),
400            Some(())
401        );
402
403        assert_eq!(
404            manager.move_to_time_marker(
405                &vanilla_world_clocks::OVERWORLD,
406                &Identifier::vanilla_static("day")
407            ),
408            Some(true)
409        );
410        assert_eq!(
411            manager.total_ticks(&vanilla_world_clocks::OVERWORLD),
412            Some(25_000)
413        );
414    }
415
416    #[test]
417    fn manager_round_trips_through_toml() {
418        init_vanilla_registry();
419        let mut manager = WorldClockManager::new();
420        assert_eq!(
421            manager.set_total_ticks(&vanilla_world_clocks::OVERWORLD, 12_345),
422            Some(())
423        );
424        assert_eq!(
425            manager.set_rate(&vanilla_world_clocks::THE_END, 2.5),
426            Some(())
427        );
428
429        let serialized = toml::to_string(&manager).expect("clock manager should serialize");
430        let mut restored: WorldClockManager =
431            toml::from_str(&serialized).expect("clock manager should deserialize");
432        assert_eq!(restored.initialize_registered_clocks(), Ok(false));
433        assert_eq!(restored, manager);
434    }
435}