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 status
109            // 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 us
136                    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                        // if we only clicked one slot, then turn this
149                        // QuickCraftClick into a PickupClick
150                        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                                    // idk just do nothing i guess
162                                    return;
163                                }
164                            },
165                            player_abilities,
166                        );
167                        return;
168                    }
169
170                    let ItemStack::Present(mut carried) = self.carried.clone() else {
171                        // this should never happen
172                        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                                    // this always returns true in most cases
197                                    // && slot.may_place(item_stack)
198                                    && (
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                        // get the ItemStackData for the slot
208                        let ItemStack::Present(slot) = slot else {
209                            unreachable!("the loop above requires the slot to be present to break")
210                        };
211
212                        // if self.can_drag_to(slot) {
213                        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                        // we have to inline self.menu_mut() here to avoid the borrow checker
234                        // complaining
235                        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        // the quick craft status should always be in start if we're not in quick craft
248        // mode
249        if self.quick_craft_status != QuickCraftStatusKind::Start {
250            return self.reset_quick_craft();
251        }
252
253        match operation {
254            // left clicking outside inventory
255            ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
256                if self.carried.is_present() {
257                    // vanilla has `player.drop`s but they're only used
258                    // server-side
259                    // they're included as comments here in case you want to adapt this for a server
260                    // implementation
261
262                    // player.drop(self.carried, true);
263                    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                    // player.drop(item, true);
270                }
271            }
272            &ClickOperation::Pickup(
273                // lol
274                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                                    // swap slot_item and carried
321                                    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                                // slot.onTake(player, removed);
334                            }
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                                // slot.onTake(player, newSlot);
346                            }
347                        }
348                    }
349                }
350            }
351            &ClickOperation::QuickMove(
352                QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
353            ) => {
354                // in vanilla it also tests if QuickMove has a slot index of -999
355                // but i don't think that's ever possible so it's not covered here
356                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                        // get the target_item but mutable
391                        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                            // if there's more than the max stack size in the target slot
406
407                            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                            // if !self.inventory_menu.add(new_source_slot) {
412                            //     player.drop(new_source_slot, true);
413                            // }
414                        } else {
415                            // normal swap
416                            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                // player.drop(dropping, true);
465            }
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                                // get the checking_slot and checking_item again but mutable
502                                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                                // now extend the carried item
507                                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    /// Get the item in the player's hotbar that is currently being held in
527    /// their main hand.
528    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    /// TODO: implement bundles
534    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        // vanilla calls .remove here but i think it has the same behavior as split?
585        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    /// Get the item at the given equipment slot, or `None` if the inventory
595    /// can't contain that slot.
596    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                // TODO: when riding entities is implemented, mount/horse inventories should be
612                // implemented too. note that horse inventories aren't a normal menu (they're
613                // not in MenuKind), maybe they should be a separate field in `Inventory`?
614                return None;
615            }
616            EquipmentSlot::Saddle => {
617                // TODO: implement riding entities, see above
618                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        // i *think* this is what vanilla does
635        // not 100% sure lol probably doesn't matter though
636        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                // simulate_click won't delete the items from here
699                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}