azalea/
container.rs

1use std::{fmt, fmt::Debug};
2
3use azalea_client::{
4    Client,
5    inventory::{CloseContainerEvent, ContainerClickEvent, Inventory},
6    packet::game::ReceiveGamePacketEvent,
7};
8use azalea_core::position::BlockPos;
9use azalea_inventory::{
10    ItemStack, Menu,
11    operations::{ClickOperation, PickupClick, QuickMoveClick},
12};
13use azalea_physics::collision::BlockWithShape;
14use azalea_protocol::packets::game::ClientboundGamePacket;
15use bevy_app::{App, Plugin, Update};
16use bevy_ecs::{component::Component, prelude::MessageReader, system::Commands};
17use derive_more::Deref;
18use futures_lite::Future;
19
20use crate::bot::BotClientExt;
21
22pub struct ContainerPlugin;
23impl Plugin for ContainerPlugin {
24    fn build(&self, app: &mut App) {
25        app.add_systems(Update, handle_menu_opened_event);
26    }
27}
28
29pub trait ContainerClientExt {
30    /// Open a container in the world, like a chest. Use
31    /// [`Client::open_inventory`] to open your own inventory.
32    ///
33    /// ```
34    /// # use azalea::prelude::*;
35    /// # async fn example(mut bot: azalea::Client) {
36    /// let target_pos = bot
37    ///     .world()
38    ///     .read()
39    ///     .find_block(bot.position(), &azalea::registry::Block::Chest.into());
40    /// let Some(target_pos) = target_pos else {
41    ///     bot.chat("no chest found");
42    ///     return;
43    /// };
44    /// let container = bot.open_container_at(target_pos).await;
45    /// # }
46    /// ```
47    fn open_container_at(
48        &self,
49        pos: BlockPos,
50    ) -> impl Future<Output = Option<ContainerHandle>> + Send;
51    /// Open the player's inventory. This will return None if another
52    /// container is open.
53    ///
54    /// Note that this will send a packet to the server once it's dropped. Also,
55    /// due to how it's implemented, you could call this function multiple times
56    /// while another inventory handle already exists (but you shouldn't).
57    ///
58    /// If you just want to get the items in the player's inventory without
59    /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`],
60    /// and [`Menu::slots`].
61    fn open_inventory(&self) -> Option<ContainerHandle>;
62    /// Returns a [`ContainerHandleRef`] to the client's currently open
63    /// container, or their inventory.
64    ///
65    /// This will not send a packet to close the container when it's dropped,
66    /// which may cause anticheat compatibility issues if you modify your
67    /// inventory without closing it afterwards.
68    ///
69    /// To simulate opening your own inventory (like pressing 'e') in a way that
70    /// won't trigger anticheats, use [`Client::open_inventory`].
71    ///
72    /// To open a container in the world, use [`Client::open_container_at`].
73    fn get_inventory(&self) -> ContainerHandleRef;
74    /// Get the item in the bot's hotbar that is currently being held in its
75    /// main hand.
76    fn get_held_item(&self) -> ItemStack;
77}
78
79impl ContainerClientExt for Client {
80    async fn open_container_at(&self, pos: BlockPos) -> Option<ContainerHandle> {
81        let mut ticks = self.get_tick_broadcaster();
82        // wait until it's not air (up to 10 ticks)
83        for _ in 0..10 {
84            if !self
85                .world()
86                .read()
87                .get_block_state(pos)
88                .unwrap_or_default()
89                .is_collision_shape_empty()
90            {
91                break;
92            }
93            let _ = ticks.recv().await;
94        }
95
96        self.ecs
97            .lock()
98            .entity_mut(self.entity)
99            .insert(WaitingForInventoryOpen);
100        self.block_interact(pos);
101
102        while ticks.recv().await.is_ok() {
103            let ecs = self.ecs.lock();
104            if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
105                break;
106            }
107        }
108
109        let ecs = self.ecs.lock();
110        let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory");
111        if inventory.id == 0 {
112            None
113        } else {
114            Some(ContainerHandle::new(inventory.id, self.clone()))
115        }
116    }
117
118    fn open_inventory(&self) -> Option<ContainerHandle> {
119        let ecs = self.ecs.lock();
120        let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory");
121        if inventory.id == 0 {
122            Some(ContainerHandle::new(0, self.clone()))
123        } else {
124            None
125        }
126    }
127
128    fn get_inventory(&self) -> ContainerHandleRef {
129        self.query_self::<&Inventory, _>(|inv| ContainerHandleRef::new(inv.id, self.clone()))
130    }
131
132    fn get_held_item(&self) -> ItemStack {
133        self.query_self::<&Inventory, _>(|inv| inv.held_item())
134    }
135}
136
137/// A handle to a container that may be open. This does not close the container
138/// when it's dropped. See [`ContainerHandle`] if that behavior is desired.
139pub struct ContainerHandleRef {
140    id: i32,
141    client: Client,
142}
143impl Debug for ContainerHandleRef {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        f.debug_struct("ContainerHandle")
146            .field("id", &self.id())
147            .finish()
148    }
149}
150impl ContainerHandleRef {
151    pub fn new(id: i32, client: Client) -> Self {
152        Self { id, client }
153    }
154
155    pub fn close(&self) {
156        self.client.ecs.lock().trigger(CloseContainerEvent {
157            entity: self.client.entity,
158            id: self.id,
159        });
160    }
161
162    /// Get the id of the container. If this is 0, that means it's the player's
163    /// inventory. Otherwise, the number isn't really meaningful since only one
164    /// container can be open at a time.
165    pub fn id(&self) -> i32 {
166        self.id
167    }
168
169    /// Returns the menu of the container. If the container is closed, this
170    /// will return `None`.
171    ///
172    /// Note that any modifications you make to the `Menu` you're given will not
173    /// actually cause any packets to be sent. If you're trying to modify your
174    /// inventory, use [`Self::click`] instead
175    pub fn menu(&self) -> Option<Menu> {
176        let ecs = self.client.ecs.lock();
177        let inventory = ecs
178            .get::<Inventory>(self.client.entity)
179            .expect("no inventory");
180
181        // this also makes sure we can't access the inventory while a container is open
182        if inventory.id == self.id {
183            if self.id == 0 {
184                Some(inventory.inventory_menu.clone())
185            } else {
186                Some(inventory.container_menu.clone().unwrap())
187            }
188        } else {
189            None
190        }
191    }
192
193    /// Returns the item slots in the container, not including the player's
194    /// inventory. If the container is closed, this will return `None`.
195    pub fn contents(&self) -> Option<Vec<ItemStack>> {
196        self.menu().map(|menu| menu.contents())
197    }
198
199    /// Return the contents of the menu, including the player's inventory. If
200    /// the container is closed, this will return `None`.
201    pub fn slots(&self) -> Option<Vec<ItemStack>> {
202        self.menu().map(|menu| menu.slots())
203    }
204
205    /// A shortcut for [`Self::click`] with `PickupClick::Left`.
206    pub fn left_click(&self, slot: impl Into<usize>) {
207        self.click(PickupClick::Left {
208            slot: Some(slot.into() as u16),
209        });
210    }
211    /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`.
212    pub fn shift_click(&self, slot: impl Into<usize>) {
213        self.click(QuickMoveClick::Left {
214            slot: slot.into() as u16,
215        });
216    }
217    /// A shortcut for [`Self::click`] with `PickupClick::Right`.
218    pub fn right_click(&self, slot: impl Into<usize>) {
219        self.click(PickupClick::Right {
220            slot: Some(slot.into() as u16),
221        });
222    }
223
224    /// Simulate a click in the container and send the packet to perform the
225    /// action.
226    pub fn click(&self, operation: impl Into<ClickOperation>) {
227        let operation = operation.into();
228        self.client.ecs.lock().trigger(ContainerClickEvent {
229            entity: self.client.entity,
230            window_id: self.id,
231            operation,
232        });
233    }
234}
235
236/// A handle to the open container. The container will be closed once this is
237/// dropped.
238#[derive(Deref)]
239pub struct ContainerHandle(ContainerHandleRef);
240
241impl Drop for ContainerHandle {
242    fn drop(&mut self) {
243        self.0.close();
244    }
245}
246impl Debug for ContainerHandle {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.debug_struct("ContainerHandle")
249            .field("id", &self.id())
250            .finish()
251    }
252}
253impl ContainerHandle {
254    fn new(id: i32, client: Client) -> Self {
255        Self(ContainerHandleRef { id, client })
256    }
257
258    /// Closes the inventory by dropping the handle.
259    pub fn close(self) {
260        // implicitly calls drop
261    }
262}
263
264#[derive(Component, Debug)]
265pub struct WaitingForInventoryOpen;
266
267pub fn handle_menu_opened_event(
268    mut commands: Commands,
269    mut events: MessageReader<ReceiveGamePacketEvent>,
270) {
271    for event in events.read() {
272        if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet.as_ref() {
273            commands
274                .entity(event.entity)
275                .remove::<WaitingForInventoryOpen>();
276        }
277    }
278}