Skip to content

Commit

Permalink
Add system to persist extra items in inventories
Browse files Browse the repository at this point in the history
Lets you let people keep their items when opening an inventory
  • Loading branch information
Aeltumn committed Apr 23, 2024
1 parent 54fe82c commit 541f85b
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,6 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
Reason.UNKNOWN,
Reason.PLUGIN
)

/** The possible valid slot range inside the player inventory. */
private val PLAYER_INVENTORY_RANGE = 0..40

/** The slot index used to indicate a click was outside the UI. */
private const val OUTSIDE_CHEST_INDEX = -999
}

/** Stores data for a single chat query. */
Expand Down Expand Up @@ -111,6 +105,12 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)

/** Updates the currently open interface for [playerId] to [view]. */
public fun setOpenInterface(playerId: UUID, view: PlayerInterfaceView?) {
// Save the contents of their currently shown inventory
val bukkitPlayer = Bukkit.getPlayer(playerId)
if (bukkitPlayer != null) {
saveInventoryContentsIfOpened(bukkitPlayer)
}

if (view == null) {
openPlayerInterfaceViews.invalidate(playerId)
} else {
Expand All @@ -119,8 +119,24 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
}
}

/**
* Saves any persistent items in the regular inventory of [player] if
* we are keeping persistent items intact.
*/
public fun saveInventoryContentsIfOpened(player: HumanEntity) {
// Saves any persistent items stored in the main inventory whenever we are currently
// showing a combined or player inventory before we draw the new one over-top
val currentlyShown = convertHolderToInterfaceView(player.openInventory.topInventory.holder)
if (currentlyShown != null && currentlyShown !is ChestInterfaceView) {
currentlyShown.savePersistentItems(player.inventory)
}
}

