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