Skip to main content

steel_core/inventory/container/
mod.rs

1//! Container trait for anything that holds items.
2//!
3//! Containers are the base abstraction for anything that can hold items,
4//! including player inventories, chests, barrels, furnaces, etc.
5
6mod crafting;
7mod result;
8mod simple;
9
10pub use crafting::CraftingContainer;
11pub use result::ResultContainer;
12pub use simple::SimpleContainer;
13
14use std::mem;
15
16use steel_registry::blocks::properties::Direction;
17use steel_registry::item_stack::ItemStack;
18use steel_utils::ErasedType;
19
20/// Default distance buffer for container interaction range checks.
21pub const DEFAULT_DISTANCE_BUFFER: f32 = 4.0;
22
23/// Something that contains items.
24/// I also use container interchangeably with inventory as they mean approximately the same thing.
25/// But inventory could also refer to the player's inventory.
26/// Example: [`crate::player::player_inventory::PlayerInventory`], [`crate::inventory::container::SimpleContainer`]
27///
28/// Concrete implementations must implement [`steel_utils::DowncastType`] with
29/// a unique, stable key so erased container references can recover their type.
30///
31/// # Locking contract
32///
33/// Implementations stored in a [`crate::inventory::lock::ContainerRef`] must keep
34/// their methods storage-local. In particular, `set_item`, `remove_item`, and
35/// `set_changed` must not call into the world or a block entity while the
36/// container mutex is held. Block-entity persistence and comparator callbacks
37/// belong to the owner supplied through
38/// [`crate::inventory::lock::ContainerRef::owned_by_block_entity`], which runs
39/// only after every container lock has been released.
40pub trait Container: ErasedType + Send + Sync {
41    /// Returns the items in this container
42    fn items(&self) -> &[ItemStack];
43    /// Returns mutable references to the items in this container.
44    fn items_mut(&mut self) -> &mut [ItemStack];
45
46    /// Returns the number of slots in this container.
47    fn get_container_size(&self) -> usize {
48        self.items().len()
49    }
50
51    /// Returns true if all slots in this container are empty.
52    fn is_empty(&self) -> bool {
53        for i in 0..self.get_container_size() {
54            if !self.get_item(i).is_empty() {
55                return false;
56            }
57        }
58        true
59    }
60
61    /// Returns a reference to the item in the specified slot.
62    fn get_item(&self, slot: usize) -> &ItemStack {
63        &self.items()[slot]
64    }
65
66    /// Returns true if this container has a non-empty stack with the same item and components.
67    ///
68    /// Mirrors vanilla `Inventory.contains(ItemStack)`.
69    fn contains_stack(&self, search_stack: &ItemStack) -> bool {
70        (0..self.get_container_size()).any(|slot| {
71            let item = self.get_item(slot);
72            !item.is_empty() && ItemStack::is_same_item_same_components(item, search_stack)
73        })
74    }
75
76    /// Returns a mutable reference to the item in the specified slot.
77    fn get_item_mut(&mut self, slot: usize) -> &mut ItemStack {
78        &mut self.items_mut()[slot]
79    }
80
81    /// Sets the item in the specified slot.
82    fn set_item(&mut self, slot: usize, stack: ItemStack) {
83        self.items_mut()[slot] = stack;
84    }
85
86    /// Removes up to `count` items from the specified slot and returns them.
87    fn remove_item(&mut self, slot: usize, count: i32) -> ItemStack {
88        let item = self.get_item_mut(slot);
89        if item.is_empty() || count <= 0 {
90            return ItemStack::empty();
91        }
92        item.split(count)
93    }
94
95    /// Removes the item from the specified slot without triggering updates.
96    fn remove_item_no_update(&mut self, slot: usize) -> ItemStack {
97        mem::take(self.get_item_mut(slot))
98    }
99
100    /// Returns the maximum stack size for this container.
101    fn get_max_stack_size(&self) -> i32 {
102        99
103    }
104
105    /// Returns the maximum stack size for a specific item in this container.
106    ///
107    /// Takes the minimum of the container's max stack size and the item's max stack size.
108    /// Based on Java's `Container.getMaxStackSize(ItemStack)`.
109    fn get_max_stack_size_for_item(&self, item: &ItemStack) -> i32 {
110        self.get_max_stack_size().min(item.max_stack_size())
111    }
112
113    /// Marks this container as changed (dirty) for saving/syncing.
114    fn set_changed(&mut self);
115
116    /// Returns true if the specified item can be placed in the specified slot.
117    fn can_place_item(&self, _slot: usize, _stack: &ItemStack) -> bool {
118        true
119    }
120
121    /// Returns true if the specified item can be taken from this slot into `destination`.
122    ///
123    /// The destination is part of Vanilla's `Container.canTakeItem` contract. Most
124    /// containers ignore it, while specialized containers such as chiseled
125    /// bookshelves use it to reject transfers that cannot fit at the destination.
126    fn can_take_item(
127        &self,
128        _destination: &dyn Container,
129        _slot: usize,
130        _stack: &ItemStack,
131    ) -> bool {
132        true
133    }
134
135    /// Returns the slots exposed to automation from one face.
136    ///
137    /// `None` means this is an ordinary container with no face-specific view.
138    fn slots_for_face(&self, _direction: Direction) -> Option<&'static [usize]> {
139        None
140    }
141
142    /// Returns whether automation may insert `stack` into `slot` through `direction`.
143    fn can_place_item_through_face(
144        &self,
145        slot: usize,
146        stack: &ItemStack,
147        _direction: Direction,
148    ) -> bool {
149        self.can_place_item(slot, stack)
150    }
151
152    /// Returns whether automation may extract `stack` from `slot` through `direction`.
153    fn can_take_item_through_face(
154        &self,
155        _slot: usize,
156        _stack: &ItemStack,
157        _direction: Direction,
158    ) -> bool {
159        true
160    }
161
162    /// Clears all items from this container.
163    fn clear_content(&mut self) -> i32 {
164        let mut count = 0;
165        for item in self.items_mut() {
166            count += item.count();
167            *item = ItemStack::empty();
168        }
169        if count > 0 {
170            self.set_changed();
171        }
172        count
173    }
174
175    /// Clears all items from this container.
176    fn clear_content_matching(&mut self, predicate: &mut dyn FnMut(&mut ItemStack) -> bool) -> i32 {
177        let mut count = 0;
178        for item in self.items_mut() {
179            if predicate(item) {
180                count += item.count();
181                *item = ItemStack::empty();
182            }
183        }
184        if count > 0 {
185            self.set_changed();
186        }
187        count
188    }
189
190    /// Removes or counts matching items using vanilla `/clear` semantics.
191    fn clear_or_count_matching_items(
192        &mut self,
193        predicate: &dyn Fn(&ItemStack) -> bool,
194        amount_to_remove: i32,
195        counting_only: bool,
196    ) -> i32 {
197        let mut count = 0;
198        for slot in 0..self.get_container_size() {
199            let stack_count = self.get_item(slot).count();
200            let amount_removed = matching_item_count(
201                self.get_item(slot),
202                predicate,
203                amount_to_remove - count,
204                counting_only,
205            );
206            if amount_removed > 0 && !counting_only {
207                if amount_removed == stack_count {
208                    self.set_item(slot, ItemStack::empty());
209                } else {
210                    self.get_item_mut(slot).shrink(amount_removed);
211                }
212            }
213            count += amount_removed;
214        }
215        if count > 0 && !counting_only {
216            self.set_changed();
217        }
218        count
219    }
220
221    /// Returns mutable references to `N` disjoint slots.
222    ///
223    /// # Panics
224    ///
225    /// Panics if any index is out of bounds or if any two indices are equal.
226    fn with_indices<const N: usize>(&mut self, indices: [usize; N]) -> [&mut ItemStack; N]
227    where
228        Self: Sized,
229    {
230        let items = self.items_mut();
231        let size = items.len();
232        for (position, index) in indices.iter().copied().enumerate() {
233            assert!(
234                index < size,
235                "with_indices: index {index} out of bounds (container size {size})",
236            );
237            assert!(
238                !indices[..position].contains(&index),
239                "with_indices: duplicate index {index}",
240            );
241        }
242        let Ok(items) = items.get_disjoint_mut(indices) else {
243            unreachable!("with_indices validated distinct in-bounds indices");
244        };
245        items
246    }
247
248    /// Tries to add an item to the container.
249    ///
250    /// First tries to stack with existing matching items, then tries empty slots.
251    /// Returns true if the entire stack was added, false if some or all couldn't fit.
252    /// The passed stack is modified to contain any remaining items.
253    ///
254    /// Based on Java's `Inventory.add(ItemStack)`.
255    fn add(&mut self, stack: &mut ItemStack) -> bool {
256        if stack.is_empty() {
257            return true;
258        }
259
260        let size = self.get_container_size();
261        let max_size = self.get_max_stack_size_for_item(stack);
262        let mut changed = false;
263
264        // First pass: try to stack with existing items
265        if stack.is_stackable() {
266            for slot in 0..size {
267                if stack.is_empty() {
268                    if changed {
269                        self.set_changed();
270                    }
271                    return true;
272                }
273                if !self.can_place_item(slot, stack) {
274                    continue;
275                }
276                let existing = self.get_item_mut(slot);
277                if !existing.is_empty() && ItemStack::is_same_item_same_components(existing, stack)
278                {
279                    let space = max_size - existing.count();
280                    if space > 0 {
281                        let to_add = stack.count().min(space);
282                        existing.grow(to_add);
283                        stack.shrink(to_add);
284                        changed = true;
285                    }
286                }
287            }
288        }
289
290        // Second pass: try empty slots
291        for slot in 0..size {
292            if stack.is_empty() {
293                if changed {
294                    self.set_changed();
295                }
296                return true;
297            }
298            if self.get_item(slot).is_empty() && self.can_place_item(slot, stack) {
299                let to_place = stack.count().min(max_size);
300                self.set_item(slot, stack.split(to_place));
301                changed = true;
302            }
303        }
304
305        if changed {
306            self.set_changed();
307        }
308        stack.is_empty()
309    }
310
311    /// Returns a boxed iterator to the items in this container
312    fn iter(&self) -> Box<dyn Iterator<Item = &ItemStack> + '_> {
313        Box::new(self.items().iter())
314    }
315
316    /// Returns a boxed iterator to mutable references of the items in this container
317    fn iter_mut(&mut self) -> Box<dyn Iterator<Item = &mut ItemStack> + '_> {
318        Box::new(self.items_mut().iter_mut())
319    }
320}
321
322/// Removes or counts matching items in one stack using vanilla `/clear` semantics.
323pub(crate) fn clear_or_count_matching_stack(
324    stack: &mut ItemStack,
325    predicate: &dyn Fn(&ItemStack) -> bool,
326    amount_to_remove: i32,
327    counting_only: bool,
328) -> i32 {
329    let amount_removed = matching_item_count(stack, predicate, amount_to_remove, counting_only);
330    if !counting_only {
331        stack.shrink(amount_removed);
332    }
333    amount_removed
334}
335
336fn matching_item_count(
337    stack: &ItemStack,
338    predicate: &dyn Fn(&ItemStack) -> bool,
339    amount_to_remove: i32,
340    counting_only: bool,
341) -> i32 {
342    if stack.is_empty() || !predicate(stack) {
343        return 0;
344    }
345    if counting_only {
346        return stack.count();
347    }
348
349    if amount_to_remove < 0 {
350        stack.count()
351    } else {
352        amount_to_remove.min(stack.count())
353    }
354}
355
356/// Calculates the redstone comparator signal strength (0-15) from a container.
357///
358/// Based on Java's `AbstractContainerMenu.getRedstoneSignalFromContainer`.
359/// The signal is proportional to how full the container is:
360/// - 0 = empty
361/// - 1-14 = partially filled (linear interpolation)
362/// - 15 = completely full
363///
364/// # Arguments
365/// * `container` - The container to calculate the signal for
366///
367/// # Returns
368/// Signal strength from 0 to 15
369#[must_use]
370pub fn calculate_redstone_signal_from_container(container: &dyn Container) -> i32 {
371    let size = container.get_container_size();
372    if size == 0 {
373        return 0;
374    }
375
376    let mut total_percent: f32 = 0.0;
377
378    for i in 0..size {
379        let item = container.get_item(i);
380        if !item.is_empty() {
381            let max_stack = container.get_max_stack_size_for_item(item);
382            total_percent += item.count() as f32 / max_stack as f32;
383        }
384    }
385
386    total_percent /= size as f32;
387
388    // Vanilla `Mth.lerpDiscrete(totalPercent, 0, 15)` gives every non-empty
389    // container at least one signal level, then distributes the other 14.
390    (total_percent * 14.0).floor() as i32 + i32::from(total_percent > 0.0)
391}
392
393#[cfg(test)]
394mod tests {
395    use std::array;
396
397    use steel_registry::{init_vanilla_registry, vanilla_items};
398
399    use super::*;
400    use steel_utils::{DowncastType, DowncastTypeKey};
401
402    struct TestContainer {
403        items: Vec<ItemStack>,
404    }
405
406    impl TestContainer {
407        fn new(size: usize) -> Self {
408            Self {
409                items: (0..size).map(|_| ItemStack::empty()).collect(),
410            }
411        }
412    }
413
414    // SAFETY: This key uniquely identifies `TestContainer` within the unit-test process.
415    unsafe impl DowncastType for TestContainer {
416        const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:test/inventory/container");
417    }
418
419    impl Container for TestContainer {
420        fn items(&self) -> &[ItemStack] {
421            &self.items
422        }
423
424        fn items_mut(&mut self) -> &mut [ItemStack] {
425            &mut self.items
426        }
427
428        fn get_container_size(&self) -> usize {
429            self.items.len()
430        }
431
432        fn set_changed(&mut self) {}
433    }
434
435    struct RedirectingContainer {
436        items: [ItemStack; 2],
437    }
438
439    // SAFETY: This key uniquely identifies `RedirectingContainer` within the unit-test process.
440    unsafe impl DowncastType for RedirectingContainer {
441        const TYPE_KEY: DowncastTypeKey =
442            DowncastTypeKey::new("steel:test/inventory/redirecting_container");
443    }
444
445    impl Container for RedirectingContainer {
446        fn items(&self) -> &[ItemStack] {
447            &self.items
448        }
449
450        fn items_mut(&mut self) -> &mut [ItemStack] {
451            &mut self.items
452        }
453
454        fn get_item_mut(&mut self, _slot: usize) -> &mut ItemStack {
455            &mut self.items[0]
456        }
457
458        fn set_changed(&mut self) {}
459    }
460
461    #[test]
462    fn test_with_indices_disjoint() {
463        let mut container = TestContainer::new(4);
464        let [a, b] = container.with_indices([1, 3]);
465        a.count = 10;
466        b.count = 20;
467        assert_eq!(container.items[1].count, 10);
468        assert_eq!(container.items[3].count, 20);
469        // Untouched slots remain at 0
470        assert_eq!(container.items[0].count, 0);
471        assert_eq!(container.items[2].count, 0);
472    }
473
474    #[test]
475    fn test_with_indices_single() {
476        let mut container = TestContainer::new(4);
477        let [a] = container.with_indices([2]);
478        a.count = 42;
479        assert_eq!(container.items[2].count, 42);
480    }
481
482    #[test]
483    fn test_with_indices_empty() {
484        let mut container = TestContainer::new(4);
485        let [] = container.with_indices([]);
486    }
487
488    #[test]
489    fn with_indices_uses_physical_item_storage() {
490        let mut container = RedirectingContainer {
491            items: array::from_fn(|_| ItemStack::empty()),
492        };
493
494        let [first, second] = container.with_indices([0, 1]);
495        first.count = 1;
496        second.count = 2;
497
498        assert_eq!(container.items[0].count, 1);
499        assert_eq!(container.items[1].count, 2);
500    }
501
502    #[test]
503    fn clear_or_count_matching_items_counts_without_mutating() {
504        init_vanilla_registry();
505        let mut container = TestContainer::new(3);
506        container.set_item(0, ItemStack::with_count(&vanilla_items::STONE, 3));
507        container.set_item(1, ItemStack::with_count(&vanilla_items::DIRT, 4));
508        container.set_item(2, ItemStack::with_count(&vanilla_items::STONE, 2));
509
510        let count = container.clear_or_count_matching_items(
511            &|stack| stack.is(&vanilla_items::STONE),
512            0,
513            true,
514        );
515
516        assert_eq!(count, 5);
517        assert_eq!(container.get_item(0).count(), 3);
518        assert_eq!(container.get_item(2).count(), 2);
519    }
520
521    #[test]
522    fn clear_or_count_matching_items_applies_cap_in_slot_order() {
523        init_vanilla_registry();
524        let mut container = TestContainer::new(2);
525        container.set_item(0, ItemStack::with_count(&vanilla_items::STONE, 3));
526        container.set_item(1, ItemStack::with_count(&vanilla_items::STONE, 4));
527
528        let count = container.clear_or_count_matching_items(
529            &|stack| stack.is(&vanilla_items::STONE),
530            5,
531            false,
532        );
533
534        assert_eq!(count, 5);
535        assert!(container.get_item(0).is_empty());
536        assert_eq!(container.get_item(1).count(), 2);
537    }
538
539    #[test]
540    fn clear_or_count_matching_items_removes_every_match_for_negative_limit() {
541        init_vanilla_registry();
542        let mut container = TestContainer::new(2);
543        container.set_item(0, ItemStack::with_count(&vanilla_items::STONE, 3));
544        container.set_item(1, ItemStack::with_count(&vanilla_items::STONE, 4));
545
546        let count = container.clear_or_count_matching_items(
547            &|stack| stack.is(&vanilla_items::STONE),
548            -1,
549            false,
550        );
551
552        assert_eq!(count, 7);
553        assert!(container.is_empty());
554    }
555
556    #[test]
557    fn comparator_signal_uses_vanilla_discrete_non_empty_floor() {
558        init_vanilla_registry();
559        let mut container = TestContainer::new(27);
560        container.set_item(0, ItemStack::new(&vanilla_items::STONE));
561        assert_eq!(calculate_redstone_signal_from_container(&container), 1);
562
563        for slot in 0..container.get_container_size() {
564            container.set_item(slot, ItemStack::with_count(&vanilla_items::STONE, 64));
565        }
566        assert_eq!(calculate_redstone_signal_from_container(&container), 15);
567    }
568
569    #[test]
570    #[should_panic(expected = "duplicate index")]
571    fn test_with_indices_duplicate_panics() {
572        let mut container = TestContainer::new(4);
573        let _ = container.with_indices([1, 1]);
574    }
575
576    #[test]
577    #[should_panic(expected = "out of bounds")]
578    fn test_with_indices_out_of_bounds_panics() {
579        let mut container = TestContainer::new(4);
580        let _ = container.with_indices([5]);
581    }
582
583    /// Verify the compiler prevents holding a `get_item_mut` reference while
584    /// calling `with_indices` on the same container. Uncomment the body to
585    /// see the expected borrow-checker error:
586    ///
587    /// ```compile_fail
588    /// use steel_core::inventory::container::Container;
589    /// use steel_utils::{DowncastType, DowncastTypeKey};
590    /// # struct C { items: Vec<steel_registry::item_stack::ItemStack> }
591    /// # // SAFETY: This doctest owns both the key and concrete type.
592    /// # unsafe impl DowncastType for C {
593    /// #     const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:doctest/container/c");
594    /// # }
595    /// # impl Container for C {
596    /// #     fn get_container_size(&self) -> usize { self.items.len() }
597    /// #     fn get_item(&self, s: usize) -> &steel_registry::item_stack::ItemStack { &self.items[s] }
598    /// #     fn get_item_mut(&mut self, s: usize) -> &mut steel_registry::item_stack::ItemStack { &mut self.items[s] }
599    /// #     fn set_item(&mut self, s: usize, v: steel_registry::item_stack::ItemStack) { self.items[s] = v; }
600    /// #     fn set_changed(&mut self) {}
601    /// # }
602    /// fn fails(c: &mut C) {
603    ///     let held = c.get_item_mut(0);
604    ///     let [a] = c.with_indices([1]); // ERROR: c already borrowed
605    ///     held.count = 1;
606    /// }
607    /// ```
608    fn _compile_fail_docs_only() {}
609}