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