azalea_client/plugins/inventory/
mod.rs1pub mod equipment_effects;
2
3use azalea_chat::FormattedText;
4use azalea_core::tick::GameTick;
5use azalea_entity::{PlayerAbilities, inventory::Inventory as Inv};
6use azalea_inventory::operations::ClickOperation;
7pub use azalea_inventory::*;
8use azalea_protocol::packets::game::{
9 s_container_click::{HashedStack, ServerboundContainerClick},
10 s_container_close::ServerboundContainerClose,
11 s_set_carried_item::ServerboundSetCarriedItem,
12};
13use azalea_registry::builtin::MenuKind;
14use azalea_world::{InstanceContainer, InstanceName};
15use bevy_app::{App, Plugin};
16use bevy_ecs::prelude::*;
17use indexmap::IndexMap;
18use tracing::{error, warn};
19
20use crate::{
21 Client,
22 inventory::equipment_effects::{collect_equipment_changes, handle_equipment_changes},
23 packet::game::SendGamePacketEvent,
24};
25
26#[doc(hidden)]
29#[deprecated = "moved to `azalea_entity::inventory::Inventory`."]
30pub type Inventory = azalea_entity::inventory::Inventory;
31
32pub struct InventoryPlugin;
33impl Plugin for InventoryPlugin {
34 fn build(&self, app: &mut App) {
35 app.add_systems(
36 GameTick,
37 (
38 ensure_has_sent_carried_item.after(super::mining::handle_mining_queued),
39 collect_equipment_changes
40 .after(super::interact::handle_start_use_item_queued)
41 .before(azalea_physics::ai_step),
42 ),
43 )
44 .add_observer(handle_client_side_close_container_trigger)
45 .add_observer(handle_menu_opened_trigger)
46 .add_observer(handle_container_close_event)
47 .add_observer(handle_set_container_content_trigger)
48 .add_observer(handle_container_click_event)
49 .add_observer(handle_set_selected_hotbar_slot_event)
52 .add_observer(handle_equipment_changes);
53 }
54}
55
56#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
57pub struct InventorySystems;
58
59impl Client {
60 pub fn menu(&self) -> Menu {
63 self.query_self::<&Inv, _>(|inv| inv.menu().clone())
64 }
65
66 pub fn selected_hotbar_slot(&self) -> u8 {
74 self.query_self::<&Inv, _>(|inv| inv.selected_hotbar_slot)
75 }
76
77 pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) {
86 assert!(
87 new_hotbar_slot_index < 9,
88 "Hotbar slot index must be in the range 0..=8"
89 );
90
91 let mut ecs = self.ecs.lock();
92 ecs.trigger(SetSelectedHotbarSlotEvent {
93 entity: self.entity,
94 slot: new_hotbar_slot_index,
95 });
96 }
97}
98
99#[derive(Clone, Debug, EntityEvent)]
106pub struct MenuOpenedEvent {
107 pub entity: Entity,
108 pub window_id: i32,
109 pub menu_type: MenuKind,
110 pub title: FormattedText,
111}
112fn handle_menu_opened_trigger(event: On<MenuOpenedEvent>, mut query: Query<&mut Inv>) {
113 let mut inventory = query.get_mut(event.entity).unwrap();
114 inventory.id = event.window_id;
115 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
116 inventory.container_menu_title = Some(event.title.clone());
117}
118
119#[derive(EntityEvent)]
124pub struct CloseContainerEvent {
125 pub entity: Entity,
126 pub id: i32,
131}
132fn handle_container_close_event(
133 close_container: On<CloseContainerEvent>,
134 mut commands: Commands,
135 query: Query<(Entity, &Inv)>,
136) {
137 let (entity, inventory) = query.get(close_container.entity).unwrap();
138 if close_container.id != inventory.id {
139 warn!(
140 "Tried to close container with ID {}, but the current container ID is {}",
141 close_container.id, inventory.id
142 );
143 return;
144 }
145
146 commands.trigger(SendGamePacketEvent::new(
147 entity,
148 ServerboundContainerClose {
149 container_id: inventory.id,
150 },
151 ));
152 commands.trigger(ClientsideCloseContainerEvent {
153 entity: close_container.entity,
154 });
155}
156
157#[derive(Clone, EntityEvent)]
166pub struct ClientsideCloseContainerEvent {
167 pub entity: Entity,
168}
169pub fn handle_client_side_close_container_trigger(
170 event: On<ClientsideCloseContainerEvent>,
171 mut query: Query<&mut Inv>,
172) {
173 let mut inventory = query.get_mut(event.entity).unwrap();
174
175 if let Some(inventory_menu) = inventory.container_menu.take() {
177 let new_inventory = inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
194 let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
195 *inventory.inventory_menu.as_player_mut().inventory = new_inventory;
196 }
197
198 inventory.id = 0;
199 inventory.container_menu_title = None;
200}
201
202#[derive(Debug, EntityEvent)]
203pub struct ContainerClickEvent {
204 pub entity: Entity,
205 pub window_id: i32,
206 pub operation: ClickOperation,
207}
208pub fn handle_container_click_event(
209 container_click: On<ContainerClickEvent>,
210 mut commands: Commands,
211 mut query: Query<(Entity, &mut Inv, Option<&PlayerAbilities>, &InstanceName)>,
212 instance_container: Res<InstanceContainer>,
213) {
214 let (entity, mut inventory, player_abilities, instance_name) =
215 query.get_mut(container_click.entity).unwrap();
216 if inventory.id != container_click.window_id {
217 error!(
218 "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
219 container_click.window_id, inventory.id
220 );
221 return;
222 }
223
224 let Some(instance) = instance_container.get(instance_name) else {
225 return;
226 };
227
228 let old_slots = inventory.menu().slots();
229 inventory.simulate_click(
230 &container_click.operation,
231 player_abilities.unwrap_or(&PlayerAbilities::default()),
232 );
233 let new_slots = inventory.menu().slots();
234
235 let registry_holder = &instance.read().registries;
236
237 let mut changed_slots: IndexMap<u16, HashedStack> = IndexMap::new();
240 for (slot_index, old_slot) in old_slots.iter().enumerate() {
241 let new_slot = &new_slots[slot_index];
242 if old_slot != new_slot {
243 changed_slots.insert(
244 slot_index as u16,
245 HashedStack::from_item_stack(new_slot, registry_holder),
246 );
247 }
248 }
249
250 commands.trigger(SendGamePacketEvent::new(
251 entity,
252 ServerboundContainerClick {
253 container_id: container_click.window_id,
254 state_id: inventory.state_id,
255 slot_num: container_click
256 .operation
257 .slot_num()
258 .map(|n| n as i16)
259 .unwrap_or(-999),
260 button_num: container_click.operation.button_num(),
261 click_type: container_click.operation.click_type(),
262 changed_slots,
263 carried_item: HashedStack::from_item_stack(&inventory.carried, registry_holder),
264 },
265 ));
266}
267
268#[derive(EntityEvent)]
272pub struct SetContainerContentEvent {
273 pub entity: Entity,
274 pub slots: Vec<ItemStack>,
275 pub container_id: i32,
276}
277pub fn handle_set_container_content_trigger(
278 set_container_content: On<SetContainerContentEvent>,
279 mut query: Query<&mut Inv>,
280) {
281 let mut inventory = query.get_mut(set_container_content.entity).unwrap();
282
283 if set_container_content.container_id != inventory.id {
284 warn!(
285 "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
286 set_container_content.container_id, inventory.id
287 );
288 return;
289 }
290
291 let menu = inventory.menu_mut();
292 for (i, slot) in set_container_content.slots.iter().enumerate() {
293 if let Some(slot_mut) = menu.slot_mut(i) {
294 *slot_mut = slot.clone();
295 }
296 }
297}
298
299#[derive(EntityEvent)]
303pub struct SetSelectedHotbarSlotEvent {
304 pub entity: Entity,
305 pub slot: u8,
307}
308pub fn handle_set_selected_hotbar_slot_event(
309 set_selected_hotbar_slot: On<SetSelectedHotbarSlotEvent>,
310 mut query: Query<&mut Inv>,
311) {
312 let mut inventory = query.get_mut(set_selected_hotbar_slot.entity).unwrap();
313 inventory.selected_hotbar_slot = set_selected_hotbar_slot.slot;
314}
315
316#[derive(Component)]
320pub struct LastSentSelectedHotbarSlot {
321 pub slot: u8,
322}
323pub fn ensure_has_sent_carried_item(
329 mut commands: Commands,
330 query: Query<(Entity, &Inv, Option<&LastSentSelectedHotbarSlot>)>,
331) {
332 for (entity, inventory, last_sent) in query.iter() {
333 if let Some(last_sent) = last_sent {
334 if last_sent.slot == inventory.selected_hotbar_slot {
335 continue;
336 }
337
338 commands.trigger(SendGamePacketEvent::new(
339 entity,
340 ServerboundSetCarriedItem {
341 slot: inventory.selected_hotbar_slot as u16,
342 },
343 ));
344 }
345
346 commands.entity(entity).insert(LastSentSelectedHotbarSlot {
347 slot: inventory.selected_hotbar_slot,
348 });
349 }
350}