Skip to content

Commit 97cb3c9

Browse files
committed
Build out support for a lot more non-standard usages
- 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
1 parent a8a5830 commit 97cb3c9

File tree

10 files changed

+121
-43
lines changed

10 files changed

+121
-43
lines changed

build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ subprojects {
4040

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

gradle/libs.versions.toml

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
[versions]
22
# Plugins
3-
shadow = "8.1.5"
3+
shadow = "8.1.7"
44
dokka = "1.8.20"
5-
run-paper = "2.2.3"
5+
run-paper = "2.2.4"
66
spotless = "6.18.0"
77

88
# Tooling
9-
kotlin = "1.7.10"
10-
kotlin-coroutines = "1.6.4"
11-
guava = "21.0"
9+
kotlin-coroutines = "1.8.0"
10+
guava = "33.1.0-jre"
1211
slf4j = "1.7.36"
13-
caffeine = "3.1.6"
14-
cloud = "1.7.1"
12+
caffeine = "3.1.8"
13+
cloud = "1.8.4"
1514

1615
# Misc
1716
adventure-core = "4.8.1"
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

interfaces/src/main/kotlin/com/noxcrew/interfaces/InterfacesListeners.kt

+89-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.noxcrew.interfaces.grid.GridPoint
1010
import com.noxcrew.interfaces.pane.PlayerPane
1111
import com.noxcrew.interfaces.utilities.runSync
1212
import com.noxcrew.interfaces.view.AbstractInterfaceView
13+
import com.noxcrew.interfaces.view.ChestInterfaceView
1314
import com.noxcrew.interfaces.view.InterfaceView
1415
import com.noxcrew.interfaces.view.PlayerInterfaceView
1516
import io.papermc.paper.event.player.AsyncChatEvent
@@ -24,13 +25,16 @@ import org.bukkit.event.EventHandler
2425
import org.bukkit.event.EventPriority
2526
import org.bukkit.event.Listener
2627
import org.bukkit.event.block.Action
28+
import org.bukkit.event.entity.PlayerDeathEvent
2729
import org.bukkit.event.inventory.ClickType
2830
import org.bukkit.event.inventory.InventoryClickEvent
2931
import org.bukkit.event.inventory.InventoryCloseEvent
3032
import org.bukkit.event.inventory.InventoryCloseEvent.Reason
3133
import org.bukkit.event.inventory.InventoryOpenEvent
34+
import org.bukkit.event.player.PlayerDropItemEvent
3235
import org.bukkit.event.player.PlayerInteractEvent
3336
import org.bukkit.event.player.PlayerQuitEvent
37+
import org.bukkit.event.player.PlayerRespawnEvent
3438
import org.bukkit.inventory.EquipmentSlot
3539
import org.bukkit.inventory.InventoryHolder
3640
import org.bukkit.plugin.Plugin
@@ -64,14 +68,6 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
6468
Reason.PLUGIN
6569
)
6670

67-
/** All valid interaction types. */
68-
private val VALID_INTERACT = EnumSet.of(
69-
Action.LEFT_CLICK_AIR,
70-
Action.LEFT_CLICK_BLOCK,
71-
Action.RIGHT_CLICK_AIR,
72-
Action.RIGHT_CLICK_BLOCK
73-
)
74-
7571
/** The possible valid slot range inside the player inventory. */
7672
private val PLAYER_INVENTORY_RANGE = 0..40
7773

