1mod behavior;
4mod builder;
5mod grid;
6mod kind;
7pub mod kinds;
8mod layout;
9
10use crate::player::player_inventory::Prediction;
11use std::sync::Arc;
12
13use crate::inventory::container::Container as _;
14pub use behavior::MenuBehavior;
15pub use builder::{
16 ContainerSlots, DataSlot, FakeResultRemainderPolicy, FillDirection, IntoSections, MenuBuilder,
17 PlayerInventorySections, Section, SectionKind, SectionSource, SlotFactory,
18};
19pub use grid::{ColSpan, GridPlacer, PlacementBuilder, Rect, Region, RowSpan, SpanBounds};
20pub use kind::{MenuKind, still_valid_block};
21pub(crate) use layout::MenuLayout;
22#[cfg(test)]
23use steel_utils::locks::Shared;
24
25use std::fmt;
26use std::mem;
27
28use steel_registry::{
29 equipment::EquipmentSlot, item_stack::ItemStack, menu_type::MenuTypeRef,
30 mob_effect::MobEffectRef,
31};
32use steel_utils::{Downcast as _, types::GameType};
33
34use crate::inventory::container::CraftingContainer;
35use crate::inventory::menu::kinds::InventoryKind;
36use crate::{
37 entity::LivingEntity,
38 inventory::lock::{ContainerId, ContainerLockGuard, ContainerRef},
39 inventory::slots::{ArmorSlot, Slot},
40 player::{Player, PlayerConnection},
41};
42
43use crate::inventory::click::{Click, ClickOutcome, SwapTarget, can_item_quick_replace};
44
45pub struct Menu {
51 behavior: MenuBehavior,
52 layout: MenuLayout,
53 kind: Box<dyn MenuKind>,
54 overrides_player_slots: bool,
55}
56
57impl fmt::Debug for Menu {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.debug_struct("Menu")
60 .field("behavior", &self.behavior)
61 .field("kind", &self.kind.downcast_type_key())
62 .finish_non_exhaustive()
63 }
64}
65
66impl Menu {
67 pub(super) fn from_parts(
69 behavior: MenuBehavior,
70 layout: MenuLayout,
71 kind: Box<dyn MenuKind>,
72 overrides_player_slots: bool,
73 ) -> Self {
74 Self {
75 behavior,
76 layout,
77 kind,
78 overrides_player_slots,
79 }
80 }
81
82 #[must_use]
84 pub const fn behavior(&self) -> &MenuBehavior {
85 &self.behavior
86 }
87
88 pub const fn behavior_mut(&mut self) -> &mut MenuBehavior {
90 &mut self.behavior
91 }
92
93 #[must_use]
95 pub fn kind(&self) -> &dyn MenuKind {
96 self.kind.as_ref()
97 }
98
99 pub fn kind_mut(&mut self) -> &mut dyn MenuKind {
101 self.kind.as_mut()
102 }
103
104 #[must_use]
106 pub const fn container_id(&self) -> u8 {
107 self.behavior.container_id()
108 }
109
110 #[must_use]
113 pub const fn menu_type(&self) -> Option<MenuTypeRef> {
114 self.behavior.menu_type()
115 }
116
117 #[must_use]
119 pub const fn overrides_player_slots(&self) -> bool {
120 self.overrides_player_slots
121 }
122
123 #[must_use]
125 pub fn still_valid(&self, player: &Player) -> bool {
126 self.kind.still_valid(&self.behavior, player)
127 }
128
129 #[must_use]
131 pub fn can_take_item_for_pick_all(&self, carried: &ItemStack, slot_index: usize) -> bool {
132 self.kind.can_take_item_for_pick_all(carried, slot_index)
133 }
134
135 pub fn update_effects(
137 &mut self,
138 primary: Option<MobEffectRef>,
139 secondary: Option<MobEffectRef>,
140 connection: &Arc<PlayerConnection>,
141 ) -> bool {
142 let accepted = {
143 let mut guard = self.behavior.lock_all_containers();
144 self.kind
145 .on_update_effects(&mut self.behavior, &mut guard, primary, secondary)
146 };
147 self.behavior.broadcast_changes(connection);
148 accepted
149 }
150
151 pub fn removed(&mut self, player: &Player) {
156 let return_to_inventory = player.returns_menu_items_to_inventory();
157
158 let carried = mem::take(self.behavior.carried_mut());
159 if !carried.is_empty() {
160 if return_to_inventory {
161 player.add_item_or_drop(carried, Prediction::ServerOnly);
162 } else {
163 let _ = player.drop_item(carried, false, Prediction::ServerOnly);
164 }
165 }
166 self.layout
167 .return_drained_items(&self.behavior, player, return_to_inventory);
168
169 let Self { behavior, kind, .. } = self;
170 kind.removed(behavior, player);
171 }
172
173 pub fn set_item_name(&mut self, name: impl Into<String>, player: &Player) {
176 let Self { behavior, kind, .. } = self;
177 kind.on_rename(behavior, name.into(), player);
178 }
179
180 pub fn click_menu_button(&mut self, button_id: i32, player: &Player) -> bool {
183 let Self { behavior, kind, .. } = self;
184 kind.click_menu_button(behavior, button_id, player)
185 }
186
187 pub(crate) fn clear_or_count_crafting_items(
190 &mut self,
191 predicate: &dyn Fn(&ItemStack) -> bool,
192 amount_to_remove: i32,
193 counting_only: bool,
194 ) -> i32 {
195 let Some(kind) = self.kind.downcast_ref::<InventoryKind>() else {
196 return 0;
197 };
198 let crafting_id = kind.crafting_id();
199 let mut guard = self.behavior.lock_all_containers();
200 let Some(crafting) = guard.get_typed_mut::<CraftingContainer>(crafting_id) else {
201 return 0;
202 };
203
204 crafting.clear_or_count_matching_items(predicate, amount_to_remove, counting_only)
205 }
206
207 #[cfg(test)]
210 pub(crate) fn crafting_container(&self) -> Option<Shared<CraftingContainer>> {
211 let kind = self.kind.downcast_ref::<InventoryKind>()?;
212 Some(kind.crafting_container())
213 }
214
215 pub(crate) fn update_crafting_result(&mut self) {
218 let Some(kind) = self.kind.downcast_mut::<InventoryKind>() else {
219 return;
220 };
221 let mut guard = self.behavior.lock_all_containers();
222 kind.update_result(&mut guard);
223 }
224
225 fn slots_changed(&mut self, guard: &mut ContainerLockGuard, player: &Player) {
227 let Self { behavior, kind, .. } = self;
228 kind.slots_changed(behavior, guard, player);
229 }
230
231 pub fn on_open(&mut self, player: &Player) {
234 let mut guard = self.behavior().lock_all_containers();
235 let Self { behavior, kind, .. } = self;
236 kind.on_open(behavior, &mut guard, player);
237 }
238
239 pub fn on_tick(&mut self, player: &Player) {
241 let mut guard = self.behavior().lock_all_containers();
242 let Self { behavior, kind, .. } = self;
243 kind.on_tick(behavior, &mut guard, player);
244 }
245
246 fn quick_move_stack(
250 &mut self,
251 guard: &mut ContainerLockGuard,
252 slot_index: usize,
253 player: &Player,
254 ) -> ItemStack {
255 let Self {
256 behavior,
257 layout,
258 kind,
259 ..
260 } = self;
261 if let Some(result) = kind.quick_move(behavior, guard, slot_index, player) {
262 result
263 } else {
264 layout.quick_move(behavior, guard, slot_index, player)
265 }
266 }
267
268 pub fn clicked(&mut self, click: Click, player: &Player) {
273 if !click.is_valid_for(self.behavior().slot_count()) {
274 log::debug!(
275 "Ignoring programmatic container click that violates parsed-click invariants: \
276 {click:?}"
277 );
278 return;
279 }
280
281 let armor_before = self.armor_items();
282 let has_infinite_materials = player.game_mode() == GameType::Creative;
283 if let Click::QuickCraft(action) = click {
284 let outcome = {
285 let mut guard = self.behavior().lock_all_containers();
286 let Self { behavior, kind, .. } = self;
287 kind.on_drag(behavior, &mut guard, action, player)
288 };
289 if outcome == ClickOutcome::Consume {
290 self.behavior_mut().reset_quick_craft();
291 } else {
292 let Self { behavior, kind, .. } = self;
293 behavior.do_quick_craft(action, has_infinite_materials, player, &|slot| {
294 kind.can_drag_to(slot)
295 });
296 }
297 } else {
298 if self.behavior().quickcraft().is_some() {
300 self.behavior_mut().reset_quick_craft();
301 }
302
303 let outcome = {
306 let mut guard = self.behavior().lock_all_containers();
307 let Self { behavior, kind, .. } = self;
308 kind.on_slot_clicked(behavior, &mut guard, click, player)
309 };
310
311 if outcome == ClickOutcome::Fallthrough {
312 match click {
313 Click::Pickup { slot, button } => {
314 self.behavior_mut().do_pickup(slot, button, player);
315 }
316 Click::DropCarried { button } => {
317 self.behavior_mut().drop_carried(button, player);
318 }
319 Click::QuickMove { slot } => {
320 self.do_quick_move(slot, player);
321 }
322 Click::Swap { slot, with } => {
323 self.do_swap(slot, with, player);
324 }
325 Click::Clone { slot } => {
326 self.behavior_mut().do_clone(slot, has_infinite_materials);
327 }
328 Click::Throw { slot, whole_stack } => {
329 self.behavior_mut().do_throw(slot, whole_stack, player);
330 }
331 Click::PickupAll { slot, direction } => {
332 self.do_pickup_all(slot, direction, player);
333 }
334 Click::QuickCraft(_) => unreachable!(),
335 }
336 }
337 }
338 let should_recompute = match click {
341 Click::DropCarried { .. } => false,
342 Click::QuickCraft(_) => !self.behavior().slots().is_empty(),
343 _ => true,
344 };
345 if should_recompute {
346 let mut guard = self.behavior().lock_all_containers();
347 self.slots_changed(&mut guard, player);
348 }
349 self.notify_armor_equipment_changes(player, armor_before);
350 }
351
352 pub(crate) fn set_slot_by_player(
355 &mut self,
356 slot_index: usize,
357 stack: ItemStack,
358 player: &Player,
359 ) {
360 let armor_before = self.armor_items();
361 {
362 let mut guard = self.behavior().lock_all_containers();
363 let Some(slot) = self.behavior().slots().get(slot_index) else {
364 return;
365 };
366 let previous = slot.get_item(&guard).clone();
367 slot.set_by_player(&mut guard, stack, &previous);
368 }
369 self.notify_armor_equipment_changes(player, armor_before);
370 }
371
372 fn armor_items(&self) -> Vec<(EquipmentSlot, ItemStack)> {
373 let guard = self.behavior().lock_all_containers();
374 self.behavior()
375 .slots()
376 .iter()
377 .filter_map(|slot| {
378 let armor_slot = slot.downcast_ref::<ArmorSlot>()?;
379 Some((
380 armor_slot.equipment_slot(),
381 armor_slot.get_item(&guard).clone(),
382 ))
383 })
384 .collect()
385 }
386
387 fn notify_armor_equipment_changes(
388 &self,
389 player: &Player,
390 armor_before: Vec<(EquipmentSlot, ItemStack)>,
391 ) {
392 for ((slot, previous), (_, current)) in armor_before.into_iter().zip(self.armor_items()) {
393 player.on_equip_item(slot, &previous, ¤t);
394 }
395 }
396
397 fn do_quick_move(&mut self, slot_index: usize, player: &Player) {
399 let mut guard = self.behavior().lock_all_containers();
400
401 if !self.behavior().slots()[slot_index].may_pickup(&guard, player) {
402 return;
403 }
404
405 let initial_item = self.behavior().slots()[slot_index].get_item(&guard).clone();
406 if initial_item.is_empty() {
407 return;
408 }
409
410 let mut result = self.quick_move_stack(&mut guard, slot_index, player);
412
413 while !result.is_empty() {
414 let current_item = self.behavior().slots()[slot_index].get_item(&guard).clone();
415 if !ItemStack::is_same_item(¤t_item, &result) {
416 break;
417 }
418 result = self.quick_move_stack(&mut guard, slot_index, player);
419 }
420 }
421
422 fn do_swap(&mut self, slot_index: usize, with: SwapTarget, player: &Player) {
425 let player_inventory = ContainerRef::from(Arc::clone(&player.inventory));
426 let player_inv_id = ContainerId::from_arc(&player.inventory);
427 let mut guard = self.behavior().lock_all_containers_with(player_inventory);
428
429 let behavior = self.behavior();
430 let target_slot = &behavior.slots()[slot_index];
431 let inventory_slot = with.inventory_slot();
432
433 let target_item = target_slot.get_item(&guard).clone();
434 let Some(inventory) = guard.get(player_inv_id) else {
435 unreachable!("the explicitly locked player inventory must be present");
436 };
437 let source_item = inventory.get_item(inventory_slot).clone();
438
439 if source_item.is_empty() && target_item.is_empty() {
440 return;
441 }
442
443 if source_item.is_empty() {
444 if target_slot.may_pickup(&guard, player) {
446 let Some(inventory) = guard.get_mut(player_inv_id) else {
447 unreachable!("the explicitly locked player inventory must be present");
448 };
449 inventory.set_item(inventory_slot, target_item.clone());
450 target_slot.on_swap_craft(target_item.count);
451 target_slot.set_by_player(&mut guard, ItemStack::empty(), &target_item);
452 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
453 player.add_item_or_drop_with_guard(
454 &mut guard,
455 remainder,
456 Prediction::Predicted,
457 );
458 }
459 }
460 } else if target_item.is_empty() {
461 if target_slot.may_place(&source_item) {
463 let max_size = target_slot.get_max_stack_size_for_item(&guard, &source_item);
464 if source_item.count > max_size {
465 let Some(inv) = guard.get_mut(player_inv_id) else {
466 unreachable!("the explicitly locked player inventory must be present");
467 };
468 let to_place = inv.get_item_mut(inventory_slot).split(max_size);
469 target_slot.set_by_player(&mut guard, to_place, &ItemStack::empty());
470 } else {
471 let Some(inventory) = guard.get_mut(player_inv_id) else {
472 unreachable!("the explicitly locked player inventory must be present");
473 };
474 inventory.set_item(inventory_slot, ItemStack::empty());
475 target_slot.set_by_player(&mut guard, source_item, &ItemStack::empty());
476 }
477 }
478 } else {
479 if target_slot.may_pickup(&guard, player) && target_slot.may_place(&source_item) {
481 let max_size = target_slot.get_max_stack_size_for_item(&guard, &source_item);
482 if source_item.count > max_size {
483 let Some(inv) = guard.get_mut(player_inv_id) else {
485 unreachable!("the explicitly locked player inventory must be present");
486 };
487 let to_place = inv.get_item_mut(inventory_slot).split(max_size);
488 target_slot.set_by_player(&mut guard, to_place, &target_item);
489 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
490 player.add_item_or_drop_with_guard(
491 &mut guard,
492 remainder,
493 Prediction::Predicted,
494 );
495 }
496 let mut displaced = target_item;
497 let Some(inventory) = guard.get_mut(player_inv_id) else {
498 unreachable!("the explicitly locked player inventory must be present");
499 };
500 let added = inventory.add(&mut displaced);
501 if !added && !player.has_infinite_materials() {
503 let _ = guard.run_unlocked(|| {
504 player.drop_item(displaced, true, Prediction::Predicted)
505 });
506 }
507 } else {
508 let Some(inventory) = guard.get_mut(player_inv_id) else {
509 unreachable!("the explicitly locked player inventory must be present");
510 };
511 inventory.set_item(inventory_slot, target_item.clone());
512 target_slot.set_by_player(&mut guard, source_item, &target_item);
513 if let Some(remainder) = target_slot.on_take(&mut guard, &target_item, player) {
514 player.add_item_or_drop_with_guard(
515 &mut guard,
516 remainder,
517 Prediction::Predicted,
518 );
519 }
520 }
521 }
522 }
523 }
524
525 fn do_pickup_all(&mut self, slot_index: usize, direction: FillDirection, player: &Player) {
528 let mut guard = self.behavior().lock_all_containers();
529
530 let behavior = self.behavior();
531 let slot = &behavior.slots()[slot_index];
532 let slot_has_item = !slot.get_item(&guard).is_empty();
533 let slot_may_pickup = slot.may_pickup(&guard, player);
534
535 if behavior.carried().is_empty() || (slot_has_item && slot_may_pickup) {
536 return;
537 }
538
539 let max_stack = behavior.carried().max_stack_size();
540 let carried_item = behavior.carried().clone();
541 let slot_count = behavior.slots().len();
542
543 let (start, step): (i32, i32) = match direction {
544 FillDirection::Forward => (0, 1),
545 FillDirection::Backward => (slot_count as i32 - 1, -1),
546 };
547
548 for pass in 0..2 {
550 let mut i = start;
551 while i >= 0 && i < slot_count as i32 && self.behavior().carried().count < max_stack {
552 let target_slot = &self.behavior().slots()[i as usize];
553 let target_item = target_slot.get_item(&guard).clone();
554
555 if !target_item.is_empty()
556 && can_item_quick_replace(&target_item, &carried_item, true)
557 && target_slot.may_pickup(&guard, player)
558 && self.can_take_item_for_pick_all(&carried_item, i as usize)
559 {
560 if pass != 0 || target_item.count != target_item.max_stack_size() {
562 let can_take = max_stack - self.behavior().carried().count;
563 let to_take = target_item.count.min(can_take);
564 let removed = target_slot.safe_take(&mut guard, to_take, can_take, player);
565 self.behavior_mut()
566 .carried_mut()
567 .grow(removed.count.min(can_take));
568 }
569 }
570
571 i += step;
572 }
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests;