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