@@ -136,15 +132,15 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
136132
@EventHandler
137133
public fun onClose(event: InventoryCloseEvent) {
138134
val holder = event.inventory.holder
139-
val view = convertHolderToInterfaceView(holder) ?: return
135+
val view = holder as? AbstractInterfaceView<*, *> ?: return
140136
val reason = event.reason
141137

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

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

167163
@EventHandler(priority = EventPriority.LOW)
168164
public fun onInteract(event: PlayerInteractEvent) {
169-
if (event.action !in VALID_INTERACT) return
170-
if (event.hand != EquipmentSlot.HAND) return
165+
if (event.action == Action.PHYSICAL) return
171166
if (event.useItemInHand() == Event.Result.DENY) return
172167

173168
val player = event.player
174169
val view = getOpenInterface(player.uniqueId) ?: return
175-
val slot = player.inventory.heldItemSlot
176-
val clickedPoint = GridPoint.at(3, slot)
170+
171+
val clickedPoint = if (event.hand == EquipmentSlot.HAND) {
172+
GridPoint.at(3, player.inventory.heldItemSlot)
173+
} else {
174+
PlayerPane.OFF_HAND_SLOT
175+
}
177176
val click = convertAction(event.action, player.isSneaking)
178177

178+
// Check if the action is prevented if this slot is not freely
179+
// movable
180+
if (!canFreelyMove(view, clickedPoint) &&
181+
event.action in view.backing.properties.preventedInteractions
182+
) {
183+
event.isCancelled = true
184+
return
185+
}
186+
179187
handleClick(view, clickedPoint, click, event, -1)
180188
}
181189

190+
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
191+
public fun onDropItem(event: PlayerDropItemEvent) {
192+
val player = event.player
193+
val view = getOpenInterface(player.uniqueId) ?: return
194+
val slot = player.inventory.heldItemSlot
195+
val droppedSlot = GridPoint.at(3, slot)
196+
197+
// Don't allow dropping items that cannot be freely edited
198+
if (!canFreelyMove(view, droppedSlot)) {
199+
event.isCancelled = true
200+
}
201+
}
202+
203+
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
204+
public fun onDeath(event: PlayerDeathEvent) {
205+
// Determine the holder of the top inventory being shown (can be open player inventory)
206+
val view = convertHolderToInterfaceView(event.player.openInventory.topInventory.holder) ?: return
207+
208+
// Ignore chest inventories!
209+
if (view is ChestInterfaceView) return
210+
211+
// Tally up all items that the player cannot modify and remove them from the drops
212+
for (index in PLAYER_INVENTORY_RANGE) {
213+
val stack = event.player.inventory.getItem(index) ?: continue
214+
val x = index / 9
215+
val adjustedX = PlayerPane.PANE_ORDERING.indexOf(x)
216+
val point = GridPoint(adjustedX, index % 9)
217+
if (!canFreelyMove(view, point)) {
218+
var removed = false
219+
220+
// Remove the first item in drops that is similar, drops will be a list
221+
// of exactly what was in the inventory, without merging any stacks. So
222+
// we do not need to do anything fancy to match the amounts.
223+
event.drops.removeIf {
224+
if (!removed && it.isSimilar(stack)) {
225+
removed = true
226+
return@removeIf true
227+
} else {
228+
return@removeIf false
229+
}
230+
}
231+
}
232+
}
233+
}
234+
235+
@EventHandler
236+
public fun onRespawn(event: PlayerRespawnEvent) {
237+
SCOPE.launch {
238+
getOpenInterface(event.player.uniqueId)?.open()
239+
}
240+
}
241+
182242
@EventHandler(priority = EventPriority.LOWEST)
183243
public fun onChat(event: AsyncChatEvent) {
184244
val player = event.player
@@ -232,6 +292,12 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
232292
return null
233293
}
234294

295+
/** Returns whether [clickedPoint] in [view] can be freely moved. */
296+
private fun canFreelyMove(
297+
view: AbstractInterfaceView<*, *>,
298+
clickedPoint: GridPoint,
299+
): Boolean = view.pane.getRaw(clickedPoint)?.clickHandler == null && !view.backing.properties.preventClickingEmptySlots
300+
235301
/** Handles a [view] being clicked at [clickedPoint] through some [event]. */
236302
private fun handleClick(
237303
view: AbstractInterfaceView<*, *>,
@@ -241,7 +307,15 @@ public class InterfacesListeners private constructor(private val plugin: Plugin)
241307
slot: Int
242308
) {
243309
// Determine the type of click, if nothing was clicked we allow it
244-
val clickHandler = view.pane.getRaw(clickedPoint)?.clickHandler ?: return
310+
val clickHandler = view.pane.getRaw(clickedPoint)?.clickHandler
311+
312+
// Optionally cancel clicking on other slots
313+
if (clickHandler == null) {
314+
if (view.backing.properties.preventClickingEmptySlots) {
315+
event.isCancelled = true
316+
}
317+
return
318+
}
245319

246320
// Automatically cancel if throttling or already processing
247321
if (view.isProcessingClick || shouldThrottle(view.player)) {

interfaces/src/main/kotlin/com/noxcrew/interfaces/click/ClickHandler.kt

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public fun interface ClickHandler {
1818
* An empty click handler that does not cancel the click event. This allows the item
1919
* to be removed from the inventory.
2020
*/
21+
@Deprecated("Will be replaced with a property of the item in a future version")
2122
public val ALLOW: ClickHandler = ClickHandler { cancelled = false }
2223

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

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

4547
/**

interfaces/src/main/kotlin/com/noxcrew/interfaces/interfaces/AbstractInterfaceBuilder.kt

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.noxcrew.interfaces.transform.AppliedTransform
77
import com.noxcrew.interfaces.transform.ReactiveTransform
88
import com.noxcrew.interfaces.transform.Transform
99
import com.noxcrew.interfaces.utilities.IncrementingInteger
10+
import org.bukkit.event.block.Action
1011
import org.bukkit.event.inventory.InventoryCloseEvent
1112
import org.bukkit.inventory.ItemStack
1213

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

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

32+
/** Whether clicking on empty slots should be cancelled. */
33+
public var preventClickingEmptySlots: Boolean = false
34+
3035
/** The properties object to use for the created interface. */
3136
public val properties: InterfaceProperties<P>
3237
get() = InterfaceProperties(
3338
closeHandlers,
3439
transforms,
3540
clickPreprocessors,
36-
itemPostProcessor
41+
itemPostProcessor,
42+
preventClickingEmptySlots,
43+
preventedInteractions,
3744
)
3845

3946
/** Adds a new transform to the interface that updates whenever [triggers] change. */
@@ -60,4 +67,9 @@ public abstract class AbstractInterfaceBuilder<P : Pane, I : Interface<P>> inter
6067
public fun withPreprocessor(handler: ClickHandler) {
6168
clickPreprocessors += handler
6269
}
70+
71+
/** Adds [action] to be cancelled without triggering any click handlers on valid items in this pane. */
72+
public fun withPreventedAction(action: Action) {
73+
preventedInteractions += action
74+
}
6375
}

interfaces/src/main/kotlin/com/noxcrew/interfaces/interfaces/InterfaceProperties.kt

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.noxcrew.interfaces.interfaces
33
import com.noxcrew.interfaces.click.ClickHandler
44
import com.noxcrew.interfaces.pane.Pane
55
import com.noxcrew.interfaces.transform.AppliedTransform
6+
import org.bukkit.event.block.Action
67
import org.bukkit.event.inventory.InventoryCloseEvent
78
import org.bukkit.inventory.ItemStack
89

@@ -16,4 +17,8 @@ public data class InterfaceProperties<P : Pane>(
1617
public val clickPreprocessors: Collection<ClickHandler> = emptySet(),
1718
/** A post-processor applied to all items placed in the inventory. */
1819
public val itemPostProcessor: ((ItemStack) -> Unit)? = {},
20+
/** Whether clicking on empty slots should be cancelled. */
21+
public val preventClickingEmptySlots: Boolean = false,
22+
/** All interactions that will be ignored on this view and cancelled on pane items without calling the handler. */
23+
public val preventedInteractions: Collection<Action> = emptySet(),
1924
)

interfaces/src/main/kotlin/com/noxcrew/interfaces/pane/CompletedPane.kt

-15
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package com.noxcrew.interfaces.pane
22

3-
import com.noxcrew.interfaces.click.ClickHandler
43
import com.noxcrew.interfaces.element.CompletedElement
54
import com.noxcrew.interfaces.element.complete
65
import com.noxcrew.interfaces.grid.GridMap
76
import com.noxcrew.interfaces.grid.GridPoint
87
import com.noxcrew.interfaces.grid.HashGridMap
9-
import com.noxcrew.interfaces.utilities.forEachInGrid
10-
import com.noxcrew.interfaces.view.AbstractInterfaceView.Companion.COLUMNS_IN_CHEST
118
import org.bukkit.entity.Player
129

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

39-
/** Fills up a completed pane with empty elements. */
40-
internal fun Pane.convertToEmptyCompletedPaneAndFill(rows: Int): CompletedPane {
41-
val pane = convertToEmptyCompletedPane()
42-
val airElement = CompletedElement(null, ClickHandler.EMPTY)
43-
44-
forEachInGrid(rows, COLUMNS_IN_CHEST) { row, column ->
45-
pane[row, column] = airElement
46-
}
47-
48-
return pane
49-
}
50-
5136
/** Converts this pane to either a [CompletedPane] or [CompletedOrderedPane] based on its type. */
5237
internal fun Pane.convertToEmptyCompletedPane(): CompletedPane {
5338
if (this is OrderedPane) {

interfaces/src/main/kotlin/com/noxcrew/interfaces/pane/PlayerPane.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import com.noxcrew.interfaces.grid.GridPoint
55

66
/** An ordered pane that wraps the player inventory. */
77
public class PlayerPane : OrderedPane(PANE_ORDERING) {
8+
89
internal companion object {
910
/** The base ordering of the player inventory to go from logical rows to Bukkit rows. */
1011
internal val PANE_ORDERING = listOf(1, 2, 3, 0, 4)
1112

1213
/** The location of the off-hand slot. */
13-
private val OFF_HAND_SLOT = GridPoint.at(4, 4)
14+
internal val OFF_HAND_SLOT = GridPoint.at(4, 4)
1415
}
1516

1617
/** The hotbar of the player inventory. */

interfaces/src/main/kotlin/com/noxcrew/interfaces/utilities/CollapsablePaneMap.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.noxcrew.interfaces.utilities
22

33
import com.noxcrew.interfaces.pane.CompletedPane
44
import com.noxcrew.interfaces.pane.Pane
5-
import com.noxcrew.interfaces.pane.convertToEmptyCompletedPaneAndFill
5+
import com.noxcrew.interfaces.pane.convertToEmptyCompletedPane
66

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

39-
val pane = basePane.convertToEmptyCompletedPaneAndFill(rows)
39+
val pane = basePane.convertToEmptyCompletedPane()
4040
val current = internal.toMap().values
4141

4242
current.forEach { layer ->

0 commit comments

Comments
 (0)