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