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 inventory::equipment_effects::{collect_equipment_changes, handle_equipment_changes},
22 packet::game::SendGamePacketEvent,
23};
24
25#[doc(hidden)]
28#[deprecated = "moved to `azalea_entity::inventory::Inventory`."]
29pub type Inventory = azalea_entity::inventory::Inventory;
30
31pub struct InventoryPlugin;
32impl Plugin for InventoryPlugin {
33 fn build(&self, app: &mut App) {
34 app.add_systems(
35 GameTick,
36 (
37 ensure_has_sent_carried_item.after(super::mining::handle_mining_queued),
38 collect_equipment_changes
39 .after(super::interact::handle_start_use_item_queued)
40 .before(azalea_physics::ai_step),
41 ),
42 )
43 .add_observer(handle_client_side_close_container_trigger)
44 .add_observer(handle_menu_opened_trigger)
45 .add_observer(handle_container_close_event)
46 .add_observer(handle_set_container_content_trigger)
47 .add_observer(handle_container_click_event)
48 .add_observer(handle_set_selected_hotbar_slot_event)
51 .add_observer(handle_equipment_changes);
52 }
53}
54
55#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
56pub struct InventorySystems;
57
58#[derive(Clone, Debug, EntityEvent)]
65pub struct MenuOpenedEvent {
66 pub entity: Entity,
67 pub window_id: i32,
68 pub menu_type: MenuKind,
69 pub title: FormattedText,
70}
71fn handle_menu_opened_trigger(event: On<MenuOpenedEvent>, mut query: Query<&mut Inv>) {
72 let mut inventory = query.get_mut(event.entity).unwrap();
73 inventory.id = event.window_id;
74 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
75 inventory.container_menu_title = Some(event.title.clone());
76}
77
78#[derive(EntityEvent)]
83pub struct CloseContainerEvent {
84 pub entity: Entity,
85 pub id: i32,
90}
91fn handle_container_close_event(
92 close_container: On<CloseContainerEvent>,
93 mut commands: Commands,
94 query: Query<(Entity, &Inv)>,
95) {
96 let (entity, inventory) = query.get(close_container.entity).unwrap();
97 if close_container.id != inventory.id {
98 warn!(
99 "Tried to close container with ID {}, but the current container ID is {}",
100 close_container.id, inventory.id
101 );
102 return;
103 }
104
105 commands.trigger(SendGamePacketEvent::new(
106 entity,
107 ServerboundContainerClose {
108 container_id: inventory.id,
109 },
110 ));
111 commands.trigger(ClientsideCloseContainerEvent {
112 entity: close_container.entity,
113 });
114}
115
116#[derive(Clone, EntityEvent)]
125pub struct ClientsideCloseContainerEvent {
126 pub entity: Entity,
127}
128pub fn handle_client_side_close_container_trigger(
129 event: On<ClientsideCloseContainerEvent>,
130 mut query: Query<&mut Inv>,
131) {
132 let mut inventory = query.get_mut(event.entity).unwrap();
133
134 if let Some(inventory_menu) = inventory.container_menu.take() {
136 let new_inventory = inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
153 let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
154 *inventory.inventory_menu.as_player_mut().inventory = new_inventory;
155 }
156
157 inventory.id = 0;
158 inventory.container_menu_title = None;
159}
160
161#[derive(Debug, EntityEvent)]
162pub struct ContainerClickEvent {
163 pub entity: Entity,
164 pub window_id: i32,
165 pub operation: ClickOperation,
166}
167pub fn handle_container_click_event(
168 container_click: On<ContainerClickEvent>,
169 mut commands: Commands,
170 mut query: Query<(Entity, &mut Inv, Option<&PlayerAbilities>, &InstanceName)>,
171 instance_container: Res<InstanceContainer>,
172) {
173 let (entity, mut inventory, player_abilities, instance_name) =
174 query.get_mut(container_click.entity).unwrap();
175 if inventory.id != container_click.window_id {
176 error!(
177 "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
178 container_click.window_id, inventory.id
179 );
180 return;
181 }
182
183 let Some(instance) = instance_container.get(instance_name) else {
184 return;
185 };
186
187 let old_slots = inventory.menu().slots();
188 inventory.simulate_click(
189 &container_click.operation,
190 player_abilities.unwrap_or(&PlayerAbilities::default()),
191 );
192 let new_slots = inventory.menu().slots();
193
194 let registry_holder = &instance.read().registries;
195
196 let mut changed_slots: IndexMap<u16, HashedStack> = IndexMap::new();
199 for (slot_index, old_slot) in old_slots.iter().enumerate() {
200 let new_slot = &new_slots[slot_index];
201 if old_slot != new_slot {
202 changed_slots.insert(
203 slot_index as u16,
204 HashedStack::from_item_stack(new_slot, registry_holder),
205 );
206 }
207 }
208
209 commands.trigger(SendGamePacketEvent::new(
210 entity,
211 ServerboundContainerClick {
212 container_id: container_click.window_id,
213 state_id: inventory.state_id,
214 slot_num: container_click
215 .operation
216 .slot_num()
217 .map(|n| n as i16)
218 .unwrap_or(-999),
219 button_num: container_click.operation.button_num(),
220 click_type: container_click.operation.click_type(),
221 changed_slots,
222 carried_item: HashedStack::from_item_stack(&inventory.carried, registry_holder),
223 },
224 ));
225}
226
227#[derive(EntityEvent)]
231pub struct SetContainerContentEvent {
232 pub entity: Entity,
233 pub slots: Vec<ItemStack>,
234 pub container_id: i32,
235}
236pub fn handle_set_container_content_trigger(
237 set_container_content: On<SetContainerContentEvent>,
238 mut query: Query<&mut Inv>,
239) {
240 let mut inventory = query.get_mut(set_container_content.entity).unwrap();
241
242 if set_container_content.container_id != inventory.id {
243 warn!(
244 "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
245 set_container_content.container_id, inventory.id
246 );
247 return;
248 }
249
250 let menu = inventory.menu_mut();
251 for (i, slot) in set_container_content.slots.iter().enumerate() {
252 if let Some(slot_mut) = menu.slot_mut(i) {
253 *slot_mut = slot.clone();
254 }
255 }
256}
257
258#[derive(EntityEvent)]
262pub struct SetSelectedHotbarSlotEvent {
263 pub entity: Entity,
264 pub slot: u8,
266}
267pub fn handle_set_selected_hotbar_slot_event(
268 set_selected_hotbar_slot: On<SetSelectedHotbarSlotEvent>,
269 mut query: Query<&mut Inv>,
270) {
271 let mut inventory = query.get_mut(set_selected_hotbar_slot.entity).unwrap();
272 inventory.selected_hotbar_slot = set_selected_hotbar_slot.slot;
273}
274
275#[derive(Component)]
279pub struct LastSentSelectedHotbarSlot {
280 pub slot: u8,
281}
282pub fn ensure_has_sent_carried_item(
288 mut commands: Commands,
289 query: Query<(Entity, &Inv, Option<&LastSentSelectedHotbarSlot>)>,
290) {
291 for (entity, inventory, last_sent) in query.iter() {
292 if let Some(last_sent) = last_sent {
293 if last_sent.slot == inventory.selected_hotbar_slot {
294 continue;
295 }
296
297 commands.trigger(SendGamePacketEvent::new(
298 entity,
299 ServerboundSetCarriedItem {
300 slot: inventory.selected_hotbar_slot as u16,
301 },
302 ));
303 }
304
305 commands.entity(entity).insert(LastSentSelectedHotbarSlot {
306 slot: inventory.selected_hotbar_slot,
307 });
308 }
309}