1use valence_server::protocol::anyhow::{self, bail, ensure};
2use valence_server::protocol::packets::play::click_slot_c2s::ClickMode;
3use valence_server::protocol::packets::play::ClickSlotC2s;
4
5use super::{CursorItem, Inventory, InventoryWindow};
6use crate::player_inventory::PlayerInventory;
7
8pub(super) fn validate_click_slot_packet(
10 packet: &ClickSlotC2s,
11 player_inventory: &Inventory,
12 open_inventory: Option<&Inventory>,
13 cursor_item: &CursorItem,
14) -> anyhow::Result<()> {
15 ensure!(
16 (packet.window_id == 0) == open_inventory.is_none(),
17 "window id and open inventory mismatch: window_id: {} open_inventory: {}",
18 packet.window_id,
19 open_inventory.is_some()
20 );
21
22 let max_slot = if let Some(open_inv) = open_inventory {
23 PlayerInventory::MAIN_SIZE + open_inv.slot_count()
26 } else {
27 player_inventory.slot_count()
28 };
29
30 ensure!(
32 packet.slot_changes.iter().all(|s| {
33 if !(0..=max_slot).contains(&(s.idx as u16)) {
34 return false;
35 }
36
37 if !s.stack.is_empty() {
38 let max_stack_size = s.stack.item.max_stack().max(s.stack.count);
39 if !(1..=max_stack_size).contains(&(s.stack.count)) {
40 return false;
41 }
42 }
43
44 true
45 }),
46 "invalid slot ids or item counts"
47 );
48
49 if !packet.carried_item.is_empty() {
51 let carried_item = &packet.carried_item;
52
53 let max_stack_size = carried_item.item.max_stack().max(carried_item.count);
54 ensure!(
55 (1..=max_stack_size).contains(&carried_item.count),
56 "invalid carried item count"
57 );
58 }
59
60 match packet.mode {
61 ClickMode::Click => {
62 ensure!((0..=1).contains(&packet.button), "invalid button");
63 ensure!(
64 (0..=max_slot).contains(&(packet.slot_idx as u16))
65 || packet.slot_idx == -999
66 || packet.slot_idx == -1,
67 "invalid slot index"
68 )
69 }
70 ClickMode::ShiftClick => {
71 ensure!((0..=1).contains(&packet.button), "invalid button");
72 ensure!(
73 packet.carried_item.is_empty(),
74 "carried item must be empty for a hotbar swap"
75 );
76 ensure!(
77 (0..=max_slot).contains(&(packet.slot_idx as u16)),
78 "invalid slot index"
79 )
80 }
81 ClickMode::Hotbar => {
82 ensure!(matches!(packet.button, 0..=8 | 40), "invalid button");
83 ensure!(
84 packet.carried_item.is_empty(),
85 "carried item must be empty for a hotbar swap"
86 );
87 }
88 ClickMode::CreativeMiddleClick => {
89 ensure!(packet.button == 2, "invalid button");
90 ensure!(
91 (0..=max_slot).contains(&(packet.slot_idx as u16)),
92 "invalid slot index"
93 )
94 }
95 ClickMode::DropKey => {
96 ensure!((0..=1).contains(&packet.button), "invalid button");
97 ensure!(
98 packet.carried_item.is_empty(),
99 "carried item must be empty for an item drop"
100 );
101 ensure!(
102 (0..=max_slot).contains(&(packet.slot_idx as u16)) || packet.slot_idx == -999,
103 "invalid slot index"
104 )
105 }
106 ClickMode::Drag => {
107 ensure!(
108 matches!(packet.button, 0..=2 | 4..=6 | 8..=10),
109 "invalid button"
110 );
111 ensure!(
112 (0..=max_slot).contains(&(packet.slot_idx as u16)) || packet.slot_idx == -999,
113 "invalid slot index"
114 )
115 }
116 ClickMode::DoubleClick => ensure!(packet.button == 0, "invalid button"),
117 }
118
119 let window = InventoryWindow {
122 player_inventory,
123 open_inventory,
124 };
125
126 match packet.mode {
127 ClickMode::Click => {
128 if packet.slot_idx == -1 {
129 ensure!(
131 packet.slot_changes.is_empty(),
132 "slot modifications must be empty"
133 );
134
135 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
136 ensure!(
137 count_deltas == 0,
138 "invalid item delta: expected 0, got {}",
139 count_deltas
140 );
141 } else if packet.slot_idx == -999 {
142 ensure!(
144 packet.slot_changes.is_empty(),
145 "slot modifications must be empty"
146 );
147
148 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
150 let expected_delta = match packet.button {
151 1 => -1,
152 0 => {
153 if !cursor_item.is_empty() {
154 -i32::from(cursor_item.0.count)
155 } else {
156 0
157 }
158 }
159 _ => unreachable!(),
160 };
161 ensure!(
162 count_deltas == expected_delta,
163 "invalid item delta: expected {}, got {}",
164 expected_delta,
165 count_deltas
166 );
167 } else {
168 if packet.slot_changes.is_empty() {
170 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
171 ensure!(
172 count_deltas == 0,
173 "invalid item delta: expected 0, got {}",
174 count_deltas
175 );
176 } else {
177 ensure!(
178 packet.slot_changes.len() == 1,
179 "click must modify one slot, got {}",
180 packet.slot_changes.len()
181 );
182
183 let old_slot = window.slot(packet.slot_changes[0].idx as u16);
184 let should_swap: bool = packet.button == 0
188 && match (!old_slot.is_empty(), !cursor_item.is_empty()) {
189 (true, true) => old_slot.item != cursor_item.item,
190 (true, false) => true,
191 (false, true) => cursor_item.count <= cursor_item.item.max_stack(),
192 (false, false) => false,
193 };
194
195 if should_swap {
196 ensure!(
198 old_slot.item == packet.carried_item.item
201 && old_slot.count == packet.carried_item.count
202 && cursor_item.0 == packet.slot_changes[0].stack,
203 "swapped items must match"
204 );
205 } else {
206 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
208 ensure!(
209 count_deltas == 0,
210 "invalid item delta for stack merge: {}",
211 count_deltas
212 );
213 }
214 }
215 }
216 }
217 ClickMode::ShiftClick => {
218 if packet.slot_changes.is_empty() {
220 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
221 ensure!(
222 count_deltas == 0,
223 "invalid item delta: expected 0, got {}",
224 count_deltas
225 );
226 } else {
227 ensure!(
228 (2..=3).contains(&packet.slot_changes.len()),
229 "shift click must modify 2 or 3 slots, got {}",
230 packet.slot_changes.len()
231 );
232
233 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
234 ensure!(
235 count_deltas == 0,
236 "invalid item delta: expected 0, got {}",
237 count_deltas
238 );
239
240 let Some(item_kind) = packet
241 .slot_changes
242 .iter()
243 .find(|s| !s.stack.is_empty())
244 .map(|s| s.stack.item)
245 else {
246 bail!("shift click must move an item");
247 };
248
249 let old_slot_kind = window.slot(packet.slot_idx as u16).item;
250 ensure!(
251 old_slot_kind == item_kind,
252 "shift click must move the same item kind as modified slots"
253 );
254
255 ensure!(
257 packet
258 .slot_changes
259 .iter()
260 .filter(|s| !s.stack.is_empty())
261 .all(|s| s.stack.item == item_kind),
262 "shift click must move the same item kind"
263 );
264 }
265 }
266
267 ClickMode::Hotbar => {
268 if packet.slot_changes.is_empty() {
269 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
270 ensure!(
271 count_deltas == 0,
272 "invalid item delta: expected 0, got {}",
273 count_deltas
274 );
275 } else {
276 ensure!(
277 packet.slot_changes.len() == 2,
278 "hotbar swap must modify two slots, got {}",
279 packet.slot_changes.len()
280 );
281
282 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
283 ensure!(
284 count_deltas == 0,
285 "invalid item delta: expected 0, got {}",
286 count_deltas
287 );
288
289 let old_slots = [
291 window.slot(packet.slot_changes[0].idx as u16),
292 window.slot(packet.slot_changes[1].idx as u16),
293 ];
294 ensure!(
297 old_slots
298 .iter()
299 .any(|s| s.item == packet.slot_changes[0].stack.item
300 && s.count == packet.slot_changes[0].stack.count)
301 && old_slots
302 .iter()
303 .any(|s| s.item == packet.slot_changes[1].stack.item
304 && s.count == packet.slot_changes[1].stack.count),
305 "swapped items must match"
306 );
307 }
308 }
309 ClickMode::CreativeMiddleClick => {}
310 ClickMode::DropKey => {
311 if packet.slot_changes.is_empty() {
312 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
313 ensure!(
314 count_deltas == 0,
315 "invalid item delta: expected 0, got {}",
316 count_deltas
317 );
318 } else {
319 ensure!(
320 packet.slot_changes.len() == 1,
321 "drop key must modify exactly one slot"
322 );
323 ensure!(
324 packet.slot_idx == packet.slot_changes.first().map_or(-2, |s| s.idx),
325 "slot index does not match modified slot"
326 );
327
328 let old_slot = window.slot(packet.slot_idx as u16);
329 let new_slot = &packet.slot_changes[0].stack;
330 let is_transmuting = match (!old_slot.is_empty(), !new_slot.is_empty()) {
331 (true, true) => old_slot.item != new_slot.item,
335 (_, false) => false,
336 (false, true) => true,
337 };
338 ensure!(!is_transmuting, "transmuting items is not allowed");
339
340 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
341
342 let expected_delta = match packet.button {
343 0 => -1,
344 1 => {
345 if !old_slot.is_empty() {
346 -i32::from(old_slot.count)
347 } else {
348 0
349 }
350 }
351 _ => unreachable!(),
352 };
353 ensure!(
354 count_deltas == expected_delta,
355 "invalid item delta: expected {}, got {}",
356 expected_delta,
357 count_deltas
358 );
359 }
360 }
361 ClickMode::Drag => {
362 if matches!(packet.button, 2 | 6 | 10) {
363 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
364 ensure!(
365 count_deltas == 0,
366 "invalid item delta: expected 0, got {}",
367 count_deltas
368 );
369 } else {
370 ensure!(packet.slot_changes.is_empty() && packet.carried_item == cursor_item.0);
371 }
372 }
373 ClickMode::DoubleClick => {
374 let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
375 ensure!(
376 count_deltas == 0,
377 "invalid item delta: expected 0, got {}",
378 count_deltas
379 );
380 }
381 }
382
383 Ok(())
384}
385
386fn calculate_net_item_delta(
392 packet: &ClickSlotC2s,
393 window: &InventoryWindow,
394 cursor_item: &CursorItem,
395) -> i32 {
396 let mut net_item_delta: i32 = 0;
397
398 for slot in packet.slot_changes.iter() {
399 let old_slot = window.slot(slot.idx as u16);
400 let new_slot = &slot.stack;
401
402 net_item_delta += match (!old_slot.is_empty(), !new_slot.is_empty()) {
403 (true, true) => i32::from(new_slot.count) - i32::from(old_slot.count),
404 (true, false) => -i32::from(old_slot.count),
405 (false, true) => i32::from(new_slot.count),
406 (false, false) => 0,
407 };
408 }
409
410 net_item_delta += match (!cursor_item.is_empty(), !packet.carried_item.is_empty()) {
411 (true, true) => i32::from(packet.carried_item.count) - i32::from(cursor_item.count),
412 (true, false) => -i32::from(cursor_item.count),
413 (false, true) => i32::from(packet.carried_item.count),
414 (false, false) => 0,
415 };
416
417 net_item_delta
418}
419
420#[cfg(test)]
421mod tests {
422 use valence_server::nbt::Compound;
423 use valence_server::nbt::Value::Int;
424 use valence_server::protocol::packets::play::click_slot_c2s::SlotChange;
425 use valence_server::protocol::VarInt;
426 use valence_server::{ItemKind, ItemStack};
427
428 use super::*;
429 use crate::InventoryKind;
430
431 #[test]
432 fn net_item_delta_1() {
433 let drag_packet = ClickSlotC2s {
434 window_id: 2,
435 state_id: VarInt(14),
436 slot_idx: -999,
437 button: 2,
438 mode: ClickMode::Drag,
439 slot_changes: vec![
440 SlotChange {
441 idx: 4,
442 stack: ItemStack::new(ItemKind::Diamond, 21, None),
443 },
444 SlotChange {
445 idx: 3,
446 stack: ItemStack::new(ItemKind::Diamond, 21, None),
447 },
448 SlotChange {
449 idx: 5,
450 stack: ItemStack::new(ItemKind::Diamond, 21, None),
451 },
452 ]
453 .into(),
454 carried_item: ItemStack::new(ItemKind::Diamond, 1, None),
455 };
456
457 let player_inventory = Inventory::new(InventoryKind::Player);
458 let inventory = Inventory::new(InventoryKind::Generic9x1);
459 let window = InventoryWindow::new(&player_inventory, Some(&inventory));
460 let cursor_item = CursorItem(ItemStack::new(ItemKind::Diamond, 64, None));
461
462 assert_eq!(
463 calculate_net_item_delta(&drag_packet, &window, &cursor_item),
464 0
465 );
466 }
467
468 #[test]
469 fn net_item_delta_2() {
470 let drag_packet = ClickSlotC2s {
471 window_id: 2,
472 state_id: VarInt(14),
473 slot_idx: -999,
474 button: 2,
475 mode: ClickMode::Click,
476 slot_changes: vec![
477 SlotChange {
478 idx: 2,
479 stack: ItemStack::new(ItemKind::Diamond, 2, None),
480 },
481 SlotChange {
482 idx: 3,
483 stack: ItemStack::new(ItemKind::IronIngot, 2, None),
484 },
485 SlotChange {
486 idx: 4,
487 stack: ItemStack::new(ItemKind::GoldIngot, 2, None),
488 },
489 SlotChange {
490 idx: 5,
491 stack: ItemStack::new(ItemKind::Emerald, 2, None),
492 },
493 ]
494 .into(),
495 carried_item: ItemStack::new(ItemKind::OakWood, 2, None),
496 };
497
498 let player_inventory = Inventory::new(InventoryKind::Player);
499 let inventory = Inventory::new(InventoryKind::Generic9x1);
500 let window = InventoryWindow::new(&player_inventory, Some(&inventory));
501 let cursor_item = CursorItem::default();
502
503 assert_eq!(
504 calculate_net_item_delta(&drag_packet, &window, &cursor_item),
505 10
506 );
507 }
508
509 #[test]
510 fn click_filled_slot_with_empty_cursor_success() {
511 let player_inventory = Inventory::new(InventoryKind::Player);
512 let mut inventory = Inventory::new(InventoryKind::Generic9x1);
513 inventory.set_slot(0, ItemStack::new(ItemKind::Diamond, 20, None));
514 let cursor_item = CursorItem::default();
515 let packet = ClickSlotC2s {
516 window_id: 1,
517 button: 0,
518 mode: ClickMode::Click,
519 state_id: VarInt(0),
520 slot_idx: 0,
521 slot_changes: vec![SlotChange {
522 idx: 0,
523 stack: ItemStack::EMPTY,
524 }]
525 .into(),
526 carried_item: inventory.slot(0).clone(),
527 };
528
529 validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
530 .expect("packet should be valid");
531 }
532
533 #[test]
534 fn click_filled_slot_with_incorrect_nbt_and_empty_cursor_success() {
535 let player_inventory = Inventory::new(InventoryKind::Player);
536 let cursor_item = CursorItem(ItemStack::EMPTY);
537
538 let mut inventory = Inventory::new(InventoryKind::Generic9x1);
539 inventory.set_slot(0, ItemStack::new(ItemKind::DiamondPickaxe, 1, None));
541
542 let mut compound = Compound::new();
544 compound.insert("Damage", Int(1));
545
546 let packet = ClickSlotC2s {
547 window_id: 1,
548 state_id: VarInt(0),
549 slot_idx: 0,
550 button: 0,
551 mode: ClickMode::Click,
552 slot_changes: vec![SlotChange {
553 idx: 0,
554 stack: ItemStack::EMPTY,
555 }]
556 .into(),
557 carried_item: ItemStack {
558 item: ItemKind::DiamondPickaxe,
559 count: 1,
560 nbt: Some(compound),
561 },
562 };
563
564 validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
565 .expect("packet should be valid");
566 }
567
568 #[test]
569 fn click_slot_with_filled_cursor_success() {
570 let player_inventory = Inventory::new(InventoryKind::Player);
571 let inventory1 = Inventory::new(InventoryKind::Generic9x1);
572 let mut inventory2 = Inventory::new(InventoryKind::Generic9x1);
573 inventory2.set_slot(0, ItemStack::new(ItemKind::Diamond, 10, None));
574 let cursor_item = CursorItem(ItemStack::new(ItemKind::Diamond, 20, None));
575 let packet1 = ClickSlotC2s {
576 window_id: 1,
577 button: 0,
578 mode: ClickMode::Click,
579 state_id: VarInt(0),
580 slot_idx: 0,
581 slot_changes: vec![SlotChange {
582 idx: 0,
583 stack: ItemStack::new(ItemKind::Diamond, 20, None),
584 }]
585 .into(),
586 carried_item: ItemStack::EMPTY,
587 };
588 let packet2 = ClickSlotC2s {
589 window_id: 1,
590 button: 0,
591 mode: ClickMode::Click,
592 state_id: VarInt(0),
593 slot_idx: 0,
594 slot_changes: vec![SlotChange {
595 idx: 0,
596 stack: ItemStack::new(ItemKind::Diamond, 30, None),
597 }]
598 .into(),
599 carried_item: ItemStack::EMPTY,
600 };
601
602 validate_click_slot_packet(&packet1, &player_inventory, Some(&inventory1), &cursor_item)
603 .expect("packet should be valid");
604
605 validate_click_slot_packet(&packet2, &player_inventory, Some(&inventory2), &cursor_item)
606 .expect("packet should be valid");
607 }
608
609 #[test]
610 fn click_filled_slot_with_filled_cursor_stack_overflow_success() {
611 let player_inventory = Inventory::new(InventoryKind::Player);
612 let mut inventory = Inventory::new(InventoryKind::Generic9x1);
613 inventory.set_slot(0, ItemStack::new(ItemKind::Diamond, 20, None));
614 let cursor_item = CursorItem(ItemStack::new(ItemKind::Diamond, 64, None));
615 let packet = ClickSlotC2s {
616 window_id: 1,
617 button: 0,
618 mode: ClickMode::Click,
619 state_id: VarInt(0),
620 slot_idx: 0,
621 slot_changes: vec![SlotChange {
622 idx: 0,
623 stack: ItemStack::new(ItemKind::Diamond, 64, None),
624 }]
625 .into(),
626 carried_item: ItemStack::new(ItemKind::Diamond, 20, None),
627 };
628
629 validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
630 .expect("packet should be valid");
631 }
632
633 #[test]
634 fn click_filled_slot_with_filled_cursor_different_item_success() {
635 let player_inventory = Inventory::new(InventoryKind::Player);
636 let mut inventory = Inventory::new(InventoryKind::Generic9x1);
637 inventory.set_slot(0, ItemStack::new(ItemKind::IronIngot, 2, None));
638 let cursor_item = CursorItem(ItemStack::new(ItemKind::Diamond, 2, None));
639 let packet = ClickSlotC2s {
640 window_id: 1,
641 button: 0,
642 mode: ClickMode::Click,
643 state_id: VarInt(0),
644 slot_idx: 0,
645 slot_changes: vec![SlotChange {
646 idx: 0,
647 stack: ItemStack::new(ItemKind::Diamond, 2, None),
648 }]
649 .into(),
650 carried_item: ItemStack::new(ItemKind::IronIngot, 2, None),
651 };
652
653 validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
654 .expect("packet should be valid");
655 }
656
657 #[test]
658 fn click_slot_with_filled_cursor_failure() {
659 let player_inventory = Inventory::new(InventoryKind::Player);
660 let inventory1 = Inventory::new(InventoryKind::Generic9x1);
661 let mut inventory2 = Inventory::new(InventoryKind::Generic9x1);
662 inventory2.set_slot(0, ItemStack::new(ItemKind::Diamond, 10, None));
663 let cursor_item = CursorItem(ItemStack::new(ItemKind::Diamond, 20, None));
664 let packet1 = ClickSlotC2s {
665 window_id: 1,
666 button: 0,
667 mode: ClickMode::Click,
668 state_id: VarInt(0),
669 slot_idx: 0,
670 slot_changes: vec![SlotChange {
671 idx: 0,
672 stack: ItemStack::new(ItemKind::Diamond, 22, None),
673 }]
674 .into(),
675 carried_item: ItemStack::EMPTY,
676 };
677 let packet2 = ClickSlotC2s {
678 window_id: 1,
679 button: 0,
680 mode: ClickMode::Click,
681 state_id: VarInt(0),
682 slot_idx: 0,
683 slot_changes: vec![SlotChange {
684 idx: 0,
685 stack: ItemStack::new(ItemKind::Diamond, 32, None),
686 }]
687 .into(),
688 carried_item: ItemStack::EMPTY,
689 };
690 let packet3 = ClickSlotC2s {
691 window_id: 1,
692 button: 0,
693 mode: ClickMode::Click,
694 state_id: VarInt(0),
695 slot_idx: 0,
696 slot_changes: vec![
697 SlotChange {
698 idx: 0,
699 stack: ItemStack::new(ItemKind::Diamond, 22, None),
700 },
701 SlotChange {
702 idx: 1,
703 stack: ItemStack::new(ItemKind::Diamond, 22, None),
704 },
705 ]
706 .into(),
707 carried_item: ItemStack::EMPTY,
708 };
709
710 validate_click_slot_packet(&packet1, &player_inventory, Some(&inventory1), &cursor_item)
711 .expect_err("packet 1 should fail item duplication check");
712
713 validate_click_slot_packet(&packet2, &player_inventory, Some(&inventory2), &cursor_item)
714 .expect_err("packet 2 should fail item duplication check");
715
716 validate_click_slot_packet(&packet3, &player_inventory, Some(&inventory1), &cursor_item)
717 .expect_err("packet 3 should fail item duplication check");
718 }
719
720 #[test]
721 fn disallow_item_transmutation() {
722 let mut player_inventory = Inventory::new(InventoryKind::Player);
725 player_inventory.set_slot(9, ItemStack::new(ItemKind::Lead, 2, None));
726 let cursor_item = CursorItem::default();
727
728 let packets = vec![
729 ClickSlotC2s {
730 window_id: 0,
731 button: 0,
732 mode: ClickMode::ShiftClick,
733 state_id: VarInt(0),
734 slot_idx: 9,
735 slot_changes: vec![
736 SlotChange {
737 idx: 9,
738 stack: ItemStack::EMPTY,
739 },
740 SlotChange {
741 idx: 36,
742 stack: ItemStack::new(ItemKind::GoldIngot, 2, None),
743 },
744 ]
745 .into(),
746 carried_item: ItemStack::EMPTY,
747 },
748 ClickSlotC2s {
749 window_id: 0,
750 button: 0,
751 mode: ClickMode::Hotbar,
752 state_id: VarInt(0),
753 slot_idx: 9,
754 slot_changes: vec![
755 SlotChange {
756 idx: 9,
757 stack: ItemStack::EMPTY,
758 },
759 SlotChange {
760 idx: 36,
761 stack: ItemStack::new(ItemKind::GoldIngot, 2, None),
762 },
763 ]
764 .into(),
765 carried_item: ItemStack::EMPTY,
766 },
767 ClickSlotC2s {
768 window_id: 0,
769 button: 0,
770 mode: ClickMode::Click,
771 state_id: VarInt(0),
772 slot_idx: 9,
773 slot_changes: vec![SlotChange {
774 idx: 9,
775 stack: ItemStack::EMPTY,
776 }]
777 .into(),
778 carried_item: ItemStack::new(ItemKind::GoldIngot, 2, None),
779 },
780 ClickSlotC2s {
781 window_id: 0,
782 button: 0,
783 mode: ClickMode::DropKey,
784 state_id: VarInt(0),
785 slot_idx: 9,
786 slot_changes: vec![SlotChange {
787 idx: 9,
788 stack: ItemStack::new(ItemKind::GoldIngot, 1, None),
789 }]
790 .into(),
791 carried_item: ItemStack::EMPTY,
792 },
793 ];
794
795 for (i, packet) in packets.iter().enumerate() {
796 validate_click_slot_packet(packet, &player_inventory, None, &cursor_item).expect_err(
797 &format!("packet {i} passed item duplication check when it should have failed"),
798 );
799 }
800 }
801
802 #[test]
803 fn allow_shift_click_overflow_to_new_stack() {
804 let mut player_inventory = Inventory::new(InventoryKind::Player);
805 player_inventory.set_slot(9, ItemStack::new(ItemKind::Diamond, 64, None));
806 player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 32, None));
807 let cursor_item = CursorItem::default();
808
809 let packet = ClickSlotC2s {
810 window_id: 0,
811 state_id: VarInt(2),
812 slot_idx: 9,
813 button: 0,
814 mode: ClickMode::ShiftClick,
815 slot_changes: vec![
816 SlotChange {
817 idx: 37,
818 stack: ItemStack::new(ItemKind::Diamond, 32, None),
819 },
820 SlotChange {
821 idx: 36,
822 stack: ItemStack::new(ItemKind::Diamond, 64, None),
823 },
824 SlotChange {
825 idx: 9,
826 stack: ItemStack::EMPTY,
827 },
828 ]
829 .into(),
830 carried_item: ItemStack::EMPTY,
831 };
832
833 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
834 .expect("packet should be valid");
835 }
836
837 #[test]
838 fn allow_pickup_overfull_stack_click() {
839 let mut player_inventory = Inventory::new(InventoryKind::Player);
840 player_inventory.set_slot(9, ItemStack::new(ItemKind::Apple, 100, None));
841 let cursor_item = CursorItem::default();
842
843 let packet = ClickSlotC2s {
844 window_id: 0,
845 state_id: VarInt(2),
846 slot_idx: 9,
847 button: 0,
848 mode: ClickMode::Click,
849 slot_changes: vec![SlotChange {
850 idx: 9,
851 stack: ItemStack::EMPTY,
852 }]
853 .into(),
854 carried_item: ItemStack::new(ItemKind::Apple, 100, None),
855 };
856
857 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
858 .expect("packet should be valid");
859 }
860
861 #[test]
862 fn allow_place_overfull_stack_click() {
863 let player_inventory = Inventory::new(InventoryKind::Player);
864 let cursor_item = CursorItem(ItemStack::new(ItemKind::Apple, 100, None));
865
866 let packet = ClickSlotC2s {
867 window_id: 0,
868 state_id: VarInt(2),
869 slot_idx: 9,
870 button: 0,
871 mode: ClickMode::Click,
872 slot_changes: vec![SlotChange {
873 idx: 9,
874 stack: ItemStack::new(ItemKind::Apple, 64, None),
875 }]
876 .into(),
877 carried_item: ItemStack::new(ItemKind::Apple, 36, None),
878 };
879
880 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
881 .expect("packet should be valid");
882 }
883 #[test]
884 fn allow_clicking_outside_inventory_when_not_holding_anything_success() {
885 let player_inventory = Inventory::new(InventoryKind::Player);
886 let cursor_item = CursorItem(ItemStack::new(ItemKind::Air, 0, None));
887
888 let packet = ClickSlotC2s {
889 window_id: 0,
890 state_id: VarInt(2),
891 slot_idx: -999, button: 0,
893 mode: ClickMode::DropKey, slot_changes: vec![].into(),
896 carried_item: ItemStack::new(ItemKind::Air, 0, None),
897 };
898
899 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
900 .expect("packet should be valid");
901 }
902 #[test]
903 fn allow_clicking_outside_inventory_when_holding_something_success() {
904 let player_inventory = Inventory::new(InventoryKind::Player);
905 let cursor_item = CursorItem(ItemStack::new(ItemKind::Air, 0, None));
906
907 let packet = ClickSlotC2s {
909 window_id: 0,
910 state_id: VarInt(2),
911 slot_idx: -999, button: 0,
913 mode: ClickMode::Click, slot_changes: vec![].into(),
915 carried_item: ItemStack::new(ItemKind::Air, 0, None),
916 };
917
918 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
919 .expect("packet should be valid");
920 }
921 #[test]
922 fn allow_clicking_on_the_margin_area_in_inventory_success() {
923 let player_inventory = Inventory::new(InventoryKind::Player);
924 let cursor_item = CursorItem(ItemStack::new(ItemKind::Air, 0, None));
925
926 let packet = ClickSlotC2s {
927 window_id: 0,
928 state_id: VarInt(2),
929 slot_idx: -1, button: 0,
931 mode: ClickMode::Click,
932 slot_changes: vec![].into(),
933 carried_item: ItemStack::new(ItemKind::Air, 0, None),
934 };
935
936 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
937 .expect("packet should be valid");
938 }
939 #[test]
940 fn allow_clicking_on_an_empty_slot_with_empty_carried_item_success() {
941 let player_inventory = Inventory::new(InventoryKind::Player);
942 let cursor_item = CursorItem(ItemStack::new(ItemKind::Air, 0, None));
943
944 let packet = ClickSlotC2s {
945 window_id: 0,
946 state_id: VarInt(2),
947 slot_idx: 3,
948 button: 0,
949 mode: ClickMode::Click,
950 slot_changes: vec![].into(),
951 carried_item: ItemStack::new(ItemKind::Air, 0, None),
952 };
953
954 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
955 .expect("packet should be valid");
956 }
957 #[test]
958 fn allow_clicking_hotbar_keybinds_when_both_source_and_target_are_empty() {
959 let player_inventory = Inventory::new(InventoryKind::Player);
960 let cursor_item = CursorItem(ItemStack::new(ItemKind::Air, 0, None));
961
962 let packet = ClickSlotC2s {
963 window_id: 0,
964 state_id: VarInt(2),
965 slot_idx: 0,
966 button: 0,
967 mode: ClickMode::Hotbar,
968 slot_changes: vec![].into(),
969 carried_item: ItemStack::new(ItemKind::Air, 0, None),
970 };
971
972 validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
973 .expect("packet should be valid");
974 }
975}