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