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;
27mod components;
28pub mod entities;
29mod registry;
30mod storage;
31
32use simdnbt::FromNbtTag;
33use simdnbt::borrow::{
34    BaseNbtCompound as BorrowedNbtCompound, NbtCompound as BorrowedNbtCompoundView,
35    read_compound as read_borrowed_compound,
36};
37use simdnbt::owned::NbtCompound;
38use smallvec::SmallVec;
39use std::{
40    io::Cursor,
41    ptr,
42    sync::{
43        Arc, Weak,
44        atomic::{AtomicBool, Ordering},
45    },
46};
47
48use steel_registry::blocks::block_state_ext::BlockStateExt as _;
49use steel_registry::{
50    block_entity_type::BlockEntityTypeRef, data_components::DataComponentMap, item_stack::ItemStack,
51};
52use steel_utils::{
53    BlockPos, BlockStateId, ErasedType,
54    locks::{SyncMutex, SyncRwLock},
55};
56
57pub use components::ImplicitComponentGetter;
58pub use registry::{BLOCK_ENTITIES, BlockEntityFactory, BlockEntityRegistry, init_block_entities};
59pub(crate) use storage::{
60    BlockEntityInsert, BlockEntityLookup, BlockEntityStorage, ClearedBlockEntities,
61    DetachedBlockEntity, LifecycleDispatchers,
62};
63
64use crate::block_entity::entities::Spawner;
65use crate::inventory::container::DEFAULT_DISTANCE_BUFFER;
66use crate::inventory::lock::ContainerRef;
67use crate::player::Player;
68
69use crate::world::World;
70use crate::world::game_event::SharedGameEventListener;
71
72/// Erased block-state-selected ticker for one concrete block-entity type.
73///
74/// Vanilla obtains this callback from the owning block behavior. Keeping the
75/// expected type with the callback preserves that selection boundary while
76/// allowing Steel's world ticker to store heterogeneous entries.
77#[derive(Clone, Copy)]
78pub struct BlockEntityTicker {
79    block_entity_type: BlockEntityTypeRef,
80    tick: fn(&Arc<World>, BlockPos, BlockStateId, &dyn BlockEntity),
81}
82
83impl BlockEntityTicker {
84    /// Creates a ticker for one block-entity type.
85    #[must_use]
86    pub const fn new(
87        block_entity_type: BlockEntityTypeRef,
88        tick: fn(&Arc<World>, BlockPos, BlockStateId, &dyn BlockEntity),
89    ) -> Self {
90        Self {
91            block_entity_type,
92            tick,
93        }
94    }
95
96    /// Creates a state-selected ticker that dispatches to [`BlockEntity::tick`].
97    #[must_use]
98    pub const fn for_entity_tick(block_entity_type: BlockEntityTypeRef) -> Self {
99        Self::new(block_entity_type, Self::tick_entity)
100    }
101
102    /// Creates the default entity callback only when Vanilla's requested type matches.
103    #[must_use]
104    pub fn for_matching_entity_tick(
105        actual: BlockEntityTypeRef,
106        expected: BlockEntityTypeRef,
107    ) -> Option<Self> {
108        ptr::eq(actual, expected).then(|| Self::for_entity_tick(expected))
109    }
110
111    /// Returns whether this ticker accepts the concrete block-entity type.
112    #[must_use]
113    pub fn accepts(self, block_entity_type: BlockEntityTypeRef) -> bool {
114        ptr::eq(self.block_entity_type, block_entity_type)
115    }
116
117    pub(crate) fn tick(
118        self,
119        world: &Arc<World>,
120        pos: BlockPos,
121        state: BlockStateId,
122        block_entity: &dyn BlockEntity,
123    ) {
124        (self.tick)(world, pos, state, block_entity);
125    }
126
127    fn tick_entity(
128        world: &Arc<World>,
129        _pos: BlockPos,
130        _state: BlockStateId,
131        block_entity: &dyn BlockEntity,
132    ) {
133        block_entity.tick(world);
134    }
135}
136
137struct BlockEntityLifecycle {
138    block_state: BlockStateId,
139    events: SmallVec<[BlockEntityLifecycleEvent; 2]>,
140    dispatching_events: bool,
141}
142
143#[derive(Clone, Copy)]
144enum BlockEntityLifecycleEvent {
145    SetRemoved,
146    ClearRemoved,
147    BlockStateChanged(BlockStateId),
148}
149
150/// Immutable block-entity identity and its short-lived lifecycle state.
151///
152/// Concrete block entities keep gameplay data behind their own focused locks.
153/// The lifecycle lock is never held while invoking world callbacks.
154pub struct BlockEntityBase {
155    block_entity_type: BlockEntityTypeRef,
156    level: Weak<World>,
157    pos: BlockPos,
158    /// Lock-free removal snapshot; lifecycle writers remain serialized below.
159    removed: AtomicBool,
160    lifecycle: SyncMutex<BlockEntityLifecycle>,
161    components: SyncRwLock<DataComponentMap>,
162}
163
164struct BlockEntityLifecycleDispatchGuard<'a> {
165    base: &'a BlockEntityBase,
166    armed: bool,
167}
168
169impl Drop for BlockEntityLifecycleDispatchGuard<'_> {
170    fn drop(&mut self) {
171        if !self.armed {
172            return;
173        }
174        let mut lifecycle = self.base.lifecycle.lock();
175        lifecycle.events.clear();
176        lifecycle.dispatching_events = false;
177    }
178}
179
180impl BlockEntityBase {
181    /// Creates common metadata for one block entity.
182    ///
183    /// # Panics
184    ///
185    /// Panics if `block_state` is not accepted by `block_entity_type`.
186    #[must_use]
187    pub fn new(
188        block_entity_type: BlockEntityTypeRef,
189        level: Weak<World>,
190        pos: BlockPos,
191        block_state: BlockStateId,
192    ) -> Self {
193        assert!(
194            block_entity_type.is_valid(block_state.get_block()),
195            "invalid block entity {} state {} at {pos:?}",
196            block_entity_type.key,
197            block_state.get_block().key,
198        );
199        Self {
200            block_entity_type,
201            level,
202            pos,
203            removed: AtomicBool::new(false),
204            lifecycle: SyncMutex::new(BlockEntityLifecycle {
205                block_state,
206                events: SmallVec::new(),
207                dispatching_events: false,
208            }),
209            components: SyncRwLock::new(DataComponentMap::new()),
210        }
211    }
212
213    #[must_use]
214    const fn block_entity_type(&self) -> BlockEntityTypeRef {
215        self.block_entity_type
216    }
217
218    #[must_use]
219    const fn pos(&self) -> BlockPos {
220        self.pos
221    }
222
223    #[must_use]
224    fn block_state(&self) -> BlockStateId {
225        self.lifecycle.lock().block_state
226    }
227
228    fn queue_block_state_change(&self, state: BlockStateId) -> bool {
229        assert!(
230            self.block_entity_type.is_valid(state.get_block()),
231            "invalid block entity {} state {} at {:?}",
232            self.block_entity_type.key,
233            state.get_block().key,
234            self.pos,
235        );
236        let mut lifecycle = self.lifecycle.lock();
237        if lifecycle.block_state == state {
238            return false;
239        }
240        lifecycle.block_state = state;
241        lifecycle
242            .events
243            .push(BlockEntityLifecycleEvent::BlockStateChanged(state));
244        if lifecycle.dispatching_events {
245            false
246        } else {
247            lifecycle.dispatching_events = true;
248            true
249        }
250    }
251
252    #[must_use]
253    fn is_removed(&self) -> bool {
254        self.removed.load(Ordering::Relaxed)
255    }
256
257    fn queue_set_removed(&self) -> bool {
258        let mut lifecycle = self.lifecycle.lock();
259        self.removed.store(true, Ordering::Relaxed);
260        lifecycle.events.push(BlockEntityLifecycleEvent::SetRemoved);
261        if lifecycle.dispatching_events {
262            false
263        } else {
264            lifecycle.dispatching_events = true;
265            true
266        }
267    }
268
269    fn queue_clear_removed(&self) -> bool {
270        let mut lifecycle = self.lifecycle.lock();
271        if !self.removed.load(Ordering::Relaxed) {
272            return false;
273        }
274        self.removed.store(false, Ordering::Relaxed);
275        lifecycle
276            .events
277            .push(BlockEntityLifecycleEvent::ClearRemoved);
278        if lifecycle.dispatching_events {
279            false
280        } else {
281            lifecycle.dispatching_events = true;
282            true
283        }
284    }
285
286    #[must_use]
287    fn level(&self) -> Option<Arc<World>> {
288        self.level.upgrade()
289    }
290
291    pub(crate) fn set_changed(&self) {
292        let Some(world) = self.level() else {
293            return;
294        };
295        let state = self.block_state();
296        world.block_entity_changed(self.pos);
297        if !state.is_air() {
298            world.update_neighbor_for_output_signal(self.pos, state.get_block());
299        }
300    }
301
302    fn load_components(&self, nbt: &BorrowedNbtCompound<'_>) {
303        let nbt_view: BorrowedNbtCompoundView<'_, '_> = nbt.into();
304        let components = match nbt_view.get("components") {
305            Some(tag) => DataComponentMap::from_nbt_tag(tag).unwrap_or_else(|| {
306                log::warn!(
307                    "Discarding malformed stored components for block entity {} at {:?}",
308                    self.block_entity_type.key,
309                    self.pos,
310                );
311                DataComponentMap::new()
312            }),
313            None => DataComponentMap::new(),
314        };
315        *self.components.write() = components;
316    }
317
318    fn stored_components(&self) -> DataComponentMap {
319        self.components.read().clone()
320    }
321
322    fn set_components(&self, components: DataComponentMap) {
323        *self.components.write() = components;
324    }
325
326    pub(crate) fn is_valid_container_for(&self, player: &Player) -> bool {
327        if self.is_removed() {
328            return false;
329        }
330        let Some(world) = self.level() else {
331            return false;
332        };
333        let Some(current) = world.get_block_entity(self.pos) else {
334            return false;
335        };
336        ptr::eq(current.base(), self)
337            && player.is_within_block_interaction_range_with_buffer(
338                self.pos,
339                f64::from(DEFAULT_DISTANCE_BUFFER),
340            )
341    }
342}
343
344/// Trait for all block entities.
345///
346/// Block entities are attached to specific blocks in the world and provide
347/// additional data storage beyond what block states can hold. Concrete
348/// implementations must claim a unique [`steel_utils::DowncastTypeKey`] through
349/// [`steel_utils::DowncastType`].
350pub trait BlockEntity: ErasedType + Send + Sync {
351    /// Returns the common metadata owned by this block entity.
352    fn base(&self) -> &BlockEntityBase;
353
354    /// Returns the type of this block entity.
355    fn get_type(&self) -> BlockEntityTypeRef {
356        self.base().block_entity_type()
357    }
358
359    /// Returns the position of this block entity in the world.
360    fn get_block_pos(&self) -> BlockPos {
361        self.base().pos()
362    }
363
364    /// Returns the current block state associated with this entity.
365    fn get_block_state(&self) -> BlockStateId {
366        self.base().block_state()
367    }
368
369    /// Returns whether this entity's registered type accepts `state`.
370    fn is_valid_block_state(&self, state: BlockStateId) -> bool {
371        self.get_type().is_valid(state.get_block())
372    }
373
374    /// Called after the cached block state changes.
375    ///
376    /// Storage and section locks are not held during this callback. This is Steel's staged
377    /// equivalent of Vanilla block entities overriding `setBlockState`; implementations should
378    /// derive any cached fields from `state` here.
379    fn on_block_state_changed(&self, _state: BlockStateId) {}
380
381    /// Called after each invocation that marks this entity removed.
382    ///
383    /// Storage locks are not held during this callback. This mirrors Vanilla overrides of
384    /// `setRemoved`, which run even if the entity was already marked removed.
385    fn on_set_removed(&self) {}
386
387    /// Called after this entity transitions from removed back to active.
388    ///
389    /// Storage locks are not held during this callback.
390    fn on_clear_removed(&self) {}
391
392    /// Called when the block entity's data changes.
393    ///
394    /// Marks the containing chunk as dirty so changes are persisted to disk.
395    fn set_changed(&self) {
396        self.base().set_changed();
397    }
398
399    /// Gets the world reference if still valid.
400    ///
401    /// Block entities receive a `Weak<World>` at construction time.
402    fn get_level(&self) -> Option<Arc<World>> {
403        self.base().level()
404    }
405
406    /// Handles a block event delegated by the owning block behavior.
407    fn trigger_event(&self, _param_a: i32, _param_b: i32) -> bool {
408        false
409    }
410
411    /// Returns this block entity as a spawner.
412    fn as_spawner(&self) -> Option<&dyn Spawner> {
413        None
414    }
415
416    /// Called before the block entity is removed to handle side effects.
417    ///
418    /// For example, containers should drop their contents here.
419    ///
420    /// # Arguments
421    /// * `pos` - The position of the block entity
422    /// * `state` - The block state being removed
423    #[expect(
424        unused_variables,
425        reason = "default trait impl; parameters used by overrides"
426    )]
427    fn pre_remove_side_effects(&self, pos: BlockPos, state: BlockStateId) {
428        // Default: no side effects
429    }
430
431    /// Loads additional data from NBT.
432    ///
433    /// Called when loading the block entity from disk or receiving initial
434    /// chunk data from the server.
435    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>);
436
437    /// Loads entity-specific data and the base block-entity component map.
438    fn load_with_components(&self, nbt: &BorrowedNbtCompound<'_>) {
439        self.load_additional(nbt);
440        self.base().load_components(nbt);
441    }
442
443    /// Reads the components this entity represents through its own fields.
444    ///
445    /// Every component read is excluded from the stored component map.
446    fn apply_implicit_components(&self, _components: &mut ImplicitComponentGetter<'_>) {}
447
448    /// Adds the components this entity represents through its own fields.
449    ///
450    /// The inverse of [`Self::apply_implicit_components`].
451    fn collect_implicit_components(&self, _components: &mut DataComponentMap) {}
452
453    /// Applies the placed stack's components: implicit ones through
454    /// [`Self::apply_implicit_components`], the rest into the stored component map.
455    ///
456    /// Do not override; customize [`Self::apply_implicit_components`] instead.
457    fn apply_components_from_item_stack(&self, stack: &ItemStack) {
458        let mut getter = ImplicitComponentGetter::new(stack);
459        self.apply_implicit_components(&mut getter);
460        self.base()
461            .set_components(getter.remaining_stored_components());
462    }
463
464    /// Returns the stored components together with the implicit ones.
465    ///
466    /// Do not override; customize [`Self::collect_implicit_components`] instead.
467    fn collect_components(&self) -> DataComponentMap {
468        let mut components = self.base().stored_components();
469        self.collect_implicit_components(&mut components);
470        components
471    }
472
473    /// Loads entity data from an owned command/runtime compound.
474    ///
475    /// `simdnbt` keeps its read-facing representation borrowed, so this
476    /// performs a checked in-memory encode/decode bridge rather than making
477    /// every command caller duplicate that conversion.
478    fn load_with_owned_components(&self, nbt: &NbtCompound) -> Result<(), simdnbt::Error> {
479        let mut encoded = Vec::new();
480        nbt.write(&mut encoded);
481        let borrowed = read_borrowed_compound(&mut Cursor::new(encoded.as_slice()))?;
482        self.load_with_components(&borrowed);
483        Ok(())
484    }
485
486    /// Saves additional data to NBT.
487    ///
488    /// Called when saving the block entity to disk.
489    fn save_additional(&self, nbt: &mut NbtCompound);
490
491    /// Saves only entity-specific data, excluding vanilla type and position metadata.
492    fn save_custom_only(&self) -> NbtCompound {
493        let mut nbt = NbtCompound::new();
494        self.save_additional(&mut nbt);
495        for key in ["id", "x", "y", "z"] {
496            while nbt.remove(key).is_some() {}
497        }
498        nbt
499    }
500
501    /// Saves entity-specific data and stored components without position/type metadata.
502    fn save_without_metadata(&self) -> NbtCompound {
503        let mut nbt = self.save_custom_only();
504        nbt.insert(
505            "components",
506            self.base().stored_components().to_nbt_tag_ref(),
507        );
508        nbt
509    }
510
511    /// Saves command-visible data together with vanilla block-entity metadata.
512    fn save_with_full_metadata(&self) -> NbtCompound {
513        let mut nbt = self.save_without_metadata();
514        let pos = self.get_block_pos();
515        nbt.insert("id", self.get_type().key.to_string());
516        nbt.insert("x", pos.x());
517        nbt.insert("y", pos.y());
518        nbt.insert("z", pos.z());
519        nbt
520    }
521
522    /// Returns the NBT data to send to clients for initial sync.
523    ///
524    /// This is included in the chunk data packet when the chunk is first sent.
525    /// Return `None` if no client sync is needed.
526    fn get_update_tag(&self) -> Option<NbtCompound> {
527        None
528    }
529
530    /// Called every game tick for ticking block entities.
531    ///
532    /// The live block behavior selects this callback through its block-entity
533    /// ticker, matching Vanilla's state-owned ticker selection.
534    #[expect(
535        unused_variables,
536        reason = "default trait impl; parameter used by overrides"
537    )]
538    fn tick(&self, world: &Arc<World>) {}
539
540    /// Returns the independently lockable container capability owned by this entity.
541    fn container_ref(&self) -> Option<ContainerRef> {
542        None
543    }
544
545    /// Returns this entity's fixed game-event listener, if it provides one.
546    ///
547    /// Mirrors Vanilla `GameEventListener.Provider.getListener`. The owning block behavior keeps
548    /// final selection authority through `BlockBehavior::get_game_event_listener`.
549    fn game_event_listener(&self) -> Option<SharedGameEventListener> {
550        None
551    }
552}
553
554/// Final block-entity common-state operations.
555///
556/// This blanket implementation prevents concrete entities from replacing metadata transitions.
557/// Custom behavior belongs in the corresponding `BlockEntity::on_*` hook, which runs after the
558/// update without storage or section locks.
559pub trait BlockEntityLifecycleExt: BlockEntity {
560    /// Returns whether this block entity has been marked for removal.
561    fn is_removed(&self) -> bool {
562        self.base().is_removed()
563    }
564
565    /// Updates the cached block state and orders its callback.
566    fn set_block_state(&self, state: BlockStateId) {
567        if self.base().queue_block_state_change(state) {
568            self.dispatch_lifecycle_events();
569        }
570    }
571
572    /// Marks this block entity as removed and orders its lifecycle callback.
573    fn set_removed(&self) {
574        if self.base().queue_set_removed() {
575            self.dispatch_lifecycle_events();
576        }
577    }
578
579    /// Reactivates this block entity and orders its lifecycle callback when the flag changed.
580    fn clear_removed(&self) {
581        if self.base().queue_clear_removed() {
582            self.dispatch_lifecycle_events();
583        }
584    }
585
586    /// Drains lifecycle callbacks in flag-update order without retaining the lifecycle lock.
587    ///
588    /// A callback may re-enter the entity. Its event is appended and drained by the active
589    /// dispatcher rather than recursively invoking another callback.
590    fn dispatch_lifecycle_events(&self) {
591        let mut guard = BlockEntityLifecycleDispatchGuard {
592            base: self.base(),
593            armed: true,
594        };
595        loop {
596            let event = {
597                let mut lifecycle = self.base().lifecycle.lock();
598                if lifecycle.events.is_empty() {
599                    lifecycle.dispatching_events = false;
600                    guard.armed = false;
601                    return;
602                }
603                lifecycle.events.remove(0)
604            };
605            match event {
606                BlockEntityLifecycleEvent::SetRemoved => self.on_set_removed(),
607                BlockEntityLifecycleEvent::ClearRemoved => self.on_clear_removed(),
608                BlockEntityLifecycleEvent::BlockStateChanged(state) => {
609                    self.on_block_state_changed(state);
610                }
611            }
612        }
613    }
614}
615
616impl<T: BlockEntity + ?Sized> BlockEntityLifecycleExt for T {}
617
618/// A stable shared block entity without a whole-object mutex.
619pub type SharedBlockEntity = Arc<dyn BlockEntity>;