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::EventReader, 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        let ecs = self.ecs.lock();
130        let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory");
131        ContainerHandleRef::new(inventory.id, self.clone())
132    }
133
134    fn get_held_item(&self) -> ItemStack {
135        self.map_get_component::<Inventory, _>(|inventory| inventory.held_item())
136            .expect("no inventory")
137    }
138}
139
140/// A handle to a container that may be open. This does not close the container
141/// when it's dropped. See [`ContainerHandle`] if that behavior is desired.
142pub struct ContainerHandleRef {
143    id: i32,
144    client: Client,
145}
146impl Debug for ContainerHandleRef {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("ContainerHandle")
149            .field("id", &self.id())
150            .finish()
151    }
152}
153impl ContainerHandleRef {
154    pub fn new(id: i32, client: Client) -> Self {
155        Self { id, client }
156    }
157
158    pub fn close(&self) {
159        self.client.ecs.lock().send_event(CloseContainerEvent {
160            entity: self.client.entity,
161            id: self.id,
162        });
163    }
164
165    /// Get the id of the container. If this is 0, that means it's the player's
166    /// inventory. Otherwise, the number isn't really meaningful since only one
167    /// container can be open at a time.
168    pub fn id(&self) -> i32 {
169        self.id
170    }
171
172    /// Returns the menu of the container. If the container is closed, this
173    /// will return `None`.
174    ///
175    /// Note that any modifications you make to the `Menu` you're given will not
176    /// actually cause any packets to be sent. If you're trying to modify your
177    /// inventory, use [`ContainerHandle::click`] instead
178    pub fn menu(&self) -> Option<Menu> {
179        let ecs = self.client.ecs.lock();
180        let inventory = ecs
181            .get::<Inventory>(self.client.entity)
182            .expect("no inventory");
183
184        // this also makes sure we can't access the inventory while a container is open
185        if inventory.id == self.id {
186            if self.id == 0 {
187                Some(inventory.inventory_menu.clone())
188            } else {
189                Some(inventory.container_menu.clone().unwrap())
190            }
191        } else {
192            None
193        }
194    }
195
196    /// Returns the item slots in the container, not including the player's
197    /// inventory. If the container is closed, this will return `None`.
198    pub fn contents(&self) -> Option<Vec<ItemStack>> {
199        self.menu().map(|menu| menu.contents())
200    }
201
202    /// Return the contents of the menu, including the player's inventory. If
203    /// the container is closed, this will return `None`.
204    pub fn slots(&self) -> Option<Vec<ItemStack>> {
205        self.menu().map(|menu| menu.slots())
206    }
207
208    /// A shortcut for [`Self::click`] with `PickupClick::Left`.
209    pub fn left_click(&self, slot: impl Into<usize>) {
210        self.click(PickupClick::Left {
211            slot: Some(slot.into() as u16),
212        });
213    }
214    /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`.
215    pub fn shift_click(&self, slot: impl Into<usize>) {
216        self.click(QuickMoveClick::Left {
217            slot: slot.into() as u16,
218        });
219    }
220    /// A shortcut for [`Self::click`] with `PickupClick::Right`.
221    pub fn right_click(&self, slot: impl Into<usize>) {
222        self.click(PickupClick::Right {
223            slot: Some(slot.into() as u16),
224        });
225    }
226
227    pub fn click(&self, operation: impl Into<ClickOperation>) {
228        let operation = operation.into();
229        self.client.ecs.lock().send_event(ContainerClickEvent {
230            entity: self.client.entity,
231            window_id: self.id,
232            operation,
233        });
234    }
235}
236
237/// A handle to the open container. The container will be closed once this is
238/// dropped.
239#[derive(Deref)]
240pub struct ContainerHandle(ContainerHandleRef);
241
242impl Drop for ContainerHandle {
243    fn drop(&mut self) {
244        self.0.close();
245    }
246}
247impl Debug for ContainerHandle {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        f.debug_struct("ContainerHandle")
250            .field("id", &self.id())
251            .finish()
252    }
253}
254impl ContainerHandle {
255    fn new(id: i32, client: Client) -> Self {
256        Self(ContainerHandleRef { id, client })
257    }
258
259    /// Closes the inventory by dropping the handle.
260    pub fn close(self) {
261        // implicitly calls drop
262    }
263}
264
265#[derive(Component, Debug)]
266pub struct WaitingForInventoryOpen;
267
268fn handle_menu_opened_event(
269    mut commands: Commands,
270    mut events: EventReader<ReceiveGamePacketEvent>,
271) {
272    for event in events.read() {
273        if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet.as_ref() {
274            commands
275                .entity(event.entity)
276                .remove::<WaitingForInventoryOpen>();
277        }
278    }
279}