1use std::{cmp, collections::HashSet};
2
3use azalea_chat::FormattedText;
4use azalea_core::tick::GameTick;
5use azalea_entity::PlayerAbilities;
6pub use azalea_inventory::*;
7use azalea_inventory::{
8 item::MaxStackSizeExt,
9 operations::{
10 ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
11 QuickCraftStatusKind, QuickMoveClick, ThrowClick,
12 },
13};
14use azalea_protocol::packets::game::{
15 s_container_click::{HashedStack, ServerboundContainerClick},
16 s_container_close::ServerboundContainerClose,
17 s_set_carried_item::ServerboundSetCarriedItem,
18};
19use azalea_registry::MenuKind;
20use azalea_world::{InstanceContainer, InstanceName};
21use bevy_app::{App, Plugin, Update};
22use bevy_ecs::prelude::*;
23use indexmap::IndexMap;
24use tracing::{error, warn};
25
26use crate::{Client, packet::game::SendPacketEvent, respawn::perform_respawn};
27
28pub struct InventoryPlugin;
29impl Plugin for InventoryPlugin {
30 fn build(&self, app: &mut App) {
31 app.add_event::<ClientsideCloseContainerEvent>()
32 .add_event::<MenuOpenedEvent>()
33 .add_event::<CloseContainerEvent>()
34 .add_event::<ContainerClickEvent>()
35 .add_event::<SetContainerContentEvent>()
36 .add_event::<SetSelectedHotbarSlotEvent>()
37 .add_systems(
38 Update,
39 (
40 handle_set_selected_hotbar_slot_event,
41 handle_menu_opened_event,
42 handle_container_click_event,
43 handle_container_close_event,
44 handle_client_side_close_container_event,
45 )
46 .chain()
47 .in_set(InventorySet)
48 .before(perform_respawn),
49 )
50 .add_systems(
51 GameTick,
52 ensure_has_sent_carried_item.after(super::mining::handle_mining_queued),
53 )
54 .add_observer(handle_client_side_close_container_trigger)
55 .add_observer(handle_menu_opened_trigger)
56 .add_observer(handle_set_container_content_trigger);
57 }
58}
59
60#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
61pub struct InventorySet;
62
63impl Client {
64 pub fn menu(&self) -> Menu {
67 let mut ecs = self.ecs.lock();
68 let inventory = self.query::<&Inventory>(&mut ecs);
69 inventory.menu().clone()
70 }
71
72 pub fn selected_hotbar_slot(&self) -> u8 {
80 let mut ecs = self.ecs.lock();
81 let inventory = self.query::<&Inventory>(&mut ecs);
82 inventory.selected_hotbar_slot
83 }
84
85 pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) {
90 assert!(
91 new_hotbar_slot_index < 9,
92 "Hotbar slot index must be in the range 0..=8"
93 );
94
95 let mut ecs = self.ecs.lock();
96 ecs.send_event(SetSelectedHotbarSlotEvent {
97 entity: self.entity,
98 slot: new_hotbar_slot_index,
99 });
100 }
101}
102
103#[derive(Component, Debug, Clone)]
105pub struct Inventory {
106 pub inventory_menu: azalea_inventory::Menu,
111
112 pub id: i32,
117 pub container_menu: Option<azalea_inventory::Menu>,
120 pub container_menu_title: Option<FormattedText>,
123 pub carried: ItemStack,
129 pub state_id: u32,
133
134 pub quick_craft_status: QuickCraftStatusKind,
135 pub quick_craft_kind: QuickCraftKind,
136 pub quick_craft_slots: HashSet<u16>,
139
140 pub selected_hotbar_slot: u8,
146}
147
148impl Inventory {
149 pub fn menu(&self) -> &azalea_inventory::Menu {
155 match &self.container_menu {
156 Some(menu) => menu,
157 _ => &self.inventory_menu,
158 }
159 }
160
161 pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
167 match &mut self.container_menu {
168 Some(menu) => menu,
169 _ => &mut self.inventory_menu,
170 }
171 }
172
173 pub fn simulate_click(
175 &mut self,
176 operation: &ClickOperation,
177 player_abilities: &PlayerAbilities,
178 ) {
179 if let ClickOperation::QuickCraft(quick_craft) = operation {
180 let last_quick_craft_status_tmp = self.quick_craft_status.clone();
181 self.quick_craft_status = last_quick_craft_status_tmp.clone();
182 let last_quick_craft_status = last_quick_craft_status_tmp;
183
184 if self.carried.is_empty() {
186 return self.reset_quick_craft();
187 }
188 if (last_quick_craft_status == QuickCraftStatusKind::Start
191 || last_quick_craft_status == QuickCraftStatusKind::End
192 || self.quick_craft_status != QuickCraftStatusKind::End)
193 && (self.quick_craft_status != last_quick_craft_status)
194 {
195 return self.reset_quick_craft();
196 }
197 if self.quick_craft_status == QuickCraftStatusKind::Start {
198 self.quick_craft_kind = quick_craft.kind.clone();
199 if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
200 {
201 self.quick_craft_status = QuickCraftStatusKind::Add;
202 self.quick_craft_slots.clear();
203 } else {
204 self.reset_quick_craft();
205 }
206 return;
207 }
208 if let QuickCraftStatus::Add { slot } = quick_craft.status {
209 let slot_item = self.menu().slot(slot as usize);
210 if let Some(slot_item) = slot_item
211 && let ItemStack::Present(carried) = &self.carried
212 {
213 if can_item_quick_replace(slot_item, &self.carried, true)
217 && (self.quick_craft_kind == QuickCraftKind::Right
218 || carried.count as usize > self.quick_craft_slots.len())
219 {
220 self.quick_craft_slots.insert(slot);
221 }
222 }
223 return;
224 }
225 if self.quick_craft_status == QuickCraftStatusKind::End {
226 if !self.quick_craft_slots.is_empty() {
227 if self.quick_craft_slots.len() == 1 {
228 let slot = *self.quick_craft_slots.iter().next().unwrap();
231 self.reset_quick_craft();
232 self.simulate_click(
233 &match self.quick_craft_kind {
234 QuickCraftKind::Left => {
235 PickupClick::Left { slot: Some(slot) }.into()
236 }
237 QuickCraftKind::Right => {
238 PickupClick::Left { slot: Some(slot) }.into()
239 }
240 QuickCraftKind::Middle => {
241 return;
243 }
244 },
245 player_abilities,
246 );
247 return;
248 }
249
250 let ItemStack::Present(mut carried) = self.carried.clone() else {
251 return self.reset_quick_craft();
253 };
254
255 let mut carried_count = carried.count;
256 let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
257
258 loop {
259 let mut slot: &ItemStack;
260 let mut slot_index: u16;
261 let mut item_stack: &ItemStack;
262
263 loop {
264 let Some(&next_slot) = quick_craft_slots_iter.next() else {
265 carried.count = carried_count;
266 self.carried = ItemStack::Present(carried);
267 return self.reset_quick_craft();
268 };
269
270 slot = self.menu().slot(next_slot as usize).unwrap();
271 slot_index = next_slot;
272 item_stack = &self.carried;
273
274 if slot.is_present()
275 && can_item_quick_replace(slot, item_stack, true)
276 && (
279 self.quick_craft_kind == QuickCraftKind::Middle
280 || item_stack.count() >= self.quick_craft_slots.len() as i32
281 )
282 {
283 break;
284 }
285 }
286
287 let ItemStack::Present(slot) = slot else {
289 unreachable!("the loop above requires the slot to be present to break")
290 };
291
292 let mut new_carried = carried.clone();
294 let slot_item_count = slot.count;
295 get_quick_craft_slot_count(
296 &self.quick_craft_slots,
297 &self.quick_craft_kind,
298 &mut new_carried,
299 slot_item_count,
300 );
301 let max_stack_size = i32::min(
302 new_carried.kind.max_stack_size(),
303 i32::min(
304 new_carried.kind.max_stack_size(),
305 slot.kind.max_stack_size(),
306 ),
307 );
308 if new_carried.count > max_stack_size {
309 new_carried.count = max_stack_size;
310 }
311
312 carried_count -= new_carried.count - slot_item_count;
313 let menu = match &mut self.container_menu {
316 Some(menu) => menu,
317 _ => &mut self.inventory_menu,
318 };
319 *menu.slot_mut(slot_index as usize).unwrap() =
320 ItemStack::Present(new_carried);
321 }
322 }
323 } else {
324 return self.reset_quick_craft();
325 }
326 }
327 if self.quick_craft_status != QuickCraftStatusKind::Start {
330 return self.reset_quick_craft();
331 }
332
333 match operation {
334 ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
336 if self.carried.is_present() {
337 self.carried = ItemStack::Empty;
344 }
345 }
346 ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
347 if self.carried.is_present() {
348 let _item = self.carried.split(1);
349 }
351 }
352 &ClickOperation::Pickup(
353 ref pickup @ (PickupClick::Left { slot: Some(slot) }
355 | PickupClick::Right { slot: Some(slot) }),
356 ) => {
357 let slot = slot as usize;
358 let Some(slot_item) = self.menu().slot(slot) else {
359 return;
360 };
361
362 if self.try_item_click_behavior_override(operation, slot) {
363 return;
364 }
365
366 let is_left_click = matches!(pickup, PickupClick::Left { .. });
367
368 match slot_item {
369 ItemStack::Empty => {
370 if self.carried.is_present() {
371 let place_count = if is_left_click {
372 self.carried.count()
373 } else {
374 1
375 };
376 self.carried =
377 self.safe_insert(slot, self.carried.clone(), place_count);
378 }
379 }
380 ItemStack::Present(_) => {
381 if !self.menu().may_pickup(slot) {
382 return;
383 }
384 if let ItemStack::Present(carried) = self.carried.clone() {
385 let slot_is_same_item_as_carried = slot_item
386 .as_present()
387 .is_some_and(|s| carried.is_same_item_and_components(s));
388
389 if self.menu().may_place(slot, &carried) {
390 if slot_is_same_item_as_carried {
391 let place_count = if is_left_click { carried.count } else { 1 };
392 self.carried =
393 self.safe_insert(slot, self.carried.clone(), place_count);
394 } else if carried.count
395 <= self
396 .menu()
397 .max_stack_size(slot)
398 .min(carried.kind.max_stack_size())
399 {
400 self.carried = slot_item.clone();
402 let slot_item = self.menu_mut().slot_mut(slot).unwrap();
403 *slot_item = carried.into();
404 }
405 } else if slot_is_same_item_as_carried
406 && let Some(removed) = self.try_remove(
407 slot,
408 slot_item.count(),
409 carried.kind.max_stack_size() - carried.count,
410 )
411 {
412 self.carried.as_present_mut().unwrap().count += removed.count();
413 }
415 } else {
416 let pickup_count = if is_left_click {
417 slot_item.count()
418 } else {
419 (slot_item.count() + 1) / 2
420 };
421 if let Some(new_slot_item) =
422 self.try_remove(slot, pickup_count, i32::MAX)
423 {
424 self.carried = new_slot_item;
425 }
427 }
428 }
429 }
430 }
431 &ClickOperation::QuickMove(
432 QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
433 ) => {
434 let slot = slot as usize;
437 loop {
438 let new_slot_item = self.menu_mut().quick_move_stack(slot);
439 let slot_item = self.menu().slot(slot).unwrap();
440 if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() {
441 break;
442 }
443 }
444 }
445 ClickOperation::Swap(s) => {
446 let source_slot_index = s.source_slot as usize;
447 let target_slot_index = s.target_slot as usize;
448
449 let Some(source_slot) = self.menu().slot(source_slot_index) else {
450 return;
451 };
452 let Some(target_slot) = self.menu().slot(target_slot_index) else {
453 return;
454 };
455 if source_slot.is_empty() && target_slot.is_empty() {
456 return;
457 }
458
459 if target_slot.is_empty() {
460 if self.menu().may_pickup(source_slot_index) {
461 let source_slot = source_slot.clone();
462 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
463 *target_slot = source_slot;
464 }
465 } else if source_slot.is_empty() {
466 let target_item = target_slot
467 .as_present()
468 .expect("target slot was already checked to not be empty");
469 if self.menu().may_place(source_slot_index, target_item) {
470 let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
472
473 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
474 let new_source_slot =
475 target_slot.split(source_max_stack_size.try_into().unwrap());
476 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
477 }
478 } else if self.menu().may_pickup(source_slot_index) {
479 let ItemStack::Present(target_item) = target_slot else {
480 unreachable!("target slot is not empty but is not present");
481 };
482 if self.menu().may_place(source_slot_index, target_item) {
483 let source_max_stack = self.menu().max_stack_size(source_slot_index);
484 if target_slot.count() > source_max_stack {
485 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
488 let new_source_slot =
489 target_slot.split(source_max_stack.try_into().unwrap());
490 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
491 } else {
495 let new_target_slot = source_slot.clone();
497 let new_source_slot = target_slot.clone();
498
499 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
500 *target_slot = new_target_slot;
501
502 let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
503 *source_slot = new_source_slot;
504 }
505 }
506 }
507 }
508 ClickOperation::Clone(CloneClick { slot }) => {
509 if !player_abilities.instant_break || self.carried.is_present() {
510 return;
511 }
512 let Some(source_slot) = self.menu().slot(*slot as usize) else {
513 return;
514 };
515 let ItemStack::Present(source_item) = source_slot else {
516 return;
517 };
518 let mut new_carried = source_item.clone();
519 new_carried.count = new_carried.kind.max_stack_size();
520 self.carried = ItemStack::Present(new_carried);
521 }
522 ClickOperation::Throw(c) => {
523 if self.carried.is_present() {
524 return;
525 }
526
527 let (ThrowClick::Single { slot: slot_index }
528 | ThrowClick::All { slot: slot_index }) = c;
529 let slot_index = *slot_index as usize;
530
531 let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
532 return;
533 };
534 let ItemStack::Present(slot_item) = slot else {
535 return;
536 };
537
538 let dropping_count = match c {
539 ThrowClick::Single { .. } => 1,
540 ThrowClick::All { .. } => slot_item.count,
541 };
542
543 let _dropping = slot_item.split(dropping_count as u32);
544 }
546 ClickOperation::PickupAll(PickupAllClick {
547 slot: source_slot_index,
548 reversed,
549 }) => {
550 let source_slot_index = *source_slot_index as usize;
551
552 let source_slot = self.menu().slot(source_slot_index).unwrap();
553 let target_slot = self.carried.clone();
554
555 if target_slot.is_empty()
556 || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
557 {
558 return;
559 }
560
561 let ItemStack::Present(target_slot_item) = &target_slot else {
562 unreachable!("target slot is not empty but is not present");
563 };
564
565 for round in 0..2 {
566 let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
567 Box::new((0..self.menu().len()).rev())
568 } else {
569 Box::new(0..self.menu().len())
570 };
571
572 for i in iterator {
573 if target_slot_item.count < target_slot_item.kind.max_stack_size() {
574 let checking_slot = self.menu().slot(i).unwrap();
575 if let ItemStack::Present(checking_item) = checking_slot
576 && can_item_quick_replace(checking_slot, &target_slot, true)
577 && self.menu().may_pickup(i)
578 && (round != 0
579 || checking_item.count != checking_item.kind.max_stack_size())
580 {
581 let checking_slot = self.menu_mut().slot_mut(i).unwrap();
583
584 let taken_item = checking_slot.split(checking_slot.count() as u32);
585
586 let target_slot = &mut self.carried;
588 let ItemStack::Present(target_slot_item) = target_slot else {
589 unreachable!("target slot is not empty but is not present");
590 };
591 target_slot_item.count += taken_item.count();
592 }
593 }
594 }
595 }
596 }
597 _ => {}
598 }
599 }
600
601 fn reset_quick_craft(&mut self) {
602 self.quick_craft_status = QuickCraftStatusKind::Start;
603 self.quick_craft_slots.clear();
604 }
605
606 pub fn held_item(&self) -> ItemStack {
609 let inventory = &self.inventory_menu;
610 let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
611 hotbar_items[self.selected_hotbar_slot as usize].clone()
612 }
613
614 fn try_item_click_behavior_override(
616 &self,
617 _operation: &ClickOperation,
618 _slot_item_index: usize,
619 ) -> bool {
620 false
621 }
622
623 fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack {
624 let Some(slot_item) = self.menu_mut().slot_mut(slot) else {
625 return src_item;
626 };
627 let ItemStack::Present(mut src_item) = src_item else {
628 return src_item;
629 };
630
631 let take_count = cmp::min(
632 cmp::min(take_count, src_item.count),
633 src_item.kind.max_stack_size() - slot_item.count(),
634 );
635 if take_count <= 0 {
636 return src_item.into();
637 }
638 let take_count = take_count as u32;
639
640 if slot_item.is_empty() {
641 *slot_item = src_item.split(take_count).into();
642 } else if let ItemStack::Present(slot_item) = slot_item
643 && slot_item.is_same_item_and_components(&src_item)
644 {
645 src_item.count -= take_count as i32;
646 slot_item.count += take_count as i32;
647 }
648
649 src_item.into()
650 }
651
652 fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> {
653 if !self.menu().may_pickup(slot) {
654 return None;
655 }
656 let mut slot_item = self.menu().slot(slot)?.clone();
657 if !self.menu().allow_modification(slot) && limit < slot_item.count() {
658 return None;
659 }
660
661 let count = count.min(limit);
662 if count <= 0 {
663 return None;
664 }
665 let removed = slot_item.split(count as u32);
667
668 if removed.is_present() && slot_item.is_empty() {
669 *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty;
670 }
671
672 Some(removed)
673 }
674}
675
676fn can_item_quick_replace(
677 target_slot: &ItemStack,
678 item: &ItemStack,
679 ignore_item_count: bool,
680) -> bool {
681 let ItemStack::Present(target_slot) = target_slot else {
682 return false;
683 };
684 let ItemStack::Present(item) = item else {
685 return false;
688 };
689
690 if !item.is_same_item_and_components(target_slot) {
691 return false;
692 }
693 let count = target_slot.count as u16
694 + if ignore_item_count {
695 0
696 } else {
697 item.count as u16
698 };
699 count <= item.kind.max_stack_size() as u16
700}
701
702fn get_quick_craft_slot_count(
703 quick_craft_slots: &HashSet<u16>,
704 quick_craft_kind: &QuickCraftKind,
705 item: &mut ItemStackData,
706 slot_item_count: i32,
707) {
708 item.count = match quick_craft_kind {
709 QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
710 QuickCraftKind::Right => 1,
711 QuickCraftKind::Middle => item.kind.max_stack_size(),
712 };
713 item.count += slot_item_count;
714}
715
716impl Default for Inventory {
717 fn default() -> Self {
718 Inventory {
719 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
720 id: 0,
721 container_menu: None,
722 container_menu_title: None,
723 carried: ItemStack::Empty,
724 state_id: 0,
725 quick_craft_status: QuickCraftStatusKind::Start,
726 quick_craft_kind: QuickCraftKind::Middle,
727 quick_craft_slots: HashSet::new(),
728 selected_hotbar_slot: 0,
729 }
730 }
731}
732
733#[derive(Event, Debug, Clone)]
740pub struct MenuOpenedEvent {
741 pub entity: Entity,
742 pub window_id: i32,
743 pub menu_type: MenuKind,
744 pub title: FormattedText,
745}
746fn handle_menu_opened_trigger(event: Trigger<MenuOpenedEvent>, mut query: Query<&mut Inventory>) {
747 let mut inventory = query.get_mut(event.entity).unwrap();
748 inventory.id = event.window_id;
749 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
750 inventory.container_menu_title = Some(event.title.clone());
751}
752pub fn handle_menu_opened_event(mut events: EventReader<MenuOpenedEvent>, mut commands: Commands) {
753 for event in events.read() {
754 commands.trigger(event.clone());
755 }
756}
757
758#[derive(Event)]
763pub struct CloseContainerEvent {
764 pub entity: Entity,
765 pub id: i32,
768}
769fn handle_container_close_event(
770 query: Query<(Entity, &Inventory)>,
771 mut events: EventReader<CloseContainerEvent>,
772 mut client_side_events: EventWriter<ClientsideCloseContainerEvent>,
773 mut commands: Commands,
774) {
775 for event in events.read() {
776 let (entity, inventory) = query.get(event.entity).unwrap();
777 if event.id != inventory.id {
778 warn!(
779 "Tried to close container with ID {}, but the current container ID is {}",
780 event.id, inventory.id
781 );
782 continue;
783 }
784
785 commands.trigger(SendPacketEvent::new(
786 entity,
787 ServerboundContainerClose {
788 container_id: inventory.id,
789 },
790 ));
791 client_side_events.write(ClientsideCloseContainerEvent {
792 entity: event.entity,
793 });
794 }
795}
796
797#[derive(Event, Clone)]
806pub struct ClientsideCloseContainerEvent {
807 pub entity: Entity,
808}
809pub fn handle_client_side_close_container_trigger(
810 event: Trigger<ClientsideCloseContainerEvent>,
811 mut query: Query<&mut Inventory>,
812) {
813 let mut inventory = query.get_mut(event.entity).unwrap();
814
815 if let Some(inventory_menu) = inventory.container_menu.take() {
817 let new_inventory = inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
834 let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
835 *inventory.inventory_menu.as_player_mut().inventory = new_inventory;
836 }
837
838 inventory.id = 0;
839 inventory.container_menu_title = None;
840}
841pub fn handle_client_side_close_container_event(
842 mut commands: Commands,
843 mut events: EventReader<ClientsideCloseContainerEvent>,
844) {
845 for event in events.read() {
846 commands.trigger(event.clone());
847 }
848}
849
850#[derive(Event, Debug)]
851pub struct ContainerClickEvent {
852 pub entity: Entity,
853 pub window_id: i32,
854 pub operation: ClickOperation,
855}
856pub fn handle_container_click_event(
857 mut query: Query<(
858 Entity,
859 &mut Inventory,
860 Option<&PlayerAbilities>,
861 &InstanceName,
862 )>,
863 mut events: EventReader<ContainerClickEvent>,
864 mut commands: Commands,
865 instance_container: Res<InstanceContainer>,
866) {
867 for event in events.read() {
868 let (entity, mut inventory, player_abilities, instance_name) =
869 query.get_mut(event.entity).unwrap();
870 if inventory.id != event.window_id {
871 error!(
872 "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
873 event.window_id, inventory.id
874 );
875 continue;
876 }
877
878 let Some(instance) = instance_container.get(instance_name) else {
879 continue;
880 };
881
882 let old_slots = inventory.menu().slots();
883 inventory.simulate_click(
884 &event.operation,
885 player_abilities.unwrap_or(&PlayerAbilities::default()),
886 );
887 let new_slots = inventory.menu().slots();
888
889 let registry_holder = &instance.read().registries;
890
891 let mut changed_slots: IndexMap<u16, HashedStack> = IndexMap::new();
894 for (slot_index, old_slot) in old_slots.iter().enumerate() {
895 let new_slot = &new_slots[slot_index];
896 if old_slot != new_slot {
897 changed_slots.insert(
898 slot_index as u16,
899 HashedStack::from_item_stack(new_slot, registry_holder),
900 );
901 }
902 }
903
904 commands.trigger(SendPacketEvent::new(
905 entity,
906 ServerboundContainerClick {
907 container_id: event.window_id,
908 state_id: inventory.state_id,
909 slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
910 button_num: event.operation.button_num(),
911 click_type: event.operation.click_type(),
912 changed_slots,
913 carried_item: HashedStack::from_item_stack(&inventory.carried, registry_holder),
914 },
915 ));
916 }
917}
918
919#[derive(Event)]
923pub struct SetContainerContentEvent {
924 pub entity: Entity,
925 pub slots: Vec<ItemStack>,
926 pub container_id: i32,
927}
928pub fn handle_set_container_content_trigger(
929 event: Trigger<SetContainerContentEvent>,
930 mut query: Query<&mut Inventory>,
931) {
932 let mut inventory = query.get_mut(event.entity).unwrap();
933
934 if event.container_id != inventory.id {
935 warn!(
936 "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
937 event.container_id, inventory.id
938 );
939 return;
940 }
941
942 let menu = inventory.menu_mut();
943 for (i, slot) in event.slots.iter().enumerate() {
944 if let Some(slot_mut) = menu.slot_mut(i) {
945 *slot_mut = slot.clone();
946 }
947 }
948}
949
950#[derive(Event)]
954pub struct SetSelectedHotbarSlotEvent {
955 pub entity: Entity,
956 pub slot: u8,
958}
959pub fn handle_set_selected_hotbar_slot_event(
960 mut events: EventReader<SetSelectedHotbarSlotEvent>,
961 mut query: Query<&mut Inventory>,
962) {
963 for event in events.read() {
964 let mut inventory = query.get_mut(event.entity).unwrap();
965
966 if inventory.selected_hotbar_slot == event.slot {
968 continue;
969 }
970
971 inventory.selected_hotbar_slot = event.slot;
972 }
973}
974
975#[derive(Component)]
979pub struct LastSentSelectedHotbarSlot {
980 pub slot: u8,
981}
982pub fn ensure_has_sent_carried_item(
988 mut commands: Commands,
989 query: Query<(Entity, &Inventory, Option<&LastSentSelectedHotbarSlot>)>,
990) {
991 for (entity, inventory, last_sent) in query.iter() {
992 if let Some(last_sent) = last_sent {
993 if last_sent.slot == inventory.selected_hotbar_slot {
994 continue;
995 }
996
997 commands.trigger(SendPacketEvent::new(
998 entity,
999 ServerboundSetCarriedItem {
1000 slot: inventory.selected_hotbar_slot as u16,
1001 },
1002 ));
1003 }
1004
1005 commands.entity(entity).insert(LastSentSelectedHotbarSlot {
1006 slot: inventory.selected_hotbar_slot,
1007 });
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use azalea_registry::Item;
1014
1015 use super::*;
1016
1017 #[test]
1018 fn test_simulate_shift_click_in_crafting_table() {
1019 let spruce_planks = ItemStack::new(Item::SprucePlanks, 4);
1020
1021 let mut inventory = Inventory {
1022 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
1023 id: 1,
1024 container_menu: Some(Menu::Crafting {
1025 result: spruce_planks.clone(),
1026 grid: SlotList::default(),
1028 player: SlotList::default(),
1029 }),
1030 container_menu_title: None,
1031 carried: ItemStack::Empty,
1032 state_id: 0,
1033 quick_craft_status: QuickCraftStatusKind::Start,
1034 quick_craft_kind: QuickCraftKind::Middle,
1035 quick_craft_slots: HashSet::new(),
1036 selected_hotbar_slot: 0,
1037 };
1038
1039 inventory.simulate_click(
1040 &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }),
1041 &PlayerAbilities::default(),
1042 );
1043
1044 let new_slots = inventory.menu().slots();
1045 assert_eq!(&new_slots[0], &ItemStack::Empty);
1046 assert_eq!(
1047 &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()],
1048 &spruce_planks
1049 );
1050 }
1051}