use std::{fmt, fmt::Debug}; use azalea_client::{ Client, inventory::{CloseContainerEvent, ContainerClickEvent}, packet::game::ReceiveGamePacketEvent, }; use azalea_core::position::BlockPos; use azalea_entity::inventory::Inventory; use azalea_inventory::{ ItemStack, Menu, operations::{ClickOperation, PickupClick, QuickMoveClick}, }; use azalea_physics::collision::BlockWithShape; use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::MessageReader, system::Commands}; use derive_more::Deref; use futures_lite::Future; use crate::bot::BotClientExt; pub struct ContainerPlugin; impl Plugin for ContainerPlugin { fn build(&self, app: &mut App) { app.add_systems(Update, handle_menu_opened_event); } } pub trait ContainerClientExt { /// Open a container in the world, like a chest. /// /// Use [`Client::open_inventory`] to open your own inventory. /// /// This function times out after 5 seconds (100 ticks). Use /// [`Self::open_container_at_with_timeout_ticks`] if you would like to /// configure this. /// /// ``` /// # use azalea::{prelude::*, registry::builtin::BlockKind}; /// # async fn example(mut bot: azalea::Client) { /// let target_pos = bot /// .world() /// .read() /// .find_block(bot.position(), &BlockKind::Chest.into()); /// let Some(target_pos) = target_pos else { /// bot.chat("no chest found"); /// return; /// }; /// let container = bot.open_container_at(target_pos).await; /// # } /// ``` fn open_container_at( &self, pos: BlockPos, ) -> impl Future> + Send; /// Open a container in the world, or time out after a specified amount of /// ticks. /// /// See [`Self::open_container_at`] for more information. That function /// defaults to a timeout of 5 seconds (100 ticks), which is usually good /// enough. However to detect failures faster or to account for server /// lag, you may find it useful to adjust the timeout to a different /// value. /// /// The timeout is measured in game ticks (on the client, not the server), /// i.e. 1/20th of a second. fn open_container_at_with_timeout_ticks( &self, pos: BlockPos, timeout_ticks: Option, ) -> impl Future> + Send; /// Wait until a container is open, up to the specified number of ticks. /// /// Returns `None` if the container was immediately opened and closed, or if /// the timeout expired. /// /// If `timeout_ticks` is None, there will be no timeout. fn wait_for_container_open( &self, timeout_ticks: Option, ) -> impl Future> + Send; /// Open the player's inventory. /// /// This will return None if another container is open. /// /// Note that this will send a packet to the server once it's dropped. Also, /// due to how it's implemented, you could call this function multiple times /// while another inventory handle already exists (but you shouldn't). /// /// If you just want to get the items in the player's inventory without /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], /// and [`Menu::slots`]. fn open_inventory(&self) -> Option; /// Returns a [`ContainerHandleRef`] to the client's currently open /// container, or their inventory. /// /// This will not send a packet to close the container when it's dropped, /// which may cause anticheat compatibility issues if you modify your /// inventory without closing it afterwards. /// /// To simulate opening your own inventory (like pressing 'e') in a way that /// won't trigger anticheats, use [`Client::open_inventory`]. /// /// To open a container in the world, use [`Client::open_container_at`]. fn get_inventory(&self) -> ContainerHandleRef; /// Get the item in the bot's hotbar that is currently being held in its /// main hand. fn get_held_item(&self) -> ItemStack; } impl ContainerClientExt for Client { async fn open_container_at(&self, pos: BlockPos) -> Option { self.open_container_at_with_timeout_ticks(pos, Some(20 * 5)) .await } async fn open_container_at_with_timeout_ticks( &self, pos: BlockPos, timeout_ticks: Option, ) -> Option { let mut ticks = self.get_tick_broadcaster(); // wait until it's not air (up to 10 ticks) for _ in 0..10 { let block = self.world().read().get_block_state(pos).unwrap_or_default(); if !block.is_collision_shape_empty() { break; } let _ = ticks.recv().await; } self.ecs .lock() .entity_mut(self.entity) .insert(WaitingForInventoryOpen); self.block_interact(pos); self.wait_for_container_open(timeout_ticks).await } async fn wait_for_container_open( &self, timeout_ticks: Option, ) -> Option { let mut ticks = self.get_tick_broadcaster(); let mut elapsed_ticks = 0; while ticks.recv().await.is_ok() { let ecs = self.ecs.lock(); if ecs.get::(self.entity).is_none() { break; } elapsed_ticks += 1; if let Some(timeout_ticks) = timeout_ticks && elapsed_ticks >= timeout_ticks { return None; } } let ecs = self.ecs.lock(); let inventory = ecs.get::(self.entity).expect("no inventory"); if inventory.id == 0 { None } else { Some(ContainerHandle::new(inventory.id, self.clone())) } } fn open_inventory(&self) -> Option { let ecs = self.ecs.lock(); let inventory = ecs.get::(self.entity).expect("no inventory"); if inventory.id == 0 { Some(ContainerHandle::new(0, self.clone())) } else { None } } fn get_inventory(&self) -> ContainerHandleRef { self.query_self::<&Inventory, _>(|inv| ContainerHandleRef::new(inv.id, self.clone())) } fn get_held_item(&self) -> ItemStack { self.query_self::<&Inventory, _>(|inv| inv.held_item().clone()) } } /// A handle to a container that may be open. /// /// This does not close the container when it's dropped. See [`ContainerHandle`] /// if that behavior is desired. pub struct ContainerHandleRef { id: i32, client: Client, } impl Debug for ContainerHandleRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ContainerHandle") .field("id", &self.id()) .finish() } } impl ContainerHandleRef { pub fn new(id: i32, client: Client) -> Self { Self { id, client } } pub fn close(&self) { self.client.ecs.lock().trigger(CloseContainerEvent { entity: self.client.entity, id: self.id, }); } /// Get the ID of the container. /// /// If this is 0, that means it's the player's inventory. Otherwise, the /// number isn't really meaningful since only one container can be open /// at a time. pub fn id(&self) -> i32 { self.id } /// Returns the menu of the container. /// /// If the container is closed, this will return `None`. /// /// Note that any modifications you make to the `Menu` you're given will not /// actually cause any packets to be sent. If you're trying to modify your /// inventory, use [`Self::click`] instead pub fn menu(&self) -> Option { let ecs = self.client.ecs.lock(); let inventory = ecs .get::(self.client.entity) .expect("no inventory"); // this also makes sure we can't access the inventory while a container is open if inventory.id == self.id { if self.id == 0 { Some(inventory.inventory_menu.clone()) } else { Some(inventory.container_menu.clone().unwrap()) } } else { None } } /// Returns the item slots in the container, not including the player's /// inventory. /// /// If the container is closed, this will return `None`. pub fn contents(&self) -> Option> { self.menu().map(|menu| menu.contents()) } /// Return the contents of the menu, including the player's inventory. /// /// If the container is closed, this will return `None`. pub fn slots(&self) -> Option> { self.menu().map(|menu| menu.slots()) } /// A shortcut for [`Self::click`] with `PickupClick::Left`. pub fn left_click(&self, slot: impl Into) { self.click(PickupClick::Left { slot: Some(slot.into() as u16), }); } /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`. pub fn shift_click(&self, slot: impl Into) { self.click(QuickMoveClick::Left { slot: slot.into() as u16, }); } /// A shortcut for [`Self::click`] with `PickupClick::Right`. pub fn right_click(&self, slot: impl Into) { self.click(PickupClick::Right { slot: Some(slot.into() as u16), }); } /// Simulate a click in the container and send the packet to perform the /// action. pub fn click(&self, operation: impl Into) { let operation = operation.into(); self.client.ecs.lock().trigger(ContainerClickEvent { entity: self.client.entity, window_id: self.id, operation, }); } } /// A handle to the open container. /// /// The container will be closed once this is dropped. #[derive(Deref)] pub struct ContainerHandle(ContainerHandleRef); impl Drop for ContainerHandle { fn drop(&mut self) { self.0.close(); } } impl Debug for ContainerHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ContainerHandle") .field("id", &self.id()) .finish() } } impl ContainerHandle { fn new(id: i32, client: Client) -> Self { Self(ContainerHandleRef { id, client }) } /// Closes the inventory by dropping the handle. pub fn close(self) { // implicitly calls drop } } #[derive(Component, Debug)] pub struct WaitingForInventoryOpen; pub fn handle_menu_opened_event( mut commands: Commands, mut events: MessageReader, ) { for event in events.read() { if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet.as_ref() { commands .entity(event.entity) .remove::(); } } }