Skip to main content

steel_core/block_entity/entities/
barrel.rs

1//! Barrel block entity implementation.
2//!
3//! Barrels are container block entities with 27 slots (3x9 grid),
4//! functioning similarly to chests but without double-chest behavior.
5
6use std::{
7    mem,
8    sync::{Arc, Weak},
9};
10
11use simdnbt::ToNbtTag;
12use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
13use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
14use steel_registry::item_stack::ItemStack;
15use steel_registry::vanilla_block_entity_types;
16use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
17
18use crate::block_entity::{BlockEntity, BlockEntityBase};
19use crate::inventory::container::Container;
20use crate::inventory::container::load_all_items;
21use crate::inventory::lock::{ContainerRef, SharedContainer};
22use crate::world::World;
23
24/// Number of slots in a barrel (3 rows of 9).
25pub const BARREL_SLOTS: usize = 27;
26
27/// Barrel block entity.
28///
29/// A simple container with 27 slots, using the same menu as chests.
30pub struct BarrelBlockEntity {
31    base: Arc<BlockEntityBase>,
32    container: Arc<SyncMutex<BarrelContainer>>,
33    container_ref: ContainerRef,
34}
35
36struct BarrelContainer {
37    items: Vec<ItemStack>,
38}
39
40// SAFETY: This key is owned by Steel and uniquely identifies `BarrelBlockEntity`.
41unsafe impl DowncastType for BarrelBlockEntity {
42    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/barrel");
43}
44
45// SAFETY: This key is owned by Steel and uniquely identifies the independently
46// lockable inventory data used by a barrel block entity.
47unsafe impl DowncastType for BarrelContainer {
48    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:container/barrel");
49}
50
51impl BarrelBlockEntity {
52    /// Creates a new barrel block entity.
53    #[must_use]
54    pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
55        let base = Arc::new(BlockEntityBase::new(
56            &vanilla_block_entity_types::BARREL,
57            level,
58            pos,
59            state,
60        ));
61        let container = Arc::new(SyncMutex::new(BarrelContainer {
62            items: vec![ItemStack::empty(); BARREL_SLOTS],
63        }));
64        let shared_container: SharedContainer =
65            Arc::<SyncMutex<BarrelContainer>>::clone(&container);
66        Self {
67            container_ref: ContainerRef::owned_by_block_entity(shared_container, Arc::clone(&base)),
68            base,
69            container,
70        }
71    }
72}
73
74impl BlockEntity for BarrelBlockEntity {
75    fn base(&self) -> &BlockEntityBase {
76        &self.base
77    }
78
79    fn pre_remove_side_effects(&self, pos: BlockPos, _state: BlockStateId) {
80        let items = {
81            let mut container = self.container.lock();
82            mem::replace(&mut container.items, vec![ItemStack::empty(); BARREL_SLOTS])
83        };
84        let Some(world) = self.get_level() else {
85            return;
86        };
87        for item in items {
88            world.drop_item_stack(pos, item);
89        }
90    }
91
92    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
93        // Convert to NbtCompound view for accessing methods
94        let nbt_view: NbtCompoundView<'_, '_> = nbt.into();
95        let mut container = self.container.lock();
96        container.items.fill(ItemStack::empty());
97
98        load_all_items(nbt_view, &mut container.items);
99    }
100
101    fn save_additional(&self, nbt: &mut NbtCompound) {
102        // Save items to NBT (only non-empty slots)
103        let container = self.container.lock();
104        let mut items: Vec<NbtCompound> = Vec::new();
105        for (slot, item) in container.items.iter().enumerate() {
106            if !item.is_empty() {
107                // Use ItemStack's ToNbtTag implementation for proper component serialization
108                if let NbtTag::Compound(mut item_nbt) = item.clone().to_nbt_tag() {
109                    item_nbt.insert("Slot", slot as i8);
110                    items.push(item_nbt);
111                }
112            }
113        }
114        nbt.insert("Items", NbtList::Compound(items));
115    }
116
117    fn get_update_tag(&self) -> Option<NbtCompound> {
118        // Barrels don't need to send inventory to clients on chunk load
119        // (unlike signs which display text)
120        None
121    }
122
123    fn container_ref(&self) -> Option<ContainerRef> {
124        Some(self.container_ref.clone())
125    }
126}
127
128impl Container for BarrelContainer {
129    fn items(&self) -> &[ItemStack] {
130        &self.items
131    }
132
133    fn items_mut(&mut self) -> &mut [ItemStack] {
134        &mut self.items
135    }
136
137    fn get_container_size(&self) -> usize {
138        BARREL_SLOTS
139    }
140
141    fn set_item(&mut self, slot: usize, mut stack: ItemStack) {
142        if slot < BARREL_SLOTS {
143            let max_stack_size = self.get_max_stack_size_for_item(&stack);
144            if !stack.is_empty() && stack.count() > max_stack_size {
145                stack.set_count(max_stack_size);
146            }
147            self.items[slot] = stack;
148        }
149    }
150
151    fn get_max_stack_size(&self) -> i32 {
152        64
153    }
154
155    fn set_changed(&mut self) {}
156}
157
158#[cfg(test)]
159mod tests {
160    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items};
161
162    use super::*;
163
164    fn test_barrel() -> BarrelBlockEntity {
165        init_vanilla_registry();
166        BarrelBlockEntity::new(
167            Weak::new(),
168            BlockPos::new(1, 2, 3),
169            vanilla_blocks::BARREL.default_state(),
170        )
171    }
172
173    #[test]
174    fn set_item_limits_stack_to_vanilla_container_maximum() {
175        let barrel = test_barrel();
176        barrel
177            .container
178            .lock()
179            .set_item(0, ItemStack::with_count(&vanilla_items::STONE, 100));
180
181        assert_eq!(barrel.container.lock().get_item(0).count(), 64);
182    }
183
184    #[test]
185    fn pre_remove_preserves_slots_for_existing_menu_references() {
186        let barrel = test_barrel();
187        barrel
188            .container
189            .lock()
190            .set_item(0, ItemStack::new(&vanilla_items::STONE));
191
192        barrel.pre_remove_side_effects(
193            BlockPos::new(1, 2, 3),
194            vanilla_blocks::BARREL.default_state(),
195        );
196
197        let container = barrel.container.lock();
198        assert_eq!(container.items.len(), BARREL_SLOTS);
199        assert!(container.items.iter().all(ItemStack::is_empty));
200    }
201}