1mod behavior;
4mod builder;
5mod grid;
6mod kind;
7pub mod kinds;
8mod layout;
9
10use crate::inventory::container::Container as _;
11pub use behavior::MenuBehavior;
12pub use builder::{
13 ContainerSlots, DataSlot, FakeResultRemainderPolicy, FillDirection, IntoSections, MenuBuilder,
14 PlayerInventorySections, Section, SectionKind, SectionSource, SlotFactory,
15};
16pub use grid::{ColSpan, GridPlacer, PlacementBuilder, Rect, Region, RowSpan, SpanBounds};
17pub use kind::MenuKind;
18pub(crate) use layout::MenuLayout;
19#[cfg(test)]
20use steel_utils::locks::Shared;
21
22use std::fmt;
23use std::mem;
24
25use steel_registry::{item_stack::ItemStack, menu_type::MenuTypeRef};
26use steel_utils::{Downcast as _, types::GameType};
27
28use crate::inventory::container::CraftingContainer;
29use crate::inventory::menu::kinds::InventoryKind;
30use crate::{
31 inventory::lock::{ContainerId, ContainerLockGuard, ContainerRef},
32 player::Player,
33};
34
35use crate::inventory::click::{Click, ClickOutcome, SwapTarget, can_item_quick_replace};
36
37pub struct Menu {
43 behavior: MenuBehavior,
44 layout: MenuLayout,
45 kind: Box<dyn MenuKind>,
46 overrides_player_slots: bool,
47}
48
49impl fmt::Debug for Menu {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.debug_struct("Menu")
52 .field("behavior", &self.behavior)
53 .field("kind", &self.kind.downcast_type_key())
54 .finish_non_exhaustive()
55 }
56}
57
58impl Menu {
59 pub(super) fn from_parts(
61 behavior: MenuBehavior,
62 layout: MenuLayout,
63 kind: Box<dyn MenuKind>,
64 overrides_player_slots: bool,
65 ) -> Self {
66 Self {
67 behavior,
68 layout,
69 kind,
70 overrides_player_slots,
71 }
72 }
73
74 #[must_use]
76 pub const fn behavior(&self) -> &MenuBehavior {
77 &self.behavior
78 }
79
80 pub const fn behavior_mut(&mut self) -> &mut MenuBehavior {
82 &mut self.behavior
83 }
84
85 #[must_use]
87 pub fn kind(&self) -> &dyn MenuKind {
88 self.kind.as_ref()
89 }
90
91 pub fn kind_mut(&mut self) -> &mut dyn MenuKind {
93 self.kind.as_mut()
94 }
95
96 #[must_use]
98 pub const fn container_id(&self) -> u8 {
99 self.behavior.container_id()
100 }
101
102 #[must_use]
105 pub const fn menu_type(&self) -> Option<MenuTypeRef> {
106 self.behavior.menu_type()
107 }
108
109 #[must_use]
111 pub const fn overrides_player_slots(&self) -> bool {
112 self.overrides_player_slots
113 }
114
115 #[must_use]
117 pub fn still_valid(&self, player: &Player) -> bool {
118 self.kind.still_valid(&self.behavior, player)
119 }
120
121 #[must_use]
123 pub fn can_take_item_for_pick_all(&self, carried: &ItemStack, slot_index: usize) -> bool {
124 self.kind.can_take_item_for_pick_all(carried, slot_index)
125 }
126
127 pub fn removed(&mut self, player: &Player) {
132 let return_to_inventory = player.returns_menu_items_to_inventory();
133
134 let carried = mem::take(self.behavior.carried_mut());
135 if !carried.is_empty() {
136 if return_to_inventory {
137 player.add_item_or_drop(carried);
138 } else {
139 let _ = player.drop_item(carried, false, false);
140 }
141 }
142 self.layout
143 .return_drained_items(&self.behavior, player, return_to_inventory);
144
145 let Self { behavior, kind, .. } = self;
146 kind.removed(behavior, player);
147 }
148
149 pub fn set_item_name(&mut self, name: impl Into<String>, player: &Player) {
152 let Self { behavior, kind, .. } = self;
153 kind.on_rename(behavior, name.into(), player);
154 }
155
156 pub(crate) fn clear_or_count_crafting_items(
159 &mut self,
160 predicate: &dyn Fn(&ItemStack) -> bool,
161 amount_to_remove: i32,
162 counting_only: bool,
163 ) -> i32 {
164 let Some(kind) = self.kind.downcast_ref::<InventoryKind>() else {
165 return 0;
166 };
167 let crafting_id = kind.crafting_id();
168 let mut guard = self.behavior.lock_all_containers();
169 let Some(crafting) = guard.get_typed_mut::<CraftingContainer>(crafting_id) else {
170 return 0;
171 };
172
173 crafting.clear_or_count_matching_items(predicate, amount_to_remove, counting_only)
174 }
175
176 #[cfg(test)]
179 pub(crate) fn crafting_container(&self) -> Option<Shared<CraftingContainer>> {
180 let kind = self.kind.downcast_ref::<InventoryKind>()?;
181 Some(kind.crafting_container())
182 }
183
184 pub(crate) fn update_crafting_result(&mut self) {
187 let Some(kind) = self.kind.downcast_mut::<InventoryKind>() else {
188 return;
189 };
190 let mut guard = self.behavior.lock_all_containers();
191 kind.update_result(&mut guard);
192 }
193
194 fn slots_changed(&mut self, guard: &mut ContainerLockGuard, player: &Player) {
196 let Self { behavior, kind, .. } = self;
197 kind.slots_changed(behavior, guard, player);
198 }
199
200 pub fn on_open(&mut self, player: &Player) {
203 let mut guard = self.behavior().lock_all_containers();
204 let Self { behavior, kind, .. } = self;
205 kind.on_open(behavior, &mut guard, player);
206 }
207
208 pub fn on_tick(&mut self, player: &Player) {
210 let mut guard = self.behavior().lock_all_containers();
211 let Self { behavior, kind, .. } = self;
212 kind.on_tick(behavior, &mut guard, player);
213 }
214
215 fn quick_move_stack(
219 &mut self,
220 guard: &mut ContainerLockGuard,
221 slot_index: usize,
222 player: &Player,
223 ) -> ItemStack {
224 let Self {
225 behavior,
226 layout,
227 kind,
228 ..
229 } = self;
230 if let Some(result) = kind.quick_move(behavior, guard, slot_index, player) {
231 result
232 } else {
233 layout.quick_move(behavior, guard, slot_index, player)
234 }
235 }
236
237 pub fn clicked(&mut self, click: Click, player: &Player) {
242 if !click.is_valid_for(self.behavior().slot_count()) {
243 log::debug!(
244 "Ignoring programmatic container click that violates parsed-click invariants: \
245 {click:?}"
246 );
247 return;
248 }
249
250 let has_infinite_materials = player.game_mode() == GameType::Creative;
251 if let Click::QuickCraft(action) = click {
252 let outcome = {
253 let mut guard = self.behavior().lock_all_containers();
254 let Self { behavior, kind, .. } = self;
255 kind.on_drag(behavior, &mut guard, action, player)
256 };
257 if outcome == ClickOutcome::Consume {
258 self.behavior_mut().reset_quick_craft();
259 } else {
260 let Self { behavior, kind, .. } = self;
261 behavior.do_quick_craft(action, has_infinite_materials, player, &|slot| {
262 kind.can_drag_to(slot)
263 });
264 }
265 } else {
266 if self.behavior().quickcraft().is_some() {
268 self.behavior_mut().reset_quick_craft();
269 }
270
271 let outcome = {
274 let mut guard = self.behavior().lock_all_containers();
275 let Self { behavior, kind, .. } = self;
276 kind.on_slot_clicked(behavior, &mut guard, click, player)
277 };
278
279 if outcome == ClickOutcome::Fallthrough {
280 match click {
281 Click::Pickup { slot, button } => {
282 self.behavior_mut().do_pickup(slot, button, player);
283 }
284 Click::DropCarried { button } => {
285 self.behavior_mut().drop_carried(button, player);
286 }
287 Click::QuickMove { slot } => {
288 self.do_quick_move(slot, player);
289 }
290 Click::Swap { slot, with } => {
291 self.do_swap(slot, with, player);
292 }
293 Click::Clone { slot } => {
294 self.behavior_mut().do_clone(slot, has_infinite_materials);
295 }
296 Click::Throw { slot, whole_stack } => {
297 self.behavior_mut().do_throw(slot, whole_stack, player);
298 }
299 Click::PickupAll { slot, direction } => {
300 self.do_pickup_all(slot, direction, player);
301 }
302 Click::QuickCraft(_) => unreachable!(),
303 }
304 }
305 }
306 let should_recompute = match click {
309 Click::DropCarried { .. } => false,
310 Click::QuickCraft(_) => !self.behavior().slots().is_empty(),
311 _ => true,
312 };
313 if should_recompute {
314 let mut guard = self.behavior().lock_all_containers();
315 self.slots_changed(&mut guard, player);
316 }
317 }
318
319 fn do_quick_move(&mut self, slot_index: usize, player: &Player) {
321 let mut guard = self.behavior().lock_all_containers();
322
323 if !self.behavior().slots()[slot_index].may_pickup(&guard, player) {
324 return;
325 }
326
327 let initial_item = self.behavior().slots()[slot_index].get_item(&guard).clone();
328 if initial_item.is_empty() {
329 return;
330 }
331
332 let mut result = self.quick_move_stack(&mut guard, slot_index, player);
334
335 while !result.is_empty() {
336 let current_item = self.behavior().slots()[slot_index].get_item(&guard).clone();
337 if !ItemStack::is_same_item(¤t_item, &result) {
338 break;
339 }
340 result = self.quick_move_stack(&mut guard, slot_index, player);
341 }
342 }
343
344 fn do_swap(&mut self, slot_index: usize, with: SwapTarget, player: &Player) {
347 let player_inventory = ContainerRef::from(player.inventory.clone());
348 let player_inv_id = ContainerId::from_arc(&player.inventory);
349 let mut guard = self.behavior().lock_all_containers_with(player_inventory);
350
351 let behavior = self.behavior();
352 let target_slot = &behavior.slots()[slot_index];
353 let inventory_slot = with.inventory_slot();
354
355 let target_item = target_slot.get_item(&guard).clone();
356 let Some(inventory) = guard.get(player_inv_id) else {
357 unreachable!("the explicitly locked player inventory must be present");
358 };
359 let source_item = inventory.get_item(inventory_slot).clone();
360
361 if source_item.is_empty() && target_item.is_empty() {
362 return;
363 }
364
365 if source_item.is_empty() {
366 if target_slot.may_pickup(&guard, player) {
368 let Some(inventory) = guard.get_mut(player_inv_id) else {
369 unreachable!("the explicitly locked player inventory must be present");
370 };
371 inventory.set_item(inventory_slot, target_item.clone());
372 target_slot.set_by_player(&mut guard, ItemStack::empty(), &target_item);
373 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
374 player.add_item_or_drop_with_guard(&mut guard, remainder);
375 }
376 }
377 } else if target_item.is_empty() {
378 if target_slot.may_place(&source_item) {
380 let max_size = target_slot.get_max_stack_size_for_item(&guard, &source_item);
381 if source_item.count > max_size {
382 let Some(inv) = guard.get_mut(player_inv_id) else {
383 unreachable!("the explicitly locked player inventory must be present");
384 };
385 let to_place = inv.get_item_mut(inventory_slot).split(max_size);
386 target_slot.set_by_player(&mut guard, to_place, &ItemStack::empty());
387 } else {
388 let Some(inventory) = guard.get_mut(player_inv_id) else {
389 unreachable!("the explicitly locked player inventory must be present");
390 };
391 inventory.set_item(inventory_slot, ItemStack::empty());
392 target_slot.set_by_player(&mut guard, source_item, &ItemStack::empty());
393 }
394 }
395 } else {
396 if target_slot.may_pickup(&guard, player) && target_slot.may_place(&source_item) {
398 let max_size = target_slot.get_max_stack_size_for_item(&guard, &source_item);
399 if source_item.count > max_size {
400 let Some(inv) = guard.get_mut(player_inv_id) else {
402 unreachable!("the explicitly locked player inventory must be present");
403 };
404 let to_place = inv.get_item_mut(inventory_slot).split(max_size);
405 target_slot.set_by_player(&mut guard, to_place, &target_item);
406 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
407 player.add_item_or_drop_with_guard(&mut guard, remainder);
408 }
409 let mut displaced = target_item;
410 let Some(inventory) = guard.get_mut(player_inv_id) else {
411 unreachable!("the explicitly locked player inventory must be present");
412 };
413 let added = inventory.add(&mut displaced);
414 if !added && !player.has_infinite_materials() {
416 let _ = guard.run_unlocked(|| player.drop_item(displaced, false, true));
417 }
418 } else {
419 let Some(inventory) = guard.get_mut(player_inv_id) else {
420 unreachable!("the explicitly locked player inventory must be present");
421 };
422 inventory.set_item(inventory_slot, target_item.clone());
423 target_slot.set_by_player(&mut guard, source_item, &target_item);
424 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
425 player.add_item_or_drop_with_guard(&mut guard, remainder);
426 }
427 }
428 }
429 }
430 }
431
432 fn do_pickup_all(&mut self, slot_index: usize, direction: FillDirection, player: &Player) {
435 let mut guard = self.behavior().lock_all_containers();
436
437 let behavior = self.behavior();
438 let slot = &behavior.slots()[slot_index];
439 let slot_has_item = !slot.get_item(&guard).is_empty();
440 let slot_may_pickup = slot.may_pickup(&guard, player);
441
442 if behavior.carried().is_empty() || (slot_has_item && slot_may_pickup) {
443 return;
444 }
445
446 let max_stack = behavior.carried().max_stack_size();
447 let carried_item = behavior.carried().clone();
448 let slot_count = behavior.slots().len();
449
450 let (start, step): (i32, i32) = match direction {
451 FillDirection::Forward => (0, 1),
452 FillDirection::Backward => (slot_count as i32 - 1, -1),
453 };
454
455 for pass in 0..2 {
457 let mut i = start;
458 while i >= 0 && i < slot_count as i32 && self.behavior().carried().count < max_stack {
459 let target_slot = &self.behavior().slots()[i as usize];
460 let target_item = target_slot.get_item(&guard).clone();
461
462 if !target_item.is_empty()
463 && can_item_quick_replace(&target_item, &carried_item, true)
464 && target_slot.may_pickup(&guard, player)
465 && self.can_take_item_for_pick_all(&carried_item, i as usize)
466 {
467 if pass != 0 || target_item.count != target_item.max_stack_size() {
469 let can_take = max_stack - self.behavior().carried().count;
470 let to_take = target_item.count.min(can_take);
471 let removed = target_slot.safe_take(&mut guard, to_take, can_take, player);
472 self.behavior_mut()
473 .carried_mut()
474 .grow(removed.count.min(can_take));
475 }
476 }
477
478 i += step;
479 }
480 }
481 }
482}
483
484#[cfg(test)]
485mod tests;