aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/inventory.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins/inventory.rs
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src/plugins/inventory.rs')
-rw-r--r--azalea-client/src/plugins/inventory.rs766
1 files changed, 766 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs
new file mode 100644
index 00000000..3f823ca2
--- /dev/null
+++ b/azalea-client/src/plugins/inventory.rs
@@ -0,0 +1,766 @@
+use std::collections::{HashMap, HashSet};
+
+use azalea_chat::FormattedText;
+pub use azalea_inventory::*;
+use azalea_inventory::{
+ item::MaxStackSizeExt,
+ operations::{
+ ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
+ QuickCraftStatusKind, QuickMoveClick, ThrowClick,
+ },
+};
+use azalea_protocol::packets::game::{
+ s_container_click::ServerboundContainerClick, s_container_close::ServerboundContainerClose,
+ s_set_carried_item::ServerboundSetCarriedItem,
+};
+use azalea_registry::MenuKind;
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ prelude::{Event, EventWriter},
+ schedule::{IntoSystemConfigs, SystemSet},
+ system::Query,
+};
+use tracing::warn;
+
+use super::packet::game::handle_outgoing_packets;
+use crate::{
+ Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
+};
+
+pub struct InventoryPlugin;
+impl Plugin for InventoryPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<ClientSideCloseContainerEvent>()
+ .add_event::<MenuOpenedEvent>()
+ .add_event::<CloseContainerEvent>()
+ .add_event::<ContainerClickEvent>()
+ .add_event::<SetContainerContentEvent>()
+ .add_event::<SetSelectedHotbarSlotEvent>()
+ .add_systems(
+ Update,
+ (
+ handle_set_selected_hotbar_slot_event,
+ handle_menu_opened_event,
+ handle_set_container_content_event,
+ handle_container_click_event,
+ handle_container_close_event.before(handle_outgoing_packets),
+ handle_client_side_close_container_event,
+ )
+ .chain()
+ .in_set(InventorySet)
+ .before(perform_respawn),
+ );
+ }
+}
+
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct InventorySet;
+
+impl Client {
+ /// Return the menu that is currently open. If no menu is open, this will
+ /// have the player's inventory.
+ pub fn menu(&self) -> Menu {
+ let mut ecs = self.ecs.lock();
+ let inventory = self.query::<&Inventory>(&mut ecs);
+ inventory.menu().clone()
+ }
+}
+
+/// A component present on all local players that have an inventory.
+#[derive(Component, Debug, Clone)]
+pub struct Inventory {
+ /// A component that contains the player's inventory menu. This is
+ /// guaranteed to be a `Menu::Player`.
+ ///
+ /// We keep it as a [`Menu`] since `Menu` has some useful functions that
+ /// bare [`azalea_inventory::Player`] doesn't have.
+ pub inventory_menu: azalea_inventory::Menu,
+
+ /// The ID of the container that's currently open. Its value is not
+ /// guaranteed to be anything specific, and may change every time you open a
+ /// container (unless it's 0, in which case it means that no container is
+ /// open).
+ pub id: i32,
+ /// The current container menu that the player has open. If no container is
+ /// open, this will be `None`.
+ pub container_menu: Option<azalea_inventory::Menu>,
+ /// The custom name of the menu that's currently open. This is Some when
+ /// `container_menu` is Some.
+ pub container_menu_title: Option<FormattedText>,
+ /// The item that is currently held by the cursor. `Slot::Empty` if nothing
+ /// is currently being held.
+ ///
+ /// This is different from [`Self::selected_hotbar_slot`], which is the
+ /// item that's selected in the hotbar.
+ pub carried: ItemStack,
+ /// An identifier used by the server to track client inventory desyncs. This
+ /// is sent on every container click, and it's only ever updated when the
+ /// server sends a new container update.
+ pub state_id: u32,
+
+ pub quick_craft_status: QuickCraftStatusKind,
+ pub quick_craft_kind: QuickCraftKind,
+ /// A set of the indexes of the slots that have been right clicked in
+ /// this "quick craft".
+ pub quick_craft_slots: HashSet<u16>,
+
+ /// The index of the item in the hotbar that's currently being held by the
+ /// player. This MUST be in the range 0..9 (not including 9).
+ ///
+ /// In a vanilla client this is changed by pressing the number keys or using
+ /// the scroll wheel.
+ pub selected_hotbar_slot: u8,
+}
+
+impl Inventory {
+ /// Returns a reference to the currently active menu. If a container is open
+ /// it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu_mut`] if you need a mutable reference.
+ pub fn menu(&self) -> &azalea_inventory::Menu {
+ match &self.container_menu {
+ Some(menu) => menu,
+ _ => &self.inventory_menu,
+ }
+ }
+
+ /// Returns a mutable reference to the currently active menu. If a container
+ /// is open it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu`] if you don't need a mutable reference.
+ pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
+ match &mut self.container_menu {
+ Some(menu) => menu,
+ _ => &mut self.inventory_menu,
+ }
+ }
+
+ /// Modify the inventory as if the given operation was performed on it.
+ pub fn simulate_click(
+ &mut self,
+ operation: &ClickOperation,
+ player_abilities: &PlayerAbilities,
+ ) {
+ if let ClickOperation::QuickCraft(quick_craft) = operation {
+ let last_quick_craft_status_tmp = self.quick_craft_status.clone();
+ self.quick_craft_status = last_quick_craft_status_tmp.clone();
+ let last_quick_craft_status = last_quick_craft_status_tmp;
+
+ // no carried item, reset
+ if self.carried.is_empty() {
+ return self.reset_quick_craft();
+ }
+ // if we were starting or ending, or now we aren't ending and the status
+ // changed, reset
+ if (last_quick_craft_status == QuickCraftStatusKind::Start
+ || last_quick_craft_status == QuickCraftStatusKind::End
+ || self.quick_craft_status != QuickCraftStatusKind::End)
+ && (self.quick_craft_status != last_quick_craft_status)
+ {
+ return self.reset_quick_craft();
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::Start {
+ self.quick_craft_kind = quick_craft.kind.clone();
+ if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
+ {
+ self.quick_craft_status = QuickCraftStatusKind::Add;
+ self.quick_craft_slots.clear();
+ } else {
+ self.reset_quick_craft();
+ }
+ return;
+ }
+ if let QuickCraftStatus::Add { slot } = quick_craft.status {
+ let slot_item = self.menu().slot(slot as usize);
+ if let Some(slot_item) = slot_item {
+ if let ItemStack::Present(carried) = &self.carried {
+ // minecraft also checks slot.may_place(carried) and
+ // menu.can_drag_to(slot)
+ // but they always return true so they're not relevant for us
+ if can_item_quick_replace(slot_item, &self.carried, true)
+ && (self.quick_craft_kind == QuickCraftKind::Right
+ || carried.count as usize > self.quick_craft_slots.len())
+ {
+ self.quick_craft_slots.insert(slot);
+ }
+ }
+ }
+ return;
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::End {
+ if !self.quick_craft_slots.is_empty() {
+ if self.quick_craft_slots.len() == 1 {
+ // if we only clicked one slot, then turn this
+ // QuickCraftClick into a PickupClick
+ let slot = *self.quick_craft_slots.iter().next().unwrap();
+ self.reset_quick_craft();
+ self.simulate_click(
+ &match self.quick_craft_kind {
+ QuickCraftKind::Left => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Right => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Middle => {
+ // idk just do nothing i guess
+ return;
+ }
+ },
+ player_abilities,
+ );
+ return;
+ }
+
+ let ItemStack::Present(mut carried) = self.carried.clone() else {
+ // this should never happen
+ return self.reset_quick_craft();
+ };
+
+ let mut carried_count = carried.count;
+ let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
+
+ loop {
+ let mut slot: &ItemStack;
+ let mut slot_index: u16;
+ let mut item_stack: &ItemStack;
+
+ loop {
+ let Some(&next_slot) = quick_craft_slots_iter.next() else {
+ carried.count = carried_count;
+ self.carried = ItemStack::Present(carried);
+ return self.reset_quick_craft();
+ };
+
+ slot = self.menu().slot(next_slot as usize).unwrap();
+ slot_index = next_slot;
+ item_stack = &self.carried;
+
+ if slot.is_present()
+ && can_item_quick_replace(slot, item_stack, true)
+ // this always returns true in most cases
+ // && slot.may_place(item_stack)
+ && (
+ self.quick_craft_kind == QuickCraftKind::Middle
+ || item_stack.count() >= self.quick_craft_slots.len() as i32
+ )
+ {
+ break;
+ }
+ }
+
+ // get the ItemStackData for the slot
+ let ItemStack::Present(slot) = slot else {
+ unreachable!("the loop above requires the slot to be present to break")
+ };
+
+ // if self.can_drag_to(slot) {
+ let mut new_carried = carried.clone();
+ let slot_item_count = slot.count;
+ get_quick_craft_slot_count(
+ &self.quick_craft_slots,
+ &self.quick_craft_kind,
+ &mut new_carried,
+ slot_item_count,
+ );
+ let max_stack_size = i32::min(
+ new_carried.kind.max_stack_size(),
+ i32::min(
+ new_carried.kind.max_stack_size(),
+ slot.kind.max_stack_size(),
+ ),
+ );
+ if new_carried.count > max_stack_size {
+ new_carried.count = max_stack_size;
+ }
+
+ carried_count -= new_carried.count - slot_item_count;
+ // we have to inline self.menu_mut() here to avoid the borrow checker
+ // complaining
+ let menu = match &mut self.container_menu {
+ Some(menu) => menu,
+ _ => &mut self.inventory_menu,
+ };
+ *menu.slot_mut(slot_index as usize).unwrap() =
+ ItemStack::Present(new_carried);
+ }
+ }
+ } else {
+ return self.reset_quick_craft();
+ }
+ }
+ // the quick craft status should always be in start if we're not in quick craft
+ // mode
+ if self.quick_craft_status != QuickCraftStatusKind::Start {
+ return self.reset_quick_craft();
+ }
+
+ match operation {
+ // left clicking outside inventory
+ ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
+ if self.carried.is_present() {
+ // vanilla has `player.drop`s but they're only used
+ // server-side
+ // they're included as comments here in case you want to adapt this for a server
+ // implementation
+
+ // player.drop(self.carried, true);
+ self.carried = ItemStack::Empty;
+ }
+ }
+ ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
+ if self.carried.is_present() {
+ let _item = self.carried.split(1);
+ // player.drop(item, true);
+ }
+ }
+ ClickOperation::Pickup(
+ PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
+ ) => {
+ let Some(slot_item) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let carried = &self.carried;
+ // vanilla does a check called tryItemClickBehaviourOverride
+ // here
+ // i don't understand it so i didn't implement it
+ match slot_item {
+ ItemStack::Empty => if carried.is_present() {},
+ ItemStack::Present(_) => todo!(),
+ }
+ }
+ ClickOperation::QuickMove(
+ QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
+ ) => {
+ // in vanilla it also tests if QuickMove has a slot index of -999
+ // but i don't think that's ever possible so it's not covered here
+ loop {
+ let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
+ let slot_item = self.menu().slot(*slot as usize).unwrap();
+ if new_slot_item.is_empty() || slot_item != &new_slot_item {
+ break;
+ }
+ }
+ }
+ ClickOperation::Swap(s) => {
+ let source_slot_index = s.source_slot as usize;
+ let target_slot_index = s.target_slot as usize;
+
+ let Some(source_slot) = self.menu().slot(source_slot_index) else {
+ return;
+ };
+ let Some(target_slot) = self.menu().slot(target_slot_index) else {
+ return;
+ };
+ if source_slot.is_empty() && target_slot.is_empty() {
+ return;
+ }
+
+ if target_slot.is_empty() {
+ if self.menu().may_pickup(source_slot_index) {
+ let source_slot = source_slot.clone();
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = source_slot;
+ }
+ } else if source_slot.is_empty() {
+ let ItemStack::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ // get the target_item but mutable
+ let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack_size);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ }
+ } else if self.menu().may_pickup(source_slot_index) {
+ let ItemStack::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ let source_max_stack = self.menu().max_stack_size(source_slot_index);
+ if target_slot.count() > source_max_stack as i32 {
+ // if there's more than the max stack size in the target slot
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ // if !self.inventory_menu.add(new_source_slot) {
+ // player.drop(new_source_slot, true);
+ // }
+ } else {
+ // normal swap
+ let new_target_slot = source_slot.clone();
+ let new_source_slot = target_slot.clone();
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = new_target_slot;
+
+ let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
+ *source_slot = new_source_slot;
+ }
+ }
+ }
+ }
+ ClickOperation::Clone(CloneClick { slot }) => {
+ if !player_abilities.instant_break || self.carried.is_present() {
+ return;
+ }
+ let Some(source_slot) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let ItemStack::Present(source_item) = source_slot else {
+ return;
+ };
+ let mut new_carried = source_item.clone();
+ new_carried.count = new_carried.kind.max_stack_size();
+ self.carried = ItemStack::Present(new_carried);
+ }
+ ClickOperation::Throw(c) => {
+ if self.carried.is_present() {
+ return;
+ }
+
+ let (ThrowClick::Single { slot: slot_index }
+ | ThrowClick::All { slot: slot_index }) = c;
+ let slot_index = *slot_index as usize;
+
+ let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
+ return;
+ };
+ let ItemStack::Present(slot_item) = slot else {
+ return;
+ };
+
+ let dropping_count = match c {
+ ThrowClick::Single { .. } => 1,
+ ThrowClick::All { .. } => slot_item.count,
+ };
+
+ let _dropping = slot_item.split(dropping_count as u32);
+ // player.drop(dropping, true);
+ }
+ ClickOperation::PickupAll(PickupAllClick {
+ slot: source_slot_index,
+ reversed,
+ }) => {
+ let source_slot_index = *source_slot_index as usize;
+
+ let source_slot = self.menu().slot(source_slot_index).unwrap();
+ let target_slot = self.carried.clone();
+
+ if target_slot.is_empty()
+ || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
+ {
+ return;
+ }
+
+ let ItemStack::Present(target_slot_item) = &target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+
+ for round in 0..2 {
+ let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
+ Box::new((0..self.menu().len()).rev())
+ } else {
+ Box::new(0..self.menu().len())
+ };
+
+ for i in iterator {
+ if target_slot_item.count < target_slot_item.kind.max_stack_size() {
+ let checking_slot = self.menu().slot(i).unwrap();
+ if let ItemStack::Present(checking_item) = checking_slot {
+ if can_item_quick_replace(checking_slot, &target_slot, true)
+ && self.menu().may_pickup(i)
+ && (round != 0
+ || checking_item.count
+ != checking_item.kind.max_stack_size())
+ {
+ // get the checking_slot and checking_item again but mutable
+ let checking_slot = self.menu_mut().slot_mut(i).unwrap();
+
+ let taken_item =
+ checking_slot.split(checking_slot.count() as u32);
+
+ // now extend the carried item
+ let target_slot = &mut self.carried;
+ let ItemStack::Present(target_slot_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ target_slot_item.count += taken_item.count();
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn reset_quick_craft(&mut self) {
+ self.quick_craft_status = QuickCraftStatusKind::Start;
+ self.quick_craft_slots.clear();
+ }
+
+ /// Get the item in the player's hotbar that is currently being held.
+ pub fn held_item(&self) -> ItemStack {
+ let inventory = &self.inventory_menu;
+ let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
+ hotbar_items[self.selected_hotbar_slot as usize].clone()
+ }
+}
+
+fn can_item_quick_replace(
+ target_slot: &ItemStack,
+ item: &ItemStack,
+ ignore_item_count: bool,
+) -> bool {
+ let ItemStack::Present(target_slot) = target_slot else {
+ return false;
+ };
+ let ItemStack::Present(item) = item else {
+ // i *think* this is what vanilla does
+ // not 100% sure lol probably doesn't matter though
+ return false;
+ };
+
+ if !item.is_same_item_and_components(target_slot) {
+ return false;
+ }
+ let count = target_slot.count as u16
+ + if ignore_item_count {
+ 0
+ } else {
+ item.count as u16
+ };
+ count <= item.kind.max_stack_size() as u16
+}
+
+fn get_quick_craft_slot_count(
+ quick_craft_slots: &HashSet<u16>,
+ quick_craft_kind: &QuickCraftKind,
+ item: &mut ItemStackData,
+ slot_item_count: i32,
+) {
+ item.count = match quick_craft_kind {
+ QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
+ QuickCraftKind::Right => 1,
+ QuickCraftKind::Middle => item.kind.max_stack_size(),
+ };
+ item.count += slot_item_count;
+}
+
+impl Default for Inventory {
+ fn default() -> Self {
+ Inventory {
+ inventory_menu: Menu::Player(azalea_inventory::Player::default()),
+ id: 0,
+ container_menu: None,
+ container_menu_title: None,
+ carried: ItemStack::Empty,
+ state_id: 0,
+ quick_craft_status: QuickCraftStatusKind::Start,
+ quick_craft_kind: QuickCraftKind::Middle,
+ quick_craft_slots: HashSet::new(),
+ selected_hotbar_slot: 0,
+ }
+ }
+}
+
+/// Sent from the server when a menu (like a chest or crafting table) was
+/// opened by the client.
+#[derive(Event, Debug)]
+pub struct MenuOpenedEvent {
+ pub entity: Entity,
+ pub window_id: i32,
+ pub menu_type: MenuKind,
+ pub title: FormattedText,
+}
+fn handle_menu_opened_event(
+ mut events: EventReader<MenuOpenedEvent>,
+ mut query: Query<&mut Inventory>,
+) {
+ for event in events.read() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.id = event.window_id;
+ inventory.container_menu = Some(Menu::from_kind(event.menu_type));
+ inventory.container_menu_title = Some(event.title.clone());
+ }
+}
+
+/// Tell the server that we want to close a container.
+///
+/// Note that this is also sent when the client closes its own inventory, even
+/// though there is no packet for opening its inventory.
+#[derive(Event)]
+pub struct CloseContainerEvent {
+ pub entity: Entity,
+ /// The ID of the container to close. 0 for the player's inventory. If this
+ /// is not the same as the currently open inventory, nothing will happen.
+ pub id: i32,
+}
+fn handle_container_close_event(
+ query: Query<(Entity, &Inventory)>,
+ mut events: EventReader<CloseContainerEvent>,
+ mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for event in events.read() {
+ let (entity, inventory) = query.get(event.entity).unwrap();
+ if event.id != inventory.id {
+ warn!(
+ "Tried to close container with ID {}, but the current container ID is {}",
+ event.id, inventory.id
+ );
+ continue;
+ }
+
+ send_packet_events.send(SendPacketEvent::new(
+ entity,
+ ServerboundContainerClose {
+ container_id: inventory.id,
+ },
+ ));
+ client_side_events.send(ClientSideCloseContainerEvent {
+ entity: event.entity,
+ });
+ }
+}
+
+/// Close a container without notifying the server.
+///
+/// Note that this also gets fired when we get a [`CloseContainerEvent`].
+#[derive(Event)]
+pub struct ClientSideCloseContainerEvent {
+ pub entity: Entity,
+}
+pub fn handle_client_side_close_container_event(
+ mut events: EventReader<ClientSideCloseContainerEvent>,
+ mut query: Query<&mut Inventory>,
+) {
+ for event in events.read() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.container_menu = None;
+ inventory.id = 0;
+ inventory.container_menu_title = None;
+ }
+}
+
+#[derive(Event, Debug)]
+pub struct ContainerClickEvent {
+ pub entity: Entity,
+ pub window_id: i32,
+ pub operation: ClickOperation,
+}
+pub fn handle_container_click_event(
+ mut query: Query<(Entity, &mut Inventory)>,
+ mut events: EventReader<ContainerClickEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for event in events.read() {
+ let (entity, mut inventory) = query.get_mut(event.entity).unwrap();
+ if inventory.id != event.window_id {
+ warn!(
+ "Tried to click container with ID {}, but the current container ID is {}",
+ event.window_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ let old_slots = menu.slots().clone();
+
+ // menu.click(&event.operation);
+
+ // see which slots changed after clicking and put them in the hashmap
+ // the server uses this to check if we desynced
+ let mut changed_slots: HashMap<u16, ItemStack> = HashMap::new();
+ for (slot_index, old_slot) in old_slots.iter().enumerate() {
+ let new_slot = &menu.slots()[slot_index];
+ if old_slot != new_slot {
+ changed_slots.insert(slot_index as u16, new_slot.clone());
+ }
+ }
+
+ send_packet_events.send(SendPacketEvent::new(
+ entity,
+ ServerboundContainerClick {
+ container_id: event.window_id,
+ state_id: inventory.state_id,
+ slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
+ button_num: event.operation.button_num(),
+ click_type: event.operation.click_type(),
+ changed_slots,
+ carried_item: inventory.carried.clone(),
+ },
+ ));
+ }
+}
+
+/// Sent from the server when the contents of a container are replaced. Usually
+/// triggered by the `ContainerSetContent` packet.
+#[derive(Event)]
+pub struct SetContainerContentEvent {
+ pub entity: Entity,
+ pub slots: Vec<ItemStack>,
+ pub container_id: i32,
+}
+fn handle_set_container_content_event(
+ mut events: EventReader<SetContainerContentEvent>,
+ mut query: Query<&mut Inventory>,
+) {
+ for event in events.read() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+
+ if event.container_id != inventory.id {
+ warn!(
+ "Tried to set container content with ID {}, but the current container ID is {}",
+ event.container_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ for (i, slot) in event.slots.iter().enumerate() {
+ if let Some(slot_mut) = menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ }
+}
+
+#[derive(Event)]
+pub struct SetSelectedHotbarSlotEvent {
+ pub entity: Entity,
+ /// The hotbar slot to select. This should be in the range 0..=8.
+ pub slot: u8,
+}
+fn handle_set_selected_hotbar_slot_event(
+ mut events: EventReader<SetSelectedHotbarSlotEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut query: Query<&mut Inventory>,
+) {
+ for event in events.read() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+
+ // if the slot is already selected, don't send a packet
+ if inventory.selected_hotbar_slot == event.slot {
+ continue;
+ }
+
+ inventory.selected_hotbar_slot = event.slot;
+ send_packet_events.send(SendPacketEvent::new(
+ event.entity,
+ ServerboundSetCarriedItem {
+ slot: event.slot as u16,
+ },
+ ));
+ }
+}