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::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    /// Number of main inventory slots.
40    pub const INVENTORY_SIZE: usize = 36;
41    /// Number of logical container slots, including equipment.
42    pub const CONTAINER_SIZE: usize = 43;
43    /// Number of hotbar slots.
44    pub const SELECTION_SIZE: usize = 9;
45    /// Slot index for offhand.
46    pub const SLOT_OFFHAND: usize = 40;
47    /// Hotbar container indices.
48    pub const HOTBAR: Range<usize> = 0..9;
49    /// Main storage container indices (everything except hotbar, armor, offhand).
50    pub const MAIN: Range<usize> = 9..36;
51    /// Armor container indices in display order (head, chest, legs, feet).
52    pub const ARMOR_TOP_DOWN: [usize; 4] = [39, 38, 37, 36];
53
54    /// Creates a new player inventory with empty slots.
55    #[must_use]
56    pub fn new() -> Self {
57        Self {
58            items: array::from_fn(|_| ItemStack::empty()),
59            selected: 0,
60            times_changed: 0,
61        }
62    }
63
64    /// Returns true if the given slot index is a hotbar slot (0-8).
65    #[must_use]
66    pub const fn is_hotbar_slot(slot: usize) -> bool {
67        slot < Self::SELECTION_SIZE
68    }
69
70    /// Returns the currently selected hotbar slot (0-8).
71    #[must_use]
72    pub const fn get_selected_slot(&self) -> u8 {
73        self.selected
74    }
75
76    /// Serializes the main inventory with vanilla's `ItemStackWithSlot` shape.
77    #[must_use]
78    pub(crate) fn to_vanilla_inventory_nbt(&self) -> NbtList {
79        let items = self.items[..Self::INVENTORY_SIZE]
80            .iter()
81            .enumerate()
82            .filter_map(|(slot, item)| {
83                if item.is_empty() {
84                    return None;
85                }
86                let NbtTag::Compound(mut nbt) = item.to_nbt_tag_ref() else {
87                    return None;
88                };
89                nbt.insert("Slot", NbtTag::Byte(slot as i8));
90                Some(nbt)
91            })
92            .collect();
93        NbtList::Compound(items)
94    }
95
96    /// Sets the selected hotbar slot.
97    ///
98    /// # Panics
99    ///
100    /// Panics if the slot is not a valid hotbar slot (must be 0-8).
101    pub fn set_selected_slot(&mut self, slot: u8) {
102        if Self::is_hotbar_slot(slot as usize) {
103            if self.selected != slot {
104                self.selected = slot;
105            }
106        } else {
107            panic!("Invalid hotbar slot: {slot}");
108        }
109    }
110
111    /// Sets the selected hotbar slot from the signed protocol field.
112    ///
113    /// Returns an error when the packet value is outside the vanilla hotbar
114    /// range instead of wrapping or panicking.
115    pub fn try_set_selected_slot_from_packet(
116        &mut self,
117        slot: i16,
118    ) -> Result<(), InvalidHotbarSlot> {
119        let Ok(slot) = u8::try_from(slot) else {
120            return Err(InvalidHotbarSlot);
121        };
122        if !Self::is_hotbar_slot(slot as usize) {
123            return Err(InvalidHotbarSlot);
124        }
125
126        self.set_selected_slot(slot);
127        Ok(())
128    }
129
130    /// Executes a function with a reference to the currently selected item.
131    pub fn with_selected_item<R>(&self, f: impl FnOnce(&ItemStack) -> R) -> R {
132        f(&self.items[self.selected as usize])
133    }
134
135    /// Returns a mutable reference to the currently selected item (main hand).
136    #[must_use]
137    pub const fn get_selected_item(&self) -> &ItemStack {
138        &self.items[self.selected as usize]
139    }
140
141    /// Returns the currently selected item (main hand).
142    pub fn get_selected_item_mut(&mut self) -> &mut ItemStack {
143        EntityEquipment::get_mut(self, EquipmentSlot::MainHand)
144    }
145
146    /// Sets the currently selected item (main hand).
147    pub fn set_selected_item(&mut self, item: ItemStack) {
148        let _ = EntityEquipment::set(self, EquipmentSlot::MainHand, item);
149    }
150
151    /// Returns the offhand item.
152    #[must_use]
153    pub fn get_offhand_item(&self) -> &ItemStack {
154        EntityEquipment::get_ref(self, EquipmentSlot::OffHand)
155    }
156
157    /// Returns a mutable reference to the offhand item.
158    pub fn get_offhand_item_mut(&mut self) -> &mut ItemStack {
159        EntityEquipment::get_mut(self, EquipmentSlot::OffHand)
160    }
161
162    /// Sets the offhand item.
163    pub fn set_offhand_item(&mut self, item: ItemStack) {
164        let _ = EntityEquipment::set(self, EquipmentSlot::OffHand, item);
165    }
166
167    /// Executes a function with a mutable reference to the currently selected item.
168    pub fn with_selected_item_mut<R>(&mut self, f: impl FnOnce(&mut ItemStack) -> R) -> R {
169        self.with_equipment_item_mut(EquipmentSlot::MainHand, f)
170    }
171
172    pub(in crate::player) fn with_equipment_item_mut<R>(
173        &mut self,
174        slot: EquipmentSlot,
175        f: impl FnOnce(&mut ItemStack) -> R,
176    ) -> R {
177        let inventory_index = self.equipment_slot_index(slot);
178        let previous = self.items[inventory_index].clone();
179        let result = f(&mut self.items[inventory_index]);
180        if !ItemStack::matches(&self.items[inventory_index], &previous) {
181            Container::set_changed(self);
182        }
183        result
184    }
185
186    /// Returns the number of times this inventory has been modified.
187    #[must_use]
188    pub const fn get_times_changed(&self) -> u32 {
189        self.times_changed
190    }
191
192    /// Returns the non-equipment items (main 36 slots).
193    #[must_use]
194    pub fn get_items(&self) -> &[ItemStack; Self::INVENTORY_SIZE] {
195        let Some(items) = self.items.first_chunk::<{ Self::INVENTORY_SIZE }>() else {
196            unreachable!("the player inventory always contains its 36 main slots");
197        };
198        items
199    }
200
201    /// Finds the first empty slot in the inventory, or -1 if full.
202    #[must_use]
203    pub fn get_free_slot(&self) -> i32 {
204        for i in 0..Self::INVENTORY_SIZE {
205            if self.items[i].is_empty() {
206                return i as i32;
207            }
208        }
209        -1
210    }
211
212    /// Finds a slot containing an item matching the given stack (same item type).
213    /// Returns -1 if not found.
214    #[must_use]
215    pub fn find_slot_matching_item(&self, stack: &ItemStack) -> i32 {
216        for i in 0..Self::INVENTORY_SIZE {
217            if !self.items[i].is_empty() && ItemStack::is_same_item(&self.items[i], stack) {
218                return i as i32;
219            }
220        }
221        -1
222    }
223
224    /// Swaps items between selected hotbar slot and the given slot.
225    /// Used for pick block when item is in main inventory but not hotbar.
226    pub fn pick_slot(&mut self, slot: i32) {
227        let slot = slot as usize;
228        if slot >= Self::INVENTORY_SIZE {
229            return;
230        }
231        let selected = self.selected as usize;
232        self.items.swap(selected, slot);
233        self.set_changed();
234    }
235
236    /// Adds an item to the hotbar (for creative pick block) and selects it.
237    /// Returns true if successful.
238    pub fn add_and_pick_item(&mut self, stack: ItemStack) -> bool {
239        // Find first empty hotbar slot
240        for i in 0..Self::SELECTION_SIZE {
241            if self.items[i].is_empty() {
242                self.items[i] = stack;
243                self.selected = i as u8;
244                self.set_changed();
245                return true;
246            }
247        }
248        // No empty slot, replace current slot
249        self.items[self.selected as usize] = stack;
250        self.set_changed();
251        true
252    }
253}