valence_inventory/
lib.rs

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    /// Contains a set bit for each modified slot in `slots`.
75    #[doc(hidden)]
76    pub changed: u64,
77    /// Makes an inventory read-only for clients. This will prevent adding
78    /// or removing items. If this is a player inventory
79    /// This will also make it impossible to drop items while not
80    /// in the inventory (e.g. by pressing Q)
81    pub readonly: bool,
82}
83
84impl Inventory {
85    pub fn new(kind: InventoryKind) -> Self {
86        // TODO: default title to the correct translation key instead
87        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    /// Sets the slot at the given index to the given item stack.
108    ///
109    /// See also [`Inventory::replace_slot`].
110    ///
111    /// ```
112    /// # use valence_inventory::*;
113    /// # use valence_server::item::{ItemStack, ItemKind};
114    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
115    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
116    /// assert_eq!(inv.slot(0).item, ItemKind::Diamond);
117    /// ```
118    #[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    /// Replaces the slot at the given index with the given item stack, and
125    /// returns the old stack in that slot.
126    ///
127    /// See also [`Inventory::set_slot`].
128    ///
129    /// ```
130    /// # use valence_inventory::*;
131    /// # use valence_server::item::{ItemStack, ItemKind};
132    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
133    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
134    /// let old = inv.replace_slot(0, ItemStack::new(ItemKind::IronIngot, 1, None));
135    /// assert_eq!(old.item, ItemKind::Diamond);
136    /// ```
137    #[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    /// Swap the contents of two slots. If the slots are the same, nothing
153    /// happens.
154    ///
155    /// ```
156    /// # use valence_inventory::*;
157    /// # use valence_server::item::{ItemStack, ItemKind};
158    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
159    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
160    /// assert!(inv.slot(1).is_empty());
161    /// inv.swap_slot(0, 1);
162    /// assert_eq!(inv.slot(1).item, ItemKind::Diamond);
163    /// ```
164    #[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            // Nothing to do here, ignore.
177            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    /// Set the amount of items in the given slot without replacing the slot
187    /// entirely. Valid values are 1-127, inclusive, and `amount` will be
188    /// clamped to this range. If the slot is empty, nothing happens.
189    ///
190    /// ```
191    /// # use valence_inventory::*;
192    /// # use valence_server::item::{ItemStack, ItemKind};
193    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
194    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
195    /// inv.set_slot_amount(0, 64);
196    /// assert_eq!(inv.slot(0).count, 64);
197    /// ```
198    #[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    /// The text displayed on the inventory's title bar.
229    ///
230    /// ```
231    /// # use valence_inventory::*;
232    /// # use valence_server::item::{ItemStack, ItemKind};
233    /// # use valence_server::text::Text;
234    /// let inv = Inventory::with_title(InventoryKind::Generic9x3, "Box of Holding");
235    /// assert_eq!(inv.title(), &Text::from("Box of Holding"));
236    /// ```
237    pub fn title(&self) -> &Text {
238        &self.title
239    }
240
241    /// Set the text displayed on the inventory's title bar.
242    ///
243    /// To get the old title, use [`Inventory::replace_title`].
244    ///
245    /// ```
246    /// # use valence_inventory::*;
247    /// let mut inv = Inventory::new(InventoryKind::Generic9x3);
248    /// inv.set_title("Box of Holding");
249    /// ```
250    #[inline]
251    pub fn set_title<'a, T: IntoText<'a>>(&mut self, title: T) {
252        let _ = self.replace_title(title);
253    }
254
255    /// Replace the text displayed on the inventory's title bar, and returns the
256    /// old text.
257    #[must_use]
258    pub fn replace_title<'a, T: IntoText<'a>>(&mut self, title: T) -> Text {
259        // TODO: set title modified flag
260        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    /// Returns the first empty slot in the given range, or `None` if there are
268    /// no empty slots in the range.
269    ///
270    /// ```
271    /// # use valence_inventory::*;
272    /// # use valence_server::item::*;
273    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
274    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
275    /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 1, None));
276    /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
277    /// assert_eq!(inv.first_empty_slot_in(0..6), Some(1));
278    /// assert_eq!(inv.first_empty_slot_in(2..6), Some(4));
279    /// ```
280    #[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    /// Returns the first empty slot in the inventory, or `None` if there are no
293    /// empty slots.
294    /// ```
295    /// # use valence_inventory::*;
296    /// # use valence_server::item::*;
297    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
298    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
299    /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 1, None));
300    /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
301    /// assert_eq!(inv.first_empty_slot(), Some(1));
302    /// ```
303    #[inline]
304    pub fn first_empty_slot(&self) -> Option<u16> {
305        self.first_empty_slot_in(0..self.slot_count())
306    }
307
308    /// Returns the first slot with the given [`ItemKind`] in the inventory
309    /// where `count < stack_max`, or `None` if there are no empty slots.
310    /// ```
311    /// # use valence_inventory::*;
312    /// # use valence_server::item::*;
313    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
314    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
315    /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 64, None));
316    /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
317    /// inv.set_slot(4, ItemStack::new(ItemKind::GoldIngot, 1, None));
318    /// assert_eq!(
319    ///     inv.first_slot_with_item_in(ItemKind::GoldIngot, 64, 0..5),
320    ///     Some(4)
321    /// );
322    /// ```
323    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    /// Returns the first slot with the given [`ItemKind`] in the inventory
343    /// where `count < stack_max`, or `None` if there are no empty slots.
344    /// ```
345    /// # use valence_inventory::*;
346    /// # use valence_server::item::*;
347    /// let mut inv = Inventory::new(InventoryKind::Generic9x1);
348    /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
349    /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 64, None));
350    /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
351    /// inv.set_slot(4, ItemStack::new(ItemKind::GoldIngot, 1, None));
352    /// assert_eq!(inv.first_slot_with_item(ItemKind::GoldIngot, 64), Some(4));
353    /// ```
354    #[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/// Miscellaneous inventory data.
361#[derive(Component, Debug)]
362pub struct ClientInventoryState {
363    /// The current window ID. Incremented when inventories are opened.
364    window_id: u8,
365    state_id: Wrapping<i32>,
366    /// Tracks what slots have been changed by this client in this tick, so we
367    /// don't need to send updates for them.
368    slots_changed: u64,
369    /// If `Some`: The item the user thinks they updated their cursor item to on
370    /// the last tick.
371    /// If `None`: the user did not update their cursor item in the last tick.
372    /// This is so we can inform the user of the update through change detection
373    /// when they differ in a given tick
374    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/// Indicates which hotbar slot the player is currently holding.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Component, Deref)]
391pub struct HeldItem {
392    held_item_slot: u16,
393}
394
395impl HeldItem {
396    /// The slot ID of the currently held item, in the range 36-44 inclusive.
397    /// This value is safe to use on the player's inventory directly.
398    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        // temp
408        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/// The item stack that the client thinks it's holding under the mouse
422/// cursor.
423#[derive(Component, Clone, PartialEq, Default, Debug, Deref, DerefMut)]
424pub struct CursorItem(pub ItemStack);
425
426/// Used to indicate that the client with this component is currently viewing
427/// an inventory.
428#[derive(Component, Clone, Debug)]
429pub struct OpenInventory {
430    /// The entity with the `Inventory` component that the client is currently
431    /// viewing.
432    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
445/// A helper to represent the inventory window that the player is currently
446/// viewing. Handles dispatching reads to the correct inventory.
447///
448/// This is a read-only version of [`InventoryWindowMut`].
449///
450/// ```
451/// # use valence_inventory::*;
452/// # use valence_server::item::*;
453/// let mut player_inventory = Inventory::new(InventoryKind::Player);
454/// player_inventory.set_slot(36, ItemStack::new(ItemKind::Diamond, 1, None));
455///
456/// let target_inventory = Inventory::new(InventoryKind::Generic9x3);
457/// let window = InventoryWindow::new(&player_inventory, Some(&target_inventory));
458///
459/// assert_eq!(window.slot(54), &ItemStack::new(ItemKind::Diamond, 1, None));
460/// ```
461pub 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            // when the window is split, we can only access the main slots of player's
492            // inventory
493            PlayerInventory::MAIN_SIZE + open_inv.slot_count()
494        } else {
495            self.player_inventory.slot_count()
496        }
497    }
498}
499
500/// A helper to represent the inventory window that the player is currently
501/// viewing. Handles dispatching reads/writes to the correct inventory.
502///
503/// This is a writable version of [`InventoryWindow`].
504///
505/// ```
506/// # use valence_inventory::*;
507/// # use valence_server::item::*;
508/// let mut player_inventory = Inventory::new(InventoryKind::Player);
509/// let mut target_inventory = Inventory::new(InventoryKind::Generic9x3);
510/// let mut window = InventoryWindowMut::new(&mut player_inventory, Some(&mut target_inventory));
511///
512/// window.set_slot(54, ItemStack::new(ItemKind::Diamond, 1, None));
513///
514/// assert_eq!(
515///     player_inventory.slot(36),
516///     &ItemStack::new(ItemKind::Diamond, 1, None)
517/// );
518/// ```
519pub 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            // when the window is split, we can only access the main slots of player's
575            // inventory
576            PlayerInventory::MAIN_SIZE + open_inv.slot_count()
577        } else {
578            self.player_inventory.slot_count()
579        }
580    }
581}
582
583/// Attach the necessary inventory components to new clients.
584fn 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                // First slot of the hotbar.
597                held_item_slot: 36,
598            },
599        ));
600    }
601}
602
603/// Send updates for each client's player inventory.
604fn 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            // Update the whole inventory.
622
623            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            // Send the modified slots.
636
637            // The slots that were NOT modified by this client, and they need to be sent
638            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
661/// Handles the `OpenInventory` component being added to a client, which
662/// indicates that the client is now viewing an inventory, and sends inventory
663/// updates to the client when the inventory is modified.
664fn 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    // These operations need to happen in this order.
676
677    // Send the inventory contents to all clients that are viewing an inventory.
678    for (client_entity, mut client, mut inv_state, cursor_item, mut open_inventory) in &mut clients
679    {
680        // Validate that the inventory exists.
681        let Ok([inventory, player_inventory]) =
682            inventories.get_many_mut([open_inventory.entity, client_entity])
683        else {
684            // The inventory no longer exists, so close the inventory.
685            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            // Send the inventory to the client if the client just opened the inventory.
696            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            // The client is already viewing the inventory.
713
714            if inventory.changed == u64::MAX {
715                // Send the entire inventory.
716
717                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                // Send the changed slots.
727
728                // The slots that were NOT changed by this client, and they need to be sent.
729                let changed_filtered =
730                    u128::from(inventory.changed & !open_inventory.client_changed);
731
732                // The slots changed in the player inventory (e.g by calling
733                // `inventory.set_slot` while the player is viewing the inventory).
734                let mut player_inventory_changed = u128::from(player_inventory.changed);
735
736                // Ignore the armor and crafting grid slots because they are not part of
737                // the open inventory.
738                player_inventory_changed >>= *PlayerInventory::SLOTS_MAIN.start();
739                // "Append" the player inventory to the end of the slots belonging to the opened
740                // inventory.
741                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        // Since these happen every gametick we only want to trigger change detection
774        // if we actually did update these. Otherwise systems that are
775        // running looking for changes to the `Inventory`,`ClientInventoryState`
776        // or `OpenInventory` components get unneccerely ran each gametick
777        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        // The cursor item was not the item the user themselves interacted with
792        if inv_state.client_updated_cursor_item.as_ref() != Some(&cursor_item.0) {
793            // Contrary to what you might think, we actually don't want to increment the
794            // state ID here because the client doesn't actually acknowledge the
795            // state_id change for this packet specifically. See #304.
796            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
810/// Handles clients telling the server that they are closing an inventory.
811fn 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
821/// Detects when a client's `OpenInventory` component is removed, which
822/// indicates that the client is no longer viewing an inventory.
823fn 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// TODO: make this event user friendly.
837#[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            // Not the packet we're looking for.
872            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            // The client does not exist, ignore.
879            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            // Resync the inventory.
898
899            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            // The client is dropping the cursor item by clicking outside the window.
915
916            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            // The client is dropping an item by pressing the drop key.
927
928            let entire_stack = pkt.button == 1;
929
930            // Needs to open the inventory for if the player is dropping an item while
931            // having an inventory open.
932            if let Some(open_inventory) = open_inventory {
933                // The player is interacting with an inventory that is open.
934
935                let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else {
936                    // The inventory does not exist, ignore.
937                    continue;
938                };
939
940                if inv_state.state_id.0 != pkt.state_id.0 {
941                    // Client is out of sync. Resync and ignore click.
942
943                    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                    // The player was just clicking outside the inventories without holding an item
958                    continue;
959                }
960
961                if (0_i16..target_inventory.slot_count() as i16).contains(&pkt.slot_idx) {
962                    // The player is dropping an item from another inventory.
963
964                    if target_inventory.readonly {
965                        // resync target inventory
966                        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                            // we already checked that the slot was not empty and that the
985                            // stack count is > 1
986                            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                    // The player is dropping an item from their inventory.
998
999                    if client_inv.readonly {
1000                        // resync the client inventory
1001                        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                            // we already checked that the slot was not empty and that the
1022                            // stack count is > 1
1023                            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                // The player has no inventory open and is dropping an item from their
1036                // inventory.
1037
1038                if client_inv.readonly {
1039                    // resync the client inventory
1040                    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                    // The player was just clicking outside the inventories without holding an item
1050                    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                        // we already checked that the slot was not empty and that the
1061                        // stack count is > 1
1062                        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            // The player is clicking a slot in an inventory.
1075
1076            // Validate the window id.
1077            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                // The player is interacting with an inventory that is
1089                // open or has an inventory open while interacting with their own inventory.
1090
1091                let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else {
1092                    // The inventory does not exist, ignore.
1093                    continue;
1094                };
1095
1096                if inv_state.state_id.0 != pkt.state_id.0 {
1097                    // Client is out of sync. Resync and ignore click.
1098
1099                    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                        // The client is interacting with a slot in their own inventory.
1140                        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                    // resync the target inventory
1152                    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                    // resync the client inventory
1160                    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                // The client is interacting with their own inventory.
1169
1170                if inv_state.state_id.0 != pkt.state_id.0 {
1171                    // Client is out of sync. Resync and ignore the click.
1172
1173                    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                        // The client is trying to interact with a slot that does not exist,
1199                        // ignore.
1200                        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                    // resync the client inventory
1212                    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                            // resync the client inventory
1254                            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                            // resync the client inventory
1282                            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                        // this check here might not actually be necessary
1318                        if inv.readonly {
1319                            // resync the client inventory
1320                            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
1338/// If the player tries to place a block while their inventory is readonly
1339/// it will be desynced, therefore we set the slot as changed.
1340fn 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// TODO: make this event user friendly.
1368#[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                // The client is not in creative mode, ignore.
1396                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                // The client is trying to interact with a slot that does not exist, ignore.
1414                continue;
1415            }
1416
1417            // Set the slot without marking it as changed.
1418            inventory.slots[pkt.slot as usize] = pkt.clicked_item.clone();
1419
1420            inv_state.state_id += 1;
1421
1422            // HACK: notchian clients rely on the server to send the slot update when in
1423            // creative mode. Simply marking the slot as changed is not enough. This was
1424            // discovered because shift-clicking the destroy item slot in creative mode does
1425            // not work without this hack.
1426            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
1448/// Handles the `HeldItem` component being changed on a client entity, which
1449/// indicates that the server has changed the selected hotbar slot.
1450fn 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
1458/// Client to Server `HeldItem` Slot
1459fn 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                // We bypass the change detection here because the server listens for changes
1468                // of `HeldItem` in order to send the update to the client.
1469                // This is not required here because the update is coming from the client.
1470                let held = mut_held.bypass_change_detection();
1471                if pkt.slot > 8 {
1472                    // The client is trying to interact with a slot that does not exist, ignore.
1473                    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/// Convert a slot that is outside a target inventory's range to a slot that is
1488/// inside the player's inventory.
1489#[doc(hidden)]
1490pub fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 {
1491    // the first slot in the player's general inventory
1492    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    /// The number of slots in this inventory. When the inventory is shown to
1527    /// clients, this number does not include the player's main inventory slots.
1528    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            // arbitrarily chosen, because a player inventory technically does not have a window
1587            // type
1588            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}