azalea_client/plugins/
inventory.rs

1use std::collections::{HashMap, HashSet};
2
3use azalea_chat::FormattedText;
4pub use azalea_inventory::*;
5use azalea_inventory::{
6    item::MaxStackSizeExt,
7    operations::{
8        ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
9        QuickCraftStatusKind, QuickMoveClick, ThrowClick,
10    },
11};
12use azalea_protocol::packets::game::{
13    s_container_click::{HashedStack, ServerboundContainerClick},
14    s_container_close::ServerboundContainerClose,
15    s_set_carried_item::ServerboundSetCarriedItem,
16};
17use azalea_registry::MenuKind;
18use bevy_app::{App, Plugin, Update};
19use bevy_ecs::prelude::*;
20use tracing::{error, warn};
21
22use crate::{
23    Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
24};
25
26pub struct InventoryPlugin;
27impl Plugin for InventoryPlugin {
28    fn build(&self, app: &mut App) {
29        app.add_event::<ClientSideCloseContainerEvent>()
30            .add_event::<MenuOpenedEvent>()
31            .add_event::<CloseContainerEvent>()
32            .add_event::<ContainerClickEvent>()
33            .add_event::<SetContainerContentEvent>()
34            .add_event::<SetSelectedHotbarSlotEvent>()
35            .add_systems(
36                Update,
37                (
38                    handle_set_selected_hotbar_slot_event,
39                    handle_menu_opened_event,
40                    handle_set_container_content_event,
41                    handle_container_click_event,
42                    handle_container_close_event,
43                    handle_client_side_close_container_event,
44                )
45                    .chain()
46                    .in_set(InventorySet)
47                    .before(perform_respawn),
48            );
49    }
50}
51
52#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
53pub struct InventorySet;
54
55impl Client {
56    /// Return the menu that is currently open. If no menu is open, this will
57    /// have the player's inventory.
58    pub fn menu(&self) -> Menu {
59        let mut ecs = self.ecs.lock();
60        let inventory = self.query::<&Inventory>(&mut ecs);
61        inventory.menu().clone()
62    }
63}
64
65/// A component present on all local players that have an inventory.
66#[derive(Component, Debug, Clone)]
67pub struct Inventory {
68    /// A component that contains the player's inventory menu. This is
69    /// guaranteed to be a `Menu::Player`.
70    ///
71    /// We keep it as a [`Menu`] since `Menu` has some useful functions that
72    /// bare [`azalea_inventory::Player`] doesn't have.
73    pub inventory_menu: azalea_inventory::Menu,
74
75    /// The ID of the container that's currently open. Its value is not
76    /// guaranteed to be anything specific, and may change every time you open a
77    /// container (unless it's 0, in which case it means that no container is
78    /// open).
79    pub id: i32,
80    /// The current container menu that the player has open. If no container is
81    /// open, this will be `None`.
82    pub container_menu: Option<azalea_inventory::Menu>,
83    /// The custom name of the menu that's currently open. This is Some when
84    /// `container_menu` is Some.
85    pub container_menu_title: Option<FormattedText>,
86    /// The item that is currently held by the cursor. `Slot::Empty` if nothing
87    /// is currently being held.
88    ///
89    /// This is different from [`Self::selected_hotbar_slot`], which is the
90    /// item that's selected in the hotbar.
91    pub carried: ItemStack,
92    /// An identifier used by the server to track client inventory desyncs. This
93    /// is sent on every container click, and it's only ever updated when the
94    /// server sends a new container update.
95    pub state_id: u32,
96
97    pub quick_craft_status: QuickCraftStatusKind,
98    pub quick_craft_kind: QuickCraftKind,
99    /// A set of the indexes of the slots that have been right clicked in
100    /// this "quick craft".
101    pub quick_craft_slots: HashSet<u16>,
102
103    /// The index of the item in the hotbar that's currently being held by the
104    /// player. This MUST be in the range 0..9 (not including 9).
105    ///
106    /// In a vanilla client this is changed by pressing the number keys or using
107    /// the scroll wheel.
108    pub selected_hotbar_slot: u8,
109}
110
111impl Inventory {
112    /// Returns a reference to the currently active menu. If a container is open
113    /// it'll return [`Self::container_menu`], otherwise
114    /// [`Self::inventory_menu`].
115    ///
116    /// Use [`Self::menu_mut`] if you need a mutable reference.
117    pub fn menu(&self) -> &azalea_inventory::Menu {
118        match &self.container_menu {
119            Some(menu) => menu,
120            _ => &self.inventory_menu,
121        }
122    }
123
124    /// Returns a mutable reference to the currently active menu. If a container
125    /// is open it'll return [`Self::container_menu`], otherwise
126    /// [`Self::inventory_menu`].
127    ///
128    /// Use [`Self::menu`] if you don't need a mutable reference.
129    pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
130        match &mut self.container_menu {
131            Some(menu) => menu,
132            _ => &mut self.inventory_menu,
133        }
134    }
135
136    /// Modify the inventory as if the given operation was performed on it.
137    pub fn simulate_click(
138        &mut self,
139        operation: &ClickOperation,
140        player_abilities: &PlayerAbilities,
141    ) {
142        if let ClickOperation::QuickCraft(quick_craft) = operation {
143            let last_quick_craft_status_tmp = self.quick_craft_status.clone();
144            self.quick_craft_status = last_quick_craft_status_tmp.clone();
145            let last_quick_craft_status = last_quick_craft_status_tmp;
146
147            // no carried item, reset
148            if self.carried.is_empty() {
149                return self.reset_quick_craft();
150            }
151            // if we were starting or ending, or now we aren't ending and the status
152            // changed, reset
153            if (last_quick_craft_status == QuickCraftStatusKind::Start
154                || last_quick_craft_status == QuickCraftStatusKind::End
155                || self.quick_craft_status != QuickCraftStatusKind::End)
156                && (self.quick_craft_status != last_quick_craft_status)
157            {
158                return self.reset_quick_craft();
159            }
160            if self.quick_craft_status == QuickCraftStatusKind::Start {
161                self.quick_craft_kind = quick_craft.kind.clone();
162                if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
163                {
164                    self.quick_craft_status = QuickCraftStatusKind::Add;
165                    self.quick_craft_slots.clear();
166                } else {
167                    self.reset_quick_craft();
168                }
169                return;
170            }
171            if let QuickCraftStatus::Add { slot } = quick_craft.status {
172                let slot_item = self.menu().slot(slot as usize);
173                if let Some(slot_item) = slot_item {
174                    if let ItemStack::Present(carried) = &self.carried {
175                        // minecraft also checks slot.may_place(carried) and
176                        // menu.can_drag_to(slot)
177                        // but they always return true so they're not relevant for us
178                        if can_item_quick_replace(slot_item, &self.carried, true)
179                            && (self.quick_craft_kind == QuickCraftKind::Right
180                                || carried.count as usize > self.quick_craft_slots.len())
181                        {
182                            self.quick_craft_slots.insert(slot);
183                        }
184                    }
185                }
186                return;
187            }
188            if self.quick_craft_status == QuickCraftStatusKind::End {
189                if !self.quick_craft_slots.is_empty() {
190                    if self.quick_craft_slots.len() == 1 {
191                        // if we only clicked one slot, then turn this
192                        // QuickCraftClick into a PickupClick
193                        let slot = *self.quick_craft_slots.iter().next().unwrap();
194                        self.reset_quick_craft();
195                        self.simulate_click(
196                            &match self.quick_craft_kind {
197                                QuickCraftKind::Left => {
198                                    PickupClick::Left { slot: Some(slot) }.into()
199                                }
200                                QuickCraftKind::Right => {
201                                    PickupClick::Left { slot: Some(slot) }.into()
202                                }
203                                QuickCraftKind::Middle => {
204                                    // idk just do nothing i guess
205                                    return;
206                                }
207                            },
208                            player_abilities,
209                        );
210                        return;
211                    }
212
213                    let ItemStack::Present(mut carried) = self.carried.clone() else {
214                        // this should never happen
215                        return self.reset_quick_craft();
216                    };
217
218                    let mut carried_count = carried.count;
219                    let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
220
221                    loop {
222                        let mut slot: &ItemStack;
223                        let mut slot_index: u16;
224                        let mut item_stack: &ItemStack;
225
226                        loop {
227                            let Some(&next_slot) = quick_craft_slots_iter.next() else {
228                                carried.count = carried_count;
229                                self.carried = ItemStack::Present(carried);
230                                return self.reset_quick_craft();
231                            };
232
233                            slot = self.menu().slot(next_slot as usize).unwrap();
234                            slot_index = next_slot;
235                            item_stack = &self.carried;
236
237                            if slot.is_present()
238                                    && can_item_quick_replace(slot, item_stack, true)
239                                    // this always returns true in most cases
240                                    // && slot.may_place(item_stack)
241                                    && (
242                                        self.quick_craft_kind == QuickCraftKind::Middle
243                                        || item_stack.count()  >= self.quick_craft_slots.len() as i32
244                                    )
245                            {
246                                break;
247                            }
248                        }
249
250                        // get the ItemStackData for the slot
251                        let ItemStack::Present(slot) = slot else {
252                            unreachable!("the loop above requires the slot to be present to break")
253                        };
254
255                        // if self.can_drag_to(slot) {
256                        let mut new_carried = carried.clone();
257                        let slot_item_count = slot.count;
258                        get_quick_craft_slot_count(
259                            &self.quick_craft_slots,
260                            &self.quick_craft_kind,
261                            &mut new_carried,
262                            slot_item_count,
263                        );
264                        let max_stack_size = i32::min(
265                            new_carried.kind.max_stack_size(),
266                            i32::min(
267                                new_carried.kind.max_stack_size(),
268                                slot.kind.max_stack_size(),
269                            ),
270                        );
271                        if new_carried.count > max_stack_size {
272                            new_carried.count = max_stack_size;
273                        }
274
275                        carried_count -= new_carried.count - slot_item_count;
276                        // we have to inline self.menu_mut() here to avoid the borrow checker
277                        // complaining
278                        let menu = match &mut self.container_menu {
279                            Some(menu) => menu,
280                            _ => &mut self.inventory_menu,
281                        };
282                        *menu.slot_mut(slot_index as usize).unwrap() =
283                            ItemStack::Present(new_carried);
284                    }
285                }
286            } else {
287                return self.reset_quick_craft();
288            }
289        }
290        // the quick craft status should always be in start if we're not in quick craft
291        // mode
292        if self.quick_craft_status != QuickCraftStatusKind::Start {
293            return self.reset_quick_craft();
294        }
295
296        match operation {
297            // left clicking outside inventory
298            ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
299                if self.carried.is_present() {
300                    // vanilla has `player.drop`s but they're only used
301                    // server-side
302                    // they're included as comments here in case you want to adapt this for a server
303                    // implementation
304
305                    // player.drop(self.carried, true);
306                    self.carried = ItemStack::Empty;
307                }
308            }
309            ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
310                if self.carried.is_present() {
311                    let _item = self.carried.split(1);
312                    // player.drop(item, true);
313                }
314            }
315            ClickOperation::Pickup(
316                PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
317            ) => {
318                let Some(slot_item) = self.menu().slot(*slot as usize) else {
319                    return;
320                };
321                let carried = &self.carried;
322                // vanilla does a check called tryItemClickBehaviourOverride
323                // here
324                // i don't understand it so i didn't implement it
325                match slot_item {
326                    ItemStack::Empty => if carried.is_present() {},
327                    ItemStack::Present(_) => todo!(),
328                }
329            }
330            ClickOperation::QuickMove(
331                QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
332            ) => {
333                // in vanilla it also tests if QuickMove has a slot index of -999
334                // but i don't think that's ever possible so it's not covered here
335                loop {
336                    let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
337                    let slot_item = self.menu().slot(*slot as usize).unwrap();
338                    if new_slot_item.is_empty() || slot_item != &new_slot_item {
339                        break;
340                    }
341                }
342            }
343            ClickOperation::Swap(s) => {
344                let source_slot_index = s.source_slot as usize;
345                let target_slot_index = s.target_slot as usize;
346
347                let Some(source_slot) = self.menu().slot(source_slot_index) else {
348                    return;
349                };
350                let Some(target_slot) = self.menu().slot(target_slot_index) else {
351                    return;
352                };
353                if source_slot.is_empty() && target_slot.is_empty() {
354                    return;
355                }
356
357                if target_slot.is_empty() {
358                    if self.menu().may_pickup(source_slot_index) {
359                        let source_slot = source_slot.clone();
360                        let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
361                        *target_slot = source_slot;
362                    }
363                } else if source_slot.is_empty() {
364                    let ItemStack::Present(target_item) = target_slot else {
365                        unreachable!("target slot is not empty but is not present");
366                    };
367                    if self.menu().may_place(source_slot_index, target_item) {
368                        // get the target_item but mutable
369                        let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
370
371                        let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
372                        let new_source_slot = target_slot.split(source_max_stack_size);
373                        *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
374                    }
375                } else if self.menu().may_pickup(source_slot_index) {
376                    let ItemStack::Present(target_item) = target_slot else {
377                        unreachable!("target slot is not empty but is not present");
378                    };
379                    if self.menu().may_place(source_slot_index, target_item) {
380                        let source_max_stack = self.menu().max_stack_size(source_slot_index);
381                        if target_slot.count() > source_max_stack as i32 {
382                            // if there's more than the max stack size in the target slot
383
384                            let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
385                            let new_source_slot = target_slot.split(source_max_stack);
386                            *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
387                            // if !self.inventory_menu.add(new_source_slot) {
388                            //     player.drop(new_source_slot, true);
389                            // }
390                        } else {
391                            // normal swap
392                            let new_target_slot = source_slot.clone();
393                            let new_source_slot = target_slot.clone();
394
395                            let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
396                            *target_slot = new_target_slot;
397
398                            let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
399                            *source_slot = new_source_slot;
400                        }
401                    }
402                }
403            }
404            ClickOperation::Clone(CloneClick { slot }) => {
405                if !player_abilities.instant_break || self.carried.is_present() {
406                    return;
407                }
408                let Some(source_slot) = self.menu().slot(*slot as usize) else {
409                    return;
410                };
411                let ItemStack::Present(source_item) = source_slot else {
412                    return;
413                };
414                let mut new_carried = source_item.clone();
415                new_carried.count = new_carried.kind.max_stack_size();
416                self.carried = ItemStack::Present(new_carried);
417            }
418            ClickOperation::Throw(c) => {
419                if self.carried.is_present() {
420                    return;
421                }
422
423                let (ThrowClick::Single { slot: slot_index }
424                | ThrowClick::All { slot: slot_index }) = c;
425                let slot_index = *slot_index as usize;
426
427                let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
428                    return;
429                };
430                let ItemStack::Present(slot_item) = slot else {
431                    return;
432                };
433
434                let dropping_count = match c {
435                    ThrowClick::Single { .. } => 1,
436                    ThrowClick::All { .. } => slot_item.count,
437                };
438
439                let _dropping = slot_item.split(dropping_count as u32);
440                // player.drop(dropping, true);
441            }
442            ClickOperation::PickupAll(PickupAllClick {
443                slot: source_slot_index,
444                reversed,
445            }) => {
446                let source_slot_index = *source_slot_index as usize;
447
448                let source_slot = self.menu().slot(source_slot_index).unwrap();
449                let target_slot = self.carried.clone();
450
451                if target_slot.is_empty()
452                    || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
453                {
454                    return;
455                }
456
457                let ItemStack::Present(target_slot_item) = &target_slot else {
458                    unreachable!("target slot is not empty but is not present");
459                };
460
461                for round in 0..2 {
462                    let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
463                        Box::new((0..self.menu().len()).rev())
464                    } else {
465                        Box::new(0..self.menu().len())
466                    };
467
468                    for i in iterator {
469                        if target_slot_item.count < target_slot_item.kind.max_stack_size() {
470                            let checking_slot = self.menu().slot(i).unwrap();
471                            if let ItemStack::Present(checking_item) = checking_slot {
472                                if can_item_quick_replace(checking_slot, &target_slot, true)
473                                    && self.menu().may_pickup(i)
474                                    && (round != 0
475                                        || checking_item.count
476                                            != checking_item.kind.max_stack_size())
477                                {
478                                    // get the checking_slot and checking_item again but mutable
479                                    let checking_slot = self.menu_mut().slot_mut(i).unwrap();
480
481                                    let taken_item =
482                                        checking_slot.split(checking_slot.count() as u32);
483
484                                    // now extend the carried item
485                                    let target_slot = &mut self.carried;
486                                    let ItemStack::Present(target_slot_item) = target_slot else {
487                                        unreachable!("target slot is not empty but is not present");
488                                    };
489                                    target_slot_item.count += taken_item.count();
490                                }
491                            }
492                        }
493                    }
494                }
495            }
496            _ => {}
497        }
498    }
499
500    fn reset_quick_craft(&mut self) {
501        self.quick_craft_status = QuickCraftStatusKind::Start;
502        self.quick_craft_slots.clear();
503    }
504
505    /// Get the item in the player's hotbar that is currently being held.
506    pub fn held_item(&self) -> ItemStack {
507        let inventory = &self.inventory_menu;
508        let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
509        hotbar_items[self.selected_hotbar_slot as usize].clone()
510    }
511}
512
513fn can_item_quick_replace(
514    target_slot: &ItemStack,
515    item: &ItemStack,
516    ignore_item_count: bool,
517) -> bool {
518    let ItemStack::Present(target_slot) = target_slot else {
519        return false;
520    };
521    let ItemStack::Present(item) = item else {
522        // i *think* this is what vanilla does
523        // not 100% sure lol probably doesn't matter though
524        return false;
525    };
526
527    if !item.is_same_item_and_components(target_slot) {
528        return false;
529    }
530    let count = target_slot.count as u16
531        + if ignore_item_count {
532            0
533        } else {
534            item.count as u16
535        };
536    count <= item.kind.max_stack_size() as u16
537}
538
539fn get_quick_craft_slot_count(
540    quick_craft_slots: &HashSet<u16>,
541    quick_craft_kind: &QuickCraftKind,
542    item: &mut ItemStackData,
543    slot_item_count: i32,
544) {
545    item.count = match quick_craft_kind {
546        QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
547        QuickCraftKind::Right => 1,
548        QuickCraftKind::Middle => item.kind.max_stack_size(),
549    };
550    item.count += slot_item_count;
551}
552
553impl Default for Inventory {
554    fn default() -> Self {
555        Inventory {
556            inventory_menu: Menu::Player(azalea_inventory::Player::default()),
557            id: 0,
558            container_menu: None,
559            container_menu_title: None,
560            carried: ItemStack::Empty,
561            state_id: 0,
562            quick_craft_status: QuickCraftStatusKind::Start,
563            quick_craft_kind: QuickCraftKind::Middle,
564            quick_craft_slots: HashSet::new(),
565            selected_hotbar_slot: 0,
566        }
567    }
568}
569
570/// Sent from the server when a menu (like a chest or crafting table) was
571/// opened by the client.
572#[derive(Event, Debug)]
573pub struct MenuOpenedEvent {
574    pub entity: Entity,
575    pub window_id: i32,
576    pub menu_type: MenuKind,
577    pub title: FormattedText,
578}
579fn handle_menu_opened_event(
580    mut events: EventReader<MenuOpenedEvent>,
581    mut query: Query<&mut Inventory>,
582) {
583    for event in events.read() {
584        let mut inventory = query.get_mut(event.entity).unwrap();
585        inventory.id = event.window_id;
586        inventory.container_menu = Some(Menu::from_kind(event.menu_type));
587        inventory.container_menu_title = Some(event.title.clone());
588    }
589}
590
591/// Tell the server that we want to close a container.
592///
593/// Note that this is also sent when the client closes its own inventory, even
594/// though there is no packet for opening its inventory.
595#[derive(Event)]
596pub struct CloseContainerEvent {
597    pub entity: Entity,
598    /// The ID of the container to close. 0 for the player's inventory. If this
599    /// is not the same as the currently open inventory, nothing will happen.
600    pub id: i32,
601}
602fn handle_container_close_event(
603    query: Query<(Entity, &Inventory)>,
604    mut events: EventReader<CloseContainerEvent>,
605    mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
606    mut commands: Commands,
607) {
608    for event in events.read() {
609        let (entity, inventory) = query.get(event.entity).unwrap();
610        if event.id != inventory.id {
611            warn!(
612                "Tried to close container with ID {}, but the current container ID is {}",
613                event.id, inventory.id
614            );
615            continue;
616        }
617
618        commands.trigger(SendPacketEvent::new(
619            entity,
620            ServerboundContainerClose {
621                container_id: inventory.id,
622            },
623        ));
624        client_side_events.write(ClientSideCloseContainerEvent {
625            entity: event.entity,
626        });
627    }
628}
629
630/// Close a container without notifying the server.
631///
632/// Note that this also gets fired when we get a [`CloseContainerEvent`].
633#[derive(Event)]
634pub struct ClientSideCloseContainerEvent {
635    pub entity: Entity,
636}
637pub fn handle_client_side_close_container_event(
638    mut events: EventReader<ClientSideCloseContainerEvent>,
639    mut query: Query<&mut Inventory>,
640) {
641    for event in events.read() {
642        let mut inventory = query.get_mut(event.entity).unwrap();
643        inventory.container_menu = None;
644        inventory.id = 0;
645        inventory.container_menu_title = None;
646    }
647}
648
649#[derive(Event, Debug)]
650pub struct ContainerClickEvent {
651    pub entity: Entity,
652    pub window_id: i32,
653    pub operation: ClickOperation,
654}
655pub fn handle_container_click_event(
656    mut query: Query<(Entity, &mut Inventory)>,
657    mut events: EventReader<ContainerClickEvent>,
658    mut commands: Commands,
659) {
660    for event in events.read() {
661        let (entity, mut inventory) = query.get_mut(event.entity).unwrap();
662        if inventory.id != event.window_id {
663            error!(
664                "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
665                event.window_id, inventory.id
666            );
667            continue;
668        }
669
670        let menu = inventory.menu_mut();
671        let old_slots = menu.slots().clone();
672
673        // menu.click(&event.operation);
674
675        // see which slots changed after clicking and put them in the hashmap
676        // the server uses this to check if we desynced
677        let mut changed_slots: HashMap<u16, HashedStack> = HashMap::new();
678        for (slot_index, old_slot) in old_slots.iter().enumerate() {
679            let new_slot = &menu.slots()[slot_index];
680            if old_slot != new_slot {
681                changed_slots.insert(slot_index as u16, HashedStack::from(new_slot));
682            }
683        }
684
685        commands.trigger(SendPacketEvent::new(
686            entity,
687            ServerboundContainerClick {
688                container_id: event.window_id,
689                state_id: inventory.state_id,
690                slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
691                button_num: event.operation.button_num(),
692                click_type: event.operation.click_type(),
693                changed_slots,
694                carried_item: (&inventory.carried).into(),
695            },
696        ));
697    }
698}
699
700/// Sent from the server when the contents of a container are replaced. Usually
701/// triggered by the `ContainerSetContent` packet.
702#[derive(Event)]
703pub struct SetContainerContentEvent {
704    pub entity: Entity,
705    pub slots: Vec<ItemStack>,
706    pub container_id: i32,
707}
708fn handle_set_container_content_event(
709    mut events: EventReader<SetContainerContentEvent>,
710    mut query: Query<&mut Inventory>,
711) {
712    for event in events.read() {
713        let mut inventory = query.get_mut(event.entity).unwrap();
714
715        if event.container_id != inventory.id {
716            warn!(
717                "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
718                event.container_id, inventory.id
719            );
720            continue;
721        }
722
723        let menu = inventory.menu_mut();
724        for (i, slot) in event.slots.iter().enumerate() {
725            if let Some(slot_mut) = menu.slot_mut(i) {
726                *slot_mut = slot.clone();
727            }
728        }
729    }
730}
731
732#[derive(Event)]
733pub struct SetSelectedHotbarSlotEvent {
734    pub entity: Entity,
735    /// The hotbar slot to select. This should be in the range 0..=8.
736    pub slot: u8,
737}
738fn handle_set_selected_hotbar_slot_event(
739    mut events: EventReader<SetSelectedHotbarSlotEvent>,
740    mut commands: Commands,
741    mut query: Query<&mut Inventory>,
742) {
743    for event in events.read() {
744        let mut inventory = query.get_mut(event.entity).unwrap();
745
746        // if the slot is already selected, don't send a packet
747        if inventory.selected_hotbar_slot == event.slot {
748            continue;
749        }
750
751        inventory.selected_hotbar_slot = event.slot;
752        commands.trigger(SendPacketEvent::new(
753            event.entity,
754            ServerboundSetCarriedItem {
755                slot: event.slot as u16,
756            },
757        ));
758    }
759}