Skip to main content

steel_core/player/player_inventory/
container.rs

1use std::sync::LazyLock;
2
3use steel_registry::item_stack::ItemStack;
4use steel_utils::types::InteractionHand;
5
6use crate::inventory::{
7    container::Container,
8    equipment::{EntityEquipment, EquipmentSlot},
9};
10
11use super::core::PlayerInventory;
12
13/// Maps vanilla player-container indices 36-42 to equipment slots.
14const fn slot_to_equipment(slot: usize) -> Option<EquipmentSlot> {
15    match slot {
16        36 => Some(EquipmentSlot::Feet),
17        37 => Some(EquipmentSlot::Legs),
18        38 => Some(EquipmentSlot::Chest),
19        39 => Some(EquipmentSlot::Head),
20        40 => Some(EquipmentSlot::OffHand),
21        41 => Some(EquipmentSlot::Body),
22        42 => Some(EquipmentSlot::Saddle),
23        _ => None,
24    }
25}
26
27/// The equipment slot for an armor/offhand container index.
28///
29/// # Panics
30/// Panics if `index` is not an equipment index. Menu sections restrict
31/// themselves to [`PlayerInventory::ARMOR_TOP_DOWN`] and
32/// [`PlayerInventory::SLOT_OFFHAND`], so this is unreachable from them.
33pub(crate) const fn armor_equipment(index: usize) -> EquipmentSlot {
34    slot_to_equipment(index).expect("armor sections only cover armor indices")
35}
36
37impl PlayerInventory {
38    /// Applies vanilla `ItemUtils.createFilledResult` to a held item.
39    ///
40    /// Mutates the held stack and inventory, returning only the result stack that
41    /// should be dropped by the caller. When `limit_creative_stack_size` is set,
42    /// creative inventory insertion discards leftover result items instead of
43    /// dropping them.
44    pub fn apply_filled_result(
45        &mut self,
46        hand: InteractionHand,
47        mut result_stack: ItemStack,
48        has_infinite_materials: bool,
49        limit_creative_stack_size: bool,
50    ) -> ItemStack {
51        if limit_creative_stack_size && has_infinite_materials {
52            if !self.contains_stack(&result_stack) {
53                let _ = self.add(&mut result_stack);
54            }
55            return ItemStack::empty();
56        }
57
58        if !has_infinite_materials {
59            self.shrink_item_in_hand(hand, 1);
60        }
61
62        if self.get_item_in_hand(hand).is_empty() {
63            self.set_item_in_hand(hand, result_stack);
64            return ItemStack::empty();
65        }
66
67        let added = self.add(&mut result_stack);
68        if added {
69            ItemStack::empty()
70        } else {
71            result_stack
72        }
73    }
74
75    /// Tries to add `stack` to the main inventory, returning whatever did
76    /// not fit (empty if it was fully absorbed).
77    pub fn add_or_return(&mut self, mut stack: ItemStack) -> ItemStack {
78        if self.add(&mut stack) {
79            ItemStack::empty()
80        } else {
81            stack
82        }
83    }
84}
85
86/// Static empty item stack for returning references to invalid slots.
87static EMPTY_ITEM: LazyLock<ItemStack> = LazyLock::new(ItemStack::empty);
88
89/// Error returned when a carried-item packet selects a non-hotbar slot.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct InvalidHotbarSlot;
92
93impl Container for PlayerInventory {
94    fn items(&self) -> &[ItemStack] {
95        &self.items
96    }
97
98    fn items_mut(&mut self) -> &mut [ItemStack] {
99        &mut self.items
100    }
101
102    fn get_container_size(&self) -> usize {
103        Self::CONTAINER_SIZE
104    }
105
106    /// Adds an item to the player's main inventory (slots 0-35 only).
107    ///
108    /// Overrides the default `Container::add()` to prevent items from being
109    /// placed in armor or equipment slots. Matches vanilla's `Inventory.add()`
110    /// behavior which only adds to `this.items` (the 36 main slots).
111    fn add(&mut self, stack: &mut ItemStack) -> bool {
112        if stack.is_empty() {
113            return true;
114        }
115
116        let max_size = self.get_max_stack_size_for_item(stack);
117        let mut changed = false;
118
119        // Vanilla prioritizes the selected slot, then an existing compatible
120        // offhand stack, before scanning the remaining main inventory.
121        if stack.is_stackable() {
122            let selected = self.selected as usize;
123            for slot in [selected, Self::SLOT_OFFHAND] {
124                if stack.is_empty() {
125                    if changed {
126                        self.set_changed();
127                    }
128                    return true;
129                }
130                let existing = &mut self.items[slot];
131                if !existing.is_empty() && ItemStack::is_same_item_same_components(existing, stack)
132                {
133                    let space = max_size - existing.count();
134                    if space > 0 {
135                        let to_add = stack.count().min(space);
136                        existing.grow(to_add);
137                        stack.shrink(to_add);
138                        changed = true;
139                    }
140                }
141            }
142
143            for slot in 0..Self::INVENTORY_SIZE {
144                if stack.is_empty() {
145                    if changed {
146                        self.set_changed();
147                    }
148                    return true;
149                }
150                if slot == selected {
151                    continue;
152                }
153                let existing = &mut self.items[slot];
154                if !existing.is_empty() && ItemStack::is_same_item_same_components(existing, stack)
155                {
156                    let space = max_size - existing.count();
157                    if space > 0 {
158                        let to_add = stack.count().min(space);
159                        existing.grow(to_add);
160                        stack.shrink(to_add);
161                        changed = true;
162                    }
163                }
164            }
165        }
166
167        // Second pass: try empty slots in main inventory only
168        for slot in 0..Self::INVENTORY_SIZE {
169            if stack.is_empty() {
170                if changed {
171                    self.set_changed();
172                }
173                return true;
174            }
175            if self.items[slot].is_empty() {
176                let to_place = stack.count().min(max_size);
177                self.items[slot] = stack.split(to_place);
178                changed = true;
179            }
180        }
181
182        if changed {
183            self.set_changed();
184        }
185        stack.is_empty()
186    }
187
188    fn get_item(&self, slot: usize) -> &ItemStack {
189        if slot < Self::CONTAINER_SIZE {
190            &self.items[slot]
191        } else {
192            &EMPTY_ITEM
193        }
194    }
195
196    fn get_item_mut(&mut self, slot: usize) -> &mut ItemStack {
197        assert!(slot < Self::CONTAINER_SIZE, "Invalid slot index: {slot}");
198        &mut self.items[slot]
199    }
200
201    fn set_item(&mut self, slot: usize, stack: ItemStack) {
202        if slot == self.selected as usize {
203            let _ = EntityEquipment::set(self, EquipmentSlot::MainHand, stack);
204            return;
205        }
206        if let Some(equipment_slot) = slot_to_equipment(slot) {
207            let _ = EntityEquipment::set(self, equipment_slot, stack);
208            return;
209        }
210        if slot < Self::INVENTORY_SIZE {
211            self.items[slot] = stack;
212        }
213        self.set_changed();
214    }
215
216    fn is_empty(&self) -> bool {
217        self.items.iter().all(ItemStack::is_empty)
218    }
219
220    fn set_changed(&mut self) {
221        self.times_changed = self.times_changed.wrapping_add(1);
222    }
223
224    fn clear_content(&mut self) -> i32 {
225        let mut count = 0;
226        for item in &mut self.items {
227            count += item.count();
228            *item = ItemStack::empty();
229        }
230        if count > 0 {
231            self.set_changed();
232        }
233        count
234    }
235
236    fn clear_content_matching(&mut self, predicate: &mut dyn FnMut(&mut ItemStack) -> bool) -> i32 {
237        let mut count = 0;
238        for item in &mut self.items {
239            if predicate(item) {
240                count += item.count();
241                *item = ItemStack::empty();
242            }
243        }
244        if count > 0 {
245            self.set_changed();
246        }
247        count
248    }
249}