Skip to main content

steel_core/inventory/menu/
layout.rs

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