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