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