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