Skip to content

Commit

Permalink
Improving event listening internals
Browse files Browse the repository at this point in the history
- Fixed transforms being entirely broken
- Fixed shift clicking into inventories being broken
- Fixed combined inventories having various issues with click protection
- Fixed many more issues around preventClickingEmptySlots = false
  • Loading branch information
Aeltumn committed Jul 14, 2024
1 parent 2bff91d commit 0c35762
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 40 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 130
max_line_length = 160
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_wrap_on_typing = true
ij_wrap_on_typing = false

[*.conf]
indent_size = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.noxcrew.interfaces.example
import com.noxcrew.interfaces.drawable.Drawable
import com.noxcrew.interfaces.element.StaticElement
import com.noxcrew.interfaces.interfaces.Interface
import com.noxcrew.interfaces.interfaces.buildChestInterface
import com.noxcrew.interfaces.interfaces.buildCombinedInterface
import com.noxcrew.interfaces.properties.interfaceProperty
import net.kyori.adventure.text.Component
Expand All @@ -13,7 +14,7 @@ public class ChangingTitleExampleInterface : RegistrableInterface {

override val subcommand: String = "changing-title"

override fun create(): Interface<*, *> = buildCombinedInterface {
override fun create(): Interface<*, *> = buildChestInterface {
rows = 1

val numberProperty = interfaceProperty(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.noxcrew.interfaces.example
import com.noxcrew.interfaces.drawable.Drawable
import com.noxcrew.interfaces.element.StaticElement
import com.noxcrew.interfaces.interfaces.Interface
import com.noxcrew.interfaces.interfaces.buildChestInterface
import com.noxcrew.interfaces.interfaces.buildCombinedInterface
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
Expand All @@ -21,7 +22,7 @@ public class DelayedRequestExampleInterface : RegistrableInterface {
override val subcommand: String = "delayed"

@OptIn(DelicateCoroutinesApi::class)
override fun create(): Interface<*, *> = buildCombinedInterface {
override fun create(): Interface<*, *> = buildChestInterface {
initialTitle = text(subcommand)
rows = 2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.bukkit.plugin.java.JavaPlugin
import org.incendo.cloud.execution.ExecutionCoordinator
import org.incendo.cloud.kotlin.coroutines.extension.suspendingHandler
import org.incendo.cloud.kotlin.extension.buildAndRegister
import org.incendo.cloud.paper.LegacyPaperCommandManager
import org.incendo.cloud.paper.PaperCommandManager

public class ExamplePlugin : JavaPlugin(), Listener {
Expand All @@ -38,11 +39,18 @@ public class ExamplePlugin : JavaPlugin(), Listener {
private var counter by counterProperty

override fun onEnable() {
val commandManager = PaperCommandManager.builder()
.executionCoordinator(ExecutionCoordinator.asyncCoordinator())
.buildOnEnable(this)

val commandManager = LegacyPaperCommandManager.createNative(this, ExecutionCoordinator.asyncCoordinator())
commandManager.buildAndRegister("interfaces") {
registerCopy {
literal("close")

suspendingHandler {
val player = it.sender() as Player
InterfacesListeners.INSTANCE.getOpenInterface(player.uniqueId)?.close()
player.inventory.clear()
}
}

registerCopy {
literal("simple")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ package com.noxcrew.interfaces.example
import com.noxcrew.interfaces.drawable.Drawable.Companion.drawable
import com.noxcrew.interfaces.element.StaticElement
import com.noxcrew.interfaces.interfaces.Interface
import com.noxcrew.interfaces.interfaces.buildChestInterface
import com.noxcrew.interfaces.interfaces.buildCombinedInterface
import com.noxcrew.interfaces.utilities.BoundInteger
import org.bukkit.Material

public class MovingExampleInterface : RegistrableInterface {
override val subcommand: String = "moving"

override fun create(): Interface<*, *> = buildCombinedInterface {
override fun create(): Interface<*, *> = buildChestInterface {
val countProperty = BoundInteger(4, 1, 7)
var count by countProperty

// Allow clicking empty slots to allow testing various interactions with tiems and chest interfaces
preventClickingEmptySlots = false

rows = 1

withTransform(countProperty) { pane, _ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.bukkit.event.player.PlayerRespawnEvent
import org.bukkit.event.player.PlayerSwapHandItemsEvent
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.inventory.InventoryHolder
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.Plugin
import org.slf4j.LoggerFactory
import java.util.EnumSet
Expand Down Expand Up @@ -78,6 +79,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val id: UUID
)

/** The view currently being opened. */
internal var viewBeingOpened: InterfaceView? = null

private val logger = LoggerFactory.getLogger(InterfacesListeners::class.java)

private val spamPrevention: Cache<UUID, Unit> = Caffeine.newBuilder()
Expand Down Expand Up @@ -152,6 +156,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val view = holder as? AbstractInterfaceView<*, *, *> ?: return
val reason = event.reason

// Ignore if the view is about to be re-opened right after
if (view == viewBeingOpened) return

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

Expand All @@ -174,24 +181,66 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
public fun onClick(event: InventoryClickEvent) {
val holder = event.inventory.holder
val view = convertHolderToInterfaceView(holder) ?: return
val clickedPoint = clickedPoint(event) ?: return
handleClick(view, clickedPoint, event.click, event, event.hotbarButton)
val clickedPoint = clickedPoint(view, event) ?: return
val isPlayerInventory = (event.clickedInventory ?: event.inventory).holder is Player
handleClick(view, clickedPoint, event.click, event, event.hotbarButton, isPlayerInventory)

// If the event is not cancelled we add extra prevention checks if any of the involved
// slots are not allowed to be modified!
if (!event.isCancelled) {
// If you use a number key we check if the item you're swapping with is
// protected.
if (event.click == ClickType.NUMBER_KEY && !canFreelyMove(view, GridPoint.at(3, event.hotbarButton))) {
event.isCancelled = true
return
if (view.backing.includesPlayerInventory) {
// If you use a number key we check if the item you're swapping with is
// protected.
if (event.click == ClickType.NUMBER_KEY && !canFreelyMove(view, view.backing.relativizePlayerInventorySlot(GridPoint.at(3, event.hotbarButton)), true)) {
event.isCancelled = true
return
}

// If you try to swap with the off-hand we have to specifically check for that.
if (event.click == ClickType.SWAP_OFFHAND && !canFreelyMove(view, view.backing.relativizePlayerInventorySlot(GridPoint.at(4, 4)), true)) {
event.isCancelled = true
return
}
}

// If you try to swap with the off-hand we have to specifically check for that.
if (event.click == ClickType.SWAP_OFFHAND && !canFreelyMove(view, GridPoint.at(4, 4))) {
event.isCancelled = true
return
// If it's a shift click we have to detect what slot is being edited
if (event.click.isShiftClick && event.clickedInventory != null) {
val topInventory = event.view.topInventory
val bottomInventory = event.view.bottomInventory
val clickedInventory = event.clickedInventory!!
val otherInventory = if (clickedInventory == topInventory) bottomInventory else topInventory

// Ideally we predict which slot got shift clicked into! We start by finding any
// stack that this item can be added onto, after that we find the first empty slot.
val isMovingIntoPlayerInventory = otherInventory.holder is Player
val firstEmptySlot = otherInventory.indexOfFirst {
it != null && !it.isEmpty && it.isSimilar(event.currentItem ?: ItemStack.empty())
}.takeIf { it != -1 } ?: otherInventory.indexOfFirst { it == null || it.isEmpty }

if (firstEmptySlot != -1) {
val targetSlot = requireNotNull(GridPoint.fromBukkitChestSlot(firstEmptySlot))

if (!canFreelyMove(
view,
// If we are shift clicking into the player inventory
// we need to offset the target point into the inventory rows.
if (isMovingIntoPlayerInventory) {
view.backing.relativizePlayerInventorySlot(targetSlot)
} else {
targetSlot
},
isMovingIntoPlayerInventory
)
) {
event.isCancelled = true
return
}
}
}

// It'd be nice if we had a way to redirect which slot gets shift clicked into, but this causes a giant mess
// of plugin compatibility. The cleanest solution is for users to place invisible items in all taken slots and
// to leave clickable slots open.
}
}

Expand All @@ -201,7 +250,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val view = convertHolderToInterfaceView(holder) ?: return
for (slot in event.rawSlots) {
val clickedPoint = GridPoint.fromBukkitChestSlot(slot) ?: continue
if (!canFreelyMove(view, clickedPoint)) {
if (!canFreelyMove(view, clickedPoint, slot >= event.inventory.size)) {
event.isCancelled = true
return
}
Expand All @@ -222,23 +271,25 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val player = event.player
val view = getOpenInterface(player.uniqueId) ?: return

val clickedPoint = if (event.hand == EquipmentSlot.HAND) {
GridPoint.at(3, player.inventory.heldItemSlot)
} else {
PlayerPane.OFF_HAND_SLOT
}
val clickedPoint = view.backing.relativizePlayerInventorySlot(
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) &&
if (!canFreelyMove(view, clickedPoint, true) &&
event.action in view.builder.preventedInteractions
) {
event.isCancelled = true
return
}

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

@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
Expand All @@ -249,7 +300,7 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val droppedSlot = GridPoint.at(3, slot)

// Don't allow dropping items that cannot be freely edited
if (!canFreelyMove(view, droppedSlot)) {
if (!canFreelyMove(view, view.backing.relativizePlayerInventorySlot(droppedSlot), true)) {
event.isCancelled = true
}
}
Expand All @@ -263,7 +314,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
val interactedSlot2 = GridPoint.at(4, 4)

// Don't allow swapping items that cannot be freely edited
if (!canFreelyMove(view, interactedSlot1) || !canFreelyMove(view, interactedSlot2)) {
if (!canFreelyMove(view, view.backing.relativizePlayerInventorySlot(interactedSlot1), true) ||
!canFreelyMove(view, view.backing.relativizePlayerInventorySlot(interactedSlot2), true)
) {
event.isCancelled = true
}
}
Expand All @@ -273,14 +326,14 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
// 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
// Ignore inventories that do not use the player inventory!
if (!view.backing.includesPlayerInventory) return

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

// Remove the first item in drops that is similar, drops will be a list
Expand Down Expand Up @@ -324,9 +377,9 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
}

/** Extracts the clicked point from an inventory click event. */
private fun clickedPoint(event: InventoryClickEvent): GridPoint? {
private fun clickedPoint(view: AbstractInterfaceView<*, *, *>, event: InventoryClickEvent): GridPoint? {
if (event.inventory.holder is Player) {
return GridPoint.fromBukkitPlayerSlot(event.slot)
return GridPoint.fromBukkitPlayerSlot(event.slot)?.let { view.backing.relativizePlayerInventorySlot(it) }
}
return GridPoint.fromBukkitChestSlot(event.rawSlot)
}
Expand All @@ -350,23 +403,25 @@ 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) == null && !view.builder.preventClickingEmptySlots
clickedPoint: GridPoint,
isPlayerInventory: Boolean
): Boolean = view.pane.getRaw(clickedPoint) == null && !(view.builder.preventClickingEmptySlots && !(view.builder.allowClickingOwnInventoryIfClickingEmptySlotsIsPrevented && isPlayerInventory))

/** Handles a [view] being clicked at [clickedPoint] through some [event]. */
private fun handleClick(
view: AbstractInterfaceView<*, *, *>,
clickedPoint: GridPoint,
click: ClickType,
event: Cancellable,
slot: Int
slot: Int,
isPlayerInventory: Boolean
) {
// Determine the type of click, if nothing was clicked we allow it
val raw = view.pane.getRaw(clickedPoint)

// Optionally cancel clicking on other slots
if (raw == null) {
if (view.builder.preventClickingEmptySlots) {
if (view.builder.preventClickingEmptySlots && !(view.builder.allowClickingOwnInventoryIfClickingEmptySlotsIsPrevented && isPlayerInventory)) {
event.isCancelled = true
}
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class ChestInterface internal constructor(
public const val MAX_NUMBER_OF_ROWS: Int = 6
}

override val includesPlayerInventory: Boolean = false

override fun createPane(): Pane = Pane()

override suspend fun open(player: Player, parent: InterfaceView?): ChestInterfaceView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class CombinedInterface internal constructor(
public const val MAX_NUMBER_OF_ROWS: Int = 9
}

override val includesPlayerInventory: Boolean = true

override fun totalRows(): Int = rows + 4

override fun createPane(): CombinedPane = CombinedPane(rows)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.noxcrew.interfaces.interfaces

import com.noxcrew.interfaces.InterfacesListeners
import com.noxcrew.interfaces.grid.GridPoint
import com.noxcrew.interfaces.pane.CombinedPane
import com.noxcrew.interfaces.pane.OrderedPane
import com.noxcrew.interfaces.pane.Pane
import com.noxcrew.interfaces.view.InterfaceView
import org.bukkit.entity.Player
Expand All @@ -14,6 +17,9 @@ public interface Interface<I : Interface<I, P>, P : Pane> {
/** The builder that creates this interface. */
public val builder: InterfaceBuilder<P, I>

/** Whether this view includes the player inventory. */
public val includesPlayerInventory: Boolean

/** Returns the total amount of rows. */
public fun totalRows(): Int = rows

Expand All @@ -29,4 +35,15 @@ public interface Interface<I : Interface<I, P>, P : Pane> {
parent: InterfaceView? =
InterfacesListeners.INSTANCE.convertHolderToInterfaceView(player.openInventory.topInventory.holder)
): InterfaceView

/** Returns the [gridPoint] relative to the player's inventory within this inventory. */
public fun relativizePlayerInventorySlot(gridPoint: GridPoint): GridPoint {
// If it's a combined pane we offset by the chest size in rows to push the player
// inventory slots to the bottom!
val pane = createPane()
if (pane is CombinedPane) {
return GridPoint.at(pane.chestRows + gridPoint.x, gridPoint.y)
}
return gridPoint
}
}
Loading

0 comments on commit 0c35762

Please sign in to comment.