Skip to content

Commit

Permalink
Implement Player Inventory as secondary GUI (#22)
Browse files Browse the repository at this point in the history
* feat: implement attached guis
Uses PlayerInventory as another GUI

* fix: cleanup gui template
Should work for Player inventories now.

* misc: attached gui cleanup

* misc: remove comment
  • Loading branch information
DebitCardz authored Nov 27, 2023
1 parent 691c54a commit 4ffaa71
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 102 deletions.
120 changes: 78 additions & 42 deletions src/main/kotlin/me/tech/mcchestui/GUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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].
Expand All @@ -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].
*/
Expand Down Expand Up @@ -127,30 +122,50 @@ 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<Slot>(type.slotsPerRow * type.rows)

/**
* Chars mapped to Slot Builders for GUI templating.
*/
internal var templateSlots = mutableMapOf<Char, Slot>()

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.
Expand All @@ -159,10 +174,7 @@ class GUI(
internal var unregistered = false

init {
eventListeners.forEach {
plugin.server.pluginManager
.registerEvents(it, plugin)
}
registerListeners(plugin)
}

/**
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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].
*/
Expand Down
73 changes: 40 additions & 33 deletions src/main/kotlin/me/tech/mcchestui/GUIType.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,54 @@
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(
"chest rows cannot be ${if(rows < 1) "below" else "above"} $rows."
)
}
}
}

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
)
}
29 changes: 29 additions & 0 deletions src/main/kotlin/me/tech/mcchestui/attached/AttachedGUI.kt
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<UUID, Array<ItemStack?>>()

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)
}
Loading

0 comments on commit 4ffaa71

Please sign in to comment.