Skip to main content

steel_core/block_entity/
mod.rs

1//! Block entity system for blocks that need additional data storage.
2//!
3//! Block entities provide additional data storage and functionality for blocks
4//! that need more than what block state properties can offer (e.g., chests,
5//! furnaces, signs, etc.).
6//!
7//! # Architecture
8//!
9//! Similar to the block/item behavior system, block entities use a registry
10//! pattern:
11//! - `BlockEntityRegistry` - maps `BlockEntityType` to factory functions
12//! - `BlockEntityStorage` - stores block entities in a chunk
13//!
14//! # Usage
15//!
16//! ```ignore
17//! use steel_core::block_entity::{init_block_entities, BLOCK_ENTITIES};
18//!
19//! // After registry is frozen, call once at startup:
20//! init_block_entities();
21//!
22//! // Create a block entity:
23//! let entity = BLOCK_ENTITIES.create(block_entity_type, pos, state);
24//! ```
25
26pub(crate) mod block_state_nbt;
27pub mod entities;
28mod registry;
29mod storage;
30
31use std::{
32    ptr,
33    sync::{
34        Arc, Weak,
35        atomic::{AtomicBool, Ordering},
36    },
37};
38
39use simdnbt::borrow::BaseNbtCompound as BorrowedNbtCompound;
40use simdnbt::owned::NbtCompound;
41use smallvec::SmallVec;
42use steel_registry::block_entity_type::BlockEntityTypeRef;
43use steel_registry::blocks::block_state_ext::BlockStateExt as _;
44use steel_utils::{BlockPos, BlockStateId, ErasedType, locks::SyncMutex};
45
46pub use registry::{BLOCK_ENTITIES, BlockEntityFactory, BlockEntityRegistry, init_block_entities};
47pub(crate) use storage::{
48    BlockEntityInsert, BlockEntityLookup, BlockEntityStorage, ClearedBlockEntities,
49    DetachedBlockEntity, LifecycleDispatchers,
50};
51
52use crate::inventory::lock::ContainerRef;
53use crate::player::Player;
54
55use crate::world::World;
56use crate::world::game_event::SharedGameEventListener;
57
58/// Erased block-state-selected ticker for one concrete block-entity type.
59///
60/// Vanilla obtains this callback from the owning block behavior. Keeping the
61/// expected type with the callback preserves that selection boundary while
62/// allowing Steel's world ticker to store heterogeneous entries.
63#[derive(Clone, Copy)]
64pub struct BlockEntityTicker {
65    block_entity_type: BlockEntityTypeRef,
66    tick: fn(&Arc<World>, BlockPos, BlockStateId, &dyn BlockEntity),
67}
68
69impl BlockEntityTicker {
70    /// Creates a ticker for one block-entity type.
71    #[must_use]
72    pub const fn new(
73        block_entity_type: BlockEntityTypeRef,
74        tick: fn(&Arc<World>, BlockPos, BlockStateId, &dyn BlockEntity),
75    ) -> Self {
76        Self {
77            block_entity_type,
78            tick,
79        }
80    }
81
82    /// Creates a state-selected ticker that dispatches to [`BlockEntity::tick`].
83    #[must_use]
84    pub const fn for_entity_tick(block_entity_type: BlockEntityTypeRef) -> Self {
85        Self::new(block_entity_type, Self::tick_entity)
86    }
87
88    /// Creates the default entity callback only when Vanilla's requested type matches.
89    #[must_use]
90    pub fn for_matching_entity_tick(
91        actual: BlockEntityTypeRef,
92        expected: BlockEntityTypeRef,
93    ) -> Option<Self> {
94        ptr::eq(actual, expected).then(|| Self::for_entity_tick(expected))
95    }
96
97    /// Returns whether this ticker accepts the concrete block-entity type.
98    #[must_use]
99    pub fn accepts(self, block_entity_type: BlockEntityTypeRef) -> bool {
100        ptr::eq(self.block_entity_type, block_entity_type)
101    }
102
103    pub(crate) fn tick(
104        self,
105        world: &Arc<World>,
106        pos: BlockPos,
107        state: BlockStateId,
108        block_entity: &dyn BlockEntity,
109    ) {
110        (self.tick)(world, pos, state, block_entity);
111    }
112
113    fn tick_entity(
114        world: &Arc<World>,
115        _pos: BlockPos,
116        _state: BlockStateId,
117        block_entity: &dyn BlockEntity,
118    ) {
119        block_entity.tick(world);
120    }
121}
122
123struct BlockEntityLifecycle {
124    block_state: BlockStateId,
125    events: SmallVec<[BlockEntityLifecycleEvent; 2]>,
126    dispatching_events: bool,
127}
128
129#[derive(Clone, Copy)]
130enum BlockEntityLifecycleEvent {
131    SetRemoved,
132    ClearRemoved,
133    BlockStateChanged(BlockStateId),
134}
135
136/// Immutable block-entity identity and its short-lived lifecycle state.
137///
138/// Concrete block entities keep gameplay data behind their own focused locks.
139/// The lifecycle lock is never held while invoking world callbacks.
140pub struct BlockEntityBase {
141    block_entity_type: BlockEntityTypeRef,
142    level: Weak<World>,
143    pos: BlockPos,
144    /// Lock-free removal snapshot; lifecycle writers remain serialized below.
145    removed: AtomicBool,
146    lifecycle: SyncMutex<BlockEntityLifecycle>,
147}
148
149struct BlockEntityLifecycleDispatchGuard<'a> {
150    base: &'a BlockEntityBase,
151    armed: bool,
152}
153
154impl Drop for BlockEntityLifecycleDispatchGuard<'_> {
155    fn drop(&mut self) {
156        if !self.armed {
157            return;
158        }
159        let mut lifecycle = self.base.lifecycle.lock();
160        lifecycle.events.clear();
161        lifecycle.dispatching_events = false;
162    }
163}
164
165impl BlockEntityBase {
166    /// Creates common metadata for one block entity.
167    ///
168    /// # Panics
169    ///
170    /// Panics if `block_state` is not accepted by `block_entity_type`.
171    #[must_use]
172    pub fn new(
173        block_entity_type: BlockEntityTypeRef,
174        level: Weak<World>,
175        pos: BlockPos,
176        block_state: BlockStateId,
177    ) -> Self {
178        assert!(
179            block_entity_type.is_valid(block_state.get_block()),
180            "invalid block entity {} state {} at {pos:?}",
181            block_entity_type.key,
182            block_state.get_block().key,
183        );
184        Self {
185            block_entity_type,
186            level,
187            pos,
188            removed: AtomicBool::new(false),
189            lifecycle: SyncMutex::new(BlockEntityLifecycle {
190                block_state,
191                events: SmallVec::new(),
192                dispatching_events: false,
193            }),
194        }
195    }
196
197    #[must_use]
198    const fn block_entity_type(&self) -> BlockEntityTypeRef {
199        self.block_entity_type
200    }
201
202    #[must_use]
203    const fn pos(&self) -> BlockPos {
204        self.pos
205    }
206
207    #[must_use]
208    fn block_state(&self) -> BlockStateId {
209        self.lifecycle.lock().block_state
210    }
211
212    fn queue_block_state_change(&self, state: BlockStateId) -> bool {
213        assert!(
214            self.block_entity_type.is_valid(state.get_block()),
215            "invalid block entity {} state {} at {:?}",
216            self.block_entity_type.key,
217            state.get_block().key,
218            self.pos,
219        );
220        let mut lifecycle = self.lifecycle.lock();
221        if lifecycle.block_state == state {
222            return false;
223        }
224        lifecycle.block_state = state;
225        lifecycle
226            .events
227            .push(BlockEntityLifecycleEvent::BlockStateChanged(state));
228        if lifecycle.dispatching_events {
229            false
230        } else {
231            lifecycle.dispatching_events = true;
232            true
233        }
234    }
235
236    #[must_use]
237    fn is_removed(&self) -> bool {
238        self.removed.load(Ordering::Relaxed)
239    }
240
241    fn queue_set_removed(&self) -> bool {
242        let mut lifecycle = self.lifecycle.lock();
243        self.removed.store(true, Ordering::Relaxed);
244        lifecycle.events.push(BlockEntityLifecycleEvent::SetRemoved);
245        if lifecycle.dispatching_events {
246            false
247        } else {
248            lifecycle.dispatching_events = true;
249            true
250        }
251    }
252
253    fn queue_clear_removed(&self) -> bool {
254        let mut lifecycle = self.lifecycle.lock();
255        if !self.removed.load(Ordering::Relaxed) {
256            return false;
257        }
258        self.removed.store(false, Ordering::Relaxed);
259        lifecycle
260            .events
261            .push(BlockEntityLifecycleEvent::ClearRemoved);
262        if lifecycle.dispatching_events {
263            false
264        } else {
265            lifecycle.dispatching_events = true;
266            true
267        }
268    }
269
270    #[must_use]
271    fn level(&self) -> Option<Arc<World>> {
272        self.level.upgrade()
273    }
274
275    pub(crate) fn set_changed(&self) {
276        let Some(world) = self.level() else {
277            return;
278        };
279        let state = self.block_state();
280        world.block_entity_changed(self.pos);
281        if !state.is_air() {
282            world.update_neighbor_for_output_signal(self.pos, state.get_block());
283        }
284    }
285
286    pub(crate) fn is_valid_container_for(&self, player: &Player) -> bool {
287        if self.is_removed() {
288            return false;
289        }
290        let Some(world) = self.level() else {
291            return false;
292        };
293        let Some(current) = world.get_block_entity(self.pos) else {
294            return false;
295        };
296        ptr::eq(current.base(), self)
297            && player.is_within_block_interaction_range_with_buffer(self.pos, 4.0)
298    }
299}
300
301/// Trait for all block entities.
302///
303/// Block entities are attached to specific blocks in the world and provide
304/// additional data storage beyond what block states can hold. Concrete
305/// implementations must claim a unique [`steel_utils::DowncastTypeKey`] through
306/// [`steel_utils::DowncastType`].
307pub trait BlockEntity: ErasedType + Send + Sync {
308    /// Returns the common metadata owned by this block entity.
309    fn base(&self) -> &BlockEntityBase;
310
311    /// Returns the type of this block entity.
312    fn get_type(&self) -> BlockEntityTypeRef {
313        self.base().block_entity_type()
314    }
315
316    /// Returns the position of this block entity in the world.
317    fn get_block_pos(&self) -> BlockPos {
318        self.base().pos()
319    }
320
321    /// Returns the current block state associated with this entity.
322    fn get_block_state(&self) -> BlockStateId {
323        self.base().block_state()
324    }
325
326    /// Returns whether this entity's registered type accepts `state`.
327    ///
328    /// Mirrors Vanilla `BlockEntity.isValidBlockState`.
329    fn is_valid_block_state(&self, state: BlockStateId) -> bool {
330        self.get_type().is_valid(state.get_block())
331    }
332
333    /// Called after the cached block state changes.
334    ///
335    /// Storage and section locks are not held during this callback. This is Steel's staged
336    /// equivalent of Vanilla block entities overriding `setBlockState`; implementations should
337    /// derive any cached fields from `state` here.
338    fn on_block_state_changed(&self, _state: BlockStateId) {}
339
340    /// Called after each invocation that marks this entity removed.
341    ///
342    /// Storage locks are not held during this callback. This mirrors Vanilla overrides of
343    /// `setRemoved`, which run even if the entity was already marked removed.
344    fn on_set_removed(&self) {}
345
346    /// Called after this entity transitions from removed back to active.
347    ///
348    /// Storage locks are not held during this callback.
349    fn on_clear_removed(&self) {}
350
351    /// Called when the block entity's data changes.
352    ///
353    /// Marks the containing chunk as dirty so changes are persisted to disk.
354    fn set_changed(&self) {
355        self.base().set_changed();
356    }
357
358    /// Gets the world reference if still valid.
359    ///
360    /// Block entities receive a `Weak<World>` at construction time.
361    fn get_level(&self) -> Option<Arc<World>> {
362        self.base().level()
363    }
364
365    /// Handles a block event delegated by the owning block behavior.
366    ///
367    /// Mirrors Vanilla `BlockEntity.triggerEvent`.
368    fn trigger_event(&self, _param_a: i32, _param_b: i32) -> bool {
369        false
370    }
371
372    /// Called before the block entity is removed to handle side effects.
373    ///
374    /// For example, containers should drop their contents here.
375    ///
376    /// # Arguments
377    /// * `pos` - The position of the block entity
378    /// * `state` - The block state being removed
379    #[expect(
380        unused_variables,
381        reason = "default trait impl; parameters used by overrides"
382    )]
383    fn pre_remove_side_effects(&self, pos: BlockPos, state: BlockStateId) {
384        // Default: no side effects
385    }
386
387    /// Loads additional data from NBT.
388    ///
389    /// Called when loading the block entity from disk or receiving initial
390    /// chunk data from the server.
391    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>);
392
393    /// Saves additional data to NBT.
394    ///
395    /// Called when saving the block entity to disk.
396    fn save_additional(&self, nbt: &mut NbtCompound);
397
398    /// Saves only entity-specific data, excluding vanilla type and position metadata.
399    fn save_custom_only(&self) -> NbtCompound {
400        let mut nbt = NbtCompound::new();
401        self.save_additional(&mut nbt);
402        for key in ["id", "x", "y", "z"] {
403            while nbt.remove(key).is_some() {}
404        }
405        nbt
406    }
407
408    /// Saves command-visible data together with vanilla block-entity metadata.
409    fn save_with_full_metadata(&self) -> NbtCompound {
410        // TODO: Include stored block-entity components once Steel has Vanilla's
411        // block-entity component foundation. NBT predicates targeting the
412        // `components` field cannot match exactly until then.
413        let mut nbt = self.save_custom_only();
414        let pos = self.get_block_pos();
415        nbt.insert("id", self.get_type().key.to_string());
416        nbt.insert("x", pos.x());
417        nbt.insert("y", pos.y());
418        nbt.insert("z", pos.z());
419        nbt
420    }
421
422    /// Returns the NBT data to send to clients for initial sync.
423    ///
424    /// This is included in the chunk data packet when the chunk is first sent.
425    /// Return `None` if no client sync is needed.
426    fn get_update_tag(&self) -> Option<NbtCompound> {
427        None
428    }
429
430    /// Called every game tick for ticking block entities.
431    ///
432    /// The live block behavior selects this callback through its block-entity
433    /// ticker, matching Vanilla's state-owned ticker selection.
434    #[expect(
435        unused_variables,
436        reason = "default trait impl; parameter used by overrides"
437    )]
438    fn tick(&self, world: &Arc<World>) {}
439
440    /// Returns the independently lockable container capability owned by this entity.
441    fn container_ref(&self) -> Option<ContainerRef> {
442        None
443    }
444
445    /// Returns this entity's fixed game-event listener, if it provides one.
446    ///
447    /// Mirrors Vanilla `GameEventListener.Provider.getListener`. The owning block behavior keeps
448    /// final selection authority through `BlockBehavior::get_game_event_listener`.
449    fn game_event_listener(&self) -> Option<SharedGameEventListener> {
450        None
451    }
452}
453
454/// Final block-entity common-state operations.
455///
456/// This blanket implementation prevents concrete entities from replacing metadata transitions.
457/// Custom behavior belongs in the corresponding `BlockEntity::on_*` hook, which runs after the
458/// update without storage or section locks.
459pub trait BlockEntityLifecycleExt: BlockEntity {
460    /// Returns whether this block entity has been marked for removal.
461    fn is_removed(&self) -> bool {
462        self.base().is_removed()
463    }
464
465    /// Updates the cached block state and orders its callback.
466    fn set_block_state(&self, state: BlockStateId) {
467        if self.base().queue_block_state_change(state) {
468            self.dispatch_lifecycle_events();
469        }
470    }
471
472    /// Marks this block entity as removed and orders its lifecycle callback.
473    fn set_removed(&self) {
474        if self.base().queue_set_removed() {
475            self.dispatch_lifecycle_events();
476        }
477    }
478
479    /// Reactivates this block entity and orders its lifecycle callback when the flag changed.
480    fn clear_removed(&self) {
481        if self.base().queue_clear_removed() {
482            self.dispatch_lifecycle_events();
483        }
484    }
485
486    /// Drains lifecycle callbacks in flag-update order without retaining the lifecycle lock.
487    ///
488    /// A callback may re-enter the entity. Its event is appended and drained by the active
489    /// dispatcher rather than recursively invoking another callback.
490    fn dispatch_lifecycle_events(&self) {
491        let mut guard = BlockEntityLifecycleDispatchGuard {
492            base: self.base(),
493            armed: true,
494        };
495        loop {
496            let event = {
497                let mut lifecycle = self.base().lifecycle.lock();
498                if lifecycle.events.is_empty() {
499                    lifecycle.dispatching_events = false;
500                    guard.armed = false;
501                    return;
502                }
503                lifecycle.events.remove(0)
504            };
505            match event {
506                BlockEntityLifecycleEvent::SetRemoved => self.on_set_removed(),
507                BlockEntityLifecycleEvent::ClearRemoved => self.on_clear_removed(),
508                BlockEntityLifecycleEvent::BlockStateChanged(state) => {
509                    self.on_block_state_changed(state);
510                }
511            }
512        }
513    }
514}
515
516impl<T: BlockEntity + ?Sized> BlockEntityLifecycleExt for T {}
517
518/// A stable shared block entity without a whole-object mutex.
519pub type SharedBlockEntity = Arc<dyn BlockEntity>;