Skip to main content

steel_core/world/tick_scheduler/
mod.rs

1//! Scheduled tick storage and selection for deterministic block and fluid updates.
2//!
3//! Scheduled ticks are stored in per-chunk priority queues against absolute world
4//! game time. Within a chunk, the queue follows vanilla's
5//! `ScheduledTick.DRAIN_ORDER`: trigger time, priority, then sub-tick order. Across
6//! chunks, only each ready queue head participates in selection, following
7//! vanilla's `LevelTicks` container-draining behavior.
8//!
9//! Saved and proto-chunk ticks remain pending until the chunk first reaches
10//! confirmed block-ticking readiness. That transition anchors their saved delays
11//! to the current game time, matching `LevelChunkTicks.unpack`. Later readiness
12//! demotions do not pause or re-anchor those deadlines.
13//!
14//! ## Exact cross-chunk ties
15//!
16//! Each loaded chunk reconstructs saved ticks with its own negative sub-tick
17//! order range, so two ready chunk heads can have the same priority and
18//! sub-tick order. A `WorldGenRegion` also owns an independent counter, as in
19//! Vanilla, and can retain that order when it schedules directly into an
20//! already-Full dependency chunk. Vanilla's final order for these exact ties
21//! follows iteration of fastutil's `Long2LongOpenHashMap` and then Java's
22//! `PriorityQueue` heap behavior. Minecraft supplies no custom hash strategy
23//! for that map. As an intentional performance tradeoff, Steel keeps the
24//! optimized `scc` chunk traversal as the final tie order instead of reproducing
25//! implementation-specific Java collection state. Ordinary live-world ticks
26//! still use a world-global sub-tick counter.
27//!
28//! ## Exact intra-chunk ties
29//!
30//! Vanilla's `LevelChunkTicks` comparator leaves ticks with identical trigger
31//! time, priority, and sub-tick order equal, so their final order depends on
32//! Java collection and priority-queue history. Steel intentionally drains those
33//! otherwise indistinguishable ticks in insertion order instead of reproducing
34//! that implementation-specific state.
35
36use std::{
37    cmp::Ordering,
38    collections::{BTreeSet, BinaryHeap},
39    ptr,
40    sync::{
41        Arc, OnceLock,
42        atomic::{AtomicI64, AtomicUsize, Ordering as AtomicOrdering},
43    },
44};
45
46use rustc_hash::{FxHashMap, FxHashSet};
47use steel_registry::blocks::BlockRef;
48use steel_registry::fluid::FluidRef;
49use steel_utils::{
50    BlockPos, ChunkPos, PackedChunkPos,
51    locks::{SyncMutex, SyncRwLock},
52};
53
54use crate::chunk::full_chunk::FullChunkRef;
55
56mod world;
57
58/// Priority levels for scheduled ticks. Lower discriminant = higher priority.
59///
60/// Matches vanilla's `TickPriority` enum. `Ord` is derived so that
61/// `ExtremelyHigh < Normal < ExtremelyLow`.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
63#[repr(i8)]
64pub enum TickPriority {
65    /// Highest priority (-3). Fires before all others.
66    ExtremelyHigh = -3,
67    /// Very high priority (-2).
68    VeryHigh = -2,
69    /// High priority (-1).
70    High = -1,
71    /// Default priority (0).
72    Normal = 0,
73    /// Low priority (1).
74    Low = 1,
75    /// Very low priority (2).
76    VeryLow = 2,
77    /// Lowest priority (3). Fires after all others.
78    ExtremelyLow = 3,
79}
80
81impl TickPriority {
82    /// Resolves Vanilla's serialized priority value, clamping invalid values to an extreme.
83    #[must_use]
84    pub const fn by_value(value: i32) -> Self {
85        match value {
86            -3 => Self::ExtremelyHigh,
87            -2 => Self::VeryHigh,
88            -1 => Self::High,
89            0 => Self::Normal,
90            1 => Self::Low,
91            2 => Self::VeryLow,
92            3 => Self::ExtremelyLow,
93            value if value < Self::ExtremelyHigh as i32 => Self::ExtremelyHigh,
94            _ => Self::ExtremelyLow,
95        }
96    }
97}
98
99/// Trait for types that can be used as the tick target in `ScheduledTick`.
100///
101/// Provides a `usize` key for deduplication (one tick per `(BlockPos, key)` pair).
102pub trait TickKey: Copy {
103    /// Returns a key suitable for dedup hashing.
104    fn key(self) -> usize;
105}
106
107impl TickKey for BlockRef {
108    #[inline]
109    fn key(self) -> usize {
110        ptr::from_ref(self) as usize
111    }
112}
113
114impl TickKey for FluidRef {
115    #[inline]
116    fn key(self) -> usize {
117        ptr::from_ref(self) as usize
118    }
119}
120
121/// A single scheduled tick targeting a block or fluid at a specific position.
122#[derive(Debug, Clone, Copy)]
123pub struct ScheduledTick<T: TickKey> {
124    /// The block or fluid type this tick targets.
125    pub tick_type: T,
126    /// The block position to tick.
127    pub pos: BlockPos,
128    /// Absolute world game-time deadline.
129    pub trigger_tick: i64,
130    /// Execution priority (lower = fires first within the same active tick).
131    pub priority: TickPriority,
132    /// Monotonic counter for stable ordering within the same priority.
133    /// Loaded ticks use negative values and therefore precede newly scheduled ticks.
134    pub sub_tick_order: i64,
135}
136
137/// A scheduled tick in the chunk persistence representation.
138///
139/// Like vanilla's `SavedTick`, this stores relative delay but not sub-tick order.
140/// Loaded ticks receive negative sub-tick orders in their saved list order.
141#[derive(Debug, Clone, Copy)]
142pub(crate) struct SavedTick<T: TickKey> {
143    /// The block or fluid type this tick targets.
144    pub(crate) tick_type: T,
145    /// The block position to tick.
146    pub(crate) pos: BlockPos,
147    /// Delay relative to the game time at which the chunk was saved.
148    pub(crate) delay: i32,
149    /// Execution priority.
150    pub(crate) priority: TickPriority,
151}
152
153/// A scheduled tick targeting a block.
154pub type BlockTick = ScheduledTick<BlockRef>;
155/// A scheduled tick targeting a fluid.
156pub type FluidTick = ScheduledTick<FluidRef>;
157/// Deduplication key used by scheduled tick containers and execution snapshots.
158pub type ScheduledTickKey = (BlockPos, usize);
159/// Per-chunk storage for scheduled block ticks.
160pub type BlockTickList = TickList<BlockRef>;
161/// Per-chunk storage for scheduled fluid ticks.
162pub type FluidTickList = TickList<FluidRef>;
163
164/// Block and fluid scheduled-tick queues belonging to one chunk.
165#[derive(Debug, Default)]
166pub(crate) struct ChunkTickLists {
167    block: BlockTickList,
168    fluid: FluidTickList,
169}
170
171impl ChunkTickLists {
172    #[must_use]
173    pub(crate) const fn new(block: BlockTickList, fluid: FluidTickList) -> Self {
174        Self { block, fluid }
175    }
176
177    pub(crate) const fn block(&self) -> &BlockTickList {
178        &self.block
179    }
180
181    pub(crate) const fn block_mut(&mut self) -> &mut BlockTickList {
182        &mut self.block
183    }
184
185    pub(crate) const fn fluid(&self) -> &FluidTickList {
186        &self.fluid
187    }
188
189    pub(crate) const fn fluid_mut(&mut self) -> &mut FluidTickList {
190        &mut self.fluid
191    }
192
193    fn packing_snapshot(&self) -> ChunkTickPackingSnapshot {
194        ChunkTickPackingSnapshot {
195            block: self.block.packing_snapshot(),
196            fluid: self.fluid.packing_snapshot(),
197        }
198    }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202enum ChunkTickContainerLifecycle {
203    Proto,
204    PrePublication,
205    Registered,
206    Finalized,
207}
208
209#[derive(Debug)]
210struct ChunkTickContainerState {
211    lifecycle: ChunkTickContainerLifecycle,
212    lists: ChunkTickLists,
213}
214
215/// Stable scheduled-tick storage owned by one chunk from creation through unload.
216///
217/// The world scheduler retains a shared handle only while the chunk is registered and indexes
218/// its current heads. Persistence reads this container directly, so packing one chunk never
219/// retains the world scheduler metadata lock.
220#[derive(Debug)]
221pub(crate) struct ChunkTickContainer {
222    state: SyncMutex<ChunkTickContainerState>,
223}
224
225impl ChunkTickContainer {
226    #[must_use]
227    pub(crate) const fn new(lists: ChunkTickLists) -> Self {
228        Self {
229            state: SyncMutex::new(ChunkTickContainerState {
230                lifecycle: ChunkTickContainerLifecycle::PrePublication,
231                lists,
232            }),
233        }
234    }
235
236    #[must_use]
237    pub(crate) const fn new_proto(lists: ChunkTickLists) -> Self {
238        Self {
239            state: SyncMutex::new(ChunkTickContainerState {
240                lifecycle: ChunkTickContainerLifecycle::Proto,
241                lists,
242            }),
243        }
244    }
245
246    /// Atomically closes proto-only mutations and opens unpublished Full scheduling.
247    pub(crate) fn promote_to_full(&self) -> bool {
248        let mut state = self.state.lock();
249        if state.lifecycle != ChunkTickContainerLifecycle::Proto {
250            return false;
251        }
252        state.lifecycle = ChunkTickContainerLifecycle::PrePublication;
253        true
254    }
255
256    pub(crate) fn schedule_unregistered_block(
257        &self,
258        block: BlockRef,
259        pos: BlockPos,
260        trigger_tick: i64,
261        priority: TickPriority,
262        sub_tick_order: i64,
263    ) -> Option<bool> {
264        let mut state = self.state.lock();
265        (state.lifecycle == ChunkTickContainerLifecycle::PrePublication).then(|| {
266            state
267                .lists
268                .block_mut()
269                .schedule(block, pos, trigger_tick, priority, sub_tick_order)
270        })
271    }
272
273    pub(crate) fn schedule_unregistered_fluid(
274        &self,
275        fluid: FluidRef,
276        pos: BlockPos,
277        trigger_tick: i64,
278        priority: TickPriority,
279        sub_tick_order: i64,
280    ) -> Option<bool> {
281        let mut state = self.state.lock();
282        (state.lifecycle == ChunkTickContainerLifecycle::PrePublication).then(|| {
283            state
284                .lists
285                .fluid_mut()
286                .schedule(fluid, pos, trigger_tick, priority, sub_tick_order)
287        })
288    }
289
290    pub(crate) fn schedule_pending_block(
291        &self,
292        block: BlockRef,
293        pos: BlockPos,
294        priority: TickPriority,
295    ) -> Option<bool> {
296        let mut state = self.state.lock();
297        (state.lifecycle == ChunkTickContainerLifecycle::Proto).then(|| {
298            state
299                .lists
300                .block_mut()
301                .schedule_pending(block, pos, priority)
302        })
303    }
304
305    pub(crate) fn schedule_pending_fluid(
306        &self,
307        fluid: FluidRef,
308        pos: BlockPos,
309        priority: TickPriority,
310    ) -> Option<bool> {
311        let mut state = self.state.lock();
312        (state.lifecycle == ChunkTickContainerLifecycle::Proto).then(|| {
313            state
314                .lists
315                .fluid_mut()
316                .schedule_pending(fluid, pos, priority)
317        })
318    }
319
320    pub(crate) fn pending_block_snapshot(&self) -> Option<Vec<SavedTick<BlockRef>>> {
321        let state = self.state.lock();
322        (state.lifecycle == ChunkTickContainerLifecycle::Proto)
323            .then(|| state.lists.block().pending_entries().to_vec())
324    }
325
326    #[cfg(test)]
327    pub(crate) fn pending_fluid_snapshot(&self) -> Option<Vec<SavedTick<FluidRef>>> {
328        let state = self.state.lock();
329        (state.lifecycle == ChunkTickContainerLifecycle::Proto)
330            .then(|| state.lists.fluid().pending_entries().to_vec())
331    }
332
333    pub(crate) fn remove_pending_blocks_matching(
334        &self,
335        predicate: impl FnMut(&SavedTick<BlockRef>) -> bool,
336    ) -> Option<usize> {
337        let mut state = self.state.lock();
338        (state.lifecycle == ChunkTickContainerLifecycle::Proto)
339            .then(|| state.lists.block_mut().remove_pending_matching(predicate))
340    }
341
342    pub(crate) fn has_block(&self, pos: BlockPos, block: BlockRef) -> Option<bool> {
343        let state = self.state.lock();
344        (state.lifecycle != ChunkTickContainerLifecycle::Finalized)
345            .then(|| state.lists.block().has_tick(pos, block))
346    }
347
348    pub(crate) fn has_fluid(&self, pos: BlockPos, fluid: FluidRef) -> Option<bool> {
349        let state = self.state.lock();
350        (state.lifecycle != ChunkTickContainerLifecycle::Finalized)
351            .then(|| state.lists.fluid().has_tick(pos, fluid))
352    }
353
354    pub(crate) fn snapshot(&self, current_tick: i64) -> Option<ScheduledTickSnapshot> {
355        let packing = {
356            let state = self.state.lock();
357            (state.lifecycle != ChunkTickContainerLifecycle::Finalized)
358                .then(|| state.lists.packing_snapshot())
359        }?;
360        Some(packing.pack(current_tick))
361    }
362}
363
364struct ChunkTickPackingSnapshot {
365    block: TickListPackingSnapshot<BlockRef>,
366    fluid: TickListPackingSnapshot<FluidRef>,
367}
368
369impl ChunkTickPackingSnapshot {
370    fn pack(self, current_tick: i64) -> ScheduledTickSnapshot {
371        ScheduledTickSnapshot {
372            block: self.block.pack(current_tick),
373            fluid: self.fluid.pack(current_tick),
374        }
375    }
376}
377
378/// Owned persistence snapshot of both scheduled-tick queues for a Full chunk.
379pub(crate) struct ScheduledTickSnapshot {
380    pub(crate) block: Vec<SavedTick<BlockRef>>,
381    pub(crate) fluid: Vec<SavedTick<FluidRef>>,
382}
383
384/// A violated Full-chunk scheduled-tick ownership invariant.
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub(crate) enum TickSchedulerError {
387    /// A second Full chunk attempted to register the same position.
388    AlreadyRegistered(ChunkPos),
389    /// The required chunk-owned container is unavailable or already finalized.
390    MissingContainer(ChunkPos),
391    /// The world index and chunk refer to different containers at the same position.
392    ContainerMismatch(ChunkPos),
393}
394
395/// Ready ticks and the active containers whose persisted delays changed.
396pub(crate) struct ScheduledTickBatch<T: TickKey> {
397    pub(crate) ticks: Vec<ScheduledTick<T>>,
398    pub(crate) changed_containers: Vec<usize>,
399}
400
401/// World index for registered Full-chunk scheduled block and fluid containers.
402///
403/// Chunks own the queues used for persistence. The metadata mutex retains shared handles, current
404/// heads, and active deadline sets; packing never acquires it. The phase lock gives collection a
405/// single world-wide cutoff without retaining any scheduler lock during callbacks.
406mod list;
407mod run_batch;
408mod scheduler;
409
410use list::{TickList, TickListPackingSnapshot, intra_tick_drain_order};
411pub(crate) use run_batch::*;
412pub(crate) use scheduler::*;
413
414#[cfg(test)]
415mod tests;