aboutsummaryrefslogtreecommitdiff
path: root/azalea-inventory/src
diff options
context:
space:
mode:
Diffstat (limited to 'azalea-inventory/src')
-rw-r--r--azalea-inventory/src/item/mod.rs21
-rw-r--r--azalea-inventory/src/lib.rs172
-rw-r--r--azalea-inventory/src/operations.rs698
-rw-r--r--azalea-inventory/src/slot.rs146
4 files changed, 1037 insertions, 0 deletions
diff --git a/azalea-inventory/src/item/mod.rs b/azalea-inventory/src/item/mod.rs
new file mode 100644
index 00000000..07e51363
--- /dev/null
+++ b/azalea-inventory/src/item/mod.rs
@@ -0,0 +1,21 @@
+pub trait MaxStackSizeExt {
+ /// Get the maximum stack size for this item.
+ ///
+ /// This is a signed integer to be consistent with the `count` field of
+ /// [`ItemSlotData`].
+ fn max_stack_size(&self) -> i8;
+
+ /// Whether this item can be stacked with other items.
+ ///
+ /// This is equivalent to `self.max_stack_size() > 1`.
+ fn stackable(&self) -> bool {
+ self.max_stack_size() > 1
+ }
+}
+
+impl MaxStackSizeExt for azalea_registry::Item {
+ fn max_stack_size(&self) -> i8 {
+ // TODO: have the properties for every item defined somewhere
+ 64
+ }
+}
diff --git a/azalea-inventory/src/lib.rs b/azalea-inventory/src/lib.rs
new file mode 100644
index 00000000..518c7a1d
--- /dev/null
+++ b/azalea-inventory/src/lib.rs
@@ -0,0 +1,172 @@
+#![doc = include_str!("../README.md")]
+
+pub mod item;
+pub mod operations;
+mod slot;
+
+use std::ops::{Deref, DerefMut, RangeInclusive};
+
+use azalea_inventory_macros::declare_menus;
+pub use slot::{ItemSlot, ItemSlotData};
+
+// TODO: remove this here and in azalea-inventory-macros when rust makes
+// Default be implemented for all array sizes (since right now it's only up to
+// 32)
+
+/// A fixed-size list of [`ItemSlot`]s.
+#[derive(Debug, Clone)]
+pub struct SlotList<const N: usize>([ItemSlot; N]);
+impl<const N: usize> Deref for SlotList<N> {
+ type Target = [ItemSlot; N];
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+impl<const N: usize> DerefMut for SlotList<N> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+impl<const N: usize> Default for SlotList<N> {
+ fn default() -> Self {
+ SlotList([(); N].map(|_| ItemSlot::Empty))
+ }
+}
+
+impl Menu {
+ /// Get the [`Player`] from this [`Menu`].
+ ///
+ /// # Panics
+ ///
+ /// Will panic if the menu isn't `Menu::Player`.
+ pub fn as_player(&self) -> &Player {
+ if let Menu::Player(player) = &self {
+ player
+ } else {
+ unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.")
+ }
+ }
+}
+
+// the player inventory part is always the last 36 slots (except in the Player
+// menu), so we don't have to explicitly specify it
+
+// Client {
+// ...
+// pub menu: Menu,
+// pub inventory: Arc<[Slot; 36]>
+// }
+
+// Generate a `struct Player`, `enum Menu`, and `impl Menu`.
+// a "player" field gets implicitly added with the player inventory
+
+declare_menus! {
+ Player {
+ craft_result: 1,
+ craft: 4,
+ armor: 4,
+ inventory: 36,
+ offhand: 1,
+ },
+ Generic9x1 {
+ contents: 9,
+ },
+ Generic9x2 {
+ contents: 18,
+ },
+ Generic9x3 {
+ contents: 27,
+ },
+ Generic9x4 {
+ contents: 36,
+ },
+ Generic9x5 {
+ contents: 45,
+ },
+ Generic9x6 {
+ contents: 54,
+ },
+ Generic3x3 {
+ contents: 9,
+ },
+ Anvil {
+ first: 1,
+ second: 1,
+ result: 1,
+ },
+ Beacon {
+ payment: 1,
+ },
+ BlastFurnace {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ BrewingStand {
+ bottles: 3,
+ ingredient: 1,
+ fuel: 1,
+ },
+ Crafting {
+ result: 1,
+ grid: 9,
+ },
+ Enchantment {
+ item: 1,
+ lapis: 1,
+ },
+ Furnace {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ Grindstone {
+ input: 1,
+ additional: 1,
+ result: 1,
+ },
+ Hopper {
+ contents: 5,
+ },
+ Lectern {
+ book: 1,
+ },
+ Loom {
+ banner: 1,
+ dye: 1,
+ pattern: 1,
+ result: 1,
+ },
+ Merchant {
+ payments: 2,
+ result: 1,
+ },
+ ShulkerBox {
+ contents: 27,
+ },
+ LegacySmithing {
+ input: 1,
+ additional: 1,
+ result: 1,
+ },
+ Smithing {
+ template: 1,
+ base: 1,
+ additional: 1,
+ result: 1,
+ },
+ Smoker {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ CartographyTable {
+ map: 1,
+ additional: 1,
+ result: 1,
+ },
+ Stonecutter {
+ input: 1,
+ result: 1,
+ },
+}
diff --git a/azalea-inventory/src/operations.rs b/azalea-inventory/src/operations.rs
new file mode 100644
index 00000000..1379b8a9
--- /dev/null
+++ b/azalea-inventory/src/operations.rs
@@ -0,0 +1,698 @@
+use std::ops::RangeInclusive;
+
+use azalea_buf::McBuf;
+
+use crate::{
+ item::MaxStackSizeExt, AnvilMenuLocation, BeaconMenuLocation, BlastFurnaceMenuLocation,
+ BrewingStandMenuLocation, CartographyTableMenuLocation, CraftingMenuLocation,
+ EnchantmentMenuLocation, FurnaceMenuLocation, Generic3x3MenuLocation, Generic9x1MenuLocation,
+ Generic9x2MenuLocation, Generic9x3MenuLocation, Generic9x4MenuLocation, Generic9x5MenuLocation,
+ Generic9x6MenuLocation, GrindstoneMenuLocation, HopperMenuLocation, ItemSlot, ItemSlotData,
+ LecternMenuLocation, LegacySmithingMenuLocation, LoomMenuLocation, Menu, MenuLocation,
+ MerchantMenuLocation, Player, PlayerMenuLocation, ShulkerBoxMenuLocation, SmithingMenuLocation,
+ SmokerMenuLocation, StonecutterMenuLocation,
+};
+
+#[derive(Debug, Clone)]
+pub enum ClickOperation {
+ Pickup(PickupClick),
+ QuickMove(QuickMoveClick),
+ Swap(SwapClick),
+ Clone(CloneClick),
+ Throw(ThrowClick),
+ QuickCraft(QuickCraftClick),
+ PickupAll(PickupAllClick),
+}
+
+#[derive(Debug, Clone)]
+pub enum PickupClick {
+ /// Left mouse click. Note that in the protocol, None is represented as
+ /// -999.
+ Left { slot: Option<u16> },
+ /// Right mouse click. Note that in the protocol, None is represented as
+ /// -999.
+ Right { slot: Option<u16> },
+ /// Drop cursor stack.
+ LeftOutside,
+ /// Drop cursor single item.
+ RightOutside,
+}
+impl From<PickupClick> for ClickOperation {
+ fn from(click: PickupClick) -> Self {
+ ClickOperation::Pickup(click)
+ }
+}
+
+/// Shift click
+#[derive(Debug, Clone)]
+pub enum QuickMoveClick {
+ /// Shift + left mouse click
+ Left { slot: u16 },
+ /// Shift + right mouse click (identical behavior)
+ Right { slot: u16 },
+}
+impl From<QuickMoveClick> for ClickOperation {
+ fn from(click: QuickMoveClick) -> Self {
+ ClickOperation::QuickMove(click)
+ }
+}
+
+/// Used when you press number keys or F in an inventory.
+#[derive(Debug, Clone)]
+pub struct SwapClick {
+ pub source_slot: u16,
+ pub target_slot: u8,
+}
+
+impl From<SwapClick> for ClickOperation {
+ fn from(click: SwapClick) -> Self {
+ ClickOperation::Swap(click)
+ }
+}
+/// Middle click, only defined for creative players in non-player
+/// inventories.
+#[derive(Debug, Clone)]
+pub struct CloneClick {
+ pub slot: u16,
+}
+impl From<CloneClick> for ClickOperation {
+ fn from(click: CloneClick) -> Self {
+ ClickOperation::Clone(click)
+ }
+}
+#[derive(Debug, Clone)]
+pub enum ThrowClick {
+ /// Drop key (Q)
+ Single { slot: u16 },
+ /// Ctrl + drop key (Q)
+ All { slot: u16 },
+}
+impl From<ThrowClick> for ClickOperation {
+ fn from(click: ThrowClick) -> Self {
+ ClickOperation::Throw(click)
+ }
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct QuickCraftClick {
+ pub kind: QuickCraftKind,
+ pub status: QuickCraftStatus,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftKind {
+ Left,
+ Right,
+ Middle,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftStatusKind {
+ /// Starting drag
+ Start,
+ /// Add slot
+ Add,
+ /// Ending drag
+ End,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftStatus {
+ /// Starting drag
+ Start,
+ /// Add a slot.
+ Add { slot: u16 },
+ /// Ending drag
+ End,
+}
+impl From<QuickCraftStatus> for QuickCraftStatusKind {
+ fn from(status: QuickCraftStatus) -> Self {
+ match status {
+ QuickCraftStatus::Start => QuickCraftStatusKind::Start,
+ QuickCraftStatus::Add { .. } => QuickCraftStatusKind::Add,
+ QuickCraftStatus::End => QuickCraftStatusKind::End,
+ }
+ }
+}
+
+/// Double click
+#[derive(Debug, Clone)]
+pub struct PickupAllClick {
+ /// The slot that we're double clicking on. It should be empty or at least
+ /// not pickup-able (since the carried item is used as the filter).
+ pub slot: u16,
+ /// Impossible in vanilla clients.
+ pub reversed: bool,
+}
+impl From<PickupAllClick> for ClickOperation {
+ fn from(click: PickupAllClick) -> Self {
+ ClickOperation::PickupAll(click)
+ }
+}
+
+impl ClickOperation {
+ /// Return the slot number that this operation is acting on, if any.
+ ///
+ /// Note that in the protocol, "None" is represented as -999.
+ pub fn slot_num(&self) -> Option<u16> {
+ match self {
+ ClickOperation::Pickup(pickup) => match pickup {
+ PickupClick::Left { slot } => *slot,
+ PickupClick::Right { slot } => *slot,
+ PickupClick::LeftOutside => None,
+ PickupClick::RightOutside => None,
+ },
+ ClickOperation::QuickMove(quick_move) => match quick_move {
+ QuickMoveClick::Left { slot } => Some(*slot),
+ QuickMoveClick::Right { slot } => Some(*slot),
+ },
+ ClickOperation::Swap(swap) => Some(swap.source_slot),
+ ClickOperation::Clone(clone) => Some(clone.slot),
+ ClickOperation::Throw(throw) => match throw {
+ ThrowClick::Single { slot } => Some(*slot),
+ ThrowClick::All { slot } => Some(*slot),
+ },
+ ClickOperation::QuickCraft(quick_craft) => match quick_craft.status {
+ QuickCraftStatus::Start => None,
+ QuickCraftStatus::Add { slot } => Some(slot),
+ QuickCraftStatus::End => None,
+ },
+ ClickOperation::PickupAll(pickup_all) => Some(pickup_all.slot),
+ }
+ }
+
+ pub fn button_num(&self) -> u8 {
+ match self {
+ ClickOperation::Pickup(pickup) => match pickup {
+ PickupClick::Left { .. } => 0,
+ PickupClick::Right { .. } => 1,
+ PickupClick::LeftOutside => 0,
+ PickupClick::RightOutside => 1,
+ },
+ ClickOperation::QuickMove(quick_move) => match quick_move {
+ QuickMoveClick::Left { .. } => 0,
+ QuickMoveClick::Right { .. } => 1,
+ },
+ ClickOperation::Swap(swap) => swap.target_slot,
+ ClickOperation::Clone(_) => 2,
+ ClickOperation::Throw(throw) => match throw {
+ ThrowClick::Single { .. } => 0,
+ ThrowClick::All { .. } => 1,
+ },
+ ClickOperation::QuickCraft(quick_craft) => match quick_craft {
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::Start,
+ } => 0,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::Start,
+ } => 4,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::Start,
+ } => 8,
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::Add { .. },
+ } => 1,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::Add { .. },
+ } => 5,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::Add { .. },
+ } => 9,
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::End,
+ } => 2,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::End,
+ } => 6,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::End,
+ } => 10,
+ },
+ ClickOperation::PickupAll(_) => 0,
+ }
+ }
+
+ pub fn click_type(&self) -> ClickType {
+ match self {
+ ClickOperation::Pickup(_) => ClickType::Pickup,
+ ClickOperation::QuickMove(_) => ClickType::QuickMove,
+ ClickOperation::Swap(_) => ClickType::Swap,
+ ClickOperation::Clone(_) => ClickType::Clone,
+ ClickOperation::Throw(_) => ClickType::Throw,
+ ClickOperation::QuickCraft(_) => ClickType::QuickCraft,
+ ClickOperation::PickupAll(_) => ClickType::PickupAll,
+ }
+ }
+}
+
+#[derive(McBuf, Clone, Copy, Debug)]
+pub enum ClickType {
+ Pickup = 0,
+ QuickMove = 1,
+ Swap = 2,
+ Clone = 3,
+ Throw = 4,
+ QuickCraft = 5,
+ PickupAll = 6,
+}
+
+impl Menu {
+ /// Shift-click a slot in this menu.
+ pub fn quick_move_stack(&mut self, slot_index: usize) -> ItemSlot {
+ let slot = self.slot(slot_index);
+ if slot.is_none() {
+ return ItemSlot::Empty;
+ };
+
+ let slot_location = self
+ .location_for_slot(slot_index)
+ .expect("we just checked to make sure the slot is Some above, so this shouldn't be able to error");
+ match slot_location {
+ MenuLocation::Player(l) => match l {
+ PlayerMenuLocation::CraftResult => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ PlayerMenuLocation::Craft => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ PlayerMenuLocation::Armor => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ _ => {
+ // TODO: armor handling (see quickMoveStack in
+ // InventoryMenu.java)
+
+ // if slot.kind().is_armor() &&
+
+ // also offhand handling
+
+ if l == PlayerMenuLocation::Inventory {
+ // shift-clicking in hotbar moves to inventory, and vice versa
+ if Player::is_hotbar_slot(slot_index) {
+ self.try_move_item_to_slots(
+ slot_index,
+ Player::INVENTORY_WITHOUT_HOTBAR_SLOTS,
+ );
+ } else {
+ self.try_move_item_to_slots(slot_index, Player::HOTBAR_SLOTS);
+ }
+ } else {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ }
+ },
+ MenuLocation::Generic9x1(l) => match l {
+ Generic9x1MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x1MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X1_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x2(l) => match l {
+ Generic9x2MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x2MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X2_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x3(l) => match l {
+ Generic9x3MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x3MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X3_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x4(l) => match l {
+ Generic9x4MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x4MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X4_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x5(l) => match l {
+ Generic9x5MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x5MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X5_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x6(l) => match l {
+ Generic9x6MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x6MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X6_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic3x3(l) => match l {
+ Generic3x3MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic3x3MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC3X3_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Anvil(l) => match l {
+ AnvilMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::ANVIL_FIRST_SLOT..=Menu::ANVIL_SECOND_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Beacon(l) => match l {
+ BeaconMenuLocation::Payment => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ BeaconMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::BEACON_PAYMENT_SLOT..=Menu::BEACON_PAYMENT_SLOT,
+ );
+ }
+ },
+ MenuLocation::BlastFurnace(l) => match l {
+ BlastFurnaceMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::BLAST_FURNACE_INGREDIENT_SLOT..=Menu::BLAST_FURNACE_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::BrewingStand(l) => match l {
+ BrewingStandMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ *Menu::BREWING_STAND_BOTTLES_SLOTS.start()
+ ..=Menu::BREWING_STAND_INGREDIENT_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Crafting(l) => match l {
+ CraftingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::CRAFTING_GRID_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Enchantment(l) => match l {
+ EnchantmentMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::ENCHANTMENT_ITEM_SLOT..=Menu::ENCHANTMENT_LAPIS_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Furnace(l) => match l {
+ FurnaceMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::FURNACE_INGREDIENT_SLOT..=Menu::FURNACE_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Grindstone(l) => match l {
+ GrindstoneMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GRINDSTONE_INPUT_SLOT..=Menu::GRINDSTONE_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Hopper(l) => match l {
+ HopperMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::HOPPER_CONTENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Lectern(l) => match l {
+ LecternMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LECTERN_BOOK_SLOT..=Menu::LECTERN_BOOK_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Loom(l) => match l {
+ LoomMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LOOM_BANNER_SLOT..=Menu::LOOM_PATTERN_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Merchant(l) => match l {
+ MerchantMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::MERCHANT_PAYMENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::ShulkerBox(l) => match l {
+ ShulkerBoxMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::SHULKER_BOX_CONTENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::LegacySmithing(l) => match l {
+ LegacySmithingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LEGACY_SMITHING_INPUT_SLOT..=Menu::LEGACY_SMITHING_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Smithing(l) => match l {
+ SmithingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::SMITHING_TEMPLATE_SLOT..=Menu::SMITHING_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Smoker(l) => match l {
+ SmokerMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::SMOKER_INGREDIENT_SLOT..=Menu::SMOKER_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::CartographyTable(l) => match l {
+ CartographyTableMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::CARTOGRAPHY_TABLE_MAP_SLOT..=Menu::CARTOGRAPHY_TABLE_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Stonecutter(l) => match l {
+ StonecutterMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::STONECUTTER_INPUT_SLOT..=Menu::STONECUTTER_INPUT_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ }
+
+ ItemSlot::Empty
+ }
+
+ fn try_move_item_to_slots_or_toggle_hotbar(
+ &mut self,
+ slot_index: usize,
+ target_slot_indexes: RangeInclusive<usize>,
+ ) {
+ if !self.try_move_item_to_slots(slot_index, target_slot_indexes) {
+ self.try_move_item_to_slots(
+ slot_index,
+ if self.is_hotbar_slot(slot_index) {
+ self.player_slots_without_hotbar_range()
+ } else {
+ self.hotbar_slots_range()
+ },
+ );
+ }
+ }
+
+ /// Whether the given item could be placed in this menu.
+ ///
+ /// TODO: right now this always returns true
+ pub fn may_place(&self, _target_slot_index: usize, _item: &ItemSlotData) -> bool {
+ true
+ }
+
+ /// Whether the item in the given slot could be clicked and picked up.
+ /// TODO: right now this always returns true
+ pub fn may_pickup(&self, _source_slot_index: usize) -> bool {
+ true
+ }
+
+ /// Get the maximum number of items that can be placed in this slot.
+ pub fn max_stack_size(&self, _target_slot_index: usize) -> u8 {
+ 64
+ }
+
+ /// Try moving an item to a set of slots in this menu.
+ ///
+ /// Returns the updated item slot.
+ fn try_move_item_to_slots(
+ &mut self,
+ item_slot_index: usize,
+ target_slot_indexes: RangeInclusive<usize>,
+ ) -> bool {
+ let mut item_slot = self.slot(item_slot_index).unwrap().clone();
+
+ // first see if we can stack it with another item
+ if item_slot.kind().stackable() {
+ for target_slot_index in target_slot_indexes.clone() {
+ self.move_item_to_slot_if_stackable(&mut item_slot, target_slot_index);
+ if item_slot.is_empty() {
+ break;
+ }
+ }
+ }
+
+ // and if not then just try putting it in an empty slot
+ if item_slot.is_present() {
+ for target_slot_index in target_slot_indexes {
+ self.move_item_to_slot_if_empty(&mut item_slot, target_slot_index);
+ if item_slot.is_empty() {
+ break;
+ }
+ }
+ }
+
+ item_slot.is_empty()
+ }
+
+ /// Merge this item slot into the target item slot, only if the target item
+ /// slot is present and the same item.
+ fn move_item_to_slot_if_stackable(
+ &mut self,
+ item_slot: &mut ItemSlot,
+ target_slot_index: usize,
+ ) {
+ let ItemSlot::Present(item) = item_slot else {
+ return;
+ };
+ let target_slot = self.slot(target_slot_index).unwrap();
+ if let ItemSlot::Present(target_item) = target_slot {
+ // the target slot is empty, so we can just move the item there
+ if self.may_place(target_slot_index, item) && target_item.is_same_item_and_nbt(item) {
+ let slot_item_limit = self.max_stack_size(target_slot_index);
+ let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
+
+ // get the target slot again but mut this time so we can update it
+ let target_slot = self.slot_mut(target_slot_index).unwrap();
+ *target_slot = ItemSlot::Present(new_target_slot_data);
+
+ item_slot.update_empty();
+ }
+ }
+ }
+
+ fn move_item_to_slot_if_empty(&mut self, item_slot: &mut ItemSlot, target_slot_index: usize) {
+ let ItemSlot::Present(item) = item_slot else {
+ return;
+ };
+ let target_slot = self.slot(target_slot_index).unwrap();
+ if target_slot.is_empty() && self.may_place(target_slot_index, item) {
+ let slot_item_limit = self.max_stack_size(target_slot_index);
+ let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
+
+ let target_slot = self.slot_mut(target_slot_index).unwrap();
+ *target_slot = ItemSlot::Present(new_target_slot_data);
+ item_slot.update_empty();
+ }
+ }
+}
diff --git a/azalea-inventory/src/slot.rs b/azalea-inventory/src/slot.rs
new file mode 100644
index 00000000..cef555d7
--- /dev/null
+++ b/azalea-inventory/src/slot.rs
@@ -0,0 +1,146 @@
+use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
+use azalea_nbt::Nbt;
+use std::io::{Cursor, Write};
+
+/// Either an item in an inventory or nothing.
+#[derive(Debug, Clone, Default, PartialEq)]
+pub enum ItemSlot {
+ #[default]
+ Empty,
+ Present(ItemSlotData),
+}
+
+impl ItemSlot {
+ /// Check if the slot is ItemSlot::Empty, if the count is <= 0, or if the
+ /// item is air.
+ ///
+ /// This is the opposite of [`ItemSlot::is_present`].
+ pub fn is_empty(&self) -> bool {
+ match self {
+ ItemSlot::Empty => true,
+ ItemSlot::Present(item) => item.is_empty(),
+ }
+ }
+ /// Check if the slot is not ItemSlot::Empty, if the count is > 0, and if
+ /// the item is not air.
+ ///
+ /// This is the opposite of [`ItemSlot::is_empty`].
+ pub fn is_present(&self) -> bool {
+ !self.is_empty()
+ }
+
+ /// Return the amount of the item in the slot, or 0 if the slot is empty.
+ ///
+ /// Note that it's possible for the count to be zero or negative when the
+ /// slot is present.
+ pub fn count(&self) -> i8 {
+ match self {
+ ItemSlot::Empty => 0,
+ ItemSlot::Present(i) => i.count,
+ }
+ }
+
+ /// Remove `count` items from this slot, returning the removed items.
+ pub fn split(&mut self, count: u8) -> ItemSlot {
+ match self {
+ ItemSlot::Empty => ItemSlot::Empty,
+ ItemSlot::Present(i) => {
+ let returning = i.split(count);
+ if i.is_empty() {
+ *self = ItemSlot::Empty;
+ }
+ ItemSlot::Present(returning)
+ }
+ }
+ }
+
+ /// Get the `kind` of the item in this slot, or
+ /// [`azalea_registry::Item::Air`]
+ pub fn kind(&self) -> azalea_registry::Item {
+ match self {
+ ItemSlot::Empty => azalea_registry::Item::Air,
+ ItemSlot::Present(i) => i.kind,
+ }
+ }
+
+ /// Update whether this slot is empty, based on the count.
+ pub fn update_empty(&mut self) {
+ if let ItemSlot::Present(i) = self {
+ if i.is_empty() {
+ *self = ItemSlot::Empty;
+ }
+ }
+ }
+}
+
+/// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`]
+/// or [`azalea_registry::Item`] instead.
+#[derive(Debug, Clone, McBuf, PartialEq)]
+pub struct ItemSlotData {
+ pub kind: azalea_registry::Item,
+ /// The amount of the item in this slot.
+ ///
+ /// The count can be zero or negative, but this is rare.
+ pub count: i8,
+ pub nbt: Nbt,
+}
+
+impl ItemSlotData {
+ /// Remove `count` items from this slot, returning the removed items.
+ pub fn split(&mut self, count: u8) -> ItemSlotData {
+ let returning_count = i8::min(count as i8, self.count);
+ let mut returning = self.clone();
+ returning.count = returning_count;
+ self.count -= returning_count;
+ returning
+ }
+
+ /// Check if the count of the item is <= 0 or if the item is air.
+ pub fn is_empty(&self) -> bool {
+ self.count <= 0 || self.kind == azalea_registry::Item::Air
+ }
+
+ /// Whether this item is the same as another item, ignoring the count.
+ ///
+ /// ```
+ /// # use azalea_inventory::ItemSlotData;
+ /// # use azalea_registry::Item;
+ /// let mut a = ItemSlotData {
+ /// kind: Item::Stone,
+ /// count: 1,
+ /// nbt: Default::default(),
+ /// };
+ /// let mut b = ItemSlotData {
+ /// kind: Item::Stone,
+ /// count: 2,
+ /// nbt: Default::default(),
+ /// };
+ /// assert!(a.is_same_item_and_nbt(&b));
+ ///
+ /// b.kind = Item::Dirt;
+ /// assert!(!a.is_same_item_and_nbt(&b));
+ /// ```
+ pub fn is_same_item_and_nbt(&self, other: &ItemSlotData) -> bool {
+ self.kind == other.kind && self.nbt == other.nbt
+ }
+}
+
+impl McBufReadable for ItemSlot {
+ fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
+ let slot = Option::<ItemSlotData>::read_from(buf)?;
+ Ok(slot.map_or(ItemSlot::Empty, ItemSlot::Present))
+ }
+}
+
+impl McBufWritable for ItemSlot {
+ fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
+ match self {
+ ItemSlot::Empty => false.write_into(buf)?,
+ ItemSlot::Present(i) => {
+ true.write_into(buf)?;
+ i.write_into(buf)?;
+ }
+ };
+ Ok(())
+ }
+}