Skip to main content

steel_core/inventory/menu/kinds/
inventory_menu.rs

1//! Player inventory menu.
2//!
3//! Slot layout (46 total):
4//! - Slot 0: Crafting result
5//! - Slots 1-4: 2x2 grid
6//! - Slots 5-8: Armor (head, chest, legs, feet)
7//! - Slots 9-35: Main inventory (27)
8//! - Slots 36-44: Hotbar (9)
9//! - Slot 45: Offhand
10
11use 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
23/// Container ID for the player inventory (always 0).
24pub const INVENTORY_MENU_CONTAINER_ID: u8 = 0;
25
26/// Builds the player inventory menu, always open when no other menu is.
27///
28/// The inventory container should contain:
29/// - Slots 0-35: Main inventory (hotbar 0-8, main 9-35)
30/// - Slots 36-39: Armor (feet, legs, chest, head)
31/// - Slot 40: Offhand
32#[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    // No routes: quick_move is a custom override. The grid drains on close.
66    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
81/// Per-menu player-inventory state: recipe handler, result container, and the
82/// section handles for its custom shift-click.
83pub struct InventoryKind {
84    /// The result container.
85    result_container: Shared<ResultContainer>,
86    handler: CraftingHandler,
87    /// The result (slot 0).
88    result: Section,
89    /// The 2x2 grid (slots 1-4).
90    grid: Section,
91    /// Armor slots (slots 5-8).
92    armor: Section,
93    /// Main inventory + hotbar (slots 9-44).
94    inv: Section,
95    /// Main inventory (slots 9-35).
96    main: Section,
97    /// Hotbar (slots 36-44).
98    hotbar: Section,
99    /// Offhand (slot 45).
100    offhand: Section,
101}
102
103// SAFETY: This Steel-owned key uniquely identifies the concrete menu kind
104// within the process.
105unsafe 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    /// `ContainerId` of the 2x2 grid.
112    pub(crate) fn crafting_id(&self) -> ContainerId {
113        self.handler.crafting_id()
114    }
115
116    /// Shared handle to the 2x2 grid container.
117    pub(crate) fn crafting_container(&self) -> Shared<CraftingContainer> {
118        self.handler.crafting_container()
119    }
120
121    /// Shared recipe handler for the 2x2 crafting grid and its result.
122    pub(crate) fn crafting_handler(&self) -> CraftingHandler {
123        self.handler.clone()
124    }
125
126    /// Recomputes the result from the current grid contents.
127    pub(crate) fn update_result(&self, guard: &mut ContainerLockGuard) {
128        self.handler.update_result(guard);
129    }
130
131    /// Moves items between the main inventory and hotbar.
132    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    /// Handles shift-click for a slot, including armor/offhand auto-equip.
172    ///
173    /// Always returns `Some`: the item originally in the slot, or empty if
174    /// nothing moved.
175    #[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        // Target range depends on the clicked slot.
204        let moved = if self.result.contains(slot_index) {
205            // Result to inventory, filling existing stacks first.
206            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            // Grid or armor to inventory.
216            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            // Item is in inventory/hotbar, try to equip it first.
226            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                    // Armor slots are ordered head, chest, legs, feet.
231                    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            // Crafting remainders like empty buckets go back to the inventory.
303            player.add_item_or_drop_with_guard(guard, remainder, Prediction::Predicted);
304        }
305
306        if self.result.contains(slot_index) {
307            // Drop result output that didn't fit.
308            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    /// Prevents taking from the result slot during pickup-all.
318    fn can_take_item_for_pick_all(&self, _carried: &ItemStack, slot_index: usize) -> bool {
319        !self.result.contains(slot_index)
320    }
321
322    /// Clears the virtual result on close. The grid is drained by [`Menu::removed`].
323    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;