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