diff --git a/src/main/kotlin/me/tech/mcchestui/GUI.kt b/src/main/kotlin/me/tech/mcchestui/GUI.kt index cd7fbda..743bea6 100644 --- a/src/main/kotlin/me/tech/mcchestui/GUI.kt +++ b/src/main/kotlin/me/tech/mcchestui/GUI.kt @@ -7,20 +7,18 @@ package me.tech.mcchestui -import me.tech.mcchestui.item.GUIItem +import me.tech.mcchestui.attached.* +import me.tech.mcchestui.item.* import me.tech.mcchestui.listeners.* -import me.tech.mcchestui.listeners.item.* import me.tech.mcchestui.listeners.hotbar.* -import me.tech.mcchestui.utils.GUICloseEvent -import me.tech.mcchestui.utils.GUIDragItemEvent -import me.tech.mcchestui.utils.GUIItemPickupEvent -import me.tech.mcchestui.utils.GUIItemPlaceEvent -import me.tech.mcchestui.utils.GUISlotClickEvent +import me.tech.mcchestui.listeners.item.* +import me.tech.mcchestui.utils.* import net.kyori.adventure.text.Component import org.bukkit.entity.Player import org.bukkit.event.* import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.PlayerInventory import org.bukkit.plugin.java.JavaPlugin /** @@ -31,24 +29,10 @@ import org.bukkit.plugin.java.JavaPlugin * @param y must be above 0 and less than rows. * @param type [GUIType] for the coordinates to map to. * @return [Int] representing the slot index. - * @throws IllegalArgumentException if x or y coordinates are invalid. */ fun toSlot(x: Int, y: Int, type: GUIType): Int { - if(x < 1 || y < 1) { - throw IllegalArgumentException("x or y cannot be below 1.") - } - - if(x > type.slotsPerRow) { - throw IllegalArgumentException("x must be between 1 and ${type.slotsPerRow}.") - } - - if(y > type.rows) { - throw IllegalArgumentException("y must be between 1 and ${type.slotsPerRow}.") - } - return (x - 1) + ((y - 1) * type.slotsPerRow) } -fun fromSlot(s: Int, type: GUIType) = Pair(s % type.slotsPerRow, s / type.slotsPerRow) /** * Construct a [GUI.Slot] to be placed in a [GUI]. @@ -62,11 +46,22 @@ fun GUI.guiSlot(builder: GUI.Slot.() -> Unit): GUI.Slot { } class GUI( - plugin: JavaPlugin, + internal val plugin: JavaPlugin, val title: Component, val type: GUIType, - private val render: GUI.() -> Unit + /** + * Bukkit [Inventory] of the [GUI]. + * + * Do not open [GUI] from this reference as it may + * lead to undefined behavior. + */ + val bukkitInventory: Inventory, + attached: Boolean, + private val render: GUI.() -> Unit = { } ) { + constructor(plugin: JavaPlugin, inventory: PlayerInventory) + : this(plugin, Component.empty(), GUIType.Chest(rows = 4), inventory, true) + /** * Allow for [ItemStack] to be placed in the [GUI]. */ @@ -127,12 +122,10 @@ class GUI( * Do not open [GUI] from this reference as it may * lead to undefined behavior. */ - val bukkitInventory: Inventory = if(type is GUIType.Chest) { - plugin.server.createInventory(null, type.slotsPerRow * type.rows, title) - } else { - plugin.server.createInventory(null, type.inventoryType, title) - } + /** + * Slots used by the [GUI]. + */ internal var slots = arrayOfNulls(type.slotsPerRow * type.rows) /** @@ -140,17 +133,39 @@ class GUI( */ internal var templateSlots = mutableMapOf() - private val eventListeners = listOf( - GUISlotClickListener(this), - - GUIItemPickupListener(this), - GUIItemPlaceListener(this), - GUIItemDragListener(this), + /** + * [PlayerInventory] attached to the [GUI] that acts + * like a [GUI]. + */ + internal var attachedGui: GUI? = null - GUIHotbarListener(this), + /** + * @return Whether another [GUI] has been attached to this one. + */ + val hasAttachedGui: Boolean + get() = attachedGui != null - GUICloseListener(this) - ) + /** + * [PlayerInventory] cache for when an [attachedGui] is present. + * Will by default use the [MemoryAttachedInventoryCache] which will lose data + * if the server closes while a player is actively using the [attachedGui]. + */ + internal var attachedInventoryCache: AttachedInventoryCache = MemoryAttachedInventoryCache + + private val eventListeners = if(!attached) { + listOf( + GUISlotClickListener(this), + GUICloseListener(this), + GUIItemPlaceListener(this), + GUIItemPickupListener(this), + GUIItemDragListener(this), + GUIHotbarListener(this) + ) + } else { + listOf( + GUISlotClickListener(this) + ) + } /** * Define weather the [GUI] has been unregistered. @@ -159,10 +174,7 @@ class GUI( internal var unregistered = false init { - eventListeners.forEach { - plugin.server.pluginManager - .registerEvents(it, plugin) - } + registerListeners(plugin) } /** @@ -193,7 +205,21 @@ class GUI( * @param slot slot */ fun slot(i: Int, slot: Slot) { - if(i > bukkitInventory.size) { + if(i >= bukkitInventory.size) { + // run slot in attached ui instead. + if(hasAttachedGui) { + var attachedUiIndex = i - (type.slotsPerRow * type.rows) + attachedUiIndex = when { + (attachedUiIndex + 9) >= 36 -> attachedUiIndex - 27 + else -> attachedUiIndex + 9 + } + + attachedGui?.slot( + attachedUiIndex, + slot + ) + } + return } @@ -318,6 +344,16 @@ class GUI( return bukkitInventory == other } + /** + * Register all [Listener] associated with the [GUI]. + */ + private fun registerListeners(plugin: JavaPlugin) { + eventListeners.forEach { + plugin.server.pluginManager + .registerEvents(it, plugin) + } + } + /** * Mark a [GUI] as unregistered and remove its [Listener]. */ diff --git a/src/main/kotlin/me/tech/mcchestui/GUIType.kt b/src/main/kotlin/me/tech/mcchestui/GUIType.kt index 4bac8c1..81425f5 100644 --- a/src/main/kotlin/me/tech/mcchestui/GUIType.kt +++ b/src/main/kotlin/me/tech/mcchestui/GUIType.kt @@ -1,19 +1,32 @@ package me.tech.mcchestui +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.Inventory + +sealed class GUIType( + val slotsPerRow: Int, + val rows: Int, + val inventoryType: InventoryType +) { + /** + * @return total amount of slots a [InventoryType] can hold. + */ + val totalSlots: Int + get() = slotsPerRow * rows + + internal open fun createBukkitInventory( + title: Component + ): Inventory { + return Bukkit.createInventory(null, inventoryType, title) + } -sealed interface GUIType { - val slotsPerRow: Int - val rows: Int - val inventoryType: InventoryType - - data class Chest(override val rows: Int) : GUIType { - override val slotsPerRow: Int - get() = 9 - - override val inventoryType: InventoryType - get() = InventoryType.CHEST - + class Chest(rows: Int) : GUIType( + slotsPerRow = 9, + rows, + inventoryType = InventoryType.CHEST + ) { init { if(rows < 1 || rows > 6) { throw IllegalArgumentException( @@ -21,27 +34,21 @@ sealed interface GUIType { ) } } - } - - data object Dispenser : GUIType { - override val slotsPerRow: Int - get() = 3 - override val rows: Int - get() = 3 - - override val inventoryType: InventoryType - get() = InventoryType.DISPENSER - } - - data object Hopper : GUIType { - override val slotsPerRow: Int - get() = 5 - - override val rows: Int - get() = 1 + override fun createBukkitInventory(title: Component): Inventory { + return Bukkit.createInventory(null, slotsPerRow * rows, title) + } + } - override val inventoryType: InventoryType - get() = InventoryType.HOPPER - } + data object Dispenser : GUIType( + slotsPerRow = 3, + rows = 3, + inventoryType = InventoryType.HOPPER + ) + + data object Hopper : GUIType( + slotsPerRow = 5, + rows = 1, + inventoryType = InventoryType.HOPPER + ) } \ No newline at end of file diff --git a/src/main/kotlin/me/tech/mcchestui/attached/AttachedGUI.kt b/src/main/kotlin/me/tech/mcchestui/attached/AttachedGUI.kt new file mode 100644 index 0000000..9fc859e --- /dev/null +++ b/src/main/kotlin/me/tech/mcchestui/attached/AttachedGUI.kt @@ -0,0 +1,29 @@ +package me.tech.mcchestui.attached + +import me.tech.mcchestui.GUI +import org.bukkit.entity.Player +import org.bukkit.inventory.PlayerInventory + +/** + * Attach a [PlayerInventory] to a [GUI] to be used as a + * secondary menu alongside the parent [GUI]. + * + * @param player [PlayerInventory] to be used. + */ +fun GUI.attach(player: Player) { + // cache inventory. + attachedInventoryCache.saveInventory(player) + player.inventory.storageContents = emptyArray() + + attach(player.inventory) +} + +/** + * Attach a [PlayerInventory] to a [GUI] to be used as a + * secondary menu alongside the parent [GUI]. + * + * @param playerInventory inventory to be used. + */ +private fun GUI.attach(playerInventory: PlayerInventory) { + attachedGui = GUI(plugin, playerInventory) +} \ No newline at end of file diff --git a/src/main/kotlin/me/tech/mcchestui/attached/AttachedInventoryCache.kt b/src/main/kotlin/me/tech/mcchestui/attached/AttachedInventoryCache.kt new file mode 100644 index 0000000..142dae5 --- /dev/null +++ b/src/main/kotlin/me/tech/mcchestui/attached/AttachedInventoryCache.kt @@ -0,0 +1,78 @@ +package me.tech.mcchestui.attached + +import me.tech.mcchestui.GUI +import org.bukkit.entity.Player + +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.PlayerInventory +import java.util.* + +/** + * Used to cache the items removed from a [Player] when + * a [GUI.attachedGui] is being used. + * + * The items of the [Player] are restored when the GUICloseEvent is called + * on the parent [GUI]. + * Contents of the [Player] inventory will then be restored. + */ +interface AttachedInventoryCache { + /** + * Save the storage contents of a [PlayerInventory] before the contents of + * the [GUI.attachedGui] override the [PlayerInventory]. + * + * @param player + * @return whether the operation was a success. + */ + fun saveInventory(player: Player): Boolean + + /** + * Restore the storage contents of a [PlayerInventory] to their state + * before being overridden by the [GUI.attachedGui]. + * + * @param player + * @return whether the operation was a success. + */ + fun restoreInventory(player: Player): Boolean +} + +/** + * Memory attached gui cache. + */ +internal object MemoryAttachedInventoryCache : AttachedInventoryCache { + private val cachedInventory = mutableMapOf>() + + override fun saveInventory(player: Player): Boolean { + cachedInventory[player.uniqueId] = player.inventory.storageContents + + return true + } + + override fun restoreInventory(player: Player): Boolean { + val inventory = cachedInventory[player.uniqueId] + ?: return false + + player.inventory.storageContents = inventory + cachedInventory.remove(player.uniqueId) + + return true + } +} + +/** + * Attach a custom [AttachedInventoryCache] to a [GUI]. + * By default, the [MemoryAttachedInventoryCache] is used. + */ +fun GUI.attachInventoryCache(cache: AttachedInventoryCache) { + attachedInventoryCache = cache +} + +/** + * Restore a players inventory from the [AttachedInventoryCache]. + * + * @param player player to cache. + * @param gui gui to cache for. + */ +internal fun restoreCachedInventory(player: Player, gui: GUI) { + player.inventory.storageContents = emptyArray() + gui.attachedInventoryCache.restoreInventory(player) +} \ No newline at end of file diff --git a/src/main/kotlin/me/tech/mcchestui/listeners/GUICloseListener.kt b/src/main/kotlin/me/tech/mcchestui/listeners/GUICloseListener.kt index deae2a3..5a2d590 100644 --- a/src/main/kotlin/me/tech/mcchestui/listeners/GUICloseListener.kt +++ b/src/main/kotlin/me/tech/mcchestui/listeners/GUICloseListener.kt @@ -1,6 +1,8 @@ package me.tech.mcchestui.listeners import me.tech.mcchestui.GUI +import me.tech.mcchestui.attached.restoreCachedInventory +import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.inventory.InventoryCloseEvent @@ -11,6 +13,14 @@ internal class GUICloseListener(gui: GUI) : GUIEventListener(gui) { return } + if(gui.hasAttachedGui) { + player.inventory.storageContents = emptyArray() + + gui.attachedGui?.unregister() + + restoreCachedInventory(player as Player, gui) + } + gui.onCloseInventory?.let { uiEvent -> uiEvent(this, player) } diff --git a/src/main/kotlin/me/tech/mcchestui/listeners/hotbar/GUIHotbarListener.kt b/src/main/kotlin/me/tech/mcchestui/listeners/hotbar/GUIHotbarListener.kt index 98e1dfb..82e5fad 100644 --- a/src/main/kotlin/me/tech/mcchestui/listeners/hotbar/GUIHotbarListener.kt +++ b/src/main/kotlin/me/tech/mcchestui/listeners/hotbar/GUIHotbarListener.kt @@ -2,6 +2,7 @@ package me.tech.mcchestui.listeners.hotbar import me.tech.mcchestui.GUI import me.tech.mcchestui.listeners.GUIEventListener +import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -11,10 +12,16 @@ import org.bukkit.event.inventory.InventoryClickEvent internal class GUIHotbarListener(gui: GUI) : GUIEventListener(gui) { @EventHandler(ignoreCancelled = true) internal fun InventoryClickEvent.hotBarSwitchToUIInventory() { + // we dont need hotbar swaps between two guis. if(!gui.isBukkitInventory(inventory)) { return } + if(gui.hasAttachedGui) { + isCancelled = true + return + } + // player inv -> gui inv if( action !in HOTBAR_ACTIONS @@ -54,10 +61,16 @@ internal class GUIHotbarListener(gui: GUI) : GUIEventListener(gui) { @EventHandler(ignoreCancelled = true) internal fun InventoryClickEvent.hotBarSwitchToPlayerInventory() { + // we dont need hotbar swaps between two guis. if(!gui.isBukkitInventory(inventory)) { return } + if(gui.hasAttachedGui) { + isCancelled = true + return + } + // gui inv -> player inv if( action !in HOTBAR_ACTIONS diff --git a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemDragListener.kt b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemDragListener.kt index 4afbab1..8952af8 100644 --- a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemDragListener.kt +++ b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemDragListener.kt @@ -14,7 +14,7 @@ internal class GUIItemDragListener(gui: GUI) : GUIEventListener(gui) { return } - if(!gui.allowItemPlacement) { + if(!gui.allowItemPlacement || gui.hasAttachedGui) { isCancelled = true return } diff --git a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPickupListener.kt b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPickupListener.kt index 5b0c3a7..63aac2a 100644 --- a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPickupListener.kt +++ b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPickupListener.kt @@ -16,6 +16,11 @@ internal class GUIItemPickupListener(gui: GUI): GUIEventListener(gui) { return } + if(gui.hasAttachedGui) { + isCancelled = true + return + } + // handle shift click if( action == InventoryAction.MOVE_TO_OTHER_INVENTORY diff --git a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPlaceListener.kt b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPlaceListener.kt index 915d98b..73834b2 100644 --- a/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPlaceListener.kt +++ b/src/main/kotlin/me/tech/mcchestui/listeners/item/GUIItemPlaceListener.kt @@ -16,6 +16,11 @@ internal class GUIItemPlaceListener(gui: GUI) : GUIEventListener(gui) { return } + if(gui.hasAttachedGui) { + isCancelled = true + return + } + if( action == InventoryAction.MOVE_TO_OTHER_INVENTORY && isShiftClick diff --git a/src/main/kotlin/me/tech/mcchestui/template/GUITemplate.kt b/src/main/kotlin/me/tech/mcchestui/template/GUITemplate.kt index ba8475f..d841146 100644 --- a/src/main/kotlin/me/tech/mcchestui/template/GUITemplate.kt +++ b/src/main/kotlin/me/tech/mcchestui/template/GUITemplate.kt @@ -6,15 +6,8 @@ import me.tech.mcchestui.guiSlot /** * Representing the structure of the GUI Template. */ -data class GUITemplate( - var firstRow: String? = null, - var secondRow: String? = null, - var thirdRow: String? = null, - var fourthRow: String? = null, - var fifthRow: String? = null, - var sixthRow: String? = null, - - var rows: String? = null +class GUITemplate( + private val rows: Array ) { /** * Converts [GUITemplate] into a [Map]. @@ -26,21 +19,23 @@ data class GUITemplate( * @return list of characters mapped to the row. */ internal fun toMap(): Map> { - return rowsMap() - .toMutableMap() - .apply { putAll(selectRowsMap().filterValues { it.isNotEmpty() }) } - } + if(rows.isEmpty()) { + return emptyMap() + } - private fun rowsMap(): Map> { - return rows - ?.split("\n") - ?.withIndex() - ?.associate { it.index + 1 to toCharList(it.value) } - ?: emptyMap() - } + if(rows.size == 1) { + val row = rows.first() + if(row.contains("\n")) { + return row + .split("\n") + .withIndex() + .associate { it.index + 1 to toCharList(it.value) } + } + + return mapOf(1 to toCharList(row)) + } - private fun selectRowsMap(): Map> { - return listOf(firstRow, secondRow, thirdRow, fourthRow, fifthRow, sixthRow) + return rows .withIndex() .associate { it.index + 1 to toCharList(it.value) } } @@ -81,10 +76,10 @@ fun GUI.addTemplateSlot(char: Char, slot: GUI.Slot) { /** * Structure the template of the [GUI]. * - * @param builder template builder + * @param template [GUI] string template. */ -fun GUI.template(builder: GUITemplate.() -> Unit) { - val map = GUITemplate().apply(builder) +fun GUI.template(vararg template: String) { + val map = GUITemplate(template) .toMap() for((yIndex, chars) in map) { diff --git a/src/main/kotlin/me/tech/mcchestui/utils/GUIHelper.kt b/src/main/kotlin/me/tech/mcchestui/utils/GUIHelper.kt index dc8aedf..bed62da 100644 --- a/src/main/kotlin/me/tech/mcchestui/utils/GUIHelper.kt +++ b/src/main/kotlin/me/tech/mcchestui/utils/GUIHelper.kt @@ -25,7 +25,7 @@ fun gui( type: GUIType, render: GUI.() -> Unit ): GUI { - return GUI(plugin, title, type, render).apply(render) + return GUI(plugin, title, type, type.createBukkitInventory(title), false, render).apply(render) } /** @@ -41,6 +41,11 @@ fun HumanEntity.openGUI(gui: GUI) { openInventory(gui.bukkitInventory) } +/** + * Anonymous function to construct new [GUI]. + */ +typealias GUIBuilder = GUI.() -> Unit + /** * Event when an [ItemStack] interaction is preformed with a [GUI]. */