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