1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4use std::iter::FusedIterator;
5use std::num::Wrapping;
6use std::ops::Range;
7
8use bevy_app::prelude::*;
9use bevy_ecs::prelude::*;
10use derive_more::{Deref, DerefMut};
11use player_inventory::PlayerInventory;
12use tracing::{debug, warn};
13use valence_server::client::{Client, FlushPacketsSet, SpawnClientsSet};
14use valence_server::event_loop::{EventLoopPreUpdate, PacketEvent};
15use valence_server::interact_block::InteractBlockEvent;
16pub use valence_server::protocol::packets::play::click_slot_c2s::{ClickMode, SlotChange};
17use valence_server::protocol::packets::play::open_screen_s2c::WindowType;
18pub use valence_server::protocol::packets::play::player_action_c2s::PlayerAction;
19use valence_server::protocol::packets::play::{
20 ClickSlotC2s, CloseHandledScreenC2s, CloseScreenS2c, CreativeInventoryActionC2s, InventoryS2c,
21 OpenScreenS2c, PlayerActionC2s, ScreenHandlerSlotUpdateS2c, UpdateSelectedSlotC2s,
22 UpdateSelectedSlotS2c,
23};
24use valence_server::protocol::{VarInt, WritePacket};
25use valence_server::text::IntoText;
26use valence_server::{GameMode, Hand, ItemKind, ItemStack, Text};
27
28pub mod player_inventory;
29mod validate;
30
31pub struct InventoryPlugin;
32
33impl Plugin for InventoryPlugin {
34 fn build(&self, app: &mut bevy_app::App) {
35 app.add_systems(
36 PreUpdate,
37 init_new_client_inventories.after(SpawnClientsSet),
38 )
39 .add_systems(
40 PostUpdate,
41 (
42 update_client_on_close_inventory.before(update_open_inventories),
43 update_player_selected_slot,
44 update_open_inventories,
45 update_player_inventories,
46 update_cursor_item,
47 )
48 .before(FlushPacketsSet),
49 )
50 .add_systems(
51 EventLoopPreUpdate,
52 (
53 handle_update_selected_slot,
54 handle_click_slot,
55 handle_creative_inventory_action,
56 handle_close_handled_screen,
57 handle_player_actions,
58 resync_readonly_inventory_after_block_interaction,
59 ),
60 )
61 .init_resource::<InventorySettings>()
62 .add_event::<ClickSlotEvent>()
63 .add_event::<DropItemStackEvent>()
64 .add_event::<CreativeInventoryActionEvent>()
65 .add_event::<UpdateSelectedSlotEvent>();
66 }
67}
68
69#[derive(Debug, Clone, Component)]
70pub struct Inventory {
71 title: Text,
72 kind: InventoryKind,
73 slots: Box<[ItemStack]>,
74 #[doc(hidden)]
76 pub changed: u64,
77 pub readonly: bool,
82}
83
84impl Inventory {
85 pub fn new(kind: InventoryKind) -> Self {
86 Self::with_title(kind, "Inventory")
88 }
89
90 pub fn with_title<'a, T: IntoText<'a>>(kind: InventoryKind, title: T) -> Self {
91 Inventory {
92 title: title.into_cow_text().into_owned(),
93 kind,
94 slots: vec![ItemStack::EMPTY; kind.slot_count()].into(),
95 changed: 0,
96 readonly: false,
97 }
98 }
99
100 #[track_caller]
101 pub fn slot(&self, idx: u16) -> &ItemStack {
102 self.slots
103 .get(idx as usize)
104 .expect("slot index out of range")
105 }
106
107 #[track_caller]
119 #[inline]
120 pub fn set_slot<I: Into<ItemStack>>(&mut self, idx: u16, item: I) {
121 let _ = self.replace_slot(idx, item);
122 }
123
124 #[track_caller]
138 #[must_use]
139 pub fn replace_slot<I: Into<ItemStack>>(&mut self, idx: u16, item: I) -> ItemStack {
140 assert!(idx < self.slot_count(), "slot index of {idx} out of bounds");
141
142 let new = item.into();
143 let old = &mut self.slots[idx as usize];
144
145 if new != *old {
146 self.changed |= 1 << idx;
147 }
148
149 std::mem::replace(old, new)
150 }
151
152 #[track_caller]
165 pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) {
166 assert!(
167 idx_a < self.slot_count(),
168 "slot index of {idx_a} out of bounds"
169 );
170 assert!(
171 idx_b < self.slot_count(),
172 "slot index of {idx_b} out of bounds"
173 );
174
175 if idx_a == idx_b || self.slots[idx_a as usize] == self.slots[idx_b as usize] {
176 return;
178 }
179
180 self.changed |= 1 << idx_a;
181 self.changed |= 1 << idx_b;
182
183 self.slots.swap(idx_a as usize, idx_b as usize);
184 }
185
186 #[track_caller]
199 pub fn set_slot_amount(&mut self, idx: u16, amount: i8) {
200 assert!(idx < self.slot_count(), "slot index out of range");
201
202 let item = &mut self.slots[idx as usize];
203
204 if !item.is_empty() {
205 if item.count == amount {
206 return;
207 }
208 item.count = amount;
209 self.changed |= 1 << idx;
210 }
211 }
212
213 pub fn slot_count(&self) -> u16 {
214 self.slots.len() as u16
215 }
216
217 pub fn slots(
218 &self,
219 ) -> impl ExactSizeIterator<Item = &ItemStack> + DoubleEndedIterator + FusedIterator + Clone + '_
220 {
221 self.slots.iter()
222 }
223
224 pub fn kind(&self) -> InventoryKind {
225 self.kind
226 }
227
228 pub fn title(&self) -> &Text {
238 &self.title
239 }
240
241 #[inline]
251 pub fn set_title<'a, T: IntoText<'a>>(&mut self, title: T) {
252 let _ = self.replace_title(title);
253 }
254
255 #[must_use]
258 pub fn replace_title<'a, T: IntoText<'a>>(&mut self, title: T) -> Text {
259 std::mem::replace(&mut self.title, title.into_cow_text().into_owned())
261 }
262
263 pub(crate) fn slot_slice(&self) -> &[ItemStack] {
264 &self.slots
265 }
266
267 #[track_caller]
281 #[must_use]
282 pub fn first_empty_slot_in(&self, mut range: Range<u16>) -> Option<u16> {
283 assert!(
284 (0..=self.slot_count()).contains(&range.start)
285 && (0..=self.slot_count()).contains(&range.end),
286 "slot range out of range"
287 );
288
289 range.find(|&idx| self.slots[idx as usize].is_empty())
290 }
291
292 #[inline]
304 pub fn first_empty_slot(&self) -> Option<u16> {
305 self.first_empty_slot_in(0..self.slot_count())
306 }
307
308 pub fn first_slot_with_item_in(
324 &self,
325 item: ItemKind,
326 stack_max: i8,
327 mut range: Range<u16>,
328 ) -> Option<u16> {
329 assert!(
330 (0..=self.slot_count()).contains(&range.start)
331 && (0..=self.slot_count()).contains(&range.end),
332 "slot range out of range"
333 );
334 assert!(stack_max > 0, "stack_max must be greater than 0");
335
336 range.find(|&idx| {
337 let stack = &self.slots[idx as usize];
338 stack.item == item && stack.count < stack_max
339 })
340 }
341
342 #[inline]
355 pub fn first_slot_with_item(&self, item: ItemKind, stack_max: i8) -> Option<u16> {
356 self.first_slot_with_item_in(item, stack_max, 0..self.slot_count())
357 }
358}
359
360#[derive(Component, Debug)]
362pub struct ClientInventoryState {
363 window_id: u8,
365 state_id: Wrapping<i32>,
366 slots_changed: u64,
369 client_updated_cursor_item: Option<ItemStack>,
375}
376
377impl ClientInventoryState {
378 #[doc(hidden)]
379 pub fn window_id(&self) -> u8 {
380 self.window_id
381 }
382
383 #[doc(hidden)]
384 pub fn state_id(&self) -> Wrapping<i32> {
385 self.state_id
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Component, Deref)]
391pub struct HeldItem {
392 held_item_slot: u16,
393}
394
395impl HeldItem {
396 pub fn slot(&self) -> u16 {
399 self.held_item_slot
400 }
401
402 pub fn hotbar_idx(&self) -> u8 {
403 PlayerInventory::slot_to_hotbar(self.held_item_slot)
404 }
405
406 pub fn set_slot(&mut self, slot: u16) {
407 assert!(
409 PlayerInventory::SLOTS_HOTBAR.contains(&slot),
410 "slot index of {slot} out of bounds"
411 );
412
413 self.held_item_slot = slot;
414 }
415
416 pub fn set_hotbar_idx(&mut self, hotbar_idx: u8) {
417 self.set_slot(PlayerInventory::hotbar_to_slot(hotbar_idx))
418 }
419}
420
421#[derive(Component, Clone, PartialEq, Default, Debug, Deref, DerefMut)]
424pub struct CursorItem(pub ItemStack);
425
426#[derive(Component, Clone, Debug)]
429pub struct OpenInventory {
430 pub entity: Entity,
433 client_changed: u64,
434}
435
436impl OpenInventory {
437 pub fn new(entity: Entity) -> Self {
438 OpenInventory {
439 entity,
440 client_changed: 0,
441 }
442 }
443}
444
445pub struct InventoryWindow<'a> {
462 player_inventory: &'a Inventory,
463 open_inventory: Option<&'a Inventory>,
464}
465
466impl<'a> InventoryWindow<'a> {
467 pub fn new(player_inventory: &'a Inventory, open_inventory: Option<&'a Inventory>) -> Self {
468 Self {
469 player_inventory,
470 open_inventory,
471 }
472 }
473
474 #[track_caller]
475 pub fn slot(&self, idx: u16) -> &ItemStack {
476 if let Some(open_inv) = self.open_inventory.as_ref() {
477 if idx < open_inv.slot_count() {
478 open_inv.slot(idx)
479 } else {
480 self.player_inventory
481 .slot(convert_to_player_slot_id(open_inv.kind(), idx))
482 }
483 } else {
484 self.player_inventory.slot(idx)
485 }
486 }
487
488 #[track_caller]
489 pub fn slot_count(&self) -> u16 {
490 if let Some(open_inv) = &self.open_inventory {
491 PlayerInventory::MAIN_SIZE + open_inv.slot_count()
494 } else {
495 self.player_inventory.slot_count()
496 }
497 }
498}
499
500pub struct InventoryWindowMut<'a> {
520 player_inventory: &'a mut Inventory,
521 open_inventory: Option<&'a mut Inventory>,
522}
523
524impl<'a> InventoryWindowMut<'a> {
525 pub fn new(
526 player_inventory: &'a mut Inventory,
527 open_inventory: Option<&'a mut Inventory>,
528 ) -> Self {
529 Self {
530 player_inventory,
531 open_inventory,
532 }
533 }
534
535 #[track_caller]
536 pub fn slot(&self, idx: u16) -> &ItemStack {
537 if let Some(open_inv) = self.open_inventory.as_ref() {
538 if idx < open_inv.slot_count() {
539 open_inv.slot(idx)
540 } else {
541 self.player_inventory
542 .slot(convert_to_player_slot_id(open_inv.kind(), idx))
543 }
544 } else {
545 self.player_inventory.slot(idx)
546 }
547 }
548
549 #[track_caller]
550 #[must_use]
551 pub fn replace_slot<I: Into<ItemStack>>(&mut self, idx: u16, item: I) -> ItemStack {
552 assert!(idx < self.slot_count(), "slot index of {idx} out of bounds");
553
554 if let Some(open_inv) = self.open_inventory.as_mut() {
555 if idx < open_inv.slot_count() {
556 open_inv.replace_slot(idx, item)
557 } else {
558 self.player_inventory
559 .replace_slot(convert_to_player_slot_id(open_inv.kind(), idx), item)
560 }
561 } else {
562 self.player_inventory.replace_slot(idx, item)
563 }
564 }
565
566 #[track_caller]
567 #[inline]
568 pub fn set_slot<I: Into<ItemStack>>(&mut self, idx: u16, item: I) {
569 let _ = self.replace_slot(idx, item);
570 }
571
572 pub fn slot_count(&self) -> u16 {
573 if let Some(open_inv) = &self.open_inventory {
574 PlayerInventory::MAIN_SIZE + open_inv.slot_count()
577 } else {
578 self.player_inventory.slot_count()
579 }
580 }
581}
582
583fn init_new_client_inventories(clients: Query<Entity, Added<Client>>, mut commands: Commands) {
585 for entity in &clients {
586 commands.entity(entity).insert((
587 Inventory::new(InventoryKind::Player),
588 CursorItem(ItemStack::EMPTY),
589 ClientInventoryState {
590 window_id: 0,
591 state_id: Wrapping(0),
592 slots_changed: 0,
593 client_updated_cursor_item: None,
594 },
595 HeldItem {
596 held_item_slot: 36,
598 },
599 ));
600 }
601}
602
603fn update_player_inventories(
605 mut query: Query<
606 (
607 &mut Inventory,
608 &mut Client,
609 &mut ClientInventoryState,
610 &CursorItem,
611 ),
612 Without<OpenInventory>,
613 >,
614) {
615 for (mut inventory, mut client, mut inv_state, cursor_item) in &mut query {
616 if inventory.kind != InventoryKind::Player {
617 warn!("Inventory on client entity is not a player inventory");
618 }
619
620 if inventory.changed == u64::MAX {
621 inv_state.state_id += 1;
624
625 client.write_packet(&InventoryS2c {
626 window_id: 0,
627 state_id: VarInt(inv_state.state_id.0),
628 slots: Cow::Borrowed(inventory.slot_slice()),
629 carried_item: Cow::Borrowed(&cursor_item.0),
630 });
631
632 inventory.changed = 0;
633 inv_state.slots_changed = 0;
634 } else if inventory.changed != 0 {
635 let changed_filtered = inventory.changed & !inv_state.slots_changed;
639
640 if changed_filtered != 0 {
641 inv_state.state_id += 1;
642
643 for (i, slot) in inventory.slots.iter().enumerate() {
644 if ((changed_filtered >> i) & 1) == 1 {
645 client.write_packet(&ScreenHandlerSlotUpdateS2c {
646 window_id: 0,
647 state_id: VarInt(inv_state.state_id.0),
648 slot_idx: i as i16,
649 slot_data: Cow::Borrowed(slot),
650 });
651 }
652 }
653 }
654
655 inventory.changed = 0;
656 inv_state.slots_changed = 0;
657 }
658 }
659}
660
661fn update_open_inventories(
665 mut clients: Query<(
666 Entity,
667 &mut Client,
668 &mut ClientInventoryState,
669 &CursorItem,
670 &mut OpenInventory,
671 )>,
672 mut inventories: Query<&mut Inventory>,
673 mut commands: Commands,
674) {
675 for (client_entity, mut client, mut inv_state, cursor_item, mut open_inventory) in &mut clients
679 {
680 let Ok([inventory, player_inventory]) =
682 inventories.get_many_mut([open_inventory.entity, client_entity])
683 else {
684 commands.entity(client_entity).remove::<OpenInventory>();
686
687 client.write_packet(&CloseScreenS2c {
688 window_id: inv_state.window_id,
689 });
690
691 continue;
692 };
693
694 if open_inventory.is_added() {
695 inv_state.window_id = inv_state.window_id % 100 + 1;
697 open_inventory.client_changed = 0;
698
699 client.write_packet(&OpenScreenS2c {
700 window_id: VarInt(inv_state.window_id.into()),
701 window_type: WindowType::from(inventory.kind),
702 window_title: Cow::Borrowed(&inventory.title),
703 });
704
705 client.write_packet(&InventoryS2c {
706 window_id: inv_state.window_id,
707 state_id: VarInt(inv_state.state_id.0),
708 slots: Cow::Borrowed(inventory.slot_slice()),
709 carried_item: Cow::Borrowed(&cursor_item.0),
710 });
711 } else {
712 if inventory.changed == u64::MAX {
715 inv_state.state_id += 1;
718
719 client.write_packet(&InventoryS2c {
720 window_id: inv_state.window_id,
721 state_id: VarInt(inv_state.state_id.0),
722 slots: Cow::Borrowed(inventory.slot_slice()),
723 carried_item: Cow::Borrowed(&cursor_item.0),
724 })
725 } else {
726 let changed_filtered =
730 u128::from(inventory.changed & !open_inventory.client_changed);
731
732 let mut player_inventory_changed = u128::from(player_inventory.changed);
735
736 player_inventory_changed >>= *PlayerInventory::SLOTS_MAIN.start();
739 player_inventory_changed <<= inventory.slot_count();
742
743 let changed_filtered = changed_filtered | player_inventory_changed;
744
745 if changed_filtered != 0 {
746 for (i, slot) in inventory
747 .slots
748 .iter()
749 .chain(
750 player_inventory
751 .slots
752 .iter()
753 .skip(*PlayerInventory::SLOTS_MAIN.start() as usize),
754 )
755 .enumerate()
756 {
757 if (changed_filtered >> i) & 1 == 1 {
758 client.write_packet(&ScreenHandlerSlotUpdateS2c {
759 window_id: inv_state.window_id as i8,
760 state_id: VarInt(inv_state.state_id.0),
761 slot_idx: i as i16,
762 slot_data: Cow::Borrowed(slot),
763 });
764 }
765 }
766
767 player_inventory
768 .map_unchanged(|f| &mut f.changed)
769 .set_if_neq(0);
770 }
771 }
772 }
773 open_inventory
778 .map_unchanged(|f| &mut f.client_changed)
779 .set_if_neq(0);
780 inv_state
781 .map_unchanged(|f| &mut f.slots_changed)
782 .set_if_neq(0);
783 inventory.map_unchanged(|f| &mut f.changed).set_if_neq(0);
784 }
785}
786
787fn update_cursor_item(
788 mut clients: Query<(&mut Client, &mut ClientInventoryState, &CursorItem), Changed<CursorItem>>,
789) {
790 for (mut client, inv_state, cursor_item) in &mut clients {
791 if inv_state.client_updated_cursor_item.as_ref() != Some(&cursor_item.0) {
793 client.write_packet(&ScreenHandlerSlotUpdateS2c {
797 window_id: -1,
798 state_id: VarInt(inv_state.state_id.0),
799 slot_idx: -1,
800 slot_data: Cow::Borrowed(&cursor_item.0),
801 });
802 }
803
804 inv_state
805 .map_unchanged(|f| &mut f.client_updated_cursor_item)
806 .set_if_neq(None);
807 }
808}
809
810fn handle_close_handled_screen(mut packets: EventReader<PacketEvent>, mut commands: Commands) {
812 for packet in packets.read() {
813 if packet.decode::<CloseHandledScreenC2s>().is_some() {
814 if let Some(mut entity) = commands.get_entity(packet.client) {
815 entity.remove::<OpenInventory>();
816 }
817 }
818 }
819}
820
821fn update_client_on_close_inventory(
824 mut removals: RemovedComponents<OpenInventory>,
825 mut clients: Query<(&mut Client, &ClientInventoryState)>,
826) {
827 for entity in &mut removals.read() {
828 if let Ok((mut client, inv_state)) = clients.get_mut(entity) {
829 client.write_packet(&CloseScreenS2c {
830 window_id: inv_state.window_id,
831 })
832 }
833 }
834}
835
836#[derive(Event, Clone, Debug)]
838pub struct ClickSlotEvent {
839 pub client: Entity,
840 pub window_id: u8,
841 pub state_id: i32,
842 pub slot_id: i16,
843 pub button: i8,
844 pub mode: ClickMode,
845 pub slot_changes: Vec<SlotChange>,
846 pub carried_item: ItemStack,
847}
848
849#[derive(Event, Clone, Debug)]
850pub struct DropItemStackEvent {
851 pub client: Entity,
852 pub from_slot: Option<u16>,
853 pub stack: ItemStack,
854}
855
856fn handle_click_slot(
857 mut packets: EventReader<PacketEvent>,
858 mut clients: Query<(
859 &mut Client,
860 &mut Inventory,
861 &mut ClientInventoryState,
862 Option<&mut OpenInventory>,
863 &mut CursorItem,
864 )>,
865 mut inventories: Query<&mut Inventory, Without<Client>>,
866 mut drop_item_stack_events: EventWriter<DropItemStackEvent>,
867 mut click_slot_events: EventWriter<ClickSlotEvent>,
868) {
869 for packet in packets.read() {
870 let Some(pkt) = packet.decode::<ClickSlotC2s>() else {
871 continue;
873 };
874
875 let Ok((mut client, mut client_inv, mut inv_state, open_inventory, mut cursor_item)) =
876 clients.get_mut(packet.client)
877 else {
878 continue;
880 };
881
882 let open_inv = open_inventory
883 .as_ref()
884 .and_then(|open| inventories.get_mut(open.entity).ok());
885
886 if let Err(e) = validate::validate_click_slot_packet(
887 &pkt,
888 &client_inv,
889 open_inv.as_deref(),
890 &cursor_item,
891 ) {
892 debug!(
893 "failed to validate click slot packet for client {:#?}: \"{e:#}\" {pkt:#?}",
894 packet.client
895 );
896
897 client.write_packet(&InventoryS2c {
900 window_id: if open_inv.is_some() {
901 inv_state.window_id
902 } else {
903 0
904 },
905 state_id: VarInt(inv_state.state_id.0),
906 slots: Cow::Borrowed(open_inv.unwrap_or(client_inv).slot_slice()),
907 carried_item: Cow::Borrowed(&cursor_item.0),
908 });
909
910 continue;
911 }
912
913 if pkt.slot_idx == -999 && pkt.mode == ClickMode::Click {
914 let stack = std::mem::take(&mut cursor_item.0);
917
918 if !stack.is_empty() {
919 drop_item_stack_events.send(DropItemStackEvent {
920 client: packet.client,
921 from_slot: None,
922 stack,
923 });
924 }
925 } else if pkt.mode == ClickMode::DropKey {
926 let entire_stack = pkt.button == 1;
929
930 if let Some(open_inventory) = open_inventory {
933 let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else {
936 continue;
938 };
939
940 if inv_state.state_id.0 != pkt.state_id.0 {
941 debug!("Client state id mismatch, resyncing");
944
945 inv_state.state_id += 1;
946
947 client.write_packet(&InventoryS2c {
948 window_id: inv_state.window_id,
949 state_id: VarInt(inv_state.state_id.0),
950 slots: Cow::Borrowed(target_inventory.slot_slice()),
951 carried_item: Cow::Borrowed(&cursor_item.0),
952 });
953
954 continue;
955 }
956 if pkt.slot_idx == -999 {
957 continue;
959 }
960
961 if (0_i16..target_inventory.slot_count() as i16).contains(&pkt.slot_idx) {
962 if target_inventory.readonly {
965 client.write_packet(&InventoryS2c {
967 window_id: inv_state.window_id,
968 state_id: VarInt(inv_state.state_id.0),
969 slots: Cow::Borrowed(target_inventory.slot_slice()),
970 carried_item: Cow::Borrowed(&cursor_item.0),
971 });
972 continue;
973 }
974
975 let stack = target_inventory.slot(pkt.slot_idx as u16);
976
977 if !stack.is_empty() {
978 let dropped = if entire_stack || stack.count == 1 {
979 target_inventory.replace_slot(pkt.slot_idx as u16, ItemStack::EMPTY)
980 } else {
981 let stack = stack.clone().with_count(stack.count - 1);
982 let mut old_slot =
983 target_inventory.replace_slot(pkt.slot_idx as u16, stack);
984 old_slot.count = 1;
987 old_slot
988 };
989
990 drop_item_stack_events.send(DropItemStackEvent {
991 client: packet.client,
992 from_slot: Some(pkt.slot_idx as u16),
993 stack: dropped,
994 });
995 }
996 } else {
997 if client_inv.readonly {
1000 client.write_packet(&InventoryS2c {
1002 window_id: 0,
1003 state_id: VarInt(inv_state.state_id.0),
1004 slots: Cow::Borrowed(client_inv.slot_slice()),
1005 carried_item: Cow::Borrowed(&cursor_item.0),
1006 });
1007 continue;
1008 }
1009
1010 let slot_id =
1011 convert_to_player_slot_id(target_inventory.kind, pkt.slot_idx as u16);
1012
1013 let stack = client_inv.slot(slot_id);
1014
1015 if !stack.is_empty() {
1016 let dropped = if entire_stack || stack.count == 1 {
1017 client_inv.replace_slot(slot_id, ItemStack::EMPTY)
1018 } else {
1019 let stack = stack.clone().with_count(stack.count - 1);
1020 let mut old_slot = client_inv.replace_slot(slot_id, stack);
1021 old_slot.count = 1;
1024 old_slot
1025 };
1026
1027 drop_item_stack_events.send(DropItemStackEvent {
1028 client: packet.client,
1029 from_slot: Some(slot_id),
1030 stack: dropped,
1031 });
1032 }
1033 }
1034 } else {
1035 if client_inv.readonly {
1039 client.write_packet(&InventoryS2c {
1041 window_id: 0,
1042 state_id: VarInt(inv_state.state_id.0),
1043 slots: Cow::Borrowed(client_inv.slot_slice()),
1044 carried_item: Cow::Borrowed(&cursor_item.0),
1045 });
1046 continue;
1047 }
1048 if pkt.slot_idx == -999 {
1049 continue;
1051 }
1052 let stack = client_inv.slot(pkt.slot_idx as u16);
1053
1054 if !stack.is_empty() {
1055 let dropped = if entire_stack || stack.count == 1 {
1056 client_inv.replace_slot(pkt.slot_idx as u16, ItemStack::EMPTY)
1057 } else {
1058 let stack = stack.clone().with_count(stack.count - 1);
1059 let mut old_slot = client_inv.replace_slot(pkt.slot_idx as u16, stack);
1060 old_slot.count = 1;
1063 old_slot
1064 };
1065
1066 drop_item_stack_events.send(DropItemStackEvent {
1067 client: packet.client,
1068 from_slot: Some(pkt.slot_idx as u16),
1069 stack: dropped,
1070 });
1071 }
1072 }
1073 } else {
1074 if (pkt.window_id == 0) != open_inventory.is_none() {
1078 warn!(
1079 "Client sent a click with an invalid window id for current state: window_id = \
1080 {}, open_inventory present = {}",
1081 pkt.window_id,
1082 open_inventory.is_some()
1083 );
1084 continue;
1085 }
1086
1087 if let Some(mut open_inventory) = open_inventory {
1088 let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else {
1092 continue;
1094 };
1095
1096 if inv_state.state_id.0 != pkt.state_id.0 {
1097 debug!("Client state id mismatch, resyncing");
1100
1101 inv_state.state_id += 1;
1102
1103 client.write_packet(&InventoryS2c {
1104 window_id: inv_state.window_id,
1105 state_id: VarInt(inv_state.state_id.0),
1106 slots: Cow::Borrowed(target_inventory.slot_slice()),
1107 carried_item: Cow::Borrowed(&cursor_item.0),
1108 });
1109
1110 continue;
1111 }
1112
1113 let mut new_cursor = pkt.carried_item.clone();
1114
1115 for slot in pkt.slot_changes.iter() {
1116 let transferred_between_inventories =
1117 ((0_i16..target_inventory.slot_count() as i16).contains(&pkt.slot_idx)
1118 && pkt.mode == ClickMode::Hotbar)
1119 || pkt.mode == ClickMode::ShiftClick;
1120
1121 if (0_i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
1122 if (client_inv.readonly && transferred_between_inventories)
1123 || target_inventory.readonly
1124 {
1125 new_cursor = cursor_item.0.clone();
1126 continue;
1127 }
1128
1129 target_inventory.set_slot(slot.idx as u16, slot.stack.clone());
1130 open_inventory.client_changed |= 1 << slot.idx;
1131 } else {
1132 if (target_inventory.readonly && transferred_between_inventories)
1133 || client_inv.readonly
1134 {
1135 new_cursor = cursor_item.0.clone();
1136 continue;
1137 }
1138
1139 let slot_id =
1141 convert_to_player_slot_id(target_inventory.kind, slot.idx as u16);
1142 client_inv.set_slot(slot_id, slot.stack.clone());
1143 inv_state.slots_changed |= 1 << slot_id;
1144 }
1145 }
1146
1147 cursor_item.set_if_neq(CursorItem(new_cursor.clone()));
1148 inv_state.client_updated_cursor_item = Some(new_cursor);
1149
1150 if target_inventory.readonly || client_inv.readonly {
1151 client.write_packet(&InventoryS2c {
1153 window_id: inv_state.window_id,
1154 state_id: VarInt(inv_state.state_id.0),
1155 slots: Cow::Borrowed(target_inventory.slot_slice()),
1156 carried_item: Cow::Borrowed(&cursor_item.0),
1157 });
1158
1159 client.write_packet(&InventoryS2c {
1161 window_id: 0,
1162 state_id: VarInt(inv_state.state_id.0),
1163 slots: Cow::Borrowed(client_inv.slot_slice()),
1164 carried_item: Cow::Borrowed(&cursor_item.0),
1165 });
1166 }
1167 } else {
1168 if inv_state.state_id.0 != pkt.state_id.0 {
1171 debug!("Client state id mismatch, resyncing");
1174
1175 inv_state.state_id += 1;
1176
1177 client.write_packet(&InventoryS2c {
1178 window_id: inv_state.window_id,
1179 state_id: VarInt(inv_state.state_id.0),
1180 slots: Cow::Borrowed(client_inv.slot_slice()),
1181 carried_item: Cow::Borrowed(&cursor_item.0),
1182 });
1183
1184 continue;
1185 }
1186
1187 let mut new_cursor = pkt.carried_item.clone();
1188
1189 for slot in pkt.slot_changes.iter() {
1190 if (0_i16..client_inv.slot_count() as i16).contains(&slot.idx) {
1191 if client_inv.readonly {
1192 new_cursor = cursor_item.0.clone();
1193 continue;
1194 }
1195 client_inv.set_slot(slot.idx as u16, slot.stack.clone());
1196 inv_state.slots_changed |= 1 << slot.idx;
1197 } else {
1198 warn!(
1201 "Client attempted to interact with slot {} which does not exist",
1202 slot.idx
1203 );
1204 }
1205 }
1206
1207 cursor_item.set_if_neq(CursorItem(new_cursor.clone()));
1208 inv_state.client_updated_cursor_item = Some(new_cursor);
1209
1210 if client_inv.readonly {
1211 client.write_packet(&InventoryS2c {
1213 window_id: 0,
1214 state_id: VarInt(inv_state.state_id.0),
1215 slots: Cow::Borrowed(client_inv.slot_slice()),
1216 carried_item: Cow::Borrowed(&cursor_item.0),
1217 });
1218 }
1219 }
1220
1221 click_slot_events.send(ClickSlotEvent {
1222 client: packet.client,
1223 window_id: pkt.window_id,
1224 state_id: pkt.state_id.0,
1225 slot_id: pkt.slot_idx,
1226 button: pkt.button,
1227 mode: pkt.mode,
1228 slot_changes: pkt.slot_changes.into(),
1229 carried_item: pkt.carried_item,
1230 });
1231 }
1232 }
1233}
1234
1235fn handle_player_actions(
1236 mut packets: EventReader<PacketEvent>,
1237 mut clients: Query<(
1238 &mut Inventory,
1239 &mut ClientInventoryState,
1240 &HeldItem,
1241 &mut Client,
1242 )>,
1243 mut drop_item_stack_events: EventWriter<DropItemStackEvent>,
1244) {
1245 for packet in packets.read() {
1246 if let Some(pkt) = packet.decode::<PlayerActionC2s>() {
1247 match pkt.action {
1248 PlayerAction::DropAllItems => {
1249 if let Ok((mut inv, mut inv_state, &held, mut client)) =
1250 clients.get_mut(packet.client)
1251 {
1252 if inv.readonly {
1253 client.write_packet(&InventoryS2c {
1255 window_id: 0,
1256 state_id: VarInt(inv_state.state_id.0),
1257 slots: Cow::Borrowed(inv.slot_slice()),
1258 carried_item: Cow::Borrowed(&ItemStack::EMPTY),
1259 });
1260 continue;
1261 }
1262
1263 let stack = inv.replace_slot(held.slot(), ItemStack::EMPTY);
1264
1265 if !stack.is_empty() {
1266 inv_state.slots_changed |= 1 << held.slot();
1267
1268 drop_item_stack_events.send(DropItemStackEvent {
1269 client: packet.client,
1270 from_slot: Some(held.slot()),
1271 stack,
1272 });
1273 }
1274 }
1275 }
1276 PlayerAction::DropItem => {
1277 if let Ok((mut inv, mut inv_state, held, mut client)) =
1278 clients.get_mut(packet.client)
1279 {
1280 if inv.readonly {
1281 client.write_packet(&InventoryS2c {
1283 window_id: 0,
1284 state_id: VarInt(inv_state.state_id.0),
1285 slots: Cow::Borrowed(inv.slot_slice()),
1286 carried_item: Cow::Borrowed(&ItemStack::EMPTY),
1287 });
1288 continue;
1289 }
1290
1291 let mut stack = inv.replace_slot(held.slot(), ItemStack::EMPTY);
1292
1293 if !stack.is_empty() {
1294 if stack.count > 1 {
1295 inv.set_slot(
1296 held.slot(),
1297 stack.clone().with_count(stack.count - 1),
1298 );
1299
1300 stack.count = 1;
1301 }
1302
1303 inv_state.slots_changed |= 1 << held.slot();
1304
1305 drop_item_stack_events.send(DropItemStackEvent {
1306 client: packet.client,
1307 from_slot: Some(held.slot()),
1308 stack,
1309 });
1310 }
1311 }
1312 }
1313 PlayerAction::SwapItemWithOffhand => {
1314 if let Ok((mut inv, inv_state, held, mut client)) =
1315 clients.get_mut(packet.client)
1316 {
1317 if inv.readonly {
1319 client.write_packet(&InventoryS2c {
1321 window_id: 0,
1322 state_id: VarInt(inv_state.state_id.0),
1323 slots: Cow::Borrowed(inv.slot_slice()),
1324 carried_item: Cow::Borrowed(&ItemStack::EMPTY),
1325 });
1326 continue;
1327 }
1328
1329 inv.swap_slot(held.slot(), PlayerInventory::SLOT_OFFHAND);
1330 }
1331 }
1332 _ => {}
1333 }
1334 }
1335 }
1336}
1337
1338fn resync_readonly_inventory_after_block_interaction(
1341 mut clients: Query<(&mut Inventory, &HeldItem)>,
1342 mut events: EventReader<InteractBlockEvent>,
1343) {
1344 for event in events.read() {
1345 let Ok((mut inventory, held_item)) = clients.get_mut(event.client) else {
1346 continue;
1347 };
1348
1349 if !inventory.readonly {
1350 continue;
1351 }
1352
1353 let slot = if event.hand == Hand::Main {
1354 held_item.slot()
1355 } else {
1356 PlayerInventory::SLOT_OFFHAND
1357 };
1358
1359 if inventory.slot(slot).is_empty() {
1360 continue;
1361 }
1362
1363 inventory.changed |= 1 << slot;
1364 }
1365}
1366
1367#[derive(Event, Clone, Debug)]
1369pub struct CreativeInventoryActionEvent {
1370 pub client: Entity,
1371 pub slot: i16,
1372 pub clicked_item: ItemStack,
1373}
1374
1375fn handle_creative_inventory_action(
1376 mut packets: EventReader<PacketEvent>,
1377 mut clients: Query<(
1378 &mut Client,
1379 &mut Inventory,
1380 &mut ClientInventoryState,
1381 &GameMode,
1382 )>,
1383 mut inv_action_events: EventWriter<CreativeInventoryActionEvent>,
1384 mut drop_item_stack_events: EventWriter<DropItemStackEvent>,
1385) {
1386 for packet in packets.read() {
1387 if let Some(pkt) = packet.decode::<CreativeInventoryActionC2s>() {
1388 let Ok((mut client, mut inventory, mut inv_state, game_mode)) =
1389 clients.get_mut(packet.client)
1390 else {
1391 continue;
1392 };
1393
1394 if *game_mode != GameMode::Creative {
1395 continue;
1397 }
1398
1399 if pkt.slot == -1 {
1400 let stack = pkt.clicked_item.clone();
1401
1402 if !stack.is_empty() {
1403 drop_item_stack_events.send(DropItemStackEvent {
1404 client: packet.client,
1405 from_slot: None,
1406 stack,
1407 });
1408 }
1409 continue;
1410 }
1411
1412 if pkt.slot < 0 || pkt.slot >= inventory.slot_count() as i16 {
1413 continue;
1415 }
1416
1417 inventory.slots[pkt.slot as usize] = pkt.clicked_item.clone();
1419
1420 inv_state.state_id += 1;
1421
1422 client.write_packet(&ScreenHandlerSlotUpdateS2c {
1427 window_id: 0,
1428 state_id: VarInt(inv_state.state_id.0),
1429 slot_idx: pkt.slot,
1430 slot_data: Cow::Borrowed(&pkt.clicked_item),
1431 });
1432
1433 inv_action_events.send(CreativeInventoryActionEvent {
1434 client: packet.client,
1435 slot: pkt.slot,
1436 clicked_item: pkt.clicked_item,
1437 });
1438 }
1439 }
1440}
1441
1442#[derive(Event, Clone, Debug)]
1443pub struct UpdateSelectedSlotEvent {
1444 pub client: Entity,
1445 pub slot: u8,
1446}
1447
1448fn update_player_selected_slot(mut clients: Query<(&mut Client, &HeldItem), Changed<HeldItem>>) {
1451 for (mut client, held_item) in &mut clients {
1452 client.write_packet(&UpdateSelectedSlotS2c {
1453 slot: held_item.hotbar_idx(),
1454 });
1455 }
1456}
1457
1458fn handle_update_selected_slot(
1460 mut packets: EventReader<PacketEvent>,
1461 mut clients: Query<&mut HeldItem>,
1462 mut events: EventWriter<UpdateSelectedSlotEvent>,
1463) {
1464 for packet in packets.read() {
1465 if let Some(pkt) = packet.decode::<UpdateSelectedSlotC2s>() {
1466 if let Ok(mut mut_held) = clients.get_mut(packet.client) {
1467 let held = mut_held.bypass_change_detection();
1471 if pkt.slot > 8 {
1472 continue;
1474 }
1475
1476 held.set_hotbar_idx(pkt.slot as u8);
1477
1478 events.send(UpdateSelectedSlotEvent {
1479 client: packet.client,
1480 slot: pkt.slot as u8,
1481 });
1482 }
1483 }
1484 }
1485}
1486
1487#[doc(hidden)]
1490pub fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 {
1491 let offset = target_kind.slot_count() as u16;
1493 slot_id - offset + 9
1494}
1495
1496#[derive(Copy, Clone, PartialEq, Eq, Debug)]
1497pub enum InventoryKind {
1498 Generic9x1,
1499 Generic9x2,
1500 Generic9x3,
1501 Generic9x4,
1502 Generic9x5,
1503 Generic9x6,
1504 Generic3x3,
1505 Anvil,
1506 Beacon,
1507 BlastFurnace,
1508 BrewingStand,
1509 Crafting,
1510 Enchantment,
1511 Furnace,
1512 Grindstone,
1513 Hopper,
1514 Lectern,
1515 Loom,
1516 Merchant,
1517 ShulkerBox,
1518 Smithing,
1519 Smoker,
1520 Cartography,
1521 Stonecutter,
1522 Player,
1523}
1524
1525impl InventoryKind {
1526 pub const fn slot_count(self) -> usize {
1529 match self {
1530 InventoryKind::Generic9x1 => 9,
1531 InventoryKind::Generic9x2 => 9 * 2,
1532 InventoryKind::Generic9x3 => 9 * 3,
1533 InventoryKind::Generic9x4 => 9 * 4,
1534 InventoryKind::Generic9x5 => 9 * 5,
1535 InventoryKind::Generic9x6 => 9 * 6,
1536 InventoryKind::Generic3x3 => 3 * 3,
1537 InventoryKind::Anvil => 4,
1538 InventoryKind::Beacon => 1,
1539 InventoryKind::BlastFurnace => 3,
1540 InventoryKind::BrewingStand => 5,
1541 InventoryKind::Crafting => 10,
1542 InventoryKind::Enchantment => 2,
1543 InventoryKind::Furnace => 3,
1544 InventoryKind::Grindstone => 3,
1545 InventoryKind::Hopper => 5,
1546 InventoryKind::Lectern => 1,
1547 InventoryKind::Loom => 4,
1548 InventoryKind::Merchant => 3,
1549 InventoryKind::ShulkerBox => 27,
1550 InventoryKind::Smithing => 3,
1551 InventoryKind::Smoker => 3,
1552 InventoryKind::Cartography => 3,
1553 InventoryKind::Stonecutter => 2,
1554 InventoryKind::Player => 46,
1555 }
1556 }
1557}
1558
1559impl From<InventoryKind> for WindowType {
1560 fn from(value: InventoryKind) -> Self {
1561 match value {
1562 InventoryKind::Generic9x1 => WindowType::Generic9x1,
1563 InventoryKind::Generic9x2 => WindowType::Generic9x2,
1564 InventoryKind::Generic9x3 => WindowType::Generic9x3,
1565 InventoryKind::Generic9x4 => WindowType::Generic9x4,
1566 InventoryKind::Generic9x5 => WindowType::Generic9x5,
1567 InventoryKind::Generic9x6 => WindowType::Generic9x6,
1568 InventoryKind::Generic3x3 => WindowType::Generic3x3,
1569 InventoryKind::Anvil => WindowType::Anvil,
1570 InventoryKind::Beacon => WindowType::Beacon,
1571 InventoryKind::BlastFurnace => WindowType::BlastFurnace,
1572 InventoryKind::BrewingStand => WindowType::BrewingStand,
1573 InventoryKind::Crafting => WindowType::Crafting,
1574 InventoryKind::Enchantment => WindowType::Enchantment,
1575 InventoryKind::Furnace => WindowType::Furnace,
1576 InventoryKind::Grindstone => WindowType::Grindstone,
1577 InventoryKind::Hopper => WindowType::Hopper,
1578 InventoryKind::Lectern => WindowType::Lectern,
1579 InventoryKind::Loom => WindowType::Loom,
1580 InventoryKind::Merchant => WindowType::Merchant,
1581 InventoryKind::ShulkerBox => WindowType::ShulkerBox,
1582 InventoryKind::Smithing => WindowType::Smithing,
1583 InventoryKind::Smoker => WindowType::Smoker,
1584 InventoryKind::Cartography => WindowType::Cartography,
1585 InventoryKind::Stonecutter => WindowType::Stonecutter,
1586 InventoryKind::Player => WindowType::Generic9x4,
1589 }
1590 }
1591}
1592
1593impl From<WindowType> for InventoryKind {
1594 fn from(value: WindowType) -> Self {
1595 match value {
1596 WindowType::Generic9x1 => InventoryKind::Generic9x1,
1597 WindowType::Generic9x2 => InventoryKind::Generic9x2,
1598 WindowType::Generic9x3 => InventoryKind::Generic9x3,
1599 WindowType::Generic9x4 => InventoryKind::Generic9x4,
1600 WindowType::Generic9x5 => InventoryKind::Generic9x5,
1601 WindowType::Generic9x6 => InventoryKind::Generic9x6,
1602 WindowType::Generic3x3 => InventoryKind::Generic3x3,
1603 WindowType::Anvil => InventoryKind::Anvil,
1604 WindowType::Beacon => InventoryKind::Beacon,
1605 WindowType::BlastFurnace => InventoryKind::BlastFurnace,
1606 WindowType::BrewingStand => InventoryKind::BrewingStand,
1607 WindowType::Crafting => InventoryKind::Crafting,
1608 WindowType::Enchantment => InventoryKind::Enchantment,
1609 WindowType::Furnace => InventoryKind::Furnace,
1610 WindowType::Grindstone => InventoryKind::Grindstone,
1611 WindowType::Hopper => InventoryKind::Hopper,
1612 WindowType::Lectern => InventoryKind::Lectern,
1613 WindowType::Loom => InventoryKind::Loom,
1614 WindowType::Merchant => InventoryKind::Merchant,
1615 WindowType::ShulkerBox => InventoryKind::ShulkerBox,
1616 WindowType::Smithing => InventoryKind::Smithing,
1617 WindowType::Smoker => InventoryKind::Smoker,
1618 WindowType::Cartography => InventoryKind::Cartography,
1619 WindowType::Stonecutter => InventoryKind::Stonecutter,
1620 }
1621 }
1622}
1623
1624#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Resource)]
1625pub struct InventorySettings {
1626 pub validate_actions: bool,
1627}
1628
1629impl Default for InventorySettings {
1630 fn default() -> Self {
1631 Self {
1632 validate_actions: true,
1633 }
1634 }
1635}
1636
1637#[cfg(test)]
1638mod tests {
1639 use super::*;
1640
1641 #[test]
1642 fn test_convert_to_player_slot() {
1643 assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 27), 9);
1644 assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 36), 18);
1645 assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x3, 54), 36);
1646 assert_eq!(convert_to_player_slot_id(InventoryKind::Generic9x1, 9), 9);
1647 }
1648
1649 #[test]
1650 fn test_convert_hotbar_slot_id() {
1651 assert_eq!(PlayerInventory::hotbar_to_slot(0), 36);
1652 assert_eq!(PlayerInventory::hotbar_to_slot(4), 40);
1653 assert_eq!(PlayerInventory::hotbar_to_slot(8), 44);
1654 }
1655}