1use crate::player::player_inventory::Prediction;
4use std::fmt;
5use std::{mem, sync::Arc};
6
7use steel_protocol::{
8 packet_traits::{ClientPacket, EncodedPacket},
9 packets::game::{
10 CContainerSetContent, CContainerSetData, CContainerSetSlot, CSetCursorItem, HashedPatchMap,
11 HashedStack,
12 },
13 utils::ConnectionProtocol,
14};
15use steel_registry::{
16 REGISTRY, RegistryEntry, RegistryExt as _, data_components::DataComponentPatch,
17 item_stack::ItemStack, menu_type::MenuTypeRef,
18};
19
20use crate::{
21 inventory::{
22 click::{DragKind, MouseButton, QuickCraft, can_item_quick_replace},
23 lock::{ContainerId, ContainerLockGuard, ContainerRef},
24 menu::builder::{FillDirection, MenuInstanceId},
25 slots::Slot,
26 },
27 player::{Player, PlayerConnection, connection::NetworkConnection},
28};
29
30pub struct MenuBehavior {
32 slots: Vec<Box<dyn Slot>>,
34 remote_slots: Vec<RemoteSlot>,
36 carried: ItemStack,
38 remote_carried: RemoteSlot,
40 container_id: u8,
42 state_id: u32,
44 menu_type: Option<MenuTypeRef>,
46 suppress_remote_updates: bool,
48 quickcraft: Option<DragKind>,
50 quickcraft_slots: Vec<usize>,
52 data_slots: Vec<i16>,
54 remote_data_slots: Vec<i16>,
56 container_refs: Vec<ContainerRef>,
57 instance: MenuInstanceId,
60}
61
62impl fmt::Debug for MenuBehavior {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.debug_struct("MenuBehavior")
65 .field("container_id", &self.container_id)
66 .field("state_id", &self.state_id)
67 .field("slot_count", &self.slots.len())
68 .field("carried", &self.carried)
69 .field("instance", &self.instance)
70 .finish_non_exhaustive()
71 }
72}
73
74impl MenuBehavior {
75 #[must_use]
77 pub(crate) fn new(
78 instance: MenuInstanceId,
79 slots: Vec<Box<dyn Slot>>,
80 container_id: u8,
81 menu_type: Option<MenuTypeRef>,
82 container_refs: Vec<ContainerRef>,
83 ) -> Self {
84 let slot_count = slots.len();
85 Self {
86 instance,
87 slots,
88 remote_slots: vec![RemoteSlot::Unknown; slot_count],
89 carried: ItemStack::empty(),
90 remote_carried: RemoteSlot::Unknown,
91 container_id,
92 state_id: 0,
93 menu_type,
94 suppress_remote_updates: false,
95 quickcraft: None,
96 quickcraft_slots: Vec::new(),
97 data_slots: Vec::new(),
98 remote_data_slots: Vec::new(),
99 container_refs,
100 }
101 }
102
103 #[must_use]
105 pub fn lock_all_containers(&self) -> ContainerLockGuard {
106 ContainerLockGuard::lock_all(&self.container_refs)
107 }
108
109 #[must_use]
111 pub(crate) fn lock_all_containers_with(&self, additional: ContainerRef) -> ContainerLockGuard {
112 let mut container_refs = self.container_refs.clone();
113 container_refs.push(additional);
114 ContainerLockGuard::lock_all(&container_refs)
115 }
116
117 #[must_use]
119 pub fn slots(&self) -> &[Box<dyn Slot>] {
120 &self.slots
121 }
122
123 #[must_use]
125 pub const fn carried(&self) -> &ItemStack {
126 &self.carried
127 }
128
129 #[must_use]
131 pub const fn carried_mut(&mut self) -> &mut ItemStack {
132 &mut self.carried
133 }
134
135 #[must_use]
137 pub const fn container_id(&self) -> u8 {
138 self.container_id
139 }
140
141 #[must_use]
143 pub const fn menu_type(&self) -> Option<MenuTypeRef> {
144 self.menu_type
145 }
146
147 #[must_use]
149 pub const fn state_id(&self) -> u32 {
150 self.state_id
151 }
152
153 #[must_use]
155 pub const fn quickcraft(&self) -> Option<DragKind> {
156 self.quickcraft
157 }
158
159 pub(crate) const fn instance(&self) -> MenuInstanceId {
161 self.instance
162 }
163
164 pub(crate) fn add_data_slot(&mut self, initial_value: i16) -> usize {
166 let index = self.data_slots.len();
167 self.data_slots.push(initial_value);
168 self.remote_data_slots.push(0);
169 index
170 }
171
172 #[must_use]
174 pub(crate) fn get_data(&self, index: usize) -> Option<i16> {
175 self.data_slots.get(index).copied()
176 }
177
178 pub(crate) fn set_data(&mut self, index: usize, value: i16) {
180 if let Some(slot) = self.data_slots.get_mut(index) {
181 *slot = value;
182 }
183 }
184
185 pub(crate) fn reset_quick_craft(&mut self) {
187 self.quickcraft = None;
188 self.quickcraft_slots.clear();
189 }
190
191 pub(crate) fn update_quick_move_source(
194 &self,
195 guard: &mut ContainerLockGuard,
196 slot_index: usize,
197 remaining: &ItemStack,
198 previous: &ItemStack,
199 ) {
200 let slot = &self.slots[slot_index];
201 if remaining.is_empty() {
202 slot.set_by_player(guard, ItemStack::empty(), previous);
203 } else {
204 if !slot.is_fake() {
205 *slot.get_item_mut(guard) = remaining.clone();
206 }
207 slot.set_changed(guard);
208 }
209 }
210
211 pub fn move_item_stack_to(
215 &self,
216 guard: &mut ContainerLockGuard,
217 source_slot: usize,
218 item_stack: &mut ItemStack,
219 start_slot: usize,
220 end_slot: usize,
221 direction: FillDirection,
222 ) -> bool {
223 if start_slot >= end_slot {
224 return false;
225 }
226
227 let backwards = direction == FillDirection::Backward;
228 let mut anything_changed = false;
229 let source_key = self.slots[source_slot].storage().physical_key();
230
231 if item_stack.is_stackable() {
233 let mut dest_slot = if backwards { end_slot - 1 } else { start_slot };
234
235 while !item_stack.is_empty() {
236 if backwards {
237 if dest_slot < start_slot {
238 break;
239 }
240 } else if dest_slot >= end_slot {
241 break;
242 }
243
244 let slot = &self.slots[dest_slot];
245 if dest_slot == source_slot
246 || source_key.is_some_and(|key| slot.storage().physical_key() == Some(key))
247 {
248 if backwards {
249 if dest_slot == 0 {
250 break;
251 }
252 dest_slot -= 1;
253 } else {
254 dest_slot += 1;
255 }
256 continue;
257 }
258 let target = slot.get_item(guard).clone();
259
260 if !target.is_empty()
261 && ItemStack::is_same_item_same_components(item_stack, &target)
262 {
263 let total_stack = target.count + item_stack.count;
264 let max_stack_size = slot.get_max_stack_size_for_item(guard, &target);
265
266 if total_stack <= max_stack_size {
267 item_stack.set_count(0);
268 slot.get_item_mut(guard).set_count(total_stack);
269 slot.set_changed(guard);
270 anything_changed = true;
271 } else if target.count < max_stack_size {
272 item_stack.shrink(max_stack_size - target.count);
273 slot.get_item_mut(guard).set_count(max_stack_size);
274 slot.set_changed(guard);
275 anything_changed = true;
276 }
277 }
278
279 if backwards {
280 if dest_slot == 0 {
281 break;
282 }
283 dest_slot -= 1;
284 } else {
285 dest_slot += 1;
286 }
287 }
288 }
289
290 if !item_stack.is_empty() {
292 let mut dest_slot = if backwards { end_slot - 1 } else { start_slot };
293
294 while if backwards {
295 dest_slot >= start_slot
296 } else {
297 dest_slot < end_slot
298 } {
299 let slot = &self.slots[dest_slot];
300 if dest_slot == source_slot
301 || source_key.is_some_and(|key| slot.storage().physical_key() == Some(key))
302 {
303 if backwards {
304 if dest_slot == 0 {
305 break;
306 }
307 dest_slot -= 1;
308 } else {
309 dest_slot += 1;
310 }
311 continue;
312 }
313 let target = slot.get_item(guard).clone();
314
315 if target.is_empty() && slot.may_place(item_stack) {
316 let max_stack_size = slot.get_max_stack_size_for_item(guard, item_stack);
317 let to_place = item_stack.count.min(max_stack_size);
318 slot.set_by_player(guard, item_stack.split(to_place), &ItemStack::empty());
319 slot.set_changed(guard);
320 anything_changed = true;
321 break;
322 }
323
324 if backwards {
325 if dest_slot == 0 {
326 break;
327 }
328 dest_slot -= 1;
329 } else {
330 dest_slot += 1;
331 }
332 }
333 }
334
335 anything_changed
336 }
337
338 pub(crate) const fn suppress_remote_updates(&mut self) {
340 self.suppress_remote_updates = true;
341 }
342
343 pub(crate) const fn resume_remote_updates(&mut self) {
345 self.suppress_remote_updates = false;
346 }
347
348 pub(crate) fn transfer_state(&mut self, other: &MenuBehavior) {
351 use rustc_hash::FxHashMap;
352
353 let mut other_slots: FxHashMap<(ContainerId, usize), usize> = FxHashMap::default();
354 for (slot_index, slot) in other.slots.iter().enumerate() {
355 if slot.is_fake() {
356 continue;
357 }
358 if let Some(key) = slot.storage().physical_key() {
359 other_slots.insert(key, slot_index);
360 }
361 }
362
363 for (slot_index, slot) in self.slots.iter().enumerate() {
364 if slot.is_fake() {
365 continue;
366 }
367 if let Some(key) = slot.storage().physical_key()
368 && let Some(&other_slot_index) = other_slots.get(&key)
369 {
370 self.remote_slots[slot_index] = other.remote_slots[other_slot_index].clone();
371 }
372 }
373 }
374
375 #[must_use]
377 pub const fn slot_count(&self) -> usize {
378 self.slots.len()
379 }
380
381 const fn increment_state_id(&mut self) -> u32 {
383 self.state_id = self.state_id.wrapping_add(1) & 0x7FFF; self.state_id
385 }
386
387 fn send_packet<P: ClientPacket>(connection: &Arc<PlayerConnection>, packet: P) {
389 let encoded =
390 EncodedPacket::from_bare(packet, connection.compression(), ConnectionProtocol::Play)
391 .expect("Failed to encode packet");
392 connection.send_encoded(encoded);
393 }
394
395 pub(crate) fn send_all_data_to_remote(&mut self, connection: &Arc<PlayerConnection>) {
398 let guard = self.lock_all_containers();
399
400 let items: Vec<ItemStack> = self
401 .slots
402 .iter()
403 .map(|slot| slot.get_item(&guard).clone())
404 .collect();
405 drop(guard);
406 let state_id = self.increment_state_id();
407
408 for (remote, item) in self.remote_slots.iter_mut().zip(&items) {
409 remote.force(item);
410 }
411 self.remote_carried.force(&self.carried);
412
413 let packet = CContainerSetContent {
414 container_id: i32::from(self.container_id),
415 state_id: state_id as i32,
416 items,
417 carried_item: self.carried.clone(),
418 };
419
420 Self::send_packet(connection, packet);
421
422 for i in 0..self.data_slots.len() {
423 self.remote_data_slots[i] = self.data_slots[i];
424 let packet = CContainerSetData {
425 container_id: i32::from(self.container_id),
426 id: i as i16,
427 value: self.data_slots[i],
428 };
429 Self::send_packet(connection, packet);
430 }
431 }
432
433 pub fn broadcast_changes(&mut self, connection: &Arc<PlayerConnection>) {
436 let guard = self.lock_all_containers();
437
438 let mut changed: Vec<(usize, ItemStack)> = Vec::new();
439 for index in 0..self.slots.len() {
440 let item = self.slots[index].get_item(&guard);
441 if self.remote_slots[index].matches(item) {
442 if matches!(self.remote_slots[index], RemoteSlot::Hashed(_)) {
444 self.remote_slots[index] = RemoteSlot::Known(item.clone());
445 }
446 } else {
447 changed.push((index, item.clone()));
448 }
449 }
450 drop(guard);
451
452 for (index, item) in changed {
453 self.synchronize_slot_to_remote(index, item, connection);
454 }
455
456 if self.remote_carried.matches(&self.carried) {
457 if matches!(self.remote_carried, RemoteSlot::Hashed(_)) {
458 self.remote_carried = RemoteSlot::Known(self.carried.clone());
459 }
460 } else {
461 self.synchronize_carried_to_remote(connection);
462 }
463
464 for i in 0..self.data_slots.len() {
465 self.synchronize_data_slot_to_remote(i, connection);
466 }
467 }
468
469 fn synchronize_data_slot_to_remote(
471 &mut self,
472 index: usize,
473 connection: &Arc<PlayerConnection>,
474 ) {
475 if self.suppress_remote_updates || index >= self.data_slots.len() {
476 return;
477 }
478
479 let current = self.data_slots[index];
480 let remote = self.remote_data_slots[index];
481
482 if current != remote {
483 self.remote_data_slots[index] = current;
484 let packet = CContainerSetData {
485 container_id: i32::from(self.container_id),
486 id: index as i16,
487 value: current,
488 };
489 Self::send_packet(connection, packet);
490 }
491 }
492
493 fn synchronize_slot_to_remote(
496 &mut self,
497 slot: usize,
498 item: ItemStack,
499 connection: &Arc<PlayerConnection>,
500 ) {
501 if self.suppress_remote_updates {
502 return;
503 }
504
505 let state_id = self.increment_state_id();
506
507 let packet = CContainerSetSlot {
508 container_id: i32::from(self.container_id),
509 state_id: state_id as i32,
510 slot: slot as i16,
511 item_stack: item.clone(),
512 };
513
514 Self::send_packet(connection, packet);
515 self.remote_slots[slot] = RemoteSlot::Known(item);
516 }
517
518 fn synchronize_carried_to_remote(&mut self, connection: &Arc<PlayerConnection>) {
520 if self.suppress_remote_updates {
521 return;
522 }
523
524 let packet = CSetCursorItem {
525 item_stack: self.carried.clone(),
526 };
527
528 Self::send_packet(connection, packet);
529 self.remote_carried.force(&self.carried);
530 }
531
532 pub(crate) fn set_remote_slot_known(&mut self, slot: usize, item: &ItemStack) {
535 if slot < self.remote_slots.len() {
536 self.remote_slots[slot].force(item);
537 }
538 }
539
540 pub(crate) fn mark_remote_slot_unknown(&mut self, slot: usize) {
542 if slot < self.remote_slots.len() {
543 self.remote_slots[slot] = RemoteSlot::Unknown;
544 }
545 }
546
547 pub(crate) fn set_remote_slot(&mut self, slot: usize, hash: HashedStack) {
549 if slot < self.remote_slots.len() {
550 self.remote_slots[slot].receive(hash);
551 } else {
552 log::debug!(
553 "Incorrect slot index: {} available slots: {}",
554 slot,
555 self.remote_slots.len()
556 );
557 }
558 }
559
560 pub(crate) fn set_remote_carried(&mut self, hash: HashedStack) {
562 self.remote_carried.receive(hash);
563 }
564
565 pub(crate) fn do_quick_craft(
568 &mut self,
569 action: QuickCraft,
570 has_infinite_materials: bool,
571 player: &Player,
572 can_drag_to: &impl Fn(usize) -> bool,
573 ) {
574 let valid_transition = match action {
576 QuickCraft::Start { .. } => self.quickcraft.is_none(),
577 QuickCraft::AddSlot { .. } | QuickCraft::End => self.quickcraft.is_some(),
578 };
579 if !valid_transition {
580 self.reset_quick_craft();
581 return;
582 }
583
584 if self.carried.is_empty() {
585 self.reset_quick_craft();
586 return;
587 }
588
589 match action {
590 QuickCraft::Start { kind } => {
591 if kind == DragKind::Clone && !has_infinite_materials {
593 self.reset_quick_craft();
594 return;
595 }
596 self.quickcraft = Some(kind);
597 self.quickcraft_slots.clear();
598 }
599 QuickCraft::AddSlot { slot: slot_index } => {
600 let slot = &self.slots[slot_index];
601
602 let guard = self.lock_all_containers();
603 let slot_item = slot.get_item(&guard).clone();
604
605 if can_item_quick_replace(&slot_item, &self.carried, true)
606 && slot.may_place(&self.carried)
607 && (self.quickcraft == Some(DragKind::Clone)
608 || self.carried.count > self.quickcraft_slots.len() as i32)
609 && can_drag_to(slot_index)
610 && !self.quickcraft_slots.contains(&slot_index)
611 {
612 self.quickcraft_slots.push(slot_index);
613 }
614 }
615 QuickCraft::End => self.finish_quick_craft(player, can_drag_to),
616 }
617 }
618
619 fn finish_quick_craft(&mut self, player: &Player, can_drag_to: &impl Fn(usize) -> bool) {
622 let Some(kind) = self.quickcraft else {
623 self.reset_quick_craft();
624 return;
625 };
626 if !self.quickcraft_slots.is_empty() {
627 if self.quickcraft_slots.len() == 1 {
628 let slot = self.quickcraft_slots[0];
630 self.reset_quick_craft();
631 let button = match kind {
632 DragKind::Left => MouseButton::Left,
633 DragKind::Right => MouseButton::Right,
634 DragKind::Clone => return,
637 };
638 self.do_pickup(slot, button, player);
639 return;
640 }
641
642 let source = self.carried.clone();
643 if source.is_empty() {
644 self.reset_quick_craft();
645 return;
646 }
647
648 let mut remaining = self.carried.count;
649 let quickcraft_slots = self.quickcraft_slots.clone();
650
651 let mut guard = self.lock_all_containers();
652
653 for &slot_index in &quickcraft_slots {
654 let slot = &self.slots[slot_index];
655 let slot_item = slot.get_item(&guard).clone();
656
657 if can_item_quick_replace(&slot_item, &self.carried, true)
658 && slot.may_place(&self.carried)
659 && (kind == DragKind::Clone
660 || self.carried.count >= quickcraft_slots.len() as i32)
661 && can_drag_to(slot_index)
662 {
663 let current_count = if slot_item.is_empty() {
664 0
665 } else {
666 slot_item.count
667 };
668 let max_size = source
669 .max_stack_size()
670 .min(slot.get_max_stack_size_for_item(&guard, &source));
671 let place_count = kind.place_count(quickcraft_slots.len(), &source);
672 let new_count = (place_count + current_count).min(max_size);
673 remaining -= new_count - current_count;
674
675 let mut new_item = source.clone();
676 new_item.set_count(new_count);
677 slot.set_by_player(&mut guard, new_item, &slot_item);
678 }
679 }
680
681 let mut new_carried = source;
682 new_carried.set_count(remaining);
683 self.carried = new_carried;
684 }
685
686 self.reset_quick_craft();
687 }
688
689 pub(crate) fn drop_carried(&mut self, button: MouseButton, player: &Player) {
692 if self.carried.is_empty() {
693 return;
694 }
695 match button {
696 MouseButton::Left => {
697 let to_drop = mem::take(&mut self.carried);
698 let _ = player.drop_item(to_drop, true, Prediction::Predicted);
699 }
700 MouseButton::Right => {
701 let _ = player.drop_item(self.carried.split(1), true, Prediction::Predicted);
702 }
703 }
704 }
705
706 pub(crate) fn do_pickup(&mut self, slot_index: usize, button: MouseButton, player: &Player) {
708 let mut guard = self.lock_all_containers();
709
710 let slot = &self.slots[slot_index];
711
712 let slot_item = slot.get_item(&guard).clone();
713 let carried = mem::take(&mut self.carried);
714
715 if slot_item.is_empty() {
716 if !carried.is_empty() && slot.may_place(&carried) {
718 let requested = if button == MouseButton::Left {
719 carried.count
720 } else {
721 1
722 };
723 self.carried = slot.safe_insert(&mut guard, carried, requested);
724 } else {
725 self.carried = carried;
726 }
727 } else if carried.is_empty() {
728 let amount = if button == MouseButton::Left {
730 slot_item.count
731 } else {
732 (slot_item.count + 1) / 2
733 };
734
735 if let Some(taken) = slot.try_remove(&mut guard, amount, i32::MAX, player) {
736 if let Some(remainder) = slot.on_take(&mut guard, &taken, player) {
737 player.add_item_or_drop_with_guard(
738 &mut guard,
739 remainder,
740 Prediction::Predicted,
741 );
742 }
743 self.carried = taken;
744 }
745 } else if ItemStack::is_same_item_same_components(&slot_item, &carried) {
746 if slot.may_pickup(&guard, player) && slot.may_place(&carried) {
748 let requested = if button == MouseButton::Left {
749 carried.count
750 } else {
751 1
752 };
753 self.carried = slot.safe_insert(&mut guard, carried, requested);
754 } else {
755 if slot.may_pickup(&guard, player) {
757 let space = carried.max_stack_size() - carried.count;
758 if space > 0 {
759 if let Some(taken) =
760 slot.try_remove(&mut guard, slot_item.count, space, player)
761 {
762 if let Some(remainder) = slot.on_take(&mut guard, &taken, player) {
763 player.add_item_or_drop_with_guard(
764 &mut guard,
765 remainder,
766 Prediction::Predicted,
767 );
768 }
769 let mut new_carried = carried;
770 new_carried.grow(taken.count);
771 self.carried = new_carried;
772 } else {
773 self.carried = carried;
774 }
775 } else {
776 self.carried = carried;
777 }
778 } else {
779 self.carried = carried;
780 }
781 }
782 } else {
783 if slot.may_pickup(&guard, player) && slot.may_place(&carried) {
785 if carried.count <= slot.get_max_stack_size_for_item(&guard, &carried) {
786 slot.set_by_player(&mut guard, carried, &slot_item);
787 self.carried = slot_item;
788 } else {
789 self.carried = carried;
790 }
791 } else {
792 self.carried = carried;
793 }
794 }
795
796 slot.set_changed(&mut guard);
797 }
798
799 pub(crate) fn do_clone(&mut self, slot_index: usize, has_infinite_materials: bool) {
801 if !has_infinite_materials || !self.carried.is_empty() {
802 return;
803 }
804
805 let guard = self.lock_all_containers();
806 let slot = &self.slots[slot_index];
807 let slot_item = slot.get_item(&guard);
808
809 if !slot_item.is_empty() {
810 self.carried = slot_item.copy_with_count(slot_item.max_stack_size());
811 }
812 }
813
814 pub(crate) fn do_throw(&mut self, slot_index: usize, whole_stack: bool, player: &Player) {
817 if !self.carried.is_empty() {
818 return;
819 }
820
821 let mut guard = self.lock_all_containers();
822 let slot = &self.slots[slot_index];
823
824 if !slot.may_pickup(&guard, player) {
825 return;
826 }
827
828 if !player.can_drop_items() {
829 return;
830 }
831
832 let amount = if whole_stack {
833 slot.get_item(&guard).count
834 } else {
835 1
836 };
837
838 let dropped = slot.safe_take(&mut guard, amount, i32::MAX, player);
839 if !dropped.is_empty() {
840 let _ = guard
841 .run_unlocked(|| player.drop_item(dropped.clone(), true, Prediction::Predicted));
842 }
843
844 if whole_stack {
846 loop {
847 if !slot.may_pickup(&guard, player) {
848 break;
849 }
850 if !player.can_drop_items() {
851 break;
852 }
853 let current_item = slot.get_item(&guard).clone();
854 if current_item.is_empty() || !ItemStack::is_same_item(¤t_item, &dropped) {
855 break;
856 }
857 let more_dropped = slot.safe_take(&mut guard, current_item.count, i32::MAX, player);
858 if more_dropped.is_empty() {
859 break;
860 }
861 let _ = guard
862 .run_unlocked(|| player.drop_item(more_dropped, true, Prediction::Predicted));
863 }
864 }
865 }
866}
867
868#[derive(Debug, Clone, Default)]
870pub(crate) enum RemoteSlot {
871 #[default]
873 Unknown,
874 Known(ItemStack),
876 Hashed(HashedStack),
878}
879
880impl RemoteSlot {
881 pub fn force(&mut self, item: &ItemStack) {
883 *self = Self::Known(item.clone());
884 }
885
886 pub fn receive(&mut self, hash: HashedStack) {
888 *self = Self::Hashed(hash);
889 }
890
891 #[must_use]
893 pub fn matches(&self, local: &ItemStack) -> bool {
894 match self {
895 Self::Unknown => false,
896 Self::Known(remote) => ItemStack::matches(remote, local),
897 Self::Hashed(hash) => hashed_stack_matches(hash, local),
898 }
899 }
900}
901
902fn hashed_stack_matches(hash: &HashedStack, item: &ItemStack) -> bool {
904 match hash {
905 HashedStack::Empty => {
906 if !item.is_empty() {
907 log::debug!("HashedStack mismatch: client sent Empty, server has {item}");
908 return false;
909 }
910 true
911 }
912 HashedStack::Item {
913 item_id,
914 count,
915 components,
916 } => {
917 if item.is_empty() {
918 log::debug!(
919 "HashedStack mismatch: client sent item_id={item_id} count={count}, server has Empty"
920 );
921 return false;
922 }
923
924 let local_id = item.item.id() as i32;
925 if local_id != *item_id {
926 log::debug!(
927 "HashedStack mismatch: item_id client={item_id} server={local_id} ({})",
928 item.item.key
929 );
930 return false;
931 }
932 if item.count != *count {
933 log::debug!(
934 "HashedStack mismatch: count client={count} server={} for {}",
935 item.count,
936 item.item.key
937 );
938 return false;
939 }
940
941 validate_component_hashes(components, item.patch())
942 }
943 }
944}
945
946fn validate_component_hashes(hashed: &HashedPatchMap, patch: &DataComponentPatch) -> bool {
948 use rustc_hash::FxHashSet;
949 use steel_registry::data_components::ComponentPatchEntry;
950
951 let local_removed: FxHashSet<i32> = patch
953 .iter_removed()
954 .filter_map(|k| REGISTRY.data_components.id_from_key(k).map(|id| id as i32))
955 .collect();
956 let hashed_removed: FxHashSet<i32> = hashed.removed_components.iter().copied().collect();
957
958 if local_removed != hashed_removed {
959 log::debug!(
960 "HashedStack mismatch: removed components differ - client={hashed_removed:?} server={local_removed:?}"
961 );
962 return false;
963 }
964
965 for (key, entry) in patch.iter() {
967 if let ComponentPatchEntry::Set(value) = entry {
968 let Some(id) = REGISTRY.data_components.id_from_key(key) else {
969 continue;
970 };
971 let id = id as i32;
972
973 let Some(&expected_hash) = hashed.added_components.get(&id) else {
974 log::debug!(
975 "HashedStack mismatch: client missing hash for component {key} (id={id})"
976 );
977 return false;
978 };
979
980 let Some(component_type) = REGISTRY.data_components.by_id(id as usize) else {
981 log::debug!("HashedStack mismatch: component {key} has no registry entry");
982 return false;
983 };
984 let Ok(actual_hash) = component_type.compute_hash(value) else {
985 log::debug!("HashedStack mismatch: component {key} is not persistently hashable");
986 return false;
987 };
988
989 if actual_hash != expected_hash {
990 log::debug!(
991 "HashedStack mismatch: component {key} hash differs - client={expected_hash} server={actual_hash}"
992 );
993 return false;
994 }
995 }
996 }
997
998 for &id in hashed.added_components.keys() {
1000 let Some(key) = REGISTRY.data_components.get_key_by_id(id as usize) else {
1001 log::debug!("HashedStack mismatch: client sent unknown component id={id}");
1002 return false;
1003 };
1004 if !matches!(patch.get_entry(key), Some(ComponentPatchEntry::Set(_))) {
1005 log::debug!(
1006 "HashedStack mismatch: client claims component {key} exists but server doesn't have it"
1007 );
1008 return false;
1009 }
1010 }
1011
1012 true
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use std::slice;
1018 use std::sync::Arc;
1019
1020 use steel_registry::{init_vanilla_registry, item_stack::ItemStack, vanilla_items};
1021 use steel_utils::{
1022 DowncastType, DowncastTypeKey,
1023 locks::{IntoShared, SyncMutex},
1024 };
1025
1026 use crate::inventory::{
1027 container::{Container, SimpleContainer},
1028 lock::ContainerRef,
1029 menu::{
1030 Menu,
1031 builder::{FillDirection, MenuBuilder},
1032 kinds::BasicKind,
1033 },
1034 slots::{NormalSlot, RestrictedSlot, Slot},
1035 };
1036
1037 struct RecordingContainer {
1038 item: ItemStack,
1039 set_item_calls: usize,
1040 set_changed_calls: usize,
1041 }
1042
1043 unsafe impl DowncastType for RecordingContainer {
1045 const TYPE_KEY: DowncastTypeKey =
1046 DowncastTypeKey::new("steel:test/container/quick_move_recording");
1047 }
1048
1049 impl Container for RecordingContainer {
1050 fn items(&self) -> &[ItemStack] {
1051 slice::from_ref(&self.item)
1052 }
1053
1054 fn items_mut(&mut self) -> &mut [ItemStack] {
1055 slice::from_mut(&mut self.item)
1056 }
1057
1058 fn set_item(&mut self, _slot: usize, stack: ItemStack) {
1059 self.item = stack;
1060 self.set_item_calls += 1;
1061 }
1062
1063 fn set_changed(&mut self) {
1064 self.set_changed_calls += 1;
1065 }
1066 }
1067
1068 fn recording_menu() -> (Menu, ContainerRef) {
1069 init_vanilla_registry();
1070 let container = Arc::new(SyncMutex::new(RecordingContainer {
1071 item: ItemStack::with_count(&vanilla_items::STONE, 5),
1072 set_item_calls: 0,
1073 set_changed_calls: 0,
1074 }));
1075 let container_ref = ContainerRef::from(container);
1076 let mut builder = MenuBuilder::new(None, 1);
1077 builder.section(container_ref.clone(), 1);
1078 (builder.build(BasicKind), container_ref)
1079 }
1080
1081 #[test]
1082 fn quick_move_source_persists_cloned_remainder_with_vanilla_callbacks() {
1083 let (menu, container_ref) = recording_menu();
1084 let behavior = menu.behavior();
1085 let container_id = container_ref.container_id();
1086 let mut guard = behavior.lock_all_containers();
1087 let previous = ItemStack::with_count(&vanilla_items::STONE, 5);
1088 let remainder = ItemStack::with_count(&vanilla_items::STONE, 2);
1089
1090 behavior.update_quick_move_source(&mut guard, 0, &remainder, &previous);
1091 let state = guard
1092 .get_typed::<RecordingContainer>(container_id)
1093 .expect("recording container should remain locked");
1094 assert_eq!(state.item.count(), 2);
1095 assert_eq!(state.set_item_calls, 0);
1096 assert_eq!(state.set_changed_calls, 1);
1097
1098 behavior.update_quick_move_source(&mut guard, 0, &ItemStack::empty(), &remainder);
1099 let state = guard
1100 .get_typed::<RecordingContainer>(container_id)
1101 .expect("recording container should remain locked");
1102 assert!(state.item.is_empty());
1103 assert_eq!(state.set_item_calls, 1);
1104 assert_eq!(state.set_changed_calls, 2);
1105 }
1106
1107 #[test]
1108 fn safe_insert_uses_set_by_player_before_the_menu_notification() {
1109 let (menu, container_ref) = recording_menu();
1110 let behavior = menu.behavior();
1111 let container_id = container_ref.container_id();
1112 let mut guard = behavior.lock_all_containers();
1113
1114 let remainder = behavior.slots()[0].safe_insert(
1115 &mut guard,
1116 ItemStack::with_count(&vanilla_items::STONE, 3),
1117 3,
1118 );
1119 behavior.slots()[0].set_changed(&mut guard);
1120
1121 assert!(remainder.is_empty());
1122 let state = guard
1123 .get_typed::<RecordingContainer>(container_id)
1124 .expect("recording container should remain locked");
1125 assert_eq!(state.item.count(), 8);
1126 assert_eq!(state.set_item_calls, 1);
1127 assert_eq!(state.set_changed_calls, 2);
1128 }
1129
1130 #[test]
1131 fn quick_move_skips_aliases_of_the_source_slot() {
1132 init_vanilla_registry();
1133 let container = SimpleContainer::new(2).into_shared();
1134 container
1135 .lock()
1136 .set_item(0, ItemStack::with_count(&vanilla_items::STONE, 5));
1137 let container_ref = ContainerRef::from(container);
1138
1139 let mut builder = MenuBuilder::new(None, 1);
1140 builder.custom_boxed_section([
1141 Box::new(NormalSlot::new(container_ref.clone(), 0)) as Box<dyn Slot>,
1142 Box::new(RestrictedSlot::new(container_ref.clone(), 0, |_, _| true)),
1143 Box::new(NormalSlot::new(container_ref.clone(), 1)),
1144 ]);
1145 let menu = builder.build(BasicKind);
1146 let behavior = menu.behavior();
1147 let mut guard = behavior.lock_all_containers();
1148 let clicked = behavior.slots()[0].get_item(&guard).clone();
1149 let mut remaining = clicked.clone();
1150
1151 assert!(behavior.move_item_stack_to(
1152 &mut guard,
1153 0,
1154 &mut remaining,
1155 1,
1156 3,
1157 FillDirection::Forward,
1158 ));
1159 behavior.update_quick_move_source(&mut guard, 0, &remaining, &clicked);
1160
1161 let container = guard
1162 .get(container_ref.container_id())
1163 .expect("simple container should remain locked");
1164 assert!(container.get_item(0).is_empty());
1165 assert_eq!(container.get_item(1).count(), 5);
1166 }
1167}