From 21d933cb11e562faa74f95c66472bc56bee80dd7 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Fri, 30 Sep 2022 14:06:39 +0100 Subject: [PATCH] Added support for 3x3 menus, improved Java Page API. --- .../com/willfp/eco/core/gui/GUIFactory.java | 5 +- .../com/willfp/eco/core/gui/menu/Menu.java | 39 ++++++++++++- .../willfp/eco/core/gui/menu/MenuBuilder.java | 16 ++++++ .../willfp/eco/core/gui/menu/MenuType.java | 56 +++++++++++++++++++ .../com/willfp/eco/core/gui/page/Page.java | 37 ++++++------ .../willfp/eco/core/gui/page/PageBuilder.java | 12 ++-- .../willfp/eco/core/gui/slot/FillerMask.java | 3 - .../com/willfp/eco/core/gui/slot/Slot.java | 1 + .../java/com/willfp/eco/util/MenuUtils.java | 39 ++++++++++--- .../willfp/eco/internal/gui/EcoGUIFactory.kt | 5 +- .../willfp/eco/internal/gui/menu/EcoMenu.kt | 40 ++++++++----- .../eco/internal/gui/menu/EcoMenuBuilder.kt | 28 ++++++---- .../gui/menu/MenuRenderedInventory.kt | 10 ++-- .../eco/internal/spigot/gui/GUIListener.kt | 4 +- 14 files changed, 226 insertions(+), 69 deletions(-) create mode 100644 eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuType.java diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/GUIFactory.java b/eco-api/src/main/java/com/willfp/eco/core/gui/GUIFactory.java index d98c3868..bb858d30 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/GUIFactory.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/GUIFactory.java @@ -3,6 +3,7 @@ package com.willfp.eco.core.gui; import com.willfp.eco.core.Eco; import com.willfp.eco.core.gui.menu.Menu; import com.willfp.eco.core.gui.menu.MenuBuilder; +import com.willfp.eco.core.gui.menu.MenuType; import com.willfp.eco.core.gui.slot.SlotBuilder; import com.willfp.eco.core.gui.slot.functional.SlotProvider; import org.bukkit.inventory.ItemStack; @@ -29,10 +30,12 @@ public interface GUIFactory { * Create menu builder. * * @param rows The amount of rows. + * @param type The type. * @return The builder. */ @NotNull - MenuBuilder createMenuBuilder(int rows); + MenuBuilder createMenuBuilder(int rows, + @NotNull MenuType type); /** * Combine the state of two menus together. diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java index cd0ed51c..f4ca2a33 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java @@ -27,6 +27,15 @@ public interface Menu { */ int getRows(); + /** + * Get the amount of columns. + * + * @return The amount of columns. + */ + default int getColumns() { + return 9; + } + /** * Get a static slot at a given row and column. *

@@ -81,6 +90,21 @@ public interface Menu { */ List getCaptiveItems(@NotNull Player player); + /** + * Get a captive item at a specific position. + * + * @param player The player. + * @param row The row. + * @param column The column. + * @return The captive item. + */ + @Nullable + default ItemStack getCaptiveItem(@NotNull final Player player, + final int row, + final int column) { + return null; + } + /** * Add state for a player. * @@ -196,6 +220,19 @@ public interface Menu { * @return The builder. */ static MenuBuilder builder(final int rows) { - return Eco.getHandler().getGUIFactory().createMenuBuilder(rows); + return Eco.getHandler().getGUIFactory().createMenuBuilder( + rows, + MenuType.NORMAL + ); + } + + /** + * Create a builder with a given type. + * + * @param type The menu type. + * @return The builder. + */ + static MenuBuilder builder(@NotNull final MenuType type) { + return Eco.getHandler().getGUIFactory().createMenuBuilder(type.getDefaultRows(), type); } } diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuBuilder.java b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuBuilder.java index 6b68afa1..c96d4821 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuBuilder.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuBuilder.java @@ -101,6 +101,22 @@ public interface MenuBuilder extends PageBuilder { return this.addComponent(MenuLayer.TOP, 1, 1, page); } + /** + * Add a page. + * + * @param pageNumber The page number. + * @param pageBuilder The page builder. + * @return The builder. + */ + default MenuBuilder addPage(final int pageNumber, + @NotNull final Consumer pageBuilder) { + MenuBuilder builder = Menu.builder(this.getRows()); + pageBuilder.accept(builder); + + Page page = new Page(pageNumber, builder.build()); + return this.addPage(page); + } + /** * Set the max pages. * diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuType.java b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuType.java new file mode 100644 index 00000000..a47d208d --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/MenuType.java @@ -0,0 +1,56 @@ +package com.willfp.eco.core.gui.menu; + +/** + * The type of menu. + */ +public enum MenuType { + /** + * Normal menu (1x9, 2x9, 3x9, etc). + */ + NORMAL(9, 6), + + /** + * Dispenser menu (3x3). + */ + DISPENSER(3, 3); + + /** + * The amount of columns. + */ + private final int columns; + + /** + * The default amount of rows. + */ + private final int defaultRows; + + /** + * Create a new menu type. + * + * @param columns The number of columns. + * @param defaultRows The default number of rows. + */ + MenuType(final int columns, + final int defaultRows) { + this.columns = columns; + this.defaultRows = defaultRows; + } + + /** + * Get the amount of columns. + * + * @return The columns. + */ + public int getColumns() { + return columns; + } + + /** + * Get the default amount of rows. + * + * @return The default amount of rows. + */ + public int getDefaultRows() { + return defaultRows; + } +} diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/page/Page.java b/eco-api/src/main/java/com/willfp/eco/core/gui/page/Page.java index 8cbfc18b..8f402b86 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/page/Page.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/page/Page.java @@ -3,14 +3,12 @@ package com.willfp.eco.core.gui.page; import com.willfp.eco.core.Eco; import com.willfp.eco.core.gui.component.GUIComponent; import com.willfp.eco.core.gui.menu.Menu; -import com.willfp.eco.core.gui.menu.MenuBuilder; import com.willfp.eco.core.gui.slot.Slot; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; -import java.util.function.Consumer; /** * A page is a component representing another menu. @@ -42,6 +40,16 @@ public final class Page implements GUIComponent { */ private Menu delegate = null; + /** + * The rows for the page to have. + */ + private int rows = 6; + + /** + * The columns for the page to have. + */ + private int columns = 9; + /** * Create a new page. * @@ -54,20 +62,6 @@ public final class Page implements GUIComponent { this.page = page; } - /** - * Create a new page. - * - * @param pageNumber The page number. - * @param page The base menu. - */ - public Page(final int pageNumber, - @NotNull final Consumer page) { - this.pageNumber = pageNumber; - MenuBuilder builder = Menu.builder(6); - page.accept(builder); - this.page = builder.build(); - } - /** * Get the current page number. * @@ -93,14 +87,21 @@ public final class Page implements GUIComponent { return page.getSlot(row, column, player, delegate); } + @Override + public void init(final int maxRows, + final int maxColumns) { + this.rows = maxRows; + this.columns = maxColumns; + } + @Override public int getRows() { - return page.getRows(); + return rows; } @Override public int getColumns() { - return 9; + return columns; } /** diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/page/PageBuilder.java b/eco-api/src/main/java/com/willfp/eco/core/gui/page/PageBuilder.java index f82fb462..7a6c5383 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/page/PageBuilder.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/page/PageBuilder.java @@ -1,19 +1,14 @@ package com.willfp.eco.core.gui.page; import com.willfp.eco.core.gui.component.GUIComponent; -import com.willfp.eco.core.gui.menu.CloseHandler; import com.willfp.eco.core.gui.menu.Menu; import com.willfp.eco.core.gui.menu.MenuLayer; -import com.willfp.eco.core.gui.menu.OpenHandler; import com.willfp.eco.core.gui.slot.FillerMask; import com.willfp.eco.core.gui.slot.Slot; import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryCloseEvent; import org.jetbrains.annotations.NotNull; import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; /** * Builder to create pages. @@ -26,6 +21,13 @@ public interface PageBuilder { */ int getRows(); + /** + * Get the amount of columns. + * + * @return The amount of columns. + */ + int getColumns(); + /** * Set a slot. * diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/slot/FillerMask.java b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/FillerMask.java index 6c9bb84a..3e2f1aa0 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/slot/FillerMask.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/FillerMask.java @@ -90,9 +90,6 @@ public class FillerMask implements GUIComponent { for (String patternRow : pattern) { int column = 0; - if (patternRow.length() != 9) { - throw new IllegalArgumentException("Invalid amount of columns in pattern!"); - } for (char c : patternRow.toCharArray()) { if (c == '0') { mask.get(row).set(column, null); diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java index f60d45c8..0294991c 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java +++ b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java @@ -9,6 +9,7 @@ import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.function.Function; diff --git a/eco-api/src/main/java/com/willfp/eco/util/MenuUtils.java b/eco-api/src/main/java/com/willfp/eco/util/MenuUtils.java index 98165f8d..b57a776f 100644 --- a/eco-api/src/main/java/com/willfp/eco/util/MenuUtils.java +++ b/eco-api/src/main/java/com/willfp/eco/util/MenuUtils.java @@ -3,14 +3,10 @@ package com.willfp.eco.util; import com.willfp.eco.core.Eco; import com.willfp.eco.core.gui.menu.Menu; import com.willfp.eco.core.tuples.Pair; -import org.apache.commons.lang.Validate; import org.bukkit.entity.Player; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.function.Function; - /** * Utilities / API methods for menus. */ @@ -23,9 +19,7 @@ public final class MenuUtils { */ @NotNull public static Pair convertSlotToRowColumn(final int slot) { - int row = Math.floorDiv(slot, 9); - int column = slot - row * 9; - return new Pair<>(row + 1, column + 1); + return convertSlotToRowColumn(slot, 9); } /** @@ -36,7 +30,36 @@ public final class MenuUtils { * @return The slot. */ public static int rowColumnToSlot(final int row, final int column) { - return (column - 1) + ((row - 1) * 9); + return rowColumnToSlot(row, column, 9); + } + + /** + * Convert 0-53 slot to row and column pair. + * + * @param slot The slot. + * @param columns The columns. + * @return The pair of row and columns. + */ + @NotNull + public static Pair convertSlotToRowColumn(final int slot, + final int columns) { + int row = Math.floorDiv(slot, columns); + int column = slot - row * columns; + return new Pair<>(row + 1, column + 1); + } + + /** + * Convert row and column to 0-53 slot. + * + * @param row The row. + * @param column The column. + * @param columns The columns in the menu. + * @return The slot. + */ + public static int rowColumnToSlot(final int row, + final int column, + final int columns) { + return (column - 1) + ((row - 1) * columns); } /** diff --git a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/EcoGUIFactory.kt b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/EcoGUIFactory.kt index f6aff538..fdfca628 100644 --- a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/EcoGUIFactory.kt +++ b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/EcoGUIFactory.kt @@ -3,6 +3,7 @@ package com.willfp.eco.internal.gui import com.willfp.eco.core.gui.GUIFactory import com.willfp.eco.core.gui.menu.Menu import com.willfp.eco.core.gui.menu.MenuBuilder +import com.willfp.eco.core.gui.menu.MenuType import com.willfp.eco.core.gui.slot.functional.SlotProvider import com.willfp.eco.internal.gui.menu.EcoMenuBuilder import com.willfp.eco.internal.gui.page.MergedStateMenu @@ -12,8 +13,8 @@ object EcoGUIFactory : GUIFactory { override fun createSlotBuilder(provider: SlotProvider) = EcoSlotBuilder(provider) - override fun createMenuBuilder(rows: Int): MenuBuilder = - EcoMenuBuilder(rows) + override fun createMenuBuilder(rows: Int, type: MenuType): MenuBuilder = + EcoMenuBuilder(rows, type.columns) override fun blendMenuState(base: Menu, additional: Menu): Menu = MergedStateMenu(base, additional) diff --git a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenu.kt b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenu.kt index 675aad1c..257c8274 100644 --- a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenu.kt +++ b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenu.kt @@ -14,6 +14,7 @@ import org.bukkit.Material import org.bukkit.NamespacedKey import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataType @@ -21,7 +22,8 @@ import org.bukkit.persistence.PersistentDataType @Suppress("UNCHECKED_CAST") class EcoMenu( private val rows: Int, - private val componentsAtPoints: Map>, + private val columns: Int, + private val componentsAtPoints: Map>, private val title: String, private val onClose: List, private val onRender: List<(Player, Menu) -> Unit>, @@ -29,12 +31,12 @@ class EcoMenu( private val signalHandlers: List> ) : Menu { private fun getPossiblyReactiveSlot(row: Int, column: Int, player: Player?, menu: Menu?): Slot { - if (row < 1 || row > this.rows || column < 1 || column > 9) { + if (row < 1 || row > this.rows || column < 1 || column > this.columns) { return emptyFillerSlot } - val anchor = Anchor(row, column) - val components = componentsAtPoints[anchor] ?: return emptyFillerSlot + val guiPosition = GUIPosition(row, column) + val components = componentsAtPoints[guiPosition] ?: return emptyFillerSlot for (component in components) { val found = if (player != null && menu != null) component.component.getSlotAt( @@ -62,7 +64,12 @@ class EcoMenu( getPossiblyReactiveSlot(row, column, player, menu) override fun open(player: Player): Inventory { - val inventory = Bukkit.createInventory(null, rows * 9, title) + val inventory = if (columns == 9) { + Bukkit.createInventory(null, rows * columns, title) + } else { + Bukkit.createInventory(null, InventoryType.DISPENSER, title) + } + player.forceMenuOpen(this) MenuHandler.registerInventory(inventory, this, player) @@ -83,17 +90,24 @@ class EcoMenu( MenuHandler.unregisterInventory(event.inventory) } - override fun getRows(): Int { - return rows - } + override fun getRows() = rows - override fun getTitle(): String { - return title - } + override fun getColumns() = columns + + override fun getTitle() = title override fun getCaptiveItems(player: Player): List { val inventory = player.openInventory.topInventory.asRenderedInventory() ?: return emptyList() - return inventory.captiveItems + return inventory.captiveItems.values.toList() + } + + override fun getCaptiveItem(player: Player, row: Int, column: Int): ItemStack? { + if (row < 1 || row > this.rows || column < 1 || column > this.columns) { + return null + } + + val inventory = player.openInventory.topInventory.asRenderedInventory() ?: return null + return inventory.captiveItems[GUIPosition(row, column)] } override fun sendSignal(player: Player, signal: Signal) { @@ -165,7 +179,7 @@ data class OffsetComponent( val columnOffset: Int ) -data class Anchor( +data class GUIPosition( val row: Int, val column: Int ) diff --git a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenuBuilder.kt b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenuBuilder.kt index c8b153ba..e36e7717 100644 --- a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenuBuilder.kt +++ b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/EcoMenuBuilder.kt @@ -12,15 +12,19 @@ import org.bukkit.entity.Player import java.util.function.BiConsumer import java.util.function.Consumer -class EcoMenuBuilder(private val rows: Int) : MenuBuilder { +class EcoMenuBuilder( + private val rows: Int, + private val columns: Int +) : MenuBuilder { private var title = "Menu" - private val components = mutableMapOf>>() + private val components = mutableMapOf>>() private val onClose = mutableListOf() private val onOpen = mutableListOf() private val onRender = mutableListOf<(Player, Menu) -> Unit>() private val signalHandlers = mutableListOf>() override fun getRows() = rows + override fun getColumns() = columns override fun setTitle(title: String): MenuBuilder { this.title = StringUtils.format(title) @@ -29,19 +33,19 @@ class EcoMenuBuilder(private val rows: Int) : MenuBuilder { override fun addComponent(layer: MenuLayer, row: Int, column: Int, component: GUIComponent): MenuBuilder { require(row in 1..rows) { "Invalid row number!" } - require(column in 1..9) { "Invalid column number!" } + require(column in 1..columns) { "Invalid column number!" } val maxRows = 1 + rows - row val maxColumns = 10 - column component.init(maxRows, maxColumns) - require(column + component.columns - 1 <= 9) { "Component is too large to be placed here!" } - require(row + component.rows - 1 <= getRows()) { "Component is too large to be placed here!" } + require(column + component.columns - 1 <= columns) { "Component is too large to be placed here!" } + require(row + component.rows - 1 <= rows) { "Component is too large to be placed here!" } - val anchor = Anchor(row, column) + val guiPosition = GUIPosition(row, column) components.computeIfAbsent(layer) { mutableMapOf() } - .computeIfAbsent(anchor) { mutableListOf() } += component + .computeIfAbsent(guiPosition) { mutableListOf() } += component return this } @@ -72,12 +76,12 @@ class EcoMenuBuilder(private val rows: Int) : MenuBuilder { } override fun build(): Menu { - val layeredComponents = mutableMapOf>>() + val layeredComponents = mutableMapOf>>() // 5 nested for loops? Shut up. Silence. Quiet. for (layer in MenuLayer.values()) { for (row in (1..rows)) { - for (column in (1..9)) { + for (column in (1..columns)) { for ((anchor, availableComponents) in components.computeIfAbsent(layer) { mutableMapOf() }) { for (component in availableComponents) { val rowOffset = 1 + row - anchor.row @@ -91,7 +95,7 @@ class EcoMenuBuilder(private val rows: Int) : MenuBuilder { continue } - val point = Anchor(row, column) + val point = GUIPosition(row, column) layeredComponents.computeIfAbsent(layer) { mutableMapOf() } .computeIfAbsent(point) { mutableListOf() } += OffsetComponent( @@ -105,7 +109,7 @@ class EcoMenuBuilder(private val rows: Int) : MenuBuilder { } } - val componentsAtPoints = mutableMapOf>() + val componentsAtPoints = mutableMapOf>() for (menuLayer in MenuLayer.values()) { for ((anchor, offsetComponents) in layeredComponents[menuLayer] ?: emptyMap()) { @@ -113,6 +117,6 @@ class EcoMenuBuilder(private val rows: Int) : MenuBuilder { } } - return EcoMenu(rows, componentsAtPoints, title, onClose, onRender, onOpen, signalHandlers) + return EcoMenu(rows, columns, componentsAtPoints, title, onClose, onRender, onOpen, signalHandlers) } } diff --git a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/MenuRenderedInventory.kt b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/MenuRenderedInventory.kt index 9a65b00b..ad8d0b4e 100644 --- a/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/MenuRenderedInventory.kt +++ b/eco-core/core-backend/src/main/kotlin/com/willfp/eco/internal/gui/menu/MenuRenderedInventory.kt @@ -26,13 +26,15 @@ class MenuRenderedInventory( val inventory: Inventory, val player: Player ) { - val captiveItems = mutableListOf() + val captiveItems = mutableMapOf() val state = mutableMapOf() fun render() { + captiveItems.clear() + for (row in (1..menu.rows)) { - for (column in (1..9)) { - val bukkit = MenuUtils.rowColumnToSlot(row, column) + for (column in (1..menu.columns)) { + val bukkit = MenuUtils.rowColumnToSlot(row, column, menu.columns) val slot = menu.getSlot(row, column, player, menu) val renderedItem = slot.getItemStack(player) @@ -50,7 +52,7 @@ class MenuRenderedInventory( continue } - captiveItems.add(itemStack) + captiveItems[GUIPosition(row, column)] = itemStack } else { inventory.setItem(bukkit, renderedItem) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/gui/GUIListener.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/gui/GUIListener.kt index 1784beb6..1801fc07 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/gui/GUIListener.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/gui/GUIListener.kt @@ -54,7 +54,7 @@ class GUIListener(private val plugin: EcoPlugin) : Listener { val menu = rendered.menu - val (row, column) = MenuUtils.convertSlotToRowColumn(event.slot) + val (row, column) = MenuUtils.convertSlotToRowColumn(event.slot, menu.columns) menu.getSlot(row, column, player, menu).handle(player, event, menu) @@ -77,7 +77,7 @@ class GUIListener(private val plugin: EcoPlugin) : Listener { val menu = inv.getMenu() ?: return - val (row, column) = MenuUtils.convertSlotToRowColumn(inv.firstEmpty()) + val (row, column) = MenuUtils.convertSlotToRowColumn(inv.firstEmpty(), menu.columns) val slot = menu.getSlot(row, column, player, menu)