steel_core/inventory/menu/kinds/
inventory_menu.rs1use crate::player::player_inventory::Prediction;
12use std::sync::Arc;
13
14use steel_registry::item_stack::ItemStack;
15use steel_utils::locks::{IntoShared, Shared};
16
17use crate::inventory::container::{CraftingContainer, ResultContainer};
18use crate::inventory::prelude::*;
19use crate::inventory::slots::{ArmorSlot, CraftingHandler};
20use crate::player::Player;
21use crate::player::player_inventory::{PlayerInventory, armor_equipment};
22
23pub const INVENTORY_MENU_CONTAINER_ID: u8 = 0;
25
26#[must_use]
33pub fn inventory_menu(inventory: Shared<PlayerInventory>) -> Menu {
34 let crafting_container = CraftingContainer::new(2, 2).into_shared();
35 let result_container = ResultContainer::new().into_shared();
36
37 let handler = CraftingHandler::new(
38 Arc::clone(&crafting_container),
39 Arc::clone(&result_container),
40 2,
41 );
42
43 let mut builder = MenuBuilder::new(None, INVENTORY_MENU_CONTAINER_ID);
44
45 let result = builder.crafting_result_slot(handler.clone());
46 let grid = builder.section_all(crafting_container);
47 let armor = builder.section_at(
48 &inventory,
49 PlayerInventory::ARMOR_TOP_DOWN,
50 SectionKind::custom(|container, index| {
51 Box::new(ArmorSlot::new(
52 container.clone(),
53 index,
54 armor_equipment(index),
55 ))
56 }),
57 );
58 let player = builder.player_inventory(&inventory);
59 let offhand = builder.section_at(
60 &inventory,
61 [PlayerInventory::SLOT_OFFHAND],
62 SectionKind::Normal,
63 );
64
65 builder.drain(grid);
67
68 builder.build(InventoryKind {
69 result_container,
70 handler,
71 result,
72 grid,
73 armor,
74 inv: player.all(),
75 main: player.main(),
76 hotbar: player.hotbar(),
77 offhand,
78 })
79}
80
81pub struct InventoryKind {
84 result_container: Shared<ResultContainer>,
86 handler: CraftingHandler,
87 result: Section,
89 grid: Section,
91 armor: Section,
93 inv: Section,
95 main: Section,
97 hotbar: Section,
99 offhand: Section,
101}
102
103unsafe impl steel_utils::DowncastType for InventoryKind {
106 const TYPE_KEY: steel_utils::DowncastTypeKey =
107 steel_utils::DowncastTypeKey::new("steel:menu/inventory");
108}
109
110impl InventoryKind {
111 pub(crate) fn crafting_id(&self) -> ContainerId {
113 self.handler.crafting_id()
114 }
115
116 pub(crate) fn crafting_container(&self) -> Shared<CraftingContainer> {
118 self.handler.crafting_container()
119 }
120
121 pub(crate) fn crafting_handler(&self) -> CraftingHandler {
123 self.handler.clone()
124 }
125
126 pub(crate) fn update_result(&self, guard: &mut ContainerLockGuard) {
128 self.handler.update_result(guard);
129 }
130
131 fn move_between_inventory_and_hotbar(
133 &self,
134 behavior: &MenuBehavior,
135 guard: &mut ContainerLockGuard,
136 slot_index: usize,
137 stack: &mut ItemStack,
138 ) -> bool {
139 if self.main.contains(slot_index) {
140 behavior.move_item_stack_to(
141 guard,
142 slot_index,
143 stack,
144 self.hotbar.start(),
145 self.hotbar.end(),
146 FillDirection::Forward,
147 )
148 } else if self.hotbar.contains(slot_index) {
149 behavior.move_item_stack_to(
150 guard,
151 slot_index,
152 stack,
153 self.main.start(),
154 self.main.end(),
155 FillDirection::Forward,
156 )
157 } else {
158 behavior.move_item_stack_to(
159 guard,
160 slot_index,
161 stack,
162 self.inv.start(),
163 self.inv.end(),
164 FillDirection::Forward,
165 )
166 }
167 }
168}
169
170impl MenuKind for InventoryKind {
171 #[expect(
176 clippy::too_many_lines,
177 reason = "mirrors Java's InventoryMenu::quickMoveStack branch structure"
178 )]
179 fn quick_move(
180 &mut self,
181 behavior: &mut MenuBehavior,
182 guard: &mut ContainerLockGuard,
183 slot_index: usize,
184 player: &Player,
185 ) -> Option<ItemStack> {
186 if slot_index >= behavior.slots().len() {
187 return Some(ItemStack::empty());
188 }
189
190 let stack = behavior.slots()[slot_index].get_item(guard).clone();
191 if stack.is_empty() {
192 return Some(ItemStack::empty());
193 }
194 if self.result.contains(slot_index)
195 && !behavior.slots()[slot_index].may_pickup(guard, player)
196 {
197 return Some(ItemStack::empty());
198 }
199
200 let clicked = stack.clone();
201 let mut stack_mut = stack;
202
203 let moved = if self.result.contains(slot_index) {
205 behavior.move_item_stack_to(
207 guard,
208 slot_index,
209 &mut stack_mut,
210 self.inv.start(),
211 self.inv.end(),
212 FillDirection::Backward,
213 )
214 } else if self.grid.contains(slot_index) || self.armor.contains(slot_index) {
215 behavior.move_item_stack_to(
217 guard,
218 slot_index,
219 &mut stack_mut,
220 self.inv.start(),
221 self.inv.end(),
222 FillDirection::Forward,
223 )
224 } else {
225 let equippable_slot = clicked.get_equippable_slot();
227
228 if let Some(eq_slot) = equippable_slot {
229 if eq_slot.slot_type() == EquipmentSlotType::HumanoidArmor {
230 let armor_slot_index = self.armor.start()
232 + match eq_slot {
233 EquipmentSlot::Head => 0,
234 EquipmentSlot::Chest => 1,
235 EquipmentSlot::Legs => 2,
236 EquipmentSlot::Feet => 3,
237 _ => unreachable!(),
238 };
239
240 if behavior.slots()[armor_slot_index].has_item(guard) {
241 self.move_between_inventory_and_hotbar(
242 behavior,
243 guard,
244 slot_index,
245 &mut stack_mut,
246 )
247 } else {
248 behavior.move_item_stack_to(
249 guard,
250 slot_index,
251 &mut stack_mut,
252 armor_slot_index,
253 armor_slot_index + 1,
254 FillDirection::Forward,
255 )
256 }
257 } else if eq_slot == EquipmentSlot::OffHand {
258 if behavior.slots()[self.offhand.start()].has_item(guard) {
259 self.move_between_inventory_and_hotbar(
260 behavior,
261 guard,
262 slot_index,
263 &mut stack_mut,
264 )
265 } else {
266 behavior.move_item_stack_to(
267 guard,
268 slot_index,
269 &mut stack_mut,
270 self.offhand.start(),
271 self.offhand.end(),
272 FillDirection::Forward,
273 )
274 }
275 } else {
276 self.move_between_inventory_and_hotbar(
277 behavior,
278 guard,
279 slot_index,
280 &mut stack_mut,
281 )
282 }
283 } else {
284 self.move_between_inventory_and_hotbar(behavior, guard, slot_index, &mut stack_mut)
285 }
286 };
287
288 if !moved {
289 return Some(ItemStack::empty());
290 }
291
292 behavior.update_quick_move_source(guard, slot_index, &stack_mut, &clicked);
293
294 if stack_mut.count == clicked.count {
295 return Some(ItemStack::empty());
296 }
297
298 let taken = clicked.copy_with_count(clicked.count - stack_mut.count);
299 let slot = &behavior.slots()[slot_index];
300 slot.on_quick_craft(&taken, player);
301 if let Some(remainder) = slot.on_take(guard, &stack_mut, player) {
302 player.add_item_or_drop_with_guard(guard, remainder, Prediction::Predicted);
304 }
305
306 if self.result.contains(slot_index) {
307 if !stack_mut.is_empty() {
309 let _ = guard
310 .run_unlocked(|| player.drop_item(stack_mut, false, Prediction::Predicted));
311 }
312 }
313
314 Some(clicked)
315 }
316
317 fn can_take_item_for_pick_all(&self, _carried: &ItemStack, slot_index: usize) -> bool {
319 !self.result.contains(slot_index)
320 }
321
322 fn removed(&mut self, _behavior: &mut MenuBehavior, _player: &Player) {
324 self.result_container.lock().set_item(0, ItemStack::empty());
325 }
326
327 fn slots_changed(
328 &mut self,
329 _behavior: &mut MenuBehavior,
330 guard: &mut ContainerLockGuard,
331 _player: &Player,
332 ) {
333 self.handler.update_result(guard);
334 }
335}
336
337#[cfg(test)]
338mod tests;