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