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