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