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