1use std::{
2 cmp,
3 collections::{HashMap, HashSet},
4};
5
6use azalea_chat::FormattedText;
7pub use azalea_inventory::*;
8use azalea_inventory::{
9 item::MaxStackSizeExt,
10 operations::{
11 ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
12 QuickCraftStatusKind, QuickMoveClick, ThrowClick,
13 },
14};
15use azalea_protocol::packets::game::{
16 s_container_click::{HashedStack, ServerboundContainerClick},
17 s_container_close::ServerboundContainerClose,
18 s_set_carried_item::ServerboundSetCarriedItem,
19};
20use azalea_registry::MenuKind;
21use bevy_app::{App, Plugin, Update};
22use bevy_ecs::prelude::*;
23use tracing::{error, warn};
24
25use crate::{
26 Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
27};
28
29pub struct InventoryPlugin;
30impl Plugin for InventoryPlugin {
31 fn build(&self, app: &mut App) {
32 app.add_event::<ClientSideCloseContainerEvent>()
33 .add_event::<MenuOpenedEvent>()
34 .add_event::<CloseContainerEvent>()
35 .add_event::<ContainerClickEvent>()
36 .add_event::<SetContainerContentEvent>()
37 .add_event::<SetSelectedHotbarSlotEvent>()
38 .add_systems(
39 Update,
40 (
41 handle_set_selected_hotbar_slot_event,
42 handle_menu_opened_event,
43 handle_set_container_content_event,
44 handle_container_click_event,
45 handle_container_close_event,
46 handle_client_side_close_container_event,
47 )
48 .chain()
49 .in_set(InventorySet)
50 .before(perform_respawn),
51 );
52 }
53}
54
55#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
56pub struct InventorySet;
57
58impl Client {
59 pub fn menu(&self) -> Menu {
62 let mut ecs = self.ecs.lock();
63 let inventory = self.query::<&Inventory>(&mut ecs);
64 inventory.menu().clone()
65 }
66
67 pub fn selected_hotbar_slot(&self) -> u8 {
75 let mut ecs = self.ecs.lock();
76 let inventory = self.query::<&Inventory>(&mut ecs);
77 inventory.selected_hotbar_slot
78 }
79
80 pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) {
85 assert!(
86 new_hotbar_slot_index < 9,
87 "Hotbar slot index must be in the range 0..=8"
88 );
89
90 let mut ecs = self.ecs.lock();
91 ecs.send_event(SetSelectedHotbarSlotEvent {
92 entity: self.entity,
93 slot: new_hotbar_slot_index,
94 });
95 }
96}
97
98#[derive(Component, Debug, Clone)]
100pub struct Inventory {
101 pub inventory_menu: azalea_inventory::Menu,
106
107 pub id: i32,
112 pub container_menu: Option<azalea_inventory::Menu>,
115 pub container_menu_title: Option<FormattedText>,
118 pub carried: ItemStack,
124 pub state_id: u32,
128
129 pub quick_craft_status: QuickCraftStatusKind,
130 pub quick_craft_kind: QuickCraftKind,
131 pub quick_craft_slots: HashSet<u16>,
134
135 pub selected_hotbar_slot: u8,
141}
142
143impl Inventory {
144 pub fn menu(&self) -> &azalea_inventory::Menu {
150 match &self.container_menu {
151 Some(menu) => menu,
152 _ => &self.inventory_menu,
153 }
154 }
155
156 pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
162 match &mut self.container_menu {
163 Some(menu) => menu,
164 _ => &mut self.inventory_menu,
165 }
166 }
167
168 pub fn simulate_click(
170 &mut self,
171 operation: &ClickOperation,
172 player_abilities: &PlayerAbilities,
173 ) {
174 if let ClickOperation::QuickCraft(quick_craft) = operation {
175 let last_quick_craft_status_tmp = self.quick_craft_status.clone();
176 self.quick_craft_status = last_quick_craft_status_tmp.clone();
177 let last_quick_craft_status = last_quick_craft_status_tmp;
178
179 if self.carried.is_empty() {
181 return self.reset_quick_craft();
182 }
183 if (last_quick_craft_status == QuickCraftStatusKind::Start
186 || last_quick_craft_status == QuickCraftStatusKind::End
187 || self.quick_craft_status != QuickCraftStatusKind::End)
188 && (self.quick_craft_status != last_quick_craft_status)
189 {
190 return self.reset_quick_craft();
191 }
192 if self.quick_craft_status == QuickCraftStatusKind::Start {
193 self.quick_craft_kind = quick_craft.kind.clone();
194 if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
195 {
196 self.quick_craft_status = QuickCraftStatusKind::Add;
197 self.quick_craft_slots.clear();
198 } else {
199 self.reset_quick_craft();
200 }
201 return;
202 }
203 if let QuickCraftStatus::Add { slot } = quick_craft.status {
204 let slot_item = self.menu().slot(slot as usize);
205 if let Some(slot_item) = slot_item
206 && let ItemStack::Present(carried) = &self.carried
207 {
208 if can_item_quick_replace(slot_item, &self.carried, true)
212 && (self.quick_craft_kind == QuickCraftKind::Right
213 || carried.count as usize > self.quick_craft_slots.len())
214 {
215 self.quick_craft_slots.insert(slot);
216 }
217 }
218 return;
219 }
220 if self.quick_craft_status == QuickCraftStatusKind::End {
221 if !self.quick_craft_slots.is_empty() {
222 if self.quick_craft_slots.len() == 1 {
223 let slot = *self.quick_craft_slots.iter().next().unwrap();
226 self.reset_quick_craft();
227 self.simulate_click(
228 &match self.quick_craft_kind {
229 QuickCraftKind::Left => {
230 PickupClick::Left { slot: Some(slot) }.into()
231 }
232 QuickCraftKind::Right => {
233 PickupClick::Left { slot: Some(slot) }.into()
234 }
235 QuickCraftKind::Middle => {
236 return;
238 }
239 },
240 player_abilities,
241 );
242 return;
243 }
244
245 let ItemStack::Present(mut carried) = self.carried.clone() else {
246 return self.reset_quick_craft();
248 };
249
250 let mut carried_count = carried.count;
251 let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
252
253 loop {
254 let mut slot: &ItemStack;
255 let mut slot_index: u16;
256 let mut item_stack: &ItemStack;
257
258 loop {
259 let Some(&next_slot) = quick_craft_slots_iter.next() else {
260 carried.count = carried_count;
261 self.carried = ItemStack::Present(carried);
262 return self.reset_quick_craft();
263 };
264
265 slot = self.menu().slot(next_slot as usize).unwrap();
266 slot_index = next_slot;
267 item_stack = &self.carried;
268
269 if slot.is_present()
270 && can_item_quick_replace(slot, item_stack, true)
271 && (
274 self.quick_craft_kind == QuickCraftKind::Middle
275 || item_stack.count() >= self.quick_craft_slots.len() as i32
276 )
277 {
278 break;
279 }
280 }
281
282 let ItemStack::Present(slot) = slot else {
284 unreachable!("the loop above requires the slot to be present to break")
285 };
286
287 let mut new_carried = carried.clone();
289 let slot_item_count = slot.count;
290 get_quick_craft_slot_count(
291 &self.quick_craft_slots,
292 &self.quick_craft_kind,
293 &mut new_carried,
294 slot_item_count,
295 );
296 let max_stack_size = i32::min(
297 new_carried.kind.max_stack_size(),
298 i32::min(
299 new_carried.kind.max_stack_size(),
300 slot.kind.max_stack_size(),
301 ),
302 );
303 if new_carried.count > max_stack_size {
304 new_carried.count = max_stack_size;
305 }
306
307 carried_count -= new_carried.count - slot_item_count;
308 let menu = match &mut self.container_menu {
311 Some(menu) => menu,
312 _ => &mut self.inventory_menu,
313 };
314 *menu.slot_mut(slot_index as usize).unwrap() =
315 ItemStack::Present(new_carried);
316 }
317 }
318 } else {
319 return self.reset_quick_craft();
320 }
321 }
322 if self.quick_craft_status != QuickCraftStatusKind::Start {
325 return self.reset_quick_craft();
326 }
327
328 match operation {
329 ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
331 if self.carried.is_present() {
332 self.carried = ItemStack::Empty;
339 }
340 }
341 ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
342 if self.carried.is_present() {
343 let _item = self.carried.split(1);
344 }
346 }
347 &ClickOperation::Pickup(
348 ref pickup @ (PickupClick::Left { slot: Some(slot) }
350 | PickupClick::Right { slot: Some(slot) }),
351 ) => {
352 let slot = slot as usize;
353 let Some(slot_item) = self.menu().slot(slot) else {
354 return;
355 };
356
357 if self.try_item_click_behavior_override(operation, slot) {
358 return;
359 }
360
361 let is_left_click = matches!(pickup, PickupClick::Left { .. });
362
363 match slot_item {
364 ItemStack::Empty => {
365 if self.carried.is_present() {
366 let place_count = if is_left_click {
367 self.carried.count()
368 } else {
369 1
370 };
371 self.carried =
372 self.safe_insert(slot, self.carried.clone(), place_count);
373 }
374 }
375 ItemStack::Present(_) => {
376 if !self.menu().may_pickup(slot) {
377 return;
378 }
379 if let ItemStack::Present(carried) = self.carried.clone() {
380 let slot_is_same_item_as_carried = slot_item
381 .as_present()
382 .is_some_and(|s| carried.is_same_item_and_components(s));
383
384 if self.menu().may_place(slot, &carried) {
385 if slot_is_same_item_as_carried {
386 let place_count = if is_left_click { carried.count } else { 1 };
387 self.carried =
388 self.safe_insert(slot, self.carried.clone(), place_count);
389 } else if carried.count
390 <= self
391 .menu()
392 .max_stack_size(slot)
393 .min(carried.kind.max_stack_size())
394 {
395 self.carried = slot_item.clone();
397 let slot_item = self.menu_mut().slot_mut(slot).unwrap();
398 *slot_item = carried.into();
399 }
400 } else if slot_is_same_item_as_carried
401 && let Some(removed) = self.try_remove(
402 slot,
403 slot_item.count(),
404 carried.kind.max_stack_size() - carried.count,
405 )
406 {
407 self.carried.as_present_mut().unwrap().count += removed.count();
408 }
410 } else {
411 let pickup_count = if is_left_click {
412 slot_item.count()
413 } else {
414 (slot_item.count() + 1) / 2
415 };
416 if let Some(new_slot_item) =
417 self.try_remove(slot, pickup_count, i32::MAX)
418 {
419 self.carried = new_slot_item;
420 }
422 }
423 }
424 }
425 }
426 &ClickOperation::QuickMove(
427 QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
428 ) => {
429 let slot = slot as usize;
432 loop {
433 let new_slot_item = self.menu_mut().quick_move_stack(slot);
434 let slot_item = self.menu().slot(slot).unwrap();
435 if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() {
436 break;
437 }
438 }
439 }
440 ClickOperation::Swap(s) => {
441 let source_slot_index = s.source_slot as usize;
442 let target_slot_index = s.target_slot as usize;
443
444 let Some(source_slot) = self.menu().slot(source_slot_index) else {
445 return;
446 };
447 let Some(target_slot) = self.menu().slot(target_slot_index) else {
448 return;
449 };
450 if source_slot.is_empty() && target_slot.is_empty() {
451 return;
452 }
453
454 if target_slot.is_empty() {
455 if self.menu().may_pickup(source_slot_index) {
456 let source_slot = source_slot.clone();
457 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
458 *target_slot = source_slot;
459 }
460 } else if source_slot.is_empty() {
461 let target_item = target_slot
462 .as_present()
463 .expect("target slot was already checked to not be empty");
464 if self.menu().may_place(source_slot_index, target_item) {
465 let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
467
468 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
469 let new_source_slot =
470 target_slot.split(source_max_stack_size.try_into().unwrap());
471 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
472 }
473 } else if self.menu().may_pickup(source_slot_index) {
474 let ItemStack::Present(target_item) = target_slot else {
475 unreachable!("target slot is not empty but is not present");
476 };
477 if self.menu().may_place(source_slot_index, target_item) {
478 let source_max_stack = self.menu().max_stack_size(source_slot_index);
479 if target_slot.count() > source_max_stack {
480 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
483 let new_source_slot =
484 target_slot.split(source_max_stack.try_into().unwrap());
485 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
486 } else {
490 let new_target_slot = source_slot.clone();
492 let new_source_slot = target_slot.clone();
493
494 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
495 *target_slot = new_target_slot;
496
497 let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
498 *source_slot = new_source_slot;
499 }
500 }
501 }
502 }
503 ClickOperation::Clone(CloneClick { slot }) => {
504 if !player_abilities.instant_break || self.carried.is_present() {
505 return;
506 }
507 let Some(source_slot) = self.menu().slot(*slot as usize) else {
508 return;
509 };
510 let ItemStack::Present(source_item) = source_slot else {
511 return;
512 };
513 let mut new_carried = source_item.clone();
514 new_carried.count = new_carried.kind.max_stack_size();
515 self.carried = ItemStack::Present(new_carried);
516 }
517 ClickOperation::Throw(c) => {
518 if self.carried.is_present() {
519 return;
520 }
521
522 let (ThrowClick::Single { slot: slot_index }
523 | ThrowClick::All { slot: slot_index }) = c;
524 let slot_index = *slot_index as usize;
525
526 let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
527 return;
528 };
529 let ItemStack::Present(slot_item) = slot else {
530 return;
531 };
532
533 let dropping_count = match c {
534 ThrowClick::Single { .. } => 1,
535 ThrowClick::All { .. } => slot_item.count,
536 };
537
538 let _dropping = slot_item.split(dropping_count as u32);
539 }
541 ClickOperation::PickupAll(PickupAllClick {
542 slot: source_slot_index,
543 reversed,
544 }) => {
545 let source_slot_index = *source_slot_index as usize;
546
547 let source_slot = self.menu().slot(source_slot_index).unwrap();
548 let target_slot = self.carried.clone();
549
550 if target_slot.is_empty()
551 || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
552 {
553 return;
554 }
555
556 let ItemStack::Present(target_slot_item) = &target_slot else {
557 unreachable!("target slot is not empty but is not present");
558 };
559
560 for round in 0..2 {
561 let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
562 Box::new((0..self.menu().len()).rev())
563 } else {
564 Box::new(0..self.menu().len())
565 };
566
567 for i in iterator {
568 if target_slot_item.count < target_slot_item.kind.max_stack_size() {
569 let checking_slot = self.menu().slot(i).unwrap();
570 if let ItemStack::Present(checking_item) = checking_slot
571 && can_item_quick_replace(checking_slot, &target_slot, true)
572 && self.menu().may_pickup(i)
573 && (round != 0
574 || checking_item.count != checking_item.kind.max_stack_size())
575 {
576 let checking_slot = self.menu_mut().slot_mut(i).unwrap();
578
579 let taken_item = checking_slot.split(checking_slot.count() as u32);
580
581 let target_slot = &mut self.carried;
583 let ItemStack::Present(target_slot_item) = target_slot else {
584 unreachable!("target slot is not empty but is not present");
585 };
586 target_slot_item.count += taken_item.count();
587 }
588 }
589 }
590 }
591 }
592 _ => {}
593 }
594 }
595
596 fn reset_quick_craft(&mut self) {
597 self.quick_craft_status = QuickCraftStatusKind::Start;
598 self.quick_craft_slots.clear();
599 }
600
601 pub fn held_item(&self) -> ItemStack {
604 let inventory = &self.inventory_menu;
605 let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
606 hotbar_items[self.selected_hotbar_slot as usize].clone()
607 }
608
609 fn try_item_click_behavior_override(
611 &self,
612 _operation: &ClickOperation,
613 _slot_item_index: usize,
614 ) -> bool {
615 false
616 }
617
618 fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack {
619 let Some(slot_item) = self.menu_mut().slot_mut(slot) else {
620 return src_item;
621 };
622 let ItemStack::Present(mut src_item) = src_item else {
623 return src_item;
624 };
625
626 let take_count = cmp::min(
627 cmp::min(take_count, src_item.count),
628 src_item.kind.max_stack_size() - slot_item.count(),
629 );
630 if take_count <= 0 {
631 return src_item.into();
632 }
633 let take_count = take_count as u32;
634
635 if slot_item.is_empty() {
636 *slot_item = src_item.split(take_count).into();
637 } else if let ItemStack::Present(slot_item) = slot_item
638 && slot_item.is_same_item_and_components(&src_item)
639 {
640 src_item.count -= take_count as i32;
641 slot_item.count += take_count as i32;
642 }
643
644 src_item.into()
645 }
646
647 fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> {
648 if !self.menu().may_pickup(slot) {
649 return None;
650 }
651 let mut slot_item = self.menu().slot(slot)?.clone();
652 if !self.menu().allow_modification(slot) && limit < slot_item.count() {
653 return None;
654 }
655
656 let count = count.min(limit);
657 if count <= 0 {
658 return None;
659 }
660 let removed = slot_item.split(count as u32);
662
663 if removed.is_present() && slot_item.is_empty() {
664 *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty;
665 }
666
667 Some(removed)
668 }
669}
670
671fn can_item_quick_replace(
672 target_slot: &ItemStack,
673 item: &ItemStack,
674 ignore_item_count: bool,
675) -> bool {
676 let ItemStack::Present(target_slot) = target_slot else {
677 return false;
678 };
679 let ItemStack::Present(item) = item else {
680 return false;
683 };
684
685 if !item.is_same_item_and_components(target_slot) {
686 return false;
687 }
688 let count = target_slot.count as u16
689 + if ignore_item_count {
690 0
691 } else {
692 item.count as u16
693 };
694 count <= item.kind.max_stack_size() as u16
695}
696
697fn get_quick_craft_slot_count(
698 quick_craft_slots: &HashSet<u16>,
699 quick_craft_kind: &QuickCraftKind,
700 item: &mut ItemStackData,
701 slot_item_count: i32,
702) {
703 item.count = match quick_craft_kind {
704 QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
705 QuickCraftKind::Right => 1,
706 QuickCraftKind::Middle => item.kind.max_stack_size(),
707 };
708 item.count += slot_item_count;
709}
710
711impl Default for Inventory {
712 fn default() -> Self {
713 Inventory {
714 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
715 id: 0,
716 container_menu: None,
717 container_menu_title: None,
718 carried: ItemStack::Empty,
719 state_id: 0,
720 quick_craft_status: QuickCraftStatusKind::Start,
721 quick_craft_kind: QuickCraftKind::Middle,
722 quick_craft_slots: HashSet::new(),
723 selected_hotbar_slot: 0,
724 }
725 }
726}
727
728#[derive(Event, Debug)]
731pub struct MenuOpenedEvent {
732 pub entity: Entity,
733 pub window_id: i32,
734 pub menu_type: MenuKind,
735 pub title: FormattedText,
736}
737fn handle_menu_opened_event(
738 mut events: EventReader<MenuOpenedEvent>,
739 mut query: Query<&mut Inventory>,
740) {
741 for event in events.read() {
742 let mut inventory = query.get_mut(event.entity).unwrap();
743 inventory.id = event.window_id;
744 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
745 inventory.container_menu_title = Some(event.title.clone());
746 }
747}
748
749#[derive(Event)]
754pub struct CloseContainerEvent {
755 pub entity: Entity,
756 pub id: i32,
759}
760fn handle_container_close_event(
761 query: Query<(Entity, &Inventory)>,
762 mut events: EventReader<CloseContainerEvent>,
763 mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
764 mut commands: Commands,
765) {
766 for event in events.read() {
767 let (entity, inventory) = query.get(event.entity).unwrap();
768 if event.id != inventory.id {
769 warn!(
770 "Tried to close container with ID {}, but the current container ID is {}",
771 event.id, inventory.id
772 );
773 continue;
774 }
775
776 commands.trigger(SendPacketEvent::new(
777 entity,
778 ServerboundContainerClose {
779 container_id: inventory.id,
780 },
781 ));
782 client_side_events.write(ClientSideCloseContainerEvent {
783 entity: event.entity,
784 });
785 }
786}
787
788#[derive(Event)]
792pub struct ClientSideCloseContainerEvent {
793 pub entity: Entity,
794}
795pub fn handle_client_side_close_container_event(
796 mut events: EventReader<ClientSideCloseContainerEvent>,
797 mut query: Query<&mut Inventory>,
798) {
799 for event in events.read() {
800 let mut inventory = query.get_mut(event.entity).unwrap();
801
802 if let Some(inventory_menu) = inventory.container_menu.take() {
804 let new_inventory =
821 inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
822 let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
823 *inventory.inventory_menu.as_player_mut().inventory = new_inventory;
824 }
825
826 inventory.id = 0;
827 inventory.container_menu_title = None;
828 }
829}
830
831#[derive(Event, Debug)]
832pub struct ContainerClickEvent {
833 pub entity: Entity,
834 pub window_id: i32,
835 pub operation: ClickOperation,
836}
837pub fn handle_container_click_event(
838 mut query: Query<(Entity, &mut Inventory, Option<&PlayerAbilities>)>,
839 mut events: EventReader<ContainerClickEvent>,
840 mut commands: Commands,
841) {
842 for event in events.read() {
843 let (entity, mut inventory, player_abilities) = query.get_mut(event.entity).unwrap();
844 if inventory.id != event.window_id {
845 error!(
846 "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
847 event.window_id, inventory.id
848 );
849 continue;
850 }
851
852 let old_slots = inventory.menu().slots();
853 inventory.simulate_click(
854 &event.operation,
855 player_abilities.unwrap_or(&PlayerAbilities::default()),
856 );
857 let new_slots = inventory.menu().slots();
858
859 let mut changed_slots: HashMap<u16, HashedStack> = HashMap::new();
862 for (slot_index, old_slot) in old_slots.iter().enumerate() {
863 let new_slot = &new_slots[slot_index];
864 if old_slot != new_slot {
865 changed_slots.insert(slot_index as u16, HashedStack::from(new_slot));
866 }
867 }
868
869 commands.trigger(SendPacketEvent::new(
870 entity,
871 ServerboundContainerClick {
872 container_id: event.window_id,
873 state_id: inventory.state_id,
874 slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
875 button_num: event.operation.button_num(),
876 click_type: event.operation.click_type(),
877 changed_slots,
878 carried_item: (&inventory.carried).into(),
879 },
880 ));
881 }
882}
883
884#[derive(Event)]
887pub struct SetContainerContentEvent {
888 pub entity: Entity,
889 pub slots: Vec<ItemStack>,
890 pub container_id: i32,
891}
892fn handle_set_container_content_event(
893 mut events: EventReader<SetContainerContentEvent>,
894 mut query: Query<&mut Inventory>,
895) {
896 for event in events.read() {
897 let mut inventory = query.get_mut(event.entity).unwrap();
898
899 if event.container_id != inventory.id {
900 warn!(
901 "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
902 event.container_id, inventory.id
903 );
904 continue;
905 }
906
907 let menu = inventory.menu_mut();
908 for (i, slot) in event.slots.iter().enumerate() {
909 if let Some(slot_mut) = menu.slot_mut(i) {
910 *slot_mut = slot.clone();
911 }
912 }
913 }
914}
915
916#[derive(Event)]
917pub struct SetSelectedHotbarSlotEvent {
918 pub entity: Entity,
919 pub slot: u8,
921}
922fn handle_set_selected_hotbar_slot_event(
923 mut events: EventReader<SetSelectedHotbarSlotEvent>,
924 mut commands: Commands,
925 mut query: Query<&mut Inventory>,
926) {
927 for event in events.read() {
928 let mut inventory = query.get_mut(event.entity).unwrap();
929
930 if inventory.selected_hotbar_slot == event.slot {
932 continue;
933 }
934
935 inventory.selected_hotbar_slot = event.slot;
936 commands.trigger(SendPacketEvent::new(
937 event.entity,
938 ServerboundSetCarriedItem {
939 slot: event.slot as u16,
940 },
941 ));
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use azalea_registry::Item;
948
949 use super::*;
950
951 #[test]
952 fn test_simulate_shift_click_in_crafting_table() {
953 let spruce_planks = ItemStack::Present(ItemStackData {
954 count: 4,
955 kind: Item::SprucePlanks,
956 components: Default::default(),
957 });
958
959 let mut inventory = Inventory {
960 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
961 id: 1,
962 container_menu: Some(Menu::Crafting {
963 result: spruce_planks.clone(),
964 grid: SlotList::default(),
966 player: SlotList::default(),
967 }),
968 container_menu_title: None,
969 carried: ItemStack::Empty,
970 state_id: 0,
971 quick_craft_status: QuickCraftStatusKind::Start,
972 quick_craft_kind: QuickCraftKind::Middle,
973 quick_craft_slots: HashSet::new(),
974 selected_hotbar_slot: 0,
975 };
976
977 inventory.simulate_click(
978 &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }),
979 &PlayerAbilities::default(),
980 );
981
982 let new_slots = inventory.menu().slots();
983 assert_eq!(&new_slots[0], &ItemStack::Empty);
984 assert_eq!(
985 &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()],
986 &spruce_planks
987 );
988 }
989}