Skip to content

Commit

Permalink
Build out support for a lot more non-standard usages
Browse files Browse the repository at this point in the history
- Support off hand interaction
- Disable dropping player inv items
- Allow having items in player invs that are not protected
- Remove interface items on death
- Re-open interface on respawn
  • Loading branch information
Aeltumn committed Apr 23, 2024
1 parent a8a5830 commit 97cb3c9
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 43 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ subprojects {

// Configure any existing RunServerTasks
tasks.withType<RunServer> {
minecraftVersion("1.19.4")
minecraftVersion("1.20.4")
jvmArgs("-Dio.papermc.paper.suppress.sout.nags=true")
}

Expand Down
13 changes: 6 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
[versions]
# Plugins
shadow = "8.1.5"
shadow = "8.1.7"
dokka = "1.8.20"
run-paper = "2.2.3"
run-paper = "2.2.4"
spotless = "6.18.0"

# Tooling
kotlin = "1.7.10"
kotlin-coroutines = "1.6.4"
guava = "21.0"
kotlin-coroutines = "1.8.0"
guava = "33.1.0-jre"
slf4j = "1.7.36"
caffeine = "3.1.6"
cloud = "1.7.1"
caffeine = "3.1.8"
cloud = "1.8.4"

# Misc
adventure-core = "4.8.1"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.noxcrew.interfaces.grid.GridPoint
import com.noxcrew.interfaces.pane.PlayerPane
import com.noxcrew.interfaces.utilities.runSync
import com.noxcrew.interfaces.view.AbstractInterfaceView
import com.noxcrew.interfaces.view.ChestInterfaceView
import com.noxcrew.interfaces.view.InterfaceView
import com.noxcrew.interfaces.view.PlayerInterfaceView
import io.papermc.paper.event.player.AsyncChatEvent
Expand All @@ -24,13 +25,16 @@ import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.block.Action
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.inventory.ClickType
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.event.inventory.InventoryCloseEvent.Reason
import org.bukkit.event.inventory.InventoryOpenEvent
import org.bukkit.event.player.PlayerDropItemEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.event.player.PlayerRespawnEvent
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.inventory.InventoryHolder
import org.bukkit.plugin.Plugin
Expand Down Expand Up @@ -64,14 +68,6 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
Reason.PLUGIN
)

/** All valid interaction types. */
private val VALID_INTERACT = EnumSet.of(
Action.LEFT_CLICK_AIR,
Action.LEFT_CLICK_BLOCK,
Action.RIGHT_CLICK_AIR,
Action.RIGHT_CLICK_BLOCK
)

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

Expand Down Expand Up @@ -136,15 +132,15 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
@EventHandler
public fun onClose(event: InventoryCloseEvent) {
val holder = event.inventory.holder
val view = convertHolderToInterfaceView(holder) ?: return
val view = holder as? AbstractInterfaceView<*, *> ?: return
val reason = event.reason

SCOPE.launch {
// Mark the current view as closed properly
view.markClosed(reason)

// Try to open back up a previous interface
if (reason in REOPEN_REASONS) {
if (reason in REOPEN_REASONS && !event.player.isDead) {
getOpenInterface(event.player.uniqueId)?.open()
}
}
Expand All @@ -166,19 +162,83 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)

@EventHandler(priority = EventPriority.LOW)
public fun onInteract(event: PlayerInteractEvent) {
if (event.action !in VALID_INTERACT) return
if (event.hand != EquipmentSlot.HAND) return
if (event.action == Action.PHYSICAL) return
if (event.useItemInHand() == Event.Result.DENY) return

val player = event.player
val view = getOpenInterface(player.uniqueId) ?: return
val slot = player.inventory.heldItemSlot
val clickedPoint = GridPoint.at(3, slot)

val clickedPoint = if (event.hand == EquipmentSlot.HAND) {
GridPoint.at(3, player.inventory.heldItemSlot)
} else {
PlayerPane.OFF_HAND_SLOT
}
val click = convertAction(event.action, player.isSneaking)

// Check if the action is prevented if this slot is not freely
// movable
if (!canFreelyMove(view, clickedPoint) &&
event.action in view.backing.properties.preventedInteractions
) {
event.isCancelled = true
return
}

handleClick(view, clickedPoint, click, event, -1)
}

@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public fun onDropItem(event: PlayerDropItemEvent) {
val player = event.player
val view = getOpenInterface(player.uniqueId) ?: return
val slot = player.inventory.heldItemSlot
val droppedSlot = GridPoint.at(3, slot)

// Don't allow dropping items that cannot be freely edited
if (!canFreelyMove(view, droppedSlot)) {
event.isCancelled = true
}
}

@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public fun onDeath(event: PlayerDeathEvent) {
// Determine the holder of the top inventory being shown (can be open player inventory)
val view = convertHolderToInterfaceView(event.player.openInventory.topInventory.holder) ?: return

// Ignore chest inventories!
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) {
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)
if (!canFreelyMove(view, point)) {
var removed = false

// Remove the first item in drops that is similar, drops will be a list
// of exactly what was in the inventory, without merging any stacks. So
// we do not need to do anything fancy to match the amounts.
event.drops.removeIf {
if (!removed && it.isSimilar(stack)) {
removed = true
return@removeIf true
} else {
return@removeIf false
}
}
}
}
}