@EventHandler
public fun onOpen(event: InventoryOpenEvent) {
// Save previous inventory contents before we open the new one
saveInventoryContentsIfOpened(event.player)

val holder = event.inventory.holder
val view = convertHolderToInterfaceView(holder) ?: return

Expand All @@ -135,6 +151,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val view = holder as? AbstractInterfaceView<*, *> ?: return
val reason = event.reason

// Saves any persistent items stored in the given inventory before we close it
view.savePersistentItems(event.inventory)

SCOPE.launch {
// Mark the current view as closed properly
view.markClosed(reason)
Expand Down Expand Up @@ -209,11 +228,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
if (view is ChestInterfaceView) return

// Tally up all items that the player cannot modify and remove them from the drops
for (index in PLAYER_INVENTORY_RANGE) {
for (index in GridPoint.PLAYER_INVENTORY_RANGE) {
val stack = event.player.inventory.getItem(index) ?: continue
val x = index / 9
val adjustedX = PlayerPane.PANE_ORDERING.indexOf(x)
val point = GridPoint(adjustedX, index % 9)
val point = GridPoint.fromBukkitPlayerSlot(index) ?: continue
if (!canFreelyMove(view, point)) {
var removed = false

Expand Down Expand Up @@ -259,21 +276,10 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)

/** Extracts the clicked point from an inventory click event. */
private fun clickedPoint(event: InventoryClickEvent): GridPoint? {
// not really sure why this special handling is required,
// the ordered pane system should solve this but this is the only
// place where it's become an issue.
if (event.inventory.holder is Player) {
val index = event.slot
if (index !in PLAYER_INVENTORY_RANGE) return null

val x = index / 9
val adjustedX = PlayerPane.PANE_ORDERING.indexOf(x)
return GridPoint(adjustedX, index % 9)
return GridPoint.fromBukkitPlayerSlot(event.slot)
}

val index = event.rawSlot
if (index == OUTSIDE_CHEST_INDEX) return null
return GridPoint.at(index / 9, index % 9)
return GridPoint.fromBukkitChestSlot(event.slot)
}

/**
Expand All @@ -295,8 +301,8 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
/** Returns whether [clickedPoint] in [view] can be freely moved. */
private fun canFreelyMove(
view: AbstractInterfaceView<*, *>,
clickedPoint: GridPoint,
): Boolean = view.pane.getRaw(clickedPoint)?.clickHandler == null && !view.backing.properties.preventClickingEmptySlots
clickedPoint: GridPoint
): Boolean = view.pane.getRaw(clickedPoint) == null && !view.backing.properties.preventClickingEmptySlots

/** Handles a [view] being clicked at [clickedPoint] through some [event]. */
private fun handleClick(
Expand All @@ -307,10 +313,10 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
slot: Int
) {
// Determine the type of click, if nothing was clicked we allow it
val clickHandler = view.pane.getRaw(clickedPoint)?.clickHandler
val raw = view.pane.getRaw(clickedPoint)

// Optionally cancel clicking on other slots
if (clickHandler == null) {
if (raw == null) {
if (view.backing.properties.preventClickingEmptySlots) {
event.isCancelled = true
}
Expand All @@ -332,7 +338,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
.forEach { handler -> ClickHandler.process(handler, clickContext) }

// Run the click handler and deal with its result
val completedClickHandler = clickHandler
val completedClickHandler = raw.clickHandler
.run { CompletableClickHandler().apply { handle(clickContext) } }
.onComplete { ex ->
if (ex != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package com.noxcrew.interfaces.event

import com.noxcrew.interfaces.view.InterfaceView
import org.bukkit.entity.Player
import org.bukkit.event.HandlerList
import org.bukkit.event.player.PlayerEvent

/** An event emitted when the inventory of [player] is drawn. */
public class DrawPaneEvent(player: Player) : PlayerEvent(player) {
public class DrawPaneEvent(
player: Player,
/** The view that was drawn. */
public val view: InterfaceView,
/** Whether any slots in the regular inventory were drawn. */
public val isRegularInventory: Boolean,
/** Whether any slots in the player inventory were drawn. */
public val isPlayerInventory: Boolean
) : PlayerEvent(player) {

public companion object {
@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
package com.noxcrew.interfaces.grid

import com.noxcrew.interfaces.pane.PlayerPane

/** A 2-dimensional vector storing integer components. */
public data class GridPoint(val x: Int, val y: Int) {

public companion object {
/** The possible valid slot range inside the player inventory. */
public val PLAYER_INVENTORY_RANGE: IntRange = 0..40

/** The slot index used to indicate a click was outside the UI. */
public const val OUTSIDE_CHEST_INDEX: Int = -999

/** Returns the grid point for a [slot] in a player inventory. */
public fun fromBukkitPlayerSlot(slot: Int): GridPoint? {
if (slot !in PLAYER_INVENTORY_RANGE) return null
val x = slot / 9
val adjustedX = PlayerPane.PANE_ORDERING.indexOf(x)
return GridPoint(adjustedX, slot % 9)
}

/** Returns the grid point for a [slot] in a chest inventory. */
public fun fromBukkitChestSlot(slot: Int): GridPoint? {
if (slot == OUTSIDE_CHEST_INDEX) return null
return GridPoint(slot / 9, slot % 9)
}

/** Creates a new [GridPoint] for ([x], [y]). */
public fun at(x: Int, y: Int): GridPoint = GridPoint(x, y)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public abstract class AbstractInterfaceBuilder<P : Pane, I : Interface<P>> inter
/** Whether clicking on empty slots should be cancelled. */
public var preventClickingEmptySlots: Boolean = false

/**
* Persists items added to this pane in a previous instance.
* Particularly useful for player inventories, this allows the non-interface items
* to function as normal inventory items and be normally added/removed.
*/
public var persistAddedItems: Boolean = false

/** The properties object to use for the created interface. */
public val properties: InterfaceProperties<P>
get() = InterfaceProperties(
Expand All @@ -41,6 +48,7 @@ public abstract class AbstractInterfaceBuilder<P : Pane, I : Interface<P>> inter
itemPostProcessor,
preventClickingEmptySlots,
preventedInteractions,
persistAddedItems
)

/** Adds a new transform to the interface that updates whenever [triggers] change. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public data class InterfaceProperties<P : Pane>(
public val preventClickingEmptySlots: Boolean = false,
/** All interactions that will be ignored on this view and cancelled on pane items without calling the handler. */
public val preventedInteractions: Collection<Action> = emptySet(),
/** Persists items added to this pane in a previous instance. */
public val persistAddedItems: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import org.bukkit.entity.Player
/** A grid map of completed elements. */
internal open class CompletedPane : GridMap<CompletedElement> by HashGridMap() {
internal open fun getRaw(vector: GridPoint): CompletedElement? = get(vector)

internal open fun getRawUnordered(vector: GridPoint): CompletedElement? = get(vector)
}

/** A completed pane with an ordering. */
internal class CompletedOrderedPane(
private val ordering: List<Int>
) : CompletedPane() {

override fun getRaw(vector: GridPoint): CompletedElement? {
return get(ordering[vector.x], vector.y)
}
override fun getRaw(vector: GridPoint): CompletedElement? =
get(ordering[vector.x], vector.y)
}

/** Completes a pane for [player] by drawing each element while suspending. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.common.collect.HashMultimap
import com.noxcrew.interfaces.Constants.SCOPE
import com.noxcrew.interfaces.InterfacesListeners
import com.noxcrew.interfaces.event.DrawPaneEvent
import com.noxcrew.interfaces.grid.GridPoint
import com.noxcrew.interfaces.interfaces.Interface
import com.noxcrew.interfaces.inventory.InterfacesInventory
import com.noxcrew.interfaces.pane.CompletedPane
Expand All @@ -18,8 +19,11 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.withTimeout
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
import org.slf4j.LoggerFactory
import java.util.WeakHashMap
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -47,6 +51,9 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, P : Pane>(

private val children = WeakHashMap<AbstractInterfaceView<*, *>, Unit>()

/** Added persistent items added when this interface was last closed. */
private val addedItems = mutableMapOf<GridPoint, ItemStack>()

/** Whether the view is being painted for the first time. */
protected var firstPaint: Boolean = true

Expand Down Expand Up @@ -273,8 +280,43 @@ public abstract class AbstractInterfaceView<I : InterfacesInventory, P : Pane>(
)
madeChanges = true
}

// Apply the overlay of persistent items on top
if (backing.properties.persistAddedItems) {
for ((point, item) in addedItems) {
currentInventory.set(
point.x,
point.y,
item
)
madeChanges = true
}
}

if (madeChanges) {
Bukkit.getPluginManager().callEvent(DrawPaneEvent(player))
Bukkit.getPluginManager().callEvent(DrawPaneEvent(player, this, drawNormalInventory, drawPlayerInventory))
}
}

/** Saves any persistent items based on [inventory]. */
public fun savePersistentItems(inventory: Inventory) {
if (!backing.properties.persistAddedItems) return

addedItems.clear()
val contents = inventory.contents
for (index in contents.indices) {
// Ignore empty slots
val stack = contents[index] ?: continue
if (stack.type == Material.AIR) continue

// Find the slot that this item is in
val point = GridPoint.fromBukkitChestSlot(index) ?: continue

// Ignore any items that are in the pane itself
if (pane.getRawUnordered(point) != null) continue

// Store this item
addedItems[point] = stack
}
}

Expand Down

0 comments on commit 541f85b

Please sign in to comment.