Skip to main content

azalea/
container.rs

1use std::{fmt, fmt::Debug};
2
3use azalea_chat::FormattedText;
4use azalea_client::{
5    inventory::{CloseContainerEvent, ContainerClickEvent},
6    packet::game::ReceiveGamePacketEvent,
7};
8use azalea_core::position::BlockPos;
9use azalea_entity::inventory::Inventory;
10use azalea_inventory::{
11    ItemStack, Menu,
12    operations::{ClickOperation, PickupClick, QuickMoveClick},
13};
14use azalea_physics::collision::BlockWithShape;
15use azalea_protocol::packets::game::ClientboundGamePacket;
16use bevy_app::{App, Plugin, Update};
17use bevy_ecs::{component::Component, prelude::MessageReader, system::Commands};
18use derive_more::Deref;
19
20use crate::{Client, client_impl::error::AzaleaResult};
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
29impl Client {
30    /// Open a container in the world, like a chest.
31    ///
32    /// Use [`Client::open_inventory`] to open your own inventory.
33    ///
34    /// This function times out after 5 seconds (100 ticks). Use
35    /// [`Self::open_container_at_with_timeout_ticks`] if you would like to
36    /// configure this.
37    ///
38    /// ```
39    /// # use azalea::{prelude::*, registry::builtin::BlockKind};
40    /// # async fn example(mut bot: azalea::Client) -> azalea::error::AzaleaResult<()> {
41    /// let target_pos = bot
42    ///     .world()?
43    ///     .read()
44    ///     .find_block(bot.position()?, &BlockKind::Chest.into());
45    /// let Some(target_pos) = target_pos else {
46    ///     bot.chat("no chest found");
47    ///     return Ok(());
48    /// };
49    /// let container = bot.open_container_at(target_pos).await?;
50    /// # Ok(())
51    /// # }
52    /// ```
53    pub async fn open_container_at(&self, pos: BlockPos) -> AzaleaResult<Option<ContainerHandle>> {
54        self.open_container_at_with_timeout_ticks(pos, Some(20 * 5))
55            .await
56    }
57
58    /// Open a container in the world, or time out after a specified amount of
59    /// ticks.
60    ///
61    /// See [`Self::open_container_at`] for more information. That function
62    /// defaults to a timeout of 5 seconds (100 ticks), which is usually good
63    /// enough. However to detect failures faster or to account for server
64    /// lag, you may find it useful to adjust the timeout to a different
65    /// value.
66    ///
67    /// The timeout is measured in game ticks (on the client, not the server),
68    /// i.e. 1/20th of a second.
69    pub async fn open_container_at_with_timeout_ticks(
70        &self,
71        pos: BlockPos,
72        timeout_ticks: Option<usize>,
73    ) -> AzaleaResult<Option<ContainerHandle>> {
74        let mut ticks = self.get_tick_broadcaster();
75        // wait until it's not air (up to 10 ticks)
76        for _ in 0..10 {
77            let block = self
78                .world()?
79                .read()
80                .get_block_state(pos)
81                .unwrap_or_default();
82            if !block.is_collision_shape_empty() {
83                break;
84            }
85            let _ = ticks.recv().await;
86        }
87
88        self.ecs
89            .write()
90            .entity_mut(self.entity)
91            .insert(WaitingForInventoryOpen);
92        self.block_interact(pos);
93
94        self.wait_for_container_open(timeout_ticks).await
95    }
96
97    /// Wait until a container is open, up to the specified number of ticks.
98    ///
99    /// Returns `None` if the container was immediately opened and closed, or if
100    /// the timeout expired.
101    ///
102    /// If `timeout_ticks` is None, there will be no timeout.
103    pub async fn wait_for_container_open(
104        &self,
105        timeout_ticks: Option<usize>,
106    ) -> AzaleaResult<Option<ContainerHandle>> {
107        let mut ticks = self.get_tick_broadcaster();
108        let mut elapsed_ticks = 0;
109        while ticks.recv().await.is_ok() {
110            let ecs = self.ecs.read();
111            if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
112                break;
113            }
114
115            elapsed_ticks += 1;
116            if let Some(timeout_ticks) = timeout_ticks
117                && elapsed_ticks >= timeout_ticks
118            {
119                return Ok(None);
120            }
121        }
122
123        let inventory_id = self.component::<Inventory>()?.id;
124        if inventory_id == 0 {
125            Ok(None)
126        } else {
127            Ok(Some(ContainerHandle::new(inventory_id, self.clone())))
128        }
129    }
130
131    /// Open the player's inventory.
132    ///
133    /// This will return None if another container is open.
134    ///
135    /// Note that this will send a packet to the server once it's dropped. Also,
136    /// due to how it's implemented, you could call this function multiple times
137    /// while another inventory handle already exists (but you shouldn't).
138    ///
139    /// If you just want to get the items in the player's inventory without
140    /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`],
141    /// and [`Menu::slots`].
142    pub fn open_inventory(&self) -> AzaleaResult<Option<ContainerHandle>> {
143        let inventory = self.component::<Inventory>()?;
144        Ok(if inventory.id == 0 {
145            Some(ContainerHandle::new(0, self.clone()))
146        } else {
147            None
148        })
149    }
150
151    /// Returns a [`ContainerHandleRef`] to the client's currently open
152    /// container, or their inventory.
153    ///
154    /// This will not send a packet to close the container when it's dropped,
155    /// which may cause anticheat compatibility issues if you modify your
156    /// inventory without closing it afterwards.
157    ///
158    /// To simulate opening your own inventory (like pressing 'e') in a way that
159    /// won't trigger anticheats, use [`Client::open_inventory`].
160    ///
161    /// To open a container in the world, use [`Client::open_container_at`].
162    pub fn get_inventory(&self) -> AzaleaResult<ContainerHandleRef> {
163        Ok(ContainerHandleRef::new(
164            self.component::<Inventory>()?.id,
165            self.clone(),
166        ))
167    }
168
169    /// Get the item in the bot's hotbar that is currently being held in its
170    /// main hand.
171    pub fn get_held_item(&self) -> AzaleaResult<ItemStack> {
172        Ok(self.component::<Inventory>()?.held_item().clone())
173    }
174}
175
176/// A handle to a container that may be open.
177///
178/// This does not close the container when it's dropped. See [`ContainerHandle`]
179/// if that behavior is desired.
180pub struct ContainerHandleRef {
181    id: i32,
182    client: Client,
183}
184impl Debug for ContainerHandleRef {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.debug_struct("ContainerHandle")
187            .field("id", &self.id())
188            .finish()
189    }
190}
191impl ContainerHandleRef {
192    pub fn new(id: i32, client: Client) -> Self {
193        Self { id, client }
194    }
195
196    pub fn close(&self) {
197        self.client.ecs.write().trigger(CloseContainerEvent {
198            entity: self.client.entity,
199            id: self.id,
200        });
201    }
202
203    /// Get the ID of the container.
204    ///
205    /// If this is 0, that means it's the player's inventory. Otherwise, the
206    /// number isn't really meaningful since only one container can be open
207    /// at a time.
208    pub fn id(&self) -> i32 {
209        self.id
210    }
211
212    /// Returns the menu of the container.
213    ///
214    /// If the container is closed, this will return `None`.
215    ///
216    /// Note that any modifications you make to the `Menu` you're given will not
217    /// actually cause any packets to be sent. If you're trying to modify your
218    /// inventory, use [`Self::click`] instead
219    pub fn menu(&self) -> AzaleaResult<Option<Menu>> {
220        self.map_inventory(|inv| {
221            if self.id == 0 {
222                inv.inventory_menu.clone()
223            } else {
224                inv.container_menu.clone().unwrap()
225            }
226        })
227    }
228
229    fn map_inventory<R>(&self, f: impl FnOnce(&Inventory) -> R) -> AzaleaResult<Option<R>> {
230        self.client.query_self::<&Inventory, _>(|inv| {
231            if inv.id == self.id {
232                Some(f(inv))
233            } else {
234                // a different inventory is open
235                None
236            }
237        })
238    }
239
240    /// Returns the item slots in the container, not including the player's
241    /// inventory.
242    ///
243    /// If the container is closed, this will return `None`.
244    pub fn contents(&self) -> Option<Vec<ItemStack>> {
245        Some(self.menu().ok()??.contents())
246    }
247
248    /// Return the contents of the menu, including the player's inventory.
249    ///
250    /// If the container is closed, this will return `None`.
251    pub fn slots(&self) -> Option<Vec<ItemStack>> {
252        Some(self.menu().ok()??.slots())
253    }
254
255    /// Returns the title of the container, or `None` if no container is open.
256    ///
257    /// ```no_run
258    /// # use azalea::prelude::*;
259    /// # fn example(bot: &Client) -> azalea::error::AzaleaResult<()> {
260    /// let inventory = bot.get_inventory()?;
261    /// let inventory_title = inventory.title().unwrap_or_default().to_string();
262    /// // would be true if an unnamed chest is open
263    /// assert_eq!(inventory_title, "Chest");
264    /// # Ok(())
265    /// # }
266    /// ```
267    pub fn title(&self) -> Option<FormattedText> {
268        self.map_inventory(|inv| inv.container_menu_title.clone())
269            .ok()??
270    }
271
272    /// A shortcut for [`Self::click`] with `PickupClick::Left`.
273    pub fn left_click(&self, slot: impl Into<usize>) {
274        self.click(PickupClick::Left {
275            slot: Some(slot.into() as u16),
276        });
277    }
278    /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`.
279    pub fn shift_click(&self, slot: impl Into<usize>) {
280        self.click(QuickMoveClick::Left {
281            slot: slot.into() as u16,
282        });
283    }
284    /// A shortcut for [`Self::click`] with `PickupClick::Right`.
285    pub fn right_click(&self, slot: impl Into<usize>) {
286        self.click(PickupClick::Right {
287            slot: Some(slot.into() as u16),
288        });
289    }
290
291    /// Simulate a click in the container and send the packet to perform the
292    /// action.
293    pub fn click(&self, operation: impl Into<ClickOperation>) {
294        let operation = operation.into();
295        self.client.ecs.write().trigger(ContainerClickEvent {
296            entity: self.client.entity,
297            window_id: self.id,
298            operation,
299        });
300    }
301}
302
303/// A handle to the open container.
304///
305/// The container will be closed once this is dropped.
306#[derive(Deref)]
307pub struct ContainerHandle(ContainerHandleRef);
308
309impl Drop for ContainerHandle {
310    fn drop(&mut self) {
311        self.0.close();
312    }
313}
314impl Debug for ContainerHandle {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        f.debug_struct("ContainerHandle")
317            .field("id", &self.id())
318            .finish()
319    }
320}
321impl ContainerHandle {
322    fn new(id: i32, client: Client) -> Self {
323        Self(ContainerHandleRef { id, client })
324    }
325
326    /// Closes the inventory by dropping the handle.
327    pub fn close(self) {
328        // implicitly calls drop
329    }
330}
331
332#[derive(Component, Debug)]
333pub struct WaitingForInventoryOpen;
334
335pub fn handle_menu_opened_event(
336    mut commands: Commands,
337    mut events: MessageReader<ReceiveGamePacketEvent>,
338) {
339    for event in events.read() {
340        if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet.as_ref() {
341            commands
342                .entity(event.entity)
343                .remove::<WaitingForInventoryOpen>();
344        }
345    }
346}