aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/inventory.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-05-03 20:57:27 -0500
committerGitHub <noreply@github.com>2023-05-03 20:57:27 -0500
commit634cb8d72c6608512aedba19e5cd669104bc35ea (patch)
treef8e76ce9eb43403d29cc0cbcf9a4f51522419dc2 /azalea-client/src/inventory.rs
parent1fb4418f2c9cbd004c64c2f23d2d0352ee12c0e5 (diff)
downloadazalea-drasl-634cb8d72c6608512aedba19e5cd669104bc35ea.tar.xz
Inventory (#48)
* start adding azalea-inventory * design more of how inventories are defined * start working on az-inv-macros * inventory macro works * start adding inventory codegen * update some deps * add inventory codegen * manually write inventory menus * put the inventories in Client * start on containersetcontent * inventory menu should hopefully work * checks in containersetcontent * format a comment * move some variant matches * inventory.rs * inventory stuff * more inventory stuff * inventory/container tracking works * start adding interact function * sequence number * start adding HitResultComponent * implement traverse_blocks * start adding clip * add clip function * update_hit_result_component * start trying to fix * fix * make some stuff simpler * clippy * lever * chest * container handle * fix ambiguity * fix some doc tests * move some container stuff from az-client to azalea * clicking container * start implementing simulate_click * keep working on simulate click * implement more of simulate_click this is really boring * inventory fixes * start implementing shift clicking * fix panic in azalea-chat i hope * shift clicking implemented * more inventory stuff * fix items not showing in containers sometimes * fix test * fix all warnings * remove a println --------- Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-client/src/inventory.rs')
-rw-r--r--azalea-client/src/inventory.rs721
1 files changed, 721 insertions, 0 deletions
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
new file mode 100644
index 00000000..d6f909a7
--- /dev/null
+++ b/azalea-client/src/inventory.rs
@@ -0,0 +1,721 @@
+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::{
+ serverbound_container_click_packet::ServerboundContainerClickPacket,
+ serverbound_container_close_packet::ServerboundContainerClosePacket,
+};
+use azalea_registry::MenuKind;
+use bevy_app::{App, Plugin};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ prelude::EventWriter,
+ schedule::{IntoSystemConfig, IntoSystemConfigs},
+ system::Query,
+};
+use log::warn;
+
+use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer};
+
+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_systems(
+ (
+ handle_menu_opened_event,
+ handle_set_container_content_event,
+ handle_container_click_event,
+ handle_container_close_event.before(handle_send_packet_event),
+ handle_client_side_close_container_event,
+ )
+ .chain(),
+ );
+ }
+}
+
+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::<&InventoryComponent>(&mut ecs);
+ inventory.menu().clone()
+ }
+}
+
+/// A component present on all local players that have an inventory.
+#[derive(Component, Debug)]
+pub struct InventoryComponent {
+ /// 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: u8,
+ /// 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 item that is currently held by the cursor. `Slot::Empty` if nothing
+ /// is currently being held.
+ pub carried: ItemSlot,
+ /// 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>,
+ // minecraft also has these fields, but i don't
+ // think they're necessary?:
+ // private final NonNullList<ItemStack>
+ // remoteSlots;
+ // private final IntList remoteDataSlots;
+ // private ItemStack remoteCarried;
+}
+impl InventoryComponent {
+ /// 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 {
+ if let Some(menu) = &self.container_menu {
+ menu
+ } else {
+ &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 {
+ if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &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 ItemSlot::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 ItemSlot::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: &ItemSlot;
+ let mut slot_index: u16;
+ let mut item_stack: &ItemSlot;
+
+ loop {
+ let Some(&next_slot) = quick_craft_slots_iter.next() else {
+ carried.count = carried_count;
+ self.carried = ItemSlot::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() as i32 >= self.quick_craft_slots.len() as i32
+ )
+ {
+ break;
+ }
+ }
+
+ // get the ItemSlotData for the slot
+ let ItemSlot::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 = i8::min(
+ new_carried.kind.max_stack_size(),
+ i8::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 = if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &mut self.inventory_menu
+ };
+ *menu.slot_mut(slot_index as usize).unwrap() =
+ ItemSlot::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 = ItemSlot::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 {
+ ItemSlot::Empty => if carried.is_present() {},
+ ItemSlot::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 ItemSlot::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 ItemSlot::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 i8 {
+ // 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 ItemSlot::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 = ItemSlot::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 ItemSlot::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 u8);
+ // 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 ItemSlot::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 ItemSlot::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 u8);
+
+ // now extend the carried item
+ let target_slot = &mut self.carried;
+ let ItemSlot::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();
+ }
+}
+
+fn can_item_quick_replace(
+ target_slot: &ItemSlot,
+ item: &ItemSlot,
+ ignore_item_count: bool,
+) -> bool {
+ let ItemSlot::Present(target_slot) = target_slot else {
+ return false;
+ };
+ let ItemSlot::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_nbt(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
+}
+
+// public static void getQuickCraftSlotCount(Set<Slot> quickCraftSlots, int
+// quickCraftType, ItemStack itemStack, int var3) {
+// switch (quickCraftType) {
+// case 0:
+// itemStack.setCount(Mth.floor((float) itemStack.getCount() / (float)
+// quickCraftSlots.size())); break;
+// case 1:
+// itemStack.setCount(1);
+// break;
+// case 2:
+// itemStack.setCount(itemStack.getItem().getMaxStackSize());
+// }
+
+// itemStack.grow(var3);
+// }
+fn get_quick_craft_slot_count(
+ quick_craft_slots: &HashSet<u16>,
+ quick_craft_kind: &QuickCraftKind,
+ item: &mut ItemSlotData,
+ slot_item_count: i8,
+) {
+ item.count = match quick_craft_kind {
+ QuickCraftKind::Left => item.count / quick_craft_slots.len() as i8,
+ QuickCraftKind::Right => 1,
+ QuickCraftKind::Middle => item.kind.max_stack_size(),
+ };
+ item.count += slot_item_count;
+}
+
+impl Default for InventoryComponent {
+ fn default() -> Self {
+ InventoryComponent {
+ inventory_menu: Menu::Player(azalea_inventory::Player::default()),
+ id: 0,
+ container_menu: None,
+ carried: ItemSlot::Empty,
+ state_id: 0,
+ quick_craft_status: QuickCraftStatusKind::Start,
+ quick_craft_kind: QuickCraftKind::Middle,
+ quick_craft_slots: HashSet::new(),
+ }
+ }
+}
+
+/// Sent from the server when a menu (like a chest or crafting table) was
+/// opened by the client.
+#[derive(Debug)]
+pub struct MenuOpenedEvent {
+ pub entity: Entity,
+ pub window_id: u32,
+ pub menu_type: MenuKind,
+ pub title: FormattedText,
+}
+fn handle_menu_opened_event(
+ mut events: EventReader<MenuOpenedEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.id = event.window_id as u8;
+ inventory.container_menu = Some(Menu::from_kind(event.menu_type));
+ }
+}
+
+/// 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.
+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: u8,
+}
+fn handle_container_close_event(
+ mut events: EventReader<CloseContainerEvent>,
+ mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
+ query: Query<(&LocalPlayer, &InventoryComponent)>,
+) {
+ for event in events.iter() {
+ let (local_player, 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;
+ }
+
+ local_player.write_packet(
+ ServerboundContainerClosePacket {
+ container_id: inventory.id,
+ }
+ .get(),
+ );
+ 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`].
+pub struct ClientSideCloseContainerEvent {
+ pub entity: Entity,
+}
+fn handle_client_side_close_container_event(
+ mut events: EventReader<ClientSideCloseContainerEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.container_menu = None;
+ inventory.id = 0;
+ }
+}
+
+#[derive(Debug)]
+pub struct ContainerClickEvent {
+ pub entity: Entity,
+ pub window_id: u8,
+ pub operation: ClickOperation,
+}
+fn handle_container_click_event(
+ mut events: EventReader<ContainerClickEvent>,
+ mut query: Query<(&mut InventoryComponent, &LocalPlayer)>,
+) {
+ for event in events.iter() {
+ let (mut inventory, local_player) = 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, ItemSlot> = 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());
+ }
+ }
+
+ local_player.write_packet(
+ ServerboundContainerClickPacket {
+ 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(),
+ }
+ .get(),
+ )
+ }
+}
+
+/// Sent from the server when the contents of a container are replaced. Usually
+/// triggered by the `ContainerSetContent` packet.
+pub struct SetContainerContentEvent {
+ pub entity: Entity,
+ pub slots: Vec<ItemSlot>,
+ pub container_id: u8,
+}
+fn handle_set_container_content_event(
+ mut events: EventReader<SetContainerContentEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ 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();
+ }
+ }
+ }
+}