1use std::collections::{HashMap, HashSet};
2
3use azalea_chat::FormattedText;
4pub use azalea_inventory::*;
5use azalea_inventory::{
6 item::MaxStackSizeExt,
7 operations::{
8 ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
9 QuickCraftStatusKind, QuickMoveClick, ThrowClick,
10 },
11};
12use azalea_protocol::packets::game::{
13 s_container_click::ServerboundContainerClick, s_container_close::ServerboundContainerClose,
14 s_set_carried_item::ServerboundSetCarriedItem,
15};
16use azalea_registry::MenuKind;
17use bevy_app::{App, Plugin, Update};
18use bevy_ecs::{
19 component::Component,
20 entity::Entity,
21 event::EventReader,
22 prelude::{Event, EventWriter},
23 schedule::{IntoSystemConfigs, SystemSet},
24 system::Query,
25};
26use tracing::warn;
27
28use crate::{
29 local_player::PlayerAbilities,
30 packet_handling::game::{handle_send_packet_event, SendPacketEvent},
31 respawn::perform_respawn,
32 Client,
33};
34
35pub struct InventoryPlugin;
36impl Plugin for InventoryPlugin {
37 fn build(&self, app: &mut App) {
38 app.add_event::<ClientSideCloseContainerEvent>()
39 .add_event::<MenuOpenedEvent>()
40 .add_event::<CloseContainerEvent>()
41 .add_event::<ContainerClickEvent>()
42 .add_event::<SetContainerContentEvent>()
43 .add_event::<SetSelectedHotbarSlotEvent>()
44 .add_systems(
45 Update,
46 (
47 handle_set_selected_hotbar_slot_event,
48 handle_menu_opened_event,
49 handle_set_container_content_event,
50 handle_container_click_event,
51 handle_container_close_event.before(handle_send_packet_event),
52 handle_client_side_close_container_event,
53 )
54 .chain()
55 .in_set(InventorySet)
56 .before(perform_respawn),
57 );
58 }
59}
60
61#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
62pub struct InventorySet;
63
64impl Client {
65 pub fn menu(&self) -> Menu {
68 let mut ecs = self.ecs.lock();
69 let inventory = self.query::<&Inventory>(&mut ecs);
70 inventory.menu().clone()
71 }
72}
73
74#[derive(Component, Debug, Clone)]
76pub struct Inventory {
77 pub inventory_menu: azalea_inventory::Menu,
83
84 pub id: i32,
89 pub container_menu: Option<azalea_inventory::Menu>,
92 pub container_menu_title: Option<FormattedText>,
95 pub carried: ItemStack,
101 pub state_id: u32,
105
106 pub quick_craft_status: QuickCraftStatusKind,
107 pub quick_craft_kind: QuickCraftKind,
108 pub quick_craft_slots: HashSet<u16>,
111
112 pub selected_hotbar_slot: u8,
118}
119
120impl Inventory {
121 pub fn menu(&self) -> &azalea_inventory::Menu {
127 if let Some(menu) = &self.container_menu {
128 menu
129 } else {
130 &self.inventory_menu
131 }
132 }
133
134 pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
140 if let Some(menu) = &mut self.container_menu {
141 menu
142 } else {
143 &mut self.inventory_menu
144 }
145 }
146
147 pub fn simulate_click(
149 &mut self,
150 operation: &ClickOperation,
151 player_abilities: &PlayerAbilities,
152 ) {
153 if let ClickOperation::QuickCraft(quick_craft) = operation {
154 let last_quick_craft_status_tmp = self.quick_craft_status.clone();
155 self.quick_craft_status = last_quick_craft_status_tmp.clone();
156 let last_quick_craft_status = last_quick_craft_status_tmp;
157
158 if self.carried.is_empty() {
160 return self.reset_quick_craft();
161 }
162 if (last_quick_craft_status == QuickCraftStatusKind::Start
165 || last_quick_craft_status == QuickCraftStatusKind::End
166 || self.quick_craft_status != QuickCraftStatusKind::End)
167 && (self.quick_craft_status != last_quick_craft_status)
168 {
169 return self.reset_quick_craft();
170 }
171 if self.quick_craft_status == QuickCraftStatusKind::Start {
172 self.quick_craft_kind = quick_craft.kind.clone();
173 if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
174 {
175 self.quick_craft_status = QuickCraftStatusKind::Add;
176 self.quick_craft_slots.clear();
177 } else {
178 self.reset_quick_craft();
179 }
180 return;
181 }
182 if let QuickCraftStatus::Add { slot } = quick_craft.status {
183 let slot_item = self.menu().slot(slot as usize);
184 if let Some(slot_item) = slot_item {
185 if let ItemStack::Present(carried) = &self.carried {
186 if can_item_quick_replace(slot_item, &self.carried, true)
190 && (self.quick_craft_kind == QuickCraftKind::Right
191 || carried.count as usize > self.quick_craft_slots.len())
192 {
193 self.quick_craft_slots.insert(slot);
194 }
195 }
196 }
197 return;
198 }
199 if self.quick_craft_status == QuickCraftStatusKind::End {
200 if !self.quick_craft_slots.is_empty() {
201 if self.quick_craft_slots.len() == 1 {
202 let slot = *self.quick_craft_slots.iter().next().unwrap();
205 self.reset_quick_craft();
206 self.simulate_click(
207 &match self.quick_craft_kind {
208 QuickCraftKind::Left => {
209 PickupClick::Left { slot: Some(slot) }.into()
210 }
211 QuickCraftKind::Right => {
212 PickupClick::Left { slot: Some(slot) }.into()
213 }
214 QuickCraftKind::Middle => {
215 return;
217 }
218 },
219 player_abilities,
220 );
221 return;
222 }
223
224 let ItemStack::Present(mut carried) = self.carried.clone() else {
225 return self.reset_quick_craft();
227 };
228
229 let mut carried_count = carried.count;
230 let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
231
232 loop {
233 let mut slot: &ItemStack;
234 let mut slot_index: u16;
235 let mut item_stack: &ItemStack;
236
237 loop {
238 let Some(&next_slot) = quick_craft_slots_iter.next() else {
239 carried.count = carried_count;
240 self.carried = ItemStack::Present(carried);
241 return self.reset_quick_craft();
242 };
243
244 slot = self.menu().slot(next_slot as usize).unwrap();
245 slot_index = next_slot;
246 item_stack = &self.carried;
247
248 if slot.is_present()
249 && can_item_quick_replace(slot, item_stack, true)
250 && (
253 self.quick_craft_kind == QuickCraftKind::Middle
254 || item_stack.count() >= self.quick_craft_slots.len() as i32
255 )
256 {
257 break;
258 }
259 }
260
261 let ItemStack::Present(slot) = slot else {
263 unreachable!("the loop above requires the slot to be present to break")
264 };
265
266 let mut new_carried = carried.clone();
268 let slot_item_count = slot.count;
269 get_quick_craft_slot_count(
270 &self.quick_craft_slots,
271 &self.quick_craft_kind,
272 &mut new_carried,
273 slot_item_count,
274 );
275 let max_stack_size = i32::min(
276 new_carried.kind.max_stack_size(),
277 i32::min(
278 new_carried.kind.max_stack_size(),
279 slot.kind.max_stack_size(),
280 ),
281 );
282 if new_carried.count > max_stack_size {
283 new_carried.count = max_stack_size;
284 }
285
286 carried_count -= new_carried.count - slot_item_count;
287 let menu = if let Some(menu) = &mut self.container_menu {
290 menu
291 } else {
292 &mut self.inventory_menu
293 };
294 *menu.slot_mut(slot_index as usize).unwrap() =
295 ItemStack::Present(new_carried);
296 }
297 }
298 } else {
299 return self.reset_quick_craft();
300 }
301 }
302 if self.quick_craft_status != QuickCraftStatusKind::Start {
305 return self.reset_quick_craft();
306 }
307
308 match operation {
309 ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
311 if self.carried.is_present() {
312 self.carried = ItemStack::Empty;
319 }
320 }
321 ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
322 if self.carried.is_present() {
323 let _item = self.carried.split(1);
324 }
326 }
327 ClickOperation::Pickup(
328 PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
329 ) => {
330 let Some(slot_item) = self.menu().slot(*slot as usize) else {
331 return;
332 };
333 let carried = &self.carried;
334 match slot_item {
338 ItemStack::Empty => if carried.is_present() {},
339 ItemStack::Present(_) => todo!(),
340 }
341 }
342 ClickOperation::QuickMove(
343 QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
344 ) => {
345 loop {
348 let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
349 let slot_item = self.menu().slot(*slot as usize).unwrap();
350 if new_slot_item.is_empty() || slot_item != &new_slot_item {
351 break;
352 }
353 }
354 }
355 ClickOperation::Swap(s) => {
356 let source_slot_index = s.source_slot as usize;
357 let target_slot_index = s.target_slot as usize;
358
359 let Some(source_slot) = self.menu().slot(source_slot_index) else {
360 return;
361 };
362 let Some(target_slot) = self.menu().slot(target_slot_index) else {
363 return;
364 };
365 if source_slot.is_empty() && target_slot.is_empty() {
366 return;
367 }
368
369 if target_slot.is_empty() {
370 if self.menu().may_pickup(source_slot_index) {
371 let source_slot = source_slot.clone();
372 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
373 *target_slot = source_slot;
374 }
375 } else if source_slot.is_empty() {
376 let ItemStack::Present(target_item) = target_slot else {
377 unreachable!("target slot is not empty but is not present");
378 };
379 if self.menu().may_place(source_slot_index, target_item) {
380 let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
382
383 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
384 let new_source_slot = target_slot.split(source_max_stack_size);
385 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
386 }
387 } else if self.menu().may_pickup(source_slot_index) {
388 let ItemStack::Present(target_item) = target_slot else {
389 unreachable!("target slot is not empty but is not present");
390 };
391 if self.menu().may_place(source_slot_index, target_item) {
392 let source_max_stack = self.menu().max_stack_size(source_slot_index);
393 if target_slot.count() > source_max_stack as i32 {
394 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
397 let new_source_slot = target_slot.split(source_max_stack);
398 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
399 } else {
403 let new_target_slot = source_slot.clone();
405 let new_source_slot = target_slot.clone();
406
407 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
408 *target_slot = new_target_slot;
409
410 let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
411 *source_slot = new_source_slot;
412 }
413 }
414 }
415 }
416 ClickOperation::Clone(CloneClick { slot }) => {
417 if !player_abilities.instant_break || self.carried.is_present() {
418 return;
419 }
420 let Some(source_slot) = self.menu().slot(*slot as usize) else {
421 return;
422 };
423 let ItemStack::Present(source_item) = source_slot else {
424 return;
425 };
426 let mut new_carried = source_item.clone();
427 new_carried.count = new_carried.kind.max_stack_size();
428 self.carried = ItemStack::Present(new_carried);
429 }
430 ClickOperation::Throw(c) => {
431 if self.carried.is_present() {
432 return;
433 }
434
435 let (ThrowClick::Single { slot: slot_index }
436 | ThrowClick::All { slot: slot_index }) = c;
437 let slot_index = *slot_index as usize;
438
439 let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
440 return;
441 };
442 let ItemStack::Present(slot_item) = slot else {
443 return;
444 };
445
446 let dropping_count = match c {
447 ThrowClick::Single { .. } => 1,
448 ThrowClick::All { .. } => slot_item.count,
449 };
450
451 let _dropping = slot_item.split(dropping_count as u32);
452 }
454 ClickOperation::PickupAll(PickupAllClick {
455 slot: source_slot_index,
456 reversed,
457 }) => {
458 let source_slot_index = *source_slot_index as usize;
459
460 let source_slot = self.menu().slot(source_slot_index).unwrap();
461 let target_slot = self.carried.clone();
462
463 if target_slot.is_empty()
464 || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
465 {
466 return;
467 }
468
469 let ItemStack::Present(target_slot_item) = &target_slot else {
470 unreachable!("target slot is not empty but is not present");
471 };
472
473 for round in 0..2 {
474 let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
475 Box::new((0..self.menu().len()).rev())
476 } else {
477 Box::new(0..self.menu().len())
478 };
479
480 for i in iterator {
481 if target_slot_item.count < target_slot_item.kind.max_stack_size() {
482 let checking_slot = self.menu().slot(i).unwrap();
483 if let ItemStack::Present(checking_item) = checking_slot {
484 if can_item_quick_replace(checking_slot, &target_slot, true)
485 && self.menu().may_pickup(i)
486 && (round != 0
487 || checking_item.count
488 != checking_item.kind.max_stack_size())
489 {
490 let checking_slot = self.menu_mut().slot_mut(i).unwrap();
492
493 let taken_item =
494 checking_slot.split(checking_slot.count() as u32);
495
496 let target_slot = &mut self.carried;
498 let ItemStack::Present(target_slot_item) = target_slot else {
499 unreachable!("target slot is not empty but is not present");
500 };
501 target_slot_item.count += taken_item.count();
502 }
503 }
504 }
505 }
506 }
507 }
508 _ => {}
509 }
510 }
511
512 fn reset_quick_craft(&mut self) {
513 self.quick_craft_status = QuickCraftStatusKind::Start;
514 self.quick_craft_slots.clear();
515 }
516
517 pub fn held_item(&self) -> ItemStack {
519 let inventory = &self.inventory_menu;
520 let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
521 hotbar_items[self.selected_hotbar_slot as usize].clone()
522 }
523}
524
525fn can_item_quick_replace(
526 target_slot: &ItemStack,
527 item: &ItemStack,
528 ignore_item_count: bool,
529) -> bool {
530 let ItemStack::Present(target_slot) = target_slot else {
531 return false;
532 };
533 let ItemStack::Present(item) = item else {
534 return false;
537 };
538
539 if !item.is_same_item_and_components(target_slot) {
540 return false;
541 }
542 let count = target_slot.count as u16
543 + if ignore_item_count {
544 0
545 } else {
546 item.count as u16
547 };
548 count <= item.kind.max_stack_size() as u16
549}
550
551fn get_quick_craft_slot_count(
552 quick_craft_slots: &HashSet<u16>,
553 quick_craft_kind: &QuickCraftKind,
554 item: &mut ItemStackData,
555 slot_item_count: i32,
556) {
557 item.count = match quick_craft_kind {
558 QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
559 QuickCraftKind::Right => 1,
560 QuickCraftKind::Middle => item.kind.max_stack_size(),
561 };
562 item.count += slot_item_count;
563}
564
565impl Default for Inventory {
566 fn default() -> Self {
567 Inventory {
568 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
569 id: 0,
570 container_menu: None,
571 container_menu_title: None,
572 carried: ItemStack::Empty,
573 state_id: 0,
574 quick_craft_status: QuickCraftStatusKind::Start,
575 quick_craft_kind: QuickCraftKind::Middle,
576 quick_craft_slots: HashSet::new(),
577 selected_hotbar_slot: 0,
578 }
579 }
580}
581
582#[derive(Event, Debug)]
585pub struct MenuOpenedEvent {
586 pub entity: Entity,
587 pub window_id: i32,
588 pub menu_type: MenuKind,
589 pub title: FormattedText,
590}
591fn handle_menu_opened_event(
592 mut events: EventReader<MenuOpenedEvent>,
593 mut query: Query<&mut Inventory>,
594) {
595 for event in events.read() {
596 let mut inventory = query.get_mut(event.entity).unwrap();
597 inventory.id = event.window_id;
598 inventory.container_menu = Some(Menu::from_kind(event.menu_type));
599 inventory.container_menu_title = Some(event.title.clone());
600 }
601}
602
603#[derive(Event)]
608pub struct CloseContainerEvent {
609 pub entity: Entity,
610 pub id: i32,
613}
614fn handle_container_close_event(
615 query: Query<(Entity, &Inventory)>,
616 mut events: EventReader<CloseContainerEvent>,
617 mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
618 mut send_packet_events: EventWriter<SendPacketEvent>,
619) {
620 for event in events.read() {
621 let (entity, inventory) = query.get(event.entity).unwrap();
622 if event.id != inventory.id {
623 warn!(
624 "Tried to close container with ID {}, but the current container ID is {}",
625 event.id, inventory.id
626 );
627 continue;
628 }
629
630 send_packet_events.send(SendPacketEvent::new(
631 entity,
632 ServerboundContainerClose {
633 container_id: inventory.id,
634 },
635 ));
636 client_side_events.send(ClientSideCloseContainerEvent {
637 entity: event.entity,
638 });
639 }
640}
641
642#[derive(Event)]
646pub struct ClientSideCloseContainerEvent {
647 pub entity: Entity,
648}
649pub fn handle_client_side_close_container_event(
650 mut events: EventReader<ClientSideCloseContainerEvent>,
651 mut query: Query<&mut Inventory>,
652) {
653 for event in events.read() {
654 let mut inventory = query.get_mut(event.entity).unwrap();
655 inventory.container_menu = None;
656 inventory.id = 0;
657 inventory.container_menu_title = None;
658 }
659}
660
661#[derive(Event, Debug)]
662pub struct ContainerClickEvent {
663 pub entity: Entity,
664 pub window_id: i32,
665 pub operation: ClickOperation,
666}
667pub fn handle_container_click_event(
668 mut query: Query<(Entity, &mut Inventory)>,
669 mut events: EventReader<ContainerClickEvent>,
670 mut send_packet_events: EventWriter<SendPacketEvent>,
671) {
672 for event in events.read() {
673 let (entity, mut inventory) = query.get_mut(event.entity).unwrap();
674 if inventory.id != event.window_id {
675 warn!(
676 "Tried to click container with ID {}, but the current container ID is {}",
677 event.window_id, inventory.id
678 );
679 continue;
680 }
681
682 let menu = inventory.menu_mut();
683 let old_slots = menu.slots().clone();
684
685 let mut changed_slots: HashMap<u16, ItemStack> = HashMap::new();
690 for (slot_index, old_slot) in old_slots.iter().enumerate() {
691 let new_slot = &menu.slots()[slot_index];
692 if old_slot != new_slot {
693 changed_slots.insert(slot_index as u16, new_slot.clone());
694 }
695 }
696
697 send_packet_events.send(SendPacketEvent::new(
698 entity,
699 ServerboundContainerClick {
700 container_id: event.window_id,
701 state_id: inventory.state_id,
702 slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
703 button_num: event.operation.button_num(),
704 click_type: event.operation.click_type(),
705 changed_slots,
706 carried_item: inventory.carried.clone(),
707 },
708 ));
709 }
710}
711
712#[derive(Event)]
715pub struct SetContainerContentEvent {
716 pub entity: Entity,
717 pub slots: Vec<ItemStack>,
718 pub container_id: i32,
719}
720fn handle_set_container_content_event(
721 mut events: EventReader<SetContainerContentEvent>,
722 mut query: Query<&mut Inventory>,
723) {
724 for event in events.read() {
725 let mut inventory = query.get_mut(event.entity).unwrap();
726
727 if event.container_id != inventory.id {
728 warn!(
729 "Tried to set container content with ID {}, but the current container ID is {}",
730 event.container_id, inventory.id
731 );
732 continue;
733 }
734
735 let menu = inventory.menu_mut();
736 for (i, slot) in event.slots.iter().enumerate() {
737 if let Some(slot_mut) = menu.slot_mut(i) {
738 *slot_mut = slot.clone();
739 }
740 }
741 }
742}
743
744#[derive(Event)]
745pub struct SetSelectedHotbarSlotEvent {
746 pub entity: Entity,
747 pub slot: u8,
749}
750fn handle_set_selected_hotbar_slot_event(
751 mut events: EventReader<SetSelectedHotbarSlotEvent>,
752 mut send_packet_events: EventWriter<SendPacketEvent>,
753 mut query: Query<&mut Inventory>,
754) {
755 for event in events.read() {
756 let mut inventory = query.get_mut(event.entity).unwrap();
757
758 if inventory.selected_hotbar_slot == event.slot {
760 continue;
761 }
762
763 inventory.selected_hotbar_slot = event.slot;
764 send_packet_events.send(SendPacketEvent::new(
765 event.entity,
766 ServerboundSetCarriedItem {
767 slot: event.slot as u16,
768 },
769 ));
770 }
771}