1use std::{cmp, collections::HashSet};
2
3use azalea_chat::FormattedText;
4use azalea_inventory::{
5 ItemStack, ItemStackData, Menu,
6 components::EquipmentSlot,
7 item::MaxStackSizeExt,
8 operations::{
9 ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
10 QuickCraftStatusKind, QuickMoveClick, ThrowClick,
11 },
12};
13
14use crate::PlayerAbilities;
15
16#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
19#[derive(Clone, Debug)]
20pub struct Inventory {
21 pub inventory_menu: azalea_inventory::Menu,
26
27 pub id: i32,
33 pub container_menu: Option<azalea_inventory::Menu>,
36 pub container_menu_title: Option<FormattedText>,
40 pub carried: ItemStack,
46 pub state_id: u32,
51
52 pub quick_craft_status: QuickCraftStatusKind,
53 pub quick_craft_kind: QuickCraftKind,
54 pub quick_craft_slots: HashSet<u16>,
57
58 pub selected_hotbar_slot: u8,
64}
65
66impl Inventory {
67 pub fn menu(&self) -> &azalea_inventory::Menu {
74 match &self.container_menu {
75 Some(menu) => menu,
76 _ => &self.inventory_menu,
77 }
78 }
79
80 pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
87 match &mut self.container_menu {
88 Some(menu) => menu,
89 _ => &mut self.inventory_menu,
90 }
91 }
92
93 pub fn simulate_click(
95 &mut self,
96 operation: &ClickOperation,
97 player_abilities: &PlayerAbilities,
98 ) {
99 if let ClickOperation::QuickCraft(quick_craft) = operation {
100 let last_quick_craft_status_tmp = self.quick_craft_status.clone();
101 self.quick_craft_status = last_quick_craft_status_tmp.clone();
102 let last_quick_craft_status = last_quick_craft_status_tmp;
103
104 if self.carried.is_empty() {
106 return self.reset_quick_craft();
107 }
108 if (last_quick_craft_status == QuickCraftStatusKind::Start
111 || last_quick_craft_status == QuickCraftStatusKind::End
112 || self.quick_craft_status != QuickCraftStatusKind::End)
113 && (self.quick_craft_status != last_quick_craft_status)
114 {
115 return self.reset_quick_craft();
116 }
117 if self.quick_craft_status == QuickCraftStatusKind::Start {
118 self.quick_craft_kind = quick_craft.kind.clone();
119 if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
120 {
121 self.quick_craft_status = QuickCraftStatusKind::Add;
122 self.quick_craft_slots.clear();
123 } else {
124 self.reset_quick_craft();
125 }
126 return;
127 }
128 if let QuickCraftStatus::Add { slot } = quick_craft.status {
129 let slot_item = self.menu().slot(slot as usize);
130 if let Some(slot_item) = slot_item
131 && let ItemStack::Present(carried) = &self.carried
132 {
133 if can_item_quick_replace(slot_item, &self.carried, true)
138 && (self.quick_craft_kind == QuickCraftKind::Right
139 || carried.count as usize > self.quick_craft_slots.len())
140 {
141 self.quick_craft_slots.insert(slot);
142 }
143 }
144 return;
145 }
146 if self.quick_craft_status == QuickCraftStatusKind::End {
147 if !self.quick_craft_slots.is_empty() {
148 if self.quick_craft_slots.len() == 1 {
149 let slot = *self.quick_craft_slots.iter().next().unwrap();
152 self.reset_quick_craft();
153 self.simulate_click(
154 &match self.quick_craft_kind {
155 QuickCraftKind::Left => {
156 PickupClick::Left { slot: Some(slot) }.into()
157 }
158 QuickCraftKind::Right => {
159 PickupClick::Left { slot: Some(slot) }.into()
160 }
161 QuickCraftKind::Middle => {
162 return;
164 }
165 },
166 player_abilities,
167 );
168 return;
169 }
170
171 let ItemStack::Present(mut carried) = self.carried.clone() else {
172 return self.reset_quick_craft();
174 };
175
176 let mut carried_count = carried.count;
177 let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
178
179 loop {
180 let mut slot: &ItemStack;
181 let mut slot_index: u16;
182 let mut item_stack: &ItemStack;
183
184 loop {
185 let Some(&next_slot) = quick_craft_slots_iter.next() else {
186 carried.count = carried_count;
187 self.carried = ItemStack::Present(carried);
188 return self.reset_quick_craft();
189 };
190
191 slot = self.menu().slot(next_slot as usize).unwrap();
192 slot_index = next_slot;
193 item_stack = &self.carried;
194
195 if slot.is_present()
196 && can_item_quick_replace(slot, item_stack, true)
197 && (
200 self.quick_craft_kind == QuickCraftKind::Middle
201 || item_stack.count() >= self.quick_craft_slots.len() as i32
202 )
203 {
204 break;
205 }
206 }
207
208 let ItemStack::Present(slot) = slot else {
210 unreachable!("the loop above requires the slot to be present to break")
211 };
212
213 let mut new_carried = carried.clone();
215 let slot_item_count = slot.count;
216 get_quick_craft_slot_count(
217 &self.quick_craft_slots,
218 &self.quick_craft_kind,
219 &mut new_carried,
220 slot_item_count,
221 );
222 let max_stack_size = i32::min(
223 new_carried.kind.max_stack_size(),
224 i32::min(
225 new_carried.kind.max_stack_size(),
226 slot.kind.max_stack_size(),
227 ),
228 );
229 if new_carried.count > max_stack_size {
230 new_carried.count = max_stack_size;
231 }
232
233 carried_count -= new_carried.count - slot_item_count;
234 let menu = match &mut self.container_menu {
237 Some(menu) => menu,
238 _ => &mut self.inventory_menu,
239 };
240 *menu.slot_mut(slot_index as usize).unwrap() =
241 ItemStack::Present(new_carried);
242 }
243 }
244 } else {
245 return self.reset_quick_craft();
246 }
247 }
248 if self.quick_craft_status != QuickCraftStatusKind::Start {
251 return self.reset_quick_craft();
252 }
253
254 match operation {
255 ClickOperation::Pickup(PickupClick::Left { slot: None })
257 if self.carried.is_present() =>
258 {
259 self.carried = ItemStack::Empty;
266 }
267 ClickOperation::Pickup(PickupClick::Right { slot: None })
268 if self.carried.is_present() =>
269 {
270 let _item = self.carried.split(1);
271 }
273 &ClickOperation::Pickup(
274 ref pickup @ (PickupClick::Left { slot: Some(slot) }
276 | PickupClick::Right { slot: Some(slot) }),
277 ) => {
278 let slot = slot as usize;
279 let Some(slot_item) = self.menu().slot(slot) else {
280 return;
281 };
282
283 if self.try_item_click_behavior_override(operation, slot) {
284 return;
285 }
286
287 let is_left_click = matches!(pickup, PickupClick::Left { .. });
288
289 match slot_item {
290 ItemStack::Empty => {
291 if self.carried.is_present() {
292 let place_count = if is_left_click {
293 self.carried.count()
294 } else {
295 1
296 };
297 self.carried =
298 self.safe_insert(slot, self.carried.clone(), place_count);
299 }
300 }
301 ItemStack::Present(_) => {
302 if !self.menu().may_pickup(slot) {
303 return;
304 }
305 if let ItemStack::Present(carried) = self.carried.clone() {
306 let slot_is_same_item_as_carried = slot_item
307 .as_present()
308 .is_some_and(|s| carried.is_same_item_and_components(s));
309
310 if self.menu().may_place(slot, &carried) {
311 if slot_is_same_item_as_carried {
312 let place_count = if is_left_click { carried.count } else { 1 };
313 self.carried =
314 self.safe_insert(slot, self.carried.clone(), place_count);
315 } else if carried.count
316 <= self
317 .menu()
318 .max_stack_size(slot)
319 .min(carried.kind.max_stack_size())
320 {
321 self.carried = slot_item.clone();
323 let slot_item = self.menu_mut().slot_mut(slot).unwrap();
324 *slot_item = carried.into();
325 }
326 } else if slot_is_same_item_as_carried
327 && let Some(removed) = self.try_remove(
328 slot,
329 slot_item.count(),
330 carried.kind.max_stack_size() - carried.count,
331 )
332 {
333 self.carried.as_present_mut().unwrap().count += removed.count();
334 }
336 } else {
337 let pickup_count = if is_left_click {
338 slot_item.count()
339 } else {
340 (slot_item.count() + 1) / 2
341 };
342 if let Some(new_slot_item) =
343 self.try_remove(slot, pickup_count, i32::MAX)
344 {
345 self.carried = new_slot_item;
346 }
348 }
349 }
350 }
351 }
352 &ClickOperation::QuickMove(
353 QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
354 ) => {
355 let slot = slot as usize;
359 loop {
360 let new_slot_item = self.menu_mut().quick_move_stack(slot);
361 let slot_item = self.menu().slot(slot).unwrap();
362 if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() {
363 break;
364 }
365 }
366 }
367 ClickOperation::Swap(s) => {
368 let source_slot_index = s.source_slot as usize;
369 let target_slot_index = s.target_slot as usize;
370
371 let Some(source_slot) = self.menu().slot(source_slot_index) else {
372 return;
373 };
374 let Some(target_slot) = self.menu().slot(target_slot_index) else {
375 return;
376 };
377 if source_slot.is_empty() && target_slot.is_empty() {
378 return;
379 }
380
381 if target_slot.is_empty() {
382 if self.menu().may_pickup(source_slot_index) {
383 let source_slot = source_slot.clone();
384 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
385 *target_slot = source_slot;
386 }
387 } else if source_slot.is_empty() {
388 let target_item = target_slot
389 .as_present()
390 .expect("target slot was already checked to not be empty");
391 if self.menu().may_place(source_slot_index, target_item) {
392 let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
394
395 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
396 let new_source_slot =
397 target_slot.split(source_max_stack_size.try_into().unwrap());
398 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
399 }
400 } else if self.menu().may_pickup(source_slot_index) {
401 let ItemStack::Present(target_item) = target_slot else {
402 unreachable!("target slot is not empty but is not present");
403 };
404 if self.menu().may_place(source_slot_index, target_item) {
405 let source_max_stack = self.menu().max_stack_size(source_slot_index);
406 if target_slot.count() > source_max_stack {
407 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
411 let new_source_slot =
412 target_slot.split(source_max_stack.try_into().unwrap());
413 *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
414 } else {
418 let new_target_slot = source_slot.clone();
420 let new_source_slot = target_slot.clone();
421
422 let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
423 *target_slot = new_target_slot;
424
425 let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
426 *source_slot = new_source_slot;
427 }
428 }
429 }
430 }
431 ClickOperation::Clone(CloneClick { slot }) => {
432 if !player_abilities.instant_break || self.carried.is_present() {
433 return;
434 }
435 let Some(source_slot) = self.menu().slot(*slot as usize) else {
436 return;
437 };
438 let ItemStack::Present(source_item) = source_slot else {
439 return;
440 };
441 let mut new_carried = source_item.clone();
442 new_carried.count = new_carried.kind.max_stack_size();
443 self.carried = ItemStack::Present(new_carried);
444 }
445 ClickOperation::Throw(c) => {
446 if self.carried.is_present() {
447 return;
448 }
449
450 let (ThrowClick::Single { slot: slot_index }
451 | ThrowClick::All { slot: slot_index }) = c;
452 let slot_index = *slot_index as usize;
453
454 let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
455 return;
456 };
457 let ItemStack::Present(slot_item) = slot else {
458 return;
459 };
460
461 let dropping_count = match c {
462 ThrowClick::Single { .. } => 1,
463 ThrowClick::All { .. } => slot_item.count,
464 };
465
466 let _dropping = slot_item.split(dropping_count as u32);
467 }
469 ClickOperation::PickupAll(PickupAllClick {
470 slot: source_slot_index,
471 reversed,
472 }) => {
473 let source_slot_index = *source_slot_index as usize;
474
475 let source_slot = self.menu().slot(source_slot_index).unwrap();
476 let target_slot = self.carried.clone();
477
478 if target_slot.is_empty()
479 || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
480 {
481 return;
482 }
483
484 let ItemStack::Present(target_slot_item) = &target_slot else {
485 unreachable!("target slot is not empty but is not present");
486 };
487
488 for round in 0..2 {
489 let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
490 Box::new((0..self.menu().len()).rev())
491 } else {
492 Box::new(0..self.menu().len())
493 };
494
495 for i in iterator {
496 if target_slot_item.count < target_slot_item.kind.max_stack_size() {
497 let checking_slot = self.menu().slot(i).unwrap();
498 if let ItemStack::Present(checking_item) = checking_slot
499 && can_item_quick_replace(checking_slot, &target_slot, true)
500 && self.menu().may_pickup(i)
501 && (round != 0
502 || checking_item.count != checking_item.kind.max_stack_size())
503 {
504 let checking_slot = self.menu_mut().slot_mut(i).unwrap();
507
508 let taken_item = checking_slot.split(checking_slot.count() as u32);
509
510 let target_slot = &mut self.carried;
512 let ItemStack::Present(target_slot_item) = target_slot else {
513 unreachable!("target slot is not empty but is not present");
514 };
515 target_slot_item.count += taken_item.count();
516 }
517 }
518 }
519 }
520 }
521 _ => {}
522 }
523 }
524
525 fn reset_quick_craft(&mut self) {
526 self.quick_craft_status = QuickCraftStatusKind::Start;
527 self.quick_craft_slots.clear();
528 }
529
530 pub fn held_item(&self) -> &ItemStack {
533 self.get_equipment(EquipmentSlot::Mainhand)
534 .expect("The main hand item should always be present")
535 }
536
537 fn try_item_click_behavior_override(
539 &self,
540 _operation: &ClickOperation,
541 _slot_item_index: usize,
542 ) -> bool {
543 false
544 }
545
546 fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack {
547 let Some(slot_item) = self.menu_mut().slot_mut(slot) else {
548 return src_item;
549 };
550 let ItemStack::Present(mut src_item) = src_item else {
551 return src_item;
552 };
553
554 let take_count = cmp::min(
555 cmp::min(take_count, src_item.count),
556 src_item.kind.max_stack_size() - slot_item.count(),
557 );
558 if take_count <= 0 {
559 return src_item.into();
560 }
561 let take_count = take_count as u32;
562
563 if slot_item.is_empty() {
564 *slot_item = src_item.split(take_count).into();
565 } else if let ItemStack::Present(slot_item) = slot_item
566 && slot_item.is_same_item_and_components(&src_item)
567 {
568 src_item.count -= take_count as i32;
569 slot_item.count += take_count as i32;
570 }
571
572 src_item.into()
573 }
574
575 fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option<ItemStack> {
576 if !self.menu().may_pickup(slot) {
577 return None;
578 }
579 let mut slot_item = self.menu().slot(slot)?.clone();
580 if !self.menu().allow_modification(slot) && limit < slot_item.count() {
581 return None;
582 }
583
584 let count = count.min(limit);
585 if count <= 0 {
586 return None;
587 }
588 let removed = slot_item.split(count as u32);
591
592 if removed.is_present() && slot_item.is_empty() {
593 *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty;
594 }
595
596 Some(removed)
597 }
598
599 pub fn get_equipment(&self, equipment_slot: EquipmentSlot) -> Option<&ItemStack> {
602 let player = self.inventory_menu.as_player();
603 let item = match equipment_slot {
604 EquipmentSlot::Mainhand => {
605 let menu = self.menu();
606 let main_hand_slot_idx =
607 *menu.hotbar_slots_range().start() + self.selected_hotbar_slot as usize;
608 menu.slot(main_hand_slot_idx)?
609 }
610 EquipmentSlot::Offhand => &player.offhand,
611 EquipmentSlot::Feet => &player.armor[3],
612 EquipmentSlot::Legs => &player.armor[2],
613 EquipmentSlot::Chest => &player.armor[1],
614 EquipmentSlot::Head => &player.armor[0],
615 EquipmentSlot::Body => {
616 return None;
621 }
622 EquipmentSlot::Saddle => {
623 return None;
625 }
626 };
627 Some(item)
628 }
629}
630
631fn can_item_quick_replace(
632 target_slot: &ItemStack,
633 item: &ItemStack,
634 ignore_item_count: bool,
635) -> bool {
636 let ItemStack::Present(target_slot) = target_slot else {
637 return false;
638 };
639 let ItemStack::Present(item) = item else {
640 return false;
643 };
644
645 if !item.is_same_item_and_components(target_slot) {
646 return false;
647 }
648 let count = target_slot.count as u16
649 + if ignore_item_count {
650 0
651 } else {
652 item.count as u16
653 };
654 count <= item.kind.max_stack_size() as u16
655}
656
657fn get_quick_craft_slot_count(
658 quick_craft_slots: &HashSet<u16>,
659 quick_craft_kind: &QuickCraftKind,
660 item: &mut ItemStackData,
661 slot_item_count: i32,
662) {
663 item.count = match quick_craft_kind {
664 QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32,
665 QuickCraftKind::Right => 1,
666 QuickCraftKind::Middle => item.kind.max_stack_size(),
667 };
668 item.count += slot_item_count;
669}
670
671impl Default for Inventory {
672 fn default() -> Self {
673 Inventory {
674 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
675 id: 0,
676 container_menu: None,
677 container_menu_title: None,
678 carried: ItemStack::Empty,
679 state_id: 0,
680 quick_craft_status: QuickCraftStatusKind::Start,
681 quick_craft_kind: QuickCraftKind::Middle,
682 quick_craft_slots: HashSet::new(),
683 selected_hotbar_slot: 0,
684 }
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use azalea_inventory::SlotList;
691 use azalea_registry::builtin::ItemKind;
692
693 use super::*;
694
695 #[test]
696 fn test_simulate_shift_click_in_crafting_table() {
697 let spruce_planks = ItemStack::new(ItemKind::SprucePlanks, 4);
698
699 let mut inventory = Inventory {
700 inventory_menu: Menu::Player(azalea_inventory::Player::default()),
701 id: 1,
702 container_menu: Some(Menu::Crafting {
703 result: spruce_planks.clone(),
704 grid: SlotList::default(),
706 player: SlotList::default(),
707 }),
708 container_menu_title: None,
709 carried: ItemStack::Empty,
710 state_id: 0,
711 quick_craft_status: QuickCraftStatusKind::Start,
712 quick_craft_kind: QuickCraftKind::Middle,
713 quick_craft_slots: HashSet::new(),
714 selected_hotbar_slot: 0,
715 };
716
717 inventory.simulate_click(
718 &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }),
719 &PlayerAbilities::default(),
720 );
721
722 let new_slots = inventory.menu().slots();
723 assert_eq!(&new_slots[0], &ItemStack::Empty);
724 assert_eq!(
725 &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()],
726 &spruce_planks
727 );
728 }
729}