Skip to main content

azalea_entity/
inventory.rs

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/// A local player's inventory and related data, including the container that
17/// they may have opened.
18#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
19#[derive(Clone, Debug)]
20pub struct Inventory {
21    /// The player's inventory menu. This is guaranteed to be a `Menu::Player`.
22    ///
23    /// We keep it as a [`Menu`] since `Menu` has some useful functions that
24    /// bare [`azalea_inventory::Player`] doesn't have.
25    pub inventory_menu: azalea_inventory::Menu,
26
27    /// The ID of the container that's currently open.
28    ///
29    /// Its value is not guaranteed to be anything specific, and it may change
30    /// every time you open a container (unless it's 0, in which case it
31    /// means that no container is open).
32    pub id: i32,
33    /// The current container menu that the player has open, or `None` if no
34    /// container is open.
35    pub container_menu: Option<azalea_inventory::Menu>,
36    /// The custom name of the menu that's currently open.
37    ///
38    /// This can only be `Some` when `container_menu` is `Some`.
39    pub container_menu_title: Option<FormattedText>,
40    /// The item that is currently held by the cursor, or `Slot::Empty` if
41    /// nothing is currently being held.
42    ///
43    /// This is different from [`Self::selected_hotbar_slot`], which is the
44    /// item that's selected in the hotbar.
45    pub carried: ItemStack,
46    /// An identifier used by the server to track client inventory desyncs.
47    ///
48    /// This is sent on every container click, and it's only ever updated when
49    /// the server sends a new container update.
50    pub state_id: u32,
51
52    pub quick_craft_status: QuickCraftStatusKind,
53    pub quick_craft_kind: QuickCraftKind,
54    /// A set of the indexes of the slots that have been right clicked in
55    /// this "quick craft".
56    pub quick_craft_slots: HashSet<u16>,
57
58    /// The index of the item in the hotbar that's currently being held by the
59    /// player. This must be in the range 0..=8.
60    ///
61    /// In a vanilla client this is changed by pressing the number keys or using
62    /// the scroll wheel.
63    pub selected_hotbar_slot: u8,
64}
65
66impl Inventory {
67    /// Returns a reference to the currently active menu.
68    ///
69    /// If a container is open then it'll return [`Self::container_menu`],
70    /// otherwise [`Self::inventory_menu`].
71    ///
72    /// Use [`Self::menu_mut`] if you need a mutable reference.
73    pub fn menu(&self) -> &azalea_inventory::Menu {
74        match &self.container_menu {
75            Some(menu) => menu,
76            _ => &self.inventory_menu,
77        }
78    }
79
80    /// Returns a mutable reference to the currently active menu.
81    ///
82    /// If a container is open then it'll return [`Self::container_menu`],
83    /// otherwise [`Self::inventory_menu`].
84    ///
85    /// Use [`Self::menu`] if you don't need a mutable reference.
86    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    /// Modify the inventory as if the given operation was performed on it.
94    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            // no carried item, reset
105            if self.carried.is_empty() {
106                return self.reset_quick_craft();
107            }
108            // if we were starting or ending, or now we aren't ending and the
109            // status changed, reset
110            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                    // minecraft also checks slot.may_place(carried) and
134                    // menu.can_drag_to(slot)
135                    // but they always return true so they're not relevant for
136                    // us
137                    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                        // if we only clicked one slot, then turn this
150                        // QuickCraftClick into a PickupClick
151                        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                                    // idk just do nothing i guess
163                                    return;
164                                }
165                            },
166                            player_abilities,
167                        );
168                        return;
169                    }
170
171                    let ItemStack::Present(mut carried) = self.carried.clone() else {
172                        // this should never happen
173                        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                                    // this always returns true in most cases
198                                    // && slot.may_place(item_stack)
199                                    && (
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                        // get the ItemStackData for the slot
209                        let ItemStack::Present(slot) = slot else {
210                            unreachable!("the loop above requires the slot to be present to break")
211                        };
212
213                        // if self.can_drag_to(slot) {
214                        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                        // we have to inline self.menu_mut() here to avoid the
235                        // borrow checker complaining
236                        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        // the quick craft status should always be in start if we're not in
249        // quick craft mode
250        if self.quick_craft_status != QuickCraftStatusKind::Start {
251            return self.reset_quick_craft();
252        }
253
254        match operation {
255            // left clicking outside inventory
256            ClickOperation::Pickup(PickupClick::Left { slot: None })
257                if self.carried.is_present() =>
258            {
259                // vanilla has `player.drop`s but they're only used
260                // server-side
261                // they're included as comments here in case you want to adapt
262                // this for a server implementation
263
264                // player.drop(self.carried, true);
265                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                // player.drop(item, true);
272            }
273            &ClickOperation::Pickup(
274                // lol
275                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                                    // swap slot_item and carried
322                                    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                                // slot.onTake(player, removed);
335                            }
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                                // slot.onTake(player, newSlot);
347                            }
348                        }
349                    }
350                }
351            }
352            &ClickOperation::QuickMove(
353                QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
354            ) => {
355                // in vanilla it also tests if QuickMove has a slot index of
356                // -999 but i don't think that's ever possible so it's not
357                // covered here
358                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                        // get the target_item but mutable
393                        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                            // if there's more than the max stack size in the
408                            // target slot
409
410                            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                            // if !self.inventory_menu.add(new_source_slot) {
415                            //     player.drop(new_source_slot, true);
416                            // }
417                        } else {
418                            // normal swap
419                            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                // player.drop(dropping, true);
468            }
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                                // get the checking_slot and checking_item again
505                                // but mutable
506                                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                                // now extend the carried item
511                                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    /// Get the item in the player's hotbar that is currently being held in
531    /// their main hand.
532    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    /// TODO: implement bundles
538    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        // vanilla calls .remove here but i think it has the same behavior as
589        // split?
590        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    /// Get the item at the given equipment slot, or `None` if the inventory
600    /// can't contain that slot.
601    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                // TODO: when riding entities is implemented, mount/horse
617                // inventories should be implemented too. note that horse
618                // inventories aren't a normal menu (they're not in MenuKind),
619                // maybe they should be a separate field in `Inventory`?
620                return None;
621            }
622            EquipmentSlot::Saddle => {
623                // TODO: implement riding entities, see above
624                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        // i *think* this is what vanilla does
641        // not 100% sure lol probably doesn't matter though
642        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                // simulate_click won't delete the items from here
705                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}