1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use steel_registry::{REGISTRY, world_clock::WorldClockRef};
4use steel_utils::Identifier;
5use thiserror::Error;
6
7const GAME_TIME_SYNC_INTERVAL_TICKS: i64 = 20;
9
10#[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#[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#[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 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 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 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 pub fn game_time(&self) -> i64 {
223 self.game_time.ticks()
224 }
225
226 pub(crate) fn clock_total_ticks(&self, clock: WorldClockRef) -> Option<i64> {
228 self.level_data.read().world_clocks().total_ticks(clock)
229 }
230
231 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 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 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}