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};
22use bevy_ecs::prelude::*;
23use indexmap::IndexMap;
24use tracing::{error, warn};
25
26use crate::{Client, packet::game::SendGamePacketEvent};
27
28pub struct InventoryPlugin;
29impl Plugin for InventoryPlugin {
30 fn build(&self, app: &mut App) {
31 app.add_systems(
32 GameTick,
33 ensure_has_sent_carried_item.after(super::mining::handle_mining_queued),
34 )
35 .add_observer(handle_client_side_close_container_trigger)
36 .add_observer(handle_menu_opened_trigger)
37 .add_observer(handle_container_close_event)
38 .add_observer(handle_set_container_content_trigger)
39 .add_observer(handle_container_click_event)
40 .add_observer(handle_set_selected_hotbar_slot_event);
43 }
44}
45
46#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
47pub struct InventorySystems;
48
49impl Client {
50 pub fn menu(&self) -> Menu {
53 self.query_self::<&Inventory, _>(|inv| inv.menu().clone())
54 }
55
56 pub fn selected_hotbar_slot(&self) -> u8 {
64 self.query_self::<&Inventory, _>(|inv| inv.selected_hotbar_slot)
65 }
66
67 pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) {
76 assert!(
77 new_hotbar_slot_index < 9,
78 "Hotbar slot index must be in the range 0..=8"
79 );
80
81 let mut ecs = self.ecs.lock();
82 ecs.trigger(SetSelectedHotbarSlotEvent {
83 entity: self.entity,
84 slot: new_hotbar_slot_index,
85 });
86 }
87}
88
89#[derive(Component, Debug, Clone)]
91pub struct Inventory {
92 pub inventory_menu: azalea_inventory::Menu,
97
98 pub id: i32,
104 pub container_menu: Option<azalea_inventory::Menu>,
107 pub container_menu_title: Option<FormattedText>,
111 pub carried: ItemStack,
117 pub state_id: u32,
122
123 pub quick_craft_status: QuickCraftStatusKind,
124 pub quick_craft_kind: QuickCraftKind,
125 pub quick_craft_slots: HashSet<u16>,
128
129 pub selected_hotbar_slot: u8,
135}
136
137impl Inventory {
138 pub fn menu(&self) -> &azalea_inventory::Menu {
145 match &self.container_menu {
146 Some(menu) => menu,
147 _ => &self.inventory_menu,
148 }
149 }
150
151 pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
158 match &mut self.container_menu {
159 Some(menu) => menu,
160 _ => &mut self.inventory_menu,
161 }
162 }
163
164 pub fn simulate_click(
166 &mut self,
167 operation: &ClickOperation,
168 player_abilities: &PlayerAbilities,
169 ) {
170 if let ClickOperation::QuickCraft(quick_craft) = operation {
171 let last_quick_craft_status_tmp = self.quick_craft_status.clone();
172 self.quick_craft_status = last_quick_craft_status_tmp.clone();
173 let last_quick_craft_status = last_quick_craft_status_tmp;
174
175 if self.carried.is_empty() {
177 return self.reset_quick_craft();
178 }
179 if (last_quick_craft_status == QuickCraftStatusKind::Start
182 || last_quick_craft_status == QuickCraftStatusKind::End
183 || self.quick_craft_status != QuickCraftStatusKind::End)
184 && (self.quick_craft_status != last_quick_craft_status)
185 {
186 return self.reset_quick_craft();
187 }
188 if self.quick_craft_status == QuickCraftStatusKind::Start {
189 self.quick_craft_kind = quick_craft.kind.clone();
190 if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
191 {
192 self.quick_craft_status = QuickCraftStatusKind::Add;
193 self.quick_craft_slots.clear();
194 } else {
195 self.reset_quick_craft();
196 }
197 return;
198 }
199 if let QuickCraftStatus::Add { slot } = quick_craft.status {
200 let slot_item = self.menu().slot(slot as usize);
201 if let Some(slot_item) = slot_item
202 && let ItemStack::Present(carried) = &self.carried
203 {
204 if can_item_quick_replace(slot_item, &self.carried, true)
208 && (self.quick_craft_kind == QuickCraftKind::Right
209 || carried.count as usize > self.quick_craft_slots.len())
210 {
211 self.quick_craft_slots.insert(slot);
212 }
213 }
214 return;
215 }
216 if self.quick_craft_status == QuickCraftStatusKind::End {
217 if !self.quick_craft_slots.is_empty() {
218 if self.quick_craft_slots.len() == 1 {
219 let slot = *self.quick_craft_slots.iter().next().unwrap();
222 self.reset_quick_craft();
223 self.simulate_click(
224 &match self.quick_craft_kind {
225 QuickCraftKind::Left => {
226 PickupClick::Left { slot: Some(slot) }.into()
227 }
228 QuickCraftKind::Right => {
229 PickupClick::Left { slot: Some(slot) }.into()
230 }
231 QuickCraftKind::Middle => {
232 return;
234 }
235 },
236 player_abilities,
237 );
238 return;
239 }
240
241 let ItemStack::Present(mut carried) = self.carried.clone() else {
242 return self.reset_quick_craft();
244 };
245
246 let mut carried_count = carried.count;
247 let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
248
249 loop {
250 let mut slot: &ItemStack;
251 let mut slot_index: u16;
252 let mut item_stack: &ItemStack;
253
254 loop {
255 let Some(&next_slot) = quick_craft_slots_iter.next() else {
256 carried.count = carried_count;
257 self.carried = ItemStack::Present(carried);
258 return self.reset_quick_craft();
259 };
260
261 slot = self.menu().slot(next_slot as usize).unwrap();
262 slot_index = next_slot;
263 item_stack = &self.carried;
264
265 if slot.is_present()
266 && can_item_quick_replace(slot, item_stack, true)
267 && (
270 self.quick_craft_kind == QuickCraftKind::Middle
271 || item_stack.count() >= self.quick_craft_slots.len() as i32
272 )
273 {
274 break;
275 }
276 }
277
278 let ItemStack::Present(slot) = slot else {
280 unreachable!("the loop above requires the slot to be present to break")
281 };
282
283 let mut new_carried = carried.clone();
285 let slot_item_count = slot.count;
286 get_quick_craft_slot_count(
287 &self.quick_craft_slots,
288 &self.quick_craft_kind,
289 &mut new_carried,
290 slot_item_count,
291 );
292 let max_stack_size = i32::min(
293 new_carried.kind.max_stack_size(),
294 i32::min(
295 new_carried.kind.max_stack_size(),
296 slot.kind.max_stack_size(),
297 ),
298 );
299 if new_carried.count > max_stack_size {
300 new_carried.count = max_stack_size;
301 }
302
303 carried_count -= new_carried.count - slot_item_count;
304 let menu = match &mut self.container_menu {
307 Some(menu) => menu,
308 _ => &mut self.inventory_menu,
309 };
310 *menu.slot_mut(slot_index as usize).unwrap() =
311 ItemStack::Present(new_carried);
312 }
313 }
314 } else {
315 return self.reset_quick_craft();
316 }
317 }
318 if self.quick_craft_status != QuickCraftStatusKind::Start {
321 return self.reset_quick_craft();
322 }
323
324 match operation {
325 ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
327 if self.carried.is_present() {
328 self.carried = ItemStack::Empty;
335 }
336 }
337 ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
338 if self.carried.is_present() {
339 let _item = self.carried.split(1);
340 }
342 }
343 &ClickOperation::Pickup(
344 ref pickup @ (PickupClick::Left { slot: Some(slot) }
346 | PickupClick::Right { slot: Some(slot) }),
347 ) => {
348 let slot = slot as usize;
349 let Some(slot_item) = self.menu().slot(slot) else {
350 return;
351 };
352
353 if self.try_item_click_behavior_override(operation, slot) {
354 return;
355 }
356
357 let is_left_click = matches!(pickup, PickupClick::Left { .. });
358
359 match slot_item {
360 ItemStack::Empty => {
361 if self.carried.is_present() {
362 let place_count = if is_left_click {
363 self.carried.count()
364 } else {
365 1
366 };
367 self.carried =
368 self.safe_insert(slot, self.carried.clone(), place_count);
369 }
370 }
371 ItemStack::Present(_) => {
372 if !self.menu().may_pickup(slot) {
373 return;
374 }
375 if let ItemStack::Present(carried) = self.carried.clone() {
376 let slot_is_same_item_as_carried = slot_item
377 .as_present()
378 .is_some_and(|s| carried.is_same_item_and_components(s));
379
380 if self.menu().may_place(slot, &carried) {
381 if slot_is_same_item_as_carried {
382 let place_count = if is_left_click { carried.count } else { 1 };
383 self.carried =
384 self.safe_insert(slot, self.carried.clone(), place_count);
385 } else if carried.count
386 <= self
387 .menu()
388 .max_stack_size(slot)
389 .min(carried.kind.max_stack_size())
390 {
391 self.carried = slot_item.clone();
393 let slot_item = self.menu_mut().slot_mut(slot).unwrap();
394 *slot_item = carried.into();
395 }
396 } else if slot_is_same_item_as_carried
397 && let Some(removed) = self.try_remove(
398 slot,
399 slot_item.count(),
400 carried.kind.max_stack_size() - carried.count,
401 )
402 {
403 self.carried.as_present_mut().unwrap().count += removed.count();
404 }
406 } else {
407 let pickup_count = if is_left_click {
408 slot_item.count()
409 } else {
410 (slot_item.count() + 1) / 2
411 };
412 if let Some(new_slot_item) =
413 self.try_remove(slot, pickup_count, i32::MAX)
414 {
415 self.carried = new_slot_item;
416 }
418 }
419 }
420 }
421 }
422 &ClickOperation::QuickMove(
423 QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
424 ) => {
425 let slot = slot as usize;
428 loop {
429 let new_slot_item = self.menu_mut().quick_move_stack(slot);
430 let slot_item = self.menu().slot(slot).unwrap();
431 if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() {
432 break;
433 }
434 }
435 }
436 ClickOperation::Swap(s) => {
437 let source_slot_index = s.source_slot as usize;
438 let target_slot_index = s.target_slot as usize;
439
440 let Some(source_slot) = self.menu().slot(source_slot_index) else {
441 return;
442 };
443 let Some(target_slot) = self.menu().slot(target_slot_index) else {
444 return;
445 };
446 if source_slot.is_empty() && target_slot.is_empty() {
447 return;
448 }
449
450 if target_slot.is_empty() {
451 if self.menu().may_pickup(source_slot_index) {
452 let source_slot = source_slot.clone();
453 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
454 *target_slot = source_slot;
455 }
456 } else if source_slot.is_empty() {
457 let target_item = target_slot
458 .as_present()
459 .expect("target slot was already checked to not be empty");
460 if self.menu().may_place(source_slot_index, target_item) {
461 let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
463
464 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
465 let new_source_slot =
466 target_slot.split(source_max_stack_size.try_into().unwrap());
467 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
468 }
469 } else if self.menu().may_pickup(source_slot_index) {
470 let ItemStack::Present(target_item) = target_slot else {
471 unreachable!("target slot is not empty but is not present");
472 };
473 if self.menu().may_place(source_slot_index, target_item) {
474 let source_max_stack = self.menu().max_stack_size(source_slot_index);
475 if target_slot.count() > source_max_stack {
476 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
479 let new_source_slot =
480 target_slot.split(source_max_stack.try_into().unwrap());
481 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
482 } else {
486 let new_target_slot = source_slot.clone();
488 let new_source_slot = target_slot.clone();
489
490 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
491 *target_slot = new_target_slot;
492
493 let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
494 *source_slot = new_source_slot;
495 }
496 }
497 }
498 }
499 ClickOperation::Clone(CloneClick { slot }) => {
500 if !player_abilities.instant_break || self.carried.is_present() {
501 return;
502 }
503 let Some(source_slot) = self.menu().slot(*slot as usize) else {
504 return;
505 };
506 let ItemStack::Present(source_item) = source_slot else {
507 return;
508 };
509 let mut new_carried = source_item.clone();
510 new_carried.count = new_carried.kind.max_stack_size();
511 self.carried = ItemStack::Present(new_carried);
512 }
513 ClickOperation::Throw(c) => {
514 if self.carried.is_present() {
515 return;
516 }
517
518 let (ThrowClick::Single { slot: slot_index }
519 | ThrowClick::All { slot: slot_index }) = c;
520 let slot_index = *slot_index as usize;
521
522 let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
523 return;
524 };
525 let ItemStack::Present(slot_item) = slot else {
526 return;
527 };
528
529 let dropping_count = match c {
530 ThrowClick::Single { .. } => 1,
531 ThrowClick::All { .. } => slot_item.count,
532 };
533
534 let _dropping = slot_item.split(dropping_count as u32);
535 }
537 ClickOperation::PickupAll(PickupAllClick {
538 slot: source_slot_index,
539 reversed,
540 }) => {
541 let source_slot_index = *source_slot_index as usize;
542
543 let source_slot = self.menu().slot(source_slot_index).unwrap();
544 let target_slot = self.carried.clone();
545
546 if target_slot.is_empty()
547 || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
548 {
549 return;
550 }
551
552 let ItemStack::Present(target_slot_item) = &target_slot else {
553 unreachable!("target slot is not empty but is not present");
554 };
555
556 for round in 0..2 {
557 let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
558 Box::new((0..self.menu().len()).rev())
559 } else {
560 Box::new(0..self.menu().len())
561 };
562
563 for i in iterator {
564 if target_slot_item.count < target_slot_item.kind.max_stack_size() {
565 let checking_slot = self.menu().slot(i).unwrap();
566 if let ItemStack::Present(checking_item) = checking_slot
567 && can_item_quick_replace(checking_slot, &target_slot, true)
568 && self.menu().may_pickup(i)
569 && (round != 0
570 || checking_item.count != checking_item.kind.max_stack_size())
571 {
572 let checking_slot = self.menu_mut().slot_mut(i).unwrap();
574
575 let taken_item = checking_slot.split(checking_slot.count() as u32);
576
577 let target_slot = &mut self.carried;
579 let ItemStack::Present(target_slot_item) = target_slot else {
580 unreachable!("target slot is not empty but is not present");
581 };
582 target_slot_item.count += taken_item.count();
583 }
584 }
585 }
586 }
587 }
588 _ => {}
589 }
590 }
591
592 fn reset_quick_craft(&mut self) {
593 self.quick_craft_status = QuickCraftStatusKind::Start;
594 self.quick_craft_slots.clear();
595 }
596
597 pub fn held_item(&self) -> ItemStack {
600 let inventory = &self.inventory_menu;
601 let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
602 hotbar_items[self.selected_hotbar_slot as usize].clone()
603 }
604
605 fn try_item_click_behavior_override(
607 &self,
608 _operation: &ClickOperation,
609 _slot_item_index: usize,
610 ) -> bool {
611 false
612 }
613
614 fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack {
615 let Some(slot_item) = self.menu_mut().slot_mut(slot) else {
616 return src_item;
617 };
618 let ItemStack::Present(mut src_item) = src_item else {
619 return src_item;
620 };
621
622 let take_count = cmp::min(
623 cmp::min(take_count, src_item.count),
624 src_item.kind.max_stack_size() - slot_item.count(),
625 );
626 if take_count <= 0 {
627 return src_item.into();
628 }
629 let take_count = take_count as u32;
630
631 if slot_item.is_empty() {
632 *slot_item = src_item.split(take_count).into();
633 } else if let ItemStack::Present(slot_item) = slot_item
634 && slot_item.is_same_item_and_components(&src_item)
635 {
636 src_item.count -= take_count as i32;
637 slot_item.count += take_count as i32;
638 }
639
640 src_item.into()
641 }
642
643 fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> {
644 if !self.menu().may_pickup(slot) {
645 return None;
646 }
647 let mut slot_item = self.menu().slot(slot)?.clone();
648 if !self.menu().allow_modification(slot) && limit < slot_item.count() {
649 return None;
650 }
651
652 let count = count.min(limit);
653 if count <= 0 {
654 return None;
655 }
656 let removed = slot_item.split(count as u32);
658
659 if removed.is_present() && slot_item.is_empty() {
660 *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty;
661 }
662
663 Some(removed)
664 }
665}
666
667fn can_item_quick_replace(
668 target_slot: &ItemStack,
669 item: &ItemStack,
670 ignore_item_count: bool,
671) -> bool {
672 let ItemStack::Present(target_slot) = target_slot else {
673 return false;
674 };
675 let ItemStack::Present(item) = item else {
676 return false;
679 };
680
681 if !item.is_same_item_and_components(target_slot) {
682 return false;
683 }
684 let count = target_slot.count as u16
685 + if ignore_item_count {
686 0
687 } else {
688 item.count as u16
689 };
690 count <= item.kind.max_stack_size() as u16
691}
692
693fn get_quick_craft_slot_count(
694 quick_craft_slots: &HashSet<u16>,
695 quick_craft_kind: &QuickCraftKind,
696 item: &mut ItemStackData,
697 slot_item_count: i32,
698) {
699 item.count = match quick_craft_kind {
700 QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
701 QuickCraftKind::Right => 1,
702 QuickCraftKind::Middle => item.kind.max_stack_size(),
703 };
704 item.count += slot_item_count;
705}
706
707impl Default for Inventory {
708 fn default() -> Self {
709 Inventory {
710 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
711 id: 0,
712 container_menu: None,
713 container_menu_title: None,
714 carried: ItemStack::Empty,
715 state_id: 0,
716 quick_craft_status: QuickCraftStatusKind::Start,
717 quick_craft_kind: QuickCraftKind::Middle,
718 quick_craft_slots: HashSet::new(),
719 selected_hotbar_slot: 0,
720 }
721 }
722}
723
724#[derive(EntityEvent, Debug, Clone)]
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_trigger(event: On<MenuOpenedEvent>, mut query: Query<&mut Inventory>) {
738 let mut inventory = query.get_mut(event.entity).unwrap();
739 inventory.id = event.window_id;
740 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
741 inventory.container_menu_title = Some(event.title.clone());
742}
743
744#[derive(EntityEvent)]
749pub struct CloseContainerEvent {
750 pub entity: Entity,
751 pub id: i32,
756}
757fn handle_container_close_event(
758 close_container: On<CloseContainerEvent>,
759 mut commands: Commands,
760 query: Query<(Entity, &Inventory)>,
761) {
762 let (entity, inventory) = query.get(close_container.entity).unwrap();
763 if close_container.id != inventory.id {
764 warn!(
765 "Tried to close container with ID {}, but the current container ID is {}",
766 close_container.id, inventory.id
767 );
768 return;
769 }
770
771 commands.trigger(SendGamePacketEvent::new(
772 entity,
773 ServerboundContainerClose {
774 container_id: inventory.id,
775 },
776 ));
777 commands.trigger(ClientsideCloseContainerEvent {
778 entity: close_container.entity,
779 });
780}
781
782#[derive(EntityEvent, Clone)]
791pub struct ClientsideCloseContainerEvent {
792 pub entity: Entity,
793}
794pub fn handle_client_side_close_container_trigger(
795 event: On<ClientsideCloseContainerEvent>,
796 mut query: Query<&mut Inventory>,
797) {
798 let mut inventory = query.get_mut(event.entity).unwrap();
799
800 if let Some(inventory_menu) = inventory.container_menu.take() {
802 let new_inventory = inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec();
819 let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap();
820 *inventory.inventory_menu.as_player_mut().inventory = new_inventory;
821 }
822
823 inventory.id = 0;
824 inventory.container_menu_title = None;
825}
826
827#[derive(EntityEvent, Debug)]
828pub struct ContainerClickEvent {
829 pub entity: Entity,
830 pub window_id: i32,
831 pub operation: ClickOperation,
832}
833pub fn handle_container_click_event(
834 container_click: On<ContainerClickEvent>,
835 mut commands: Commands,
836 mut query: Query<(
837 Entity,
838 &mut Inventory,
839 Option<&PlayerAbilities>,
840 &InstanceName,
841 )>,
842 instance_container: Res<InstanceContainer>,
843) {
844 let (entity, mut inventory, player_abilities, instance_name) =
845 query.get_mut(container_click.entity).unwrap();
846 if inventory.id != container_click.window_id {
847 error!(
848 "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.",
849 container_click.window_id, inventory.id
850 );
851 return;
852 }
853
854 let Some(instance) = instance_container.get(instance_name) else {
855 return;
856 };
857
858 let old_slots = inventory.menu().slots();
859 inventory.simulate_click(
860 &container_click.operation,
861 player_abilities.unwrap_or(&PlayerAbilities::default()),
862 );
863 let new_slots = inventory.menu().slots();
864
865 let registry_holder = &instance.read().registries;
866
867 let mut changed_slots: IndexMap<u16, HashedStack> = IndexMap::new();
870 for (slot_index, old_slot) in old_slots.iter().enumerate() {
871 let new_slot = &new_slots[slot_index];
872 if old_slot != new_slot {
873 changed_slots.insert(
874 slot_index as u16,
875 HashedStack::from_item_stack(new_slot, registry_holder),
876 );
877 }
878 }
879
880 commands.trigger(SendGamePacketEvent::new(
881 entity,
882 ServerboundContainerClick {
883 container_id: container_click.window_id,
884 state_id: inventory.state_id,
885 slot_num: container_click
886 .operation
887 .slot_num()
888 .map(|n| n as i16)
889 .unwrap_or(-999),
890 button_num: container_click.operation.button_num(),
891 click_type: container_click.operation.click_type(),
892 changed_slots,
893 carried_item: HashedStack::from_item_stack(&inventory.carried, registry_holder),
894 },
895 ));
896}
897
898#[derive(EntityEvent)]
902pub struct SetContainerContentEvent {
903 pub entity: Entity,
904 pub slots: Vec<ItemStack>,
905 pub container_id: i32,
906}
907pub fn handle_set_container_content_trigger(
908 set_container_content: On<SetContainerContentEvent>,
909 mut query: Query<&mut Inventory>,
910) {
911 let mut inventory = query.get_mut(set_container_content.entity).unwrap();
912
913 if set_container_content.container_id != inventory.id {
914 warn!(
915 "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}",
916 set_container_content.container_id, inventory.id
917 );
918 return;
919 }
920
921 let menu = inventory.menu_mut();
922 for (i, slot) in set_container_content.slots.iter().enumerate() {
923 if let Some(slot_mut) = menu.slot_mut(i) {
924 *slot_mut = slot.clone();
925 }
926 }
927}
928
929#[derive(EntityEvent)]
933pub struct SetSelectedHotbarSlotEvent {
934 pub entity: Entity,
935 pub slot: u8,
937}
938pub fn handle_set_selected_hotbar_slot_event(
939 set_selected_hotbar_slot: On<SetSelectedHotbarSlotEvent>,
940 mut query: Query<&mut Inventory>,
941) {
942 let mut inventory = query.get_mut(set_selected_hotbar_slot.entity).unwrap();
943 inventory.selected_hotbar_slot = set_selected_hotbar_slot.slot;
944}
945
946#[derive(Component)]
950pub struct LastSentSelectedHotbarSlot {
951 pub slot: u8,
952}
953pub fn ensure_has_sent_carried_item(
959 mut commands: Commands,
960 query: Query<(Entity, &Inventory, Option<&LastSentSelectedHotbarSlot>)>,
961) {
962 for (entity, inventory, last_sent) in query.iter() {
963 if let Some(last_sent) = last_sent {
964 if last_sent.slot == inventory.selected_hotbar_slot {
965 continue;
966 }
967
968 commands.trigger(SendGamePacketEvent::new(
969 entity,
970 ServerboundSetCarriedItem {
971 slot: inventory.selected_hotbar_slot as u16,
972 },
973 ));
974 }
975
976 commands.entity(entity).insert(LastSentSelectedHotbarSlot {
977 slot: inventory.selected_hotbar_slot,
978 });
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use azalea_registry::Item;
985
986 use super::*;
987
988 #[test]
989 fn test_simulate_shift_click_in_crafting_table() {
990 let spruce_planks = ItemStack::new(Item::SprucePlanks, 4);
991
992 let mut inventory = Inventory {
993 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
994 id: 1,
995 container_menu: Some(Menu::Crafting {
996 result: spruce_planks.clone(),
997 grid: SlotList::default(),
999 player: SlotList::default(),
1000 }),
1001 container_menu_title: None,
1002 carried: ItemStack::Empty,
1003 state_id: 0,
1004 quick_craft_status: QuickCraftStatusKind::Start,
1005 quick_craft_kind: QuickCraftKind::Middle,
1006 quick_craft_slots: HashSet::new(),
1007 selected_hotbar_slot: 0,
1008 };
1009
1010 inventory.simulate_click(
1011 &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }),
1012 &PlayerAbilities::default(),
1013 );
1014
1015 let new_slots = inventory.menu().slots();
1016 assert_eq!(&new_slots[0], &ItemStack::Empty);
1017 assert_eq!(
1018 &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()],
1019 &spruce_planks
1020 );
1021 }
1022}