azalea/
container.rs

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