1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use steel_registry::{REGISTRY, world_clock::WorldClockRef};
4use steel_utils::Identifier;
5use thiserror::Error;
6
7#[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#[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#[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 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 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 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 pub fn game_time(&self) -> i64 {
220 self.level_data.read().game_time()
221 }
222
223 pub(crate) fn clock_total_ticks(&self, clock: WorldClockRef) -> Option<i64> {
225 self.level_data.read().world_clocks().total_ticks(clock)
226 }
227
228 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 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 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}