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}