Skip to main content

steel_core/world/
redstone.rs

1//! Per-world runtime state used by vanilla redstone components.
2
3use std::collections::VecDeque;
4
5use steel_utils::BlockPos;
6
7use super::World;
8
9const RECENT_TOGGLE_TIMER: i64 = 60;
10const MAX_RECENT_TOGGLES: usize = 8;
11
12#[derive(Debug, Clone, Copy)]
13struct RedstoneTorchToggle {
14    pos: BlockPos,
15    when: i64,
16}
17
18/// Vanilla's per-level `RedstoneTorchBlock.RECENT_TOGGLES` list.
19#[derive(Debug, Default)]
20pub(super) struct RedstoneTorchToggleTracker {
21    toggles: VecDeque<RedstoneTorchToggle>,
22}
23
24impl RedstoneTorchToggleTracker {
25    fn prune(&mut self, game_time: i64) {
26        while self
27            .toggles
28            .front()
29            .is_some_and(|toggle| game_time.wrapping_sub(toggle.when) > RECENT_TOGGLE_TIMER)
30        {
31            self.toggles.pop_front();
32        }
33    }
34
35    fn is_toggled_too_frequently(&mut self, pos: BlockPos, game_time: i64, add: bool) -> bool {
36        if add {
37            self.toggles.push_back(RedstoneTorchToggle {
38                pos,
39                when: game_time,
40            });
41        }
42
43        self.toggles
44            .iter()
45            .filter(|toggle| toggle.pos == pos)
46            .take(MAX_RECENT_TOGGLES)
47            .count()
48            >= MAX_RECENT_TOGGLES
49    }
50}
51
52impl World {
53    /// Removes torch toggles older than vanilla's 60-game-tick window.
54    pub(crate) fn prune_recent_redstone_torch_toggles(&self) {
55        let game_time = self.game_time();
56        self.redstone_torch_toggles.lock().prune(game_time);
57    }
58
59    /// Counts recent toggles for `pos`, optionally recording one first.
60    ///
61    /// This bookkeeping and scheduled tick deadlines both use world game time,
62    /// matching Vanilla's loaded-world timing model.
63    pub(crate) fn redstone_torch_toggled_too_frequently(&self, pos: BlockPos, add: bool) -> bool {
64        let game_time = self.game_time();
65        self.redstone_torch_toggles
66            .lock()
67            .is_toggled_too_frequently(pos, game_time, add)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn burnout_requires_eight_toggles_at_the_same_position() {
77        let mut tracker = RedstoneTorchToggleTracker::default();
78        let pos = BlockPos::new(4, 70, -2);
79        for _ in 0..7 {
80            assert!(!tracker.is_toggled_too_frequently(pos, 100, true));
81        }
82        assert!(tracker.is_toggled_too_frequently(pos, 100, true));
83        assert!(!tracker.is_toggled_too_frequently(pos.east(), 100, true));
84    }
85
86    #[test]
87    fn toggle_window_expires_only_after_sixty_game_ticks() {
88        let mut tracker = RedstoneTorchToggleTracker::default();
89        let pos = BlockPos::new(0, 64, 0);
90        for _ in 0..8 {
91            tracker.is_toggled_too_frequently(pos, 10, true);
92        }
93
94        tracker.prune(70);
95        assert!(tracker.is_toggled_too_frequently(pos, 70, false));
96        tracker.prune(71);
97        assert!(!tracker.is_toggled_too_frequently(pos, 71, false));
98    }
99}