Skip to main content

steel_core/inventory/menu/
layout.rs

1use std::{mem, range::Range, sync::Arc};
2
3use steel_registry::item_stack::ItemStack;
4
5use crate::{
6    inventory::{
7        lock::{ContainerLockGuard, ContainerRef},
8        menu::{
9            behavior::MenuBehavior,
10            builder::{FakeResultRemainderPolicy, Route},
11        },
12    },
13    player::Player,
14};
15
16/// Static layout of a built menu: section ranges and the shift-click route table.
17pub(crate) struct MenuLayout {
18    pub(crate) routes: Vec<Route>,
19    pub(crate) drain_sections: Vec<Range<usize>>,
20}
21
22impl MenuLayout {
23    /// Returns every item in the drain sections to the player, emptying those slots.
24    ///
25    /// When `return_to_inventory` is false the items are dropped into the world.
26    pub fn return_drained_items(
27        &self,
28        behavior: &MenuBehavior,
29        player: &Player,
30        return_to_inventory: bool,
31    ) {
32        if self.drain_sections.is_empty() {
33            return;
34        }
35
36        let mut guard = if return_to_inventory {
37            behavior.lock_all_containers_with(ContainerRef::from(Arc::clone(&player.inventory)))
38        } else {
39            behavior.lock_all_containers()
40        };
41        for range in &self.drain_sections {
42            for slot_index in *range {
43                let slot = &behavior.slots()[slot_index];
44                let item = mem::take(slot.get_item_mut(&mut guard));
45                if item.is_empty() {
46                    continue;
47                }
48                slot.set_changed(&mut guard);
49                if return_to_inventory {
50                    player.add_item_or_drop_with_guard(&mut guard, item);
51                } else {
52                    let _ = guard.run_unlocked(|| player.drop_item(item, false, false));
53                }
54            }
55        }
56    }
57
58    /// Generic shift-click for `slot_index` via the route table.
59    ///
60    /// Returns the item originally in the slot, or empty if nothing moved.
61    pub fn quick_move(
62        &self,
63        behavior: &MenuBehavior,
64        guard: &mut ContainerLockGuard,
65        slot_index: usize,
66        player: &Player,
67    ) -> ItemStack {
68        let Some(route) = self.routes.iter().find(|r| r.from.contains(&slot_index)) else {
69            return ItemStack::empty();
70        };
71
72        let clicked = behavior.slots()[slot_index].get_item(guard).clone();
73        if clicked.is_empty() {
74            return ItemStack::empty();
75        }
76
77        // Reject stale pickups like a result slot whose recipe no longer matches.
78        if !behavior.slots()[slot_index].may_pickup(guard, player) {
79            return ItemStack::empty();
80        }
81
82        let mut remaining = clicked.clone();
83        let moved = route.targets.iter().any(|target| {
84            behavior.move_item_stack_to(
85                guard,
86                slot_index,
87                &mut remaining,
88                target.start,
89                target.end,
90                route.direction,
91            )
92        });
93        if !moved {
94            return ItemStack::empty();
95        }
96
97        behavior.update_quick_move_source(guard, slot_index, &remaining, &clicked);
98
99        // Nothing left the slot.
100        if remaining.count == clicked.count {
101            return ItemStack::empty();
102        }
103
104        let slot = &behavior.slots()[slot_index];
105        if let Some(leftover) = slot.on_take(guard, &remaining, player) {
106            player.add_item_or_drop_with_guard(guard, leftover);
107        }
108        // Result handlers may replace the fake source in `on_take`; apply the
109        // route's policy to any unresolved output from the old result.
110        if slot.is_fake()
111            && !remaining.is_empty()
112            && route.fake_result_remainder == FakeResultRemainderPolicy::Drop
113        {
114            let _ = guard.run_unlocked(|| player.drop_item(remaining.clone(), false, false));
115        }
116
117        clicked
118    }
119}