Skip to main content

steel_core/player/player_inventory/
core.rs

1use std::{array, ops::Range};
2
3use simdnbt::owned::{NbtList, NbtTag};
4use steel_registry::{enchantment_effect::EnchantmentEffectComponent, item_stack::ItemStack};
5use steel_utils::{DowncastType, DowncastTypeKey};
6
7use crate::inventory::{
8    container::Container,
9    equipment::{EntityEquipment, EquipmentSlot},
10};
11
12use super::container::InvalidHotbarSlot;
13
14/// Player inventory container managing the main inventory and equipment.
15///
16/// Contains 36 main inventory slots (0-8 hotbar, 9-35 main) plus equipment slots
17/// (armor, offhand, etc.) accessed through the Container trait.
18pub struct PlayerInventory {
19    /// All 43 logical inventory slots in vanilla container order.
20    pub(super) items: [ItemStack; Self::CONTAINER_SIZE],
21    /// Currently selected hotbar slot (0-8).
22    pub(super) selected: u8,
23    /// Counter incremented on every change.
24    pub(super) times_changed: u32,
25}
26
27impl Default for PlayerInventory {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33// SAFETY: This key is owned by Steel and uniquely identifies `PlayerInventory`.
34unsafe impl DowncastType for PlayerInventory {
35    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:container/player_inventory");
36}
37
38impl PlayerInventory {
39    pub(crate) fn take_death_drops(&mut self) -> Vec<ItemStack> {
40        (0..self.get_container_size())
41            .filter_map(|slot| {
42                let item = self.get_item(slot).clone();
43                if item.is_empty() {
44                    return None;
45                }
46                self.set_item(slot, ItemStack::empty());
47                (!item.has_enchantment_effect(EnchantmentEffectComponent::PreventEquipmentDrop))
48                    .then_some(item)
49            })
50            .collect()
51    }
52
53    /// Number of main inventory slots.
54    pub const INVENTORY_SIZE: usize = 36;
55    /// Number of logical container slots, including equipment.
56    pub const CONTAINER_SIZE: usize = 43;
57    /// Number of hotbar slots.
58    pub const SELECTION_SIZE: u8 = 9;
59    /// Slot index for offhand.
60    pub const SLOT_OFFHAND: usize = 40;
61    /// Hotbar container indices.
62    pub const HOTBAR: Range<usize> = 0..9;
63    /// Main storage container indices (everything except hotbar, armor, offhand).
64    pub const MAIN: Range<usize> = 9..36;
65    /// Armor container indices in display order (head, chest, legs, feet).
66    pub const ARMOR_TOP_DOWN: [usize; 4] = [39, 38, 37, 36];
67
68    /// Creates a new player inventory with empty slots.
69    #[must_use]
70    pub fn new() -> Self {
71        Self {
72            items: array::from_fn(|_| ItemStack::empty()),
73            selected: 0,
74            times_changed: 0,
75        }
76    }
77
78    /// Copies this inventory into a fresh player inventory for player replacement.
79    #[must_use]
80    pub(crate) fn replacement_copy(&self) -> Self {
81        Self {
82            items: array::from_fn(|slot| {
83                let item = &self.items[slot];
84                item.copy_with_count(item.count())
85            }),
86            selected: self.selected,
87            times_changed: 0,
88        }
89    }
90
91    /// Returns true if the given slot index is a hotbar slot (0-8).
92    #[must_use]
93    pub const fn is_hotbar_slot(slot: u8) -> bool {
94        slot < Self::SELECTION_SIZE
95    }
96
97    /// Returns the currently selected hotbar slot (0-8).
98    #[must_use]
99    pub const fn get_selected_slot(&self) -> u8 {
100        self.selected
101    }
102
103    /// Serializes the main inventory with vanilla's `ItemStackWithSlot` shape.
104    #[must_use]
105    pub(crate) fn to_vanilla_inventory_nbt(&self) -> NbtList {
106        let items = self.items[..Self::INVENTORY_SIZE]
107            .iter()
108            .enumerate()
109            .filter_map(|(slot, item)| {
110                if item.is_empty() {
111                    return None;
112                }
113                let NbtTag::Compound(mut nbt) = item.to_nbt_tag_ref() else {
114                    return None;
115                };
116                nbt.insert("Slot", NbtTag::Byte(slot as i8));
117                Some(nbt)
118            })
119            .collect();
120        NbtList::Compound(items)
121    }
122
123    /// Sets the selected hotbar slot.
124    ///
125    /// # Panics
126    ///
127    /// Panics if the slot is not a valid hotbar slot (must be 0-8).
128    pub fn set_selected_slot(&mut self, slot: u8) {
129        if Self::is_hotbar_slot(slot) {
130            if self.selected != slot {
131                self.selected = slot;
132            }
133        } else {
134            panic!("Invalid hotbar slot: {slot}");
135        }
136    }
137
138    /// Sets the selected hotbar slot from the signed protocol field.
139    ///
140    /// Returns an error when the packet value is outside the vanilla hotbar
141    /// range instead of wrapping or panicking.
142    pub fn try_set_selected_slot_from_packet(
143        &mut self,
144        slot: i16,
145    ) -> Result<(), InvalidHotbarSlot> {
146        let Ok(slot) = u8::try_from(slot) else {
147            return Err(InvalidHotbarSlot);
148        };
149        if !Self::is_hotbar_slot(slot) {
150            return Err(InvalidHotbarSlot);
151        }
152
153        self.set_selected_slot(slot);
154        Ok(())
155    }
156
157    /// Executes a function with a reference to the currently selected item.
158    pub fn with_selected_item<R>(&self, f: impl FnOnce(&ItemStack) -> R) -> R {
159        f(&self.items[self.selected as usize])
160    }
161
162    /// Returns a mutable reference to the currently selected item (main hand).
163    #[must_use]
164    pub const fn get_selected_item(&self) -> &ItemStack {
165        &self.items[self.selected as usize]
166    }
167
168    /// Returns the currently selected item (main hand).
169    pub fn get_selected_item_mut(&mut self) -> &mut ItemStack {
170        EntityEquipment::get_mut(self, EquipmentSlot::MainHand)
171    }
172
173    /// Sets the currently selected item (main hand).
174    pub fn set_selected_item(&mut self, item: ItemStack) {
175        let _ = EntityEquipment::set(self, EquipmentSlot::MainHand, item);
176    }
177
178    /// Returns the offhand item.
179    #[must_use]
180    pub fn get_offhand_item(&self) -> &ItemStack {
181        EntityEquipment::get_ref(self, EquipmentSlot::OffHand)
182    }
183
184    /// Returns a mutable reference to the offhand item.
185    pub fn get_offhand_item_mut(&mut self) -> &mut ItemStack {
186        EntityEquipment::get_mut(self, EquipmentSlot::OffHand)
187    }
188
189    /// Sets the offhand item.
190    pub fn set_offhand_item(&mut self, item: ItemStack) {
191        let _ = EntityEquipment::set(self, EquipmentSlot::OffHand, item);
192    }
193
194    /// Executes a function with a mutable reference to the currently selected item.
195    pub fn with_selected_item_mut<R>(&mut self, f: impl FnOnce(&mut ItemStack) -> R) -> R {
196        self.with_equipment_item_mut(EquipmentSlot::MainHand, f)
197    }
198
199    pub(in crate::player) fn with_equipment_item_mut<R>(
200        &mut self,
201        slot: EquipmentSlot,
202        f: impl FnOnce(&mut ItemStack) -> R,
203    ) -> R {
204        let inventory_index = self.equipment_slot_index(slot);
205        let previous = self.items[inventory_index].clone();
206        let result = f(&mut self.items[inventory_index]);
207        if !ItemStack::matches(&self.items[inventory_index], &previous) {
208            Container::set_changed(self);
209        }
210        result
211    }
212
213    /// Returns the number of times this inventory has been modified.
214    #[must_use]
215    pub const fn get_times_changed(&self) -> u32 {
216        self.times_changed
217    }
218
219    /// Returns the non-equipment items (main 36 slots).
220    #[must_use]
221    pub fn get_items(&self) -> &[ItemStack; Self::INVENTORY_SIZE] {
222        let Some(items) = self.items.first_chunk::<{ Self::INVENTORY_SIZE }>() else {
223            unreachable!("the player inventory always contains its 36 main slots");
224        };
225        items
226    }
227
228    /// Finds the first empty slot in the inventory, or `None` if full.
229    #[must_use]
230    pub fn get_free_slot(&self) -> Option<u8> {
231        (0..Self::INVENTORY_SIZE)
232            .find(|&i| self.items[i].is_empty())
233            .map(|i| i as u8)
234    }
235
236    /// Finds next empty slot in hotbar (and returns)
237    /// if none, then
238    ///     it finds the next slot with a non-enchanted item (and returns)
239    /// if none, then
240    ///     returns the current slot
241    #[must_use]
242    pub fn get_suitable_hotbar_slot(&self) -> u8 {
243        for slot in 0..Self::SELECTION_SIZE {
244            let index = (self.selected + slot) % Self::SELECTION_SIZE;
245            if self.items[usize::from(index)].is_empty() {
246                return index;
247            }
248        }
249
250        for slot in 0..Self::SELECTION_SIZE {
251            let index = (self.selected + slot) % Self::SELECTION_SIZE;
252            if !self.items[usize::from(index)].is_enchanted() {
253                return index;
254            }
255        }
256
257        self.selected
258    }
259
260    /// Finds a slot containing an item matching the given stack (same item type).
261    #[must_use]
262    pub fn find_slot_matching_item(&self, stack: &ItemStack) -> Option<u8> {
263        (0..Self::INVENTORY_SIZE)
264            .find(|&i| !self.items[i].is_empty() && ItemStack::is_same_item(&self.items[i], stack))
265            .map(|i| i as u8)
266    }
267
268    /// Finds a slot containing an item matching the exact given stack (same item type).
269    #[must_use]
270    pub fn find_slot_matching_item_with_same_components(&self, stack: &ItemStack) -> Option<u8> {
271        (0..Self::INVENTORY_SIZE)
272            .find(|&i| {
273                !self.items[i].is_empty()
274                    && ItemStack::is_same_item_same_components(&self.items[i], stack)
275            })
276            .map(|i| i as u8)
277    }
278
279    /// Swaps items between selected hotbar slot and the given slot.
280    /// Used for pick block when item is in main inventory but not hotbar.
281    pub fn pick_slot(&mut self, slot: u8) {
282        let slot = slot as usize;
283        if slot >= Self::INVENTORY_SIZE {
284            return;
285        }
286        let selected = self.selected as usize;
287        self.items.swap(selected, slot);
288        self.set_changed();
289    }
290
291    /// Adds an item to the hotbar (for creative pick block) and selects it.
292    /// Returns true if successful.
293    pub fn add_and_pick_item(&mut self, stack: ItemStack) -> bool {
294        // Find first empty hotbar slot
295        for i in 0..Self::SELECTION_SIZE {
296            let j = usize::from(i);
297            if self.items[j].is_empty() {
298                self.items[j] = stack;
299                self.selected = i;
300                self.set_changed();
301                return true;
302            }
303        }
304        // No empty slot, replace current slot
305        self.items[self.selected as usize] = stack;
306        self.set_changed();
307        true
308    }
309}