@EventHandler
public fun onRespawn(event: PlayerRespawnEvent) {
SCOPE.launch {
getOpenInterface(event.player.uniqueId)?.open()
}
}

@EventHandler(priority = EventPriority.LOWEST)
public fun onChat(event: AsyncChatEvent) {
val player = event.player
Expand Down Expand Up @@ -232,6 +292,12 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
return null
}

/** 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

/** Handles a [view] being clicked at [clickedPoint] through some [event]. */
private fun handleClick(
view: AbstractInterfaceView<*, *>,
Expand All @@ -241,7 +307,15 @@ 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 ?: return
val clickHandler = view.pane.getRaw(clickedPoint)?.clickHandler

// Optionally cancel clicking on other slots
if (clickHandler == null) {
if (view.backing.properties.preventClickingEmptySlots) {
event.isCancelled = true
}
return
}

// Automatically cancel if throttling or already processing
if (view.isProcessingClick || shouldThrottle(view.player)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public fun interface ClickHandler {
* An empty click handler that does not cancel the click event. This allows the item
* to be removed from the inventory.
*/
@Deprecated("Will be replaced with a property of the item in a future version")
public val ALLOW: ClickHandler = ClickHandler { cancelled = false }

/** Runs a [CompletableClickHandler] with [clickHandler] and [context]. */
Expand All @@ -40,6 +41,7 @@ public class CompletableClickHandler {
get() = deferred.isCancelled || deferred.isCompleted

/** Whether the base click event should be cancelled. */
@Deprecated("Will be replaced with a property of the item in a future version")
public var cancelled: Boolean = true

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.noxcrew.interfaces.transform.AppliedTransform
import com.noxcrew.interfaces.transform.ReactiveTransform
import com.noxcrew.interfaces.transform.Transform
import com.noxcrew.interfaces.utilities.IncrementingInteger
import org.bukkit.event.block.Action
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.ItemStack

Expand All @@ -23,17 +24,23 @@ public abstract class AbstractInterfaceBuilder<P : Pane, I : Interface<P>> inter
protected val closeHandlers: MutableMap<InventoryCloseEvent.Reason, CloseHandler> = mutableMapOf()
protected val transforms: MutableCollection<AppliedTransform<P>> = mutableListOf()
protected val clickPreprocessors: MutableCollection<ClickHandler> = mutableListOf()
protected val preventedInteractions: MutableCollection<Action> = mutableListOf()

/** Sets an item post processor to apply to every item in the interface. */
public var itemPostProcessor: ((ItemStack) -> Unit)? = null

/** Whether clicking on empty slots should be cancelled. */
public var preventClickingEmptySlots: Boolean = false

/** The properties object to use for the created interface. */
public val properties: InterfaceProperties<P>
get() = InterfaceProperties(
closeHandlers,
transforms,
clickPreprocessors,
itemPostProcessor
itemPostProcessor,
preventClickingEmptySlots,
preventedInteractions,
)

/** Adds a new transform to the interface that updates whenever [triggers] change. */
Expand All @@ -60,4 +67,9 @@ public abstract class AbstractInterfaceBuilder<P : Pane, I : Interface<P>> inter
public fun withPreprocessor(handler: ClickHandler) {
clickPreprocessors += handler
}

/** Adds [action] to be cancelled without triggering any click handlers on valid items in this pane. */
public fun withPreventedAction(action: Action) {
preventedInteractions += action
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.noxcrew.interfaces.interfaces
import com.noxcrew.interfaces.click.ClickHandler
import com.noxcrew.interfaces.pane.Pane
import com.noxcrew.interfaces.transform.AppliedTransform
import org.bukkit.event.block.Action
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.ItemStack

Expand All @@ -16,4 +17,8 @@ public data class InterfaceProperties<P : Pane>(
public val clickPreprocessors: Collection<ClickHandler> = emptySet(),
/** A post-processor applied to all items placed in the inventory. */
public val itemPostProcessor: ((ItemStack) -> Unit)? = {},
/** Whether clicking on empty slots should be cancelled. */
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(),
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.noxcrew.interfaces.pane

import com.noxcrew.interfaces.click.ClickHandler
import com.noxcrew.interfaces.element.CompletedElement
import com.noxcrew.interfaces.element.complete
import com.noxcrew.interfaces.grid.GridMap
import com.noxcrew.interfaces.grid.GridPoint
import com.noxcrew.interfaces.grid.HashGridMap
import com.noxcrew.interfaces.utilities.forEachInGrid
import com.noxcrew.interfaces.view.AbstractInterfaceView.Companion.COLUMNS_IN_CHEST
import org.bukkit.entity.Player

/** A grid map of completed elements. */
Expand Down Expand Up @@ -36,18 +33,6 @@ internal suspend fun Pane.complete(player: Player): CompletedPane {
return pane
}

/** Fills up a completed pane with empty elements. */
internal fun Pane.convertToEmptyCompletedPaneAndFill(rows: Int): CompletedPane {
val pane = convertToEmptyCompletedPane()
val airElement = CompletedElement(null, ClickHandler.EMPTY)

forEachInGrid(rows, COLUMNS_IN_CHEST) { row, column ->
pane[row, column] = airElement
}

return pane
}

/** Converts this pane to either a [CompletedPane] or [CompletedOrderedPane] based on its type. */
internal fun Pane.convertToEmptyCompletedPane(): CompletedPane {
if (this is OrderedPane) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import com.noxcrew.interfaces.grid.GridPoint

/** An ordered pane that wraps the player inventory. */
public class PlayerPane : OrderedPane(PANE_ORDERING) {

internal companion object {
/** The base ordering of the player inventory to go from logical rows to Bukkit rows. */
internal val PANE_ORDERING = listOf(1, 2, 3, 0, 4)

/** The location of the off-hand slot. */
private val OFF_HAND_SLOT = GridPoint.at(4, 4)
internal val OFF_HAND_SLOT = GridPoint.at(4, 4)
}

/** The hotbar of the player inventory. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.noxcrew.interfaces.utilities

import com.noxcrew.interfaces.pane.CompletedPane
import com.noxcrew.interfaces.pane.Pane
import com.noxcrew.interfaces.pane.convertToEmptyCompletedPaneAndFill
import com.noxcrew.interfaces.pane.convertToEmptyCompletedPane

/** A collection of completed panes that can be collapsed to create a new merged [CompletedPane]. */
internal class CollapsablePaneMap private constructor(
Expand Down Expand Up @@ -36,7 +36,7 @@ internal class CollapsablePaneMap private constructor(
return pane
}

val pane = basePane.convertToEmptyCompletedPaneAndFill(rows)
val pane = basePane.convertToEmptyCompletedPane()
val current = internal.toMap().values

current.forEach { layer ->
Expand Down

0 comments on commit 97cb3c9

Please sign in to comment.