steel_core/player/player_inventory/
core.rs1use 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
14pub struct PlayerInventory {
19 pub(super) items: [ItemStack; Self::CONTAINER_SIZE],
21 pub(super) selected: u8,
23 pub(super) times_changed: u32,
25}
26
27impl Default for PlayerInventory {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33unsafe impl DowncastType for PlayerInventory {
35 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:container/player_inventory");
36}
37
38impl PlayerInventory {
39 pub const INVENTORY_SIZE: usize = 36;
41 pub const CONTAINER_SIZE: usize = 43;
43 pub const SELECTION_SIZE: usize = 9;
45 pub const SLOT_OFFHAND: usize = 40;
47 pub const HOTBAR: Range<usize> = 0..9;
49 pub const MAIN: Range<usize> = 9..36;
51 pub const ARMOR_TOP_DOWN: [usize; 4] = [39, 38, 37, 36];
53
54 #[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 #[must_use]
66 pub const fn is_hotbar_slot(slot: usize) -> bool {
67 slot < Self::SELECTION_SIZE
68 }
69
70 #[must_use]
72 pub const fn get_selected_slot(&self) -> u8 {
73 self.selected
74 }
75
76 #[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 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 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 pub fn with_selected_item<R>(&self, f: impl FnOnce(&ItemStack) -> R) -> R {
132 f(&self.items[self.selected as usize])
133 }
134
135 #[must_use]
137 pub const fn get_selected_item(&self) -> &ItemStack {
138 &self.items[self.selected as usize]
139 }
140
141 pub fn get_selected_item_mut(&mut self) -> &mut ItemStack {
143 EntityEquipment::get_mut(self, EquipmentSlot::MainHand)
144 }
145
146 pub fn set_selected_item(&mut self, item: ItemStack) {
148 let _ = EntityEquipment::set(self, EquipmentSlot::MainHand, item);
149 }
150
151 #[must_use]
153 pub fn get_offhand_item(&self) -> &ItemStack {
154 EntityEquipment::get_ref(self, EquipmentSlot::OffHand)
155 }
156
157 pub fn get_offhand_item_mut(&mut self) -> &mut ItemStack {
159 EntityEquipment::get_mut(self, EquipmentSlot::OffHand)
160 }
161
162 pub fn set_offhand_item(&mut self, item: ItemStack) {
164 let _ = EntityEquipment::set(self, EquipmentSlot::OffHand, item);
165 }
166
167 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 #[must_use]
188 pub const fn get_times_changed(&self) -> u32 {
189 self.times_changed
190 }
191
192 #[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 #[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 #[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 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 pub fn add_and_pick_item(&mut self, stack: ItemStack) -> bool {
239 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 self.items[self.selected as usize] = stack;
250 self.set_changed();
251 true
252 }
253}