From 9a9e8bc70e407744ecc5a20aeedc504e11ac0ab1 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 15 Mar 2026 01:46:33 +0100 Subject: [PATCH 1/5] feat: enhance PacketLoreListener with improved lore handling and snapshot management --- .../server/packet/lore/PacketLoreListener.kt | 101 ++++++++++++++---- 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt index 13be6c4a..b3b27776 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt @@ -9,6 +9,7 @@ import dev.slne.surf.surfapi.bukkit.api.util.key import dev.slne.surf.surfapi.bukkit.server.nms.toBukkit import dev.slne.surf.surfapi.bukkit.server.nms.toNms import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.objects.ObjectArrayList import net.kyori.adventure.text.format.TextDecoration import net.minecraft.core.component.DataComponents import net.minecraft.network.protocol.game.* @@ -18,6 +19,8 @@ import net.minecraft.world.item.component.ItemLore import org.bukkit.NamespacedKey import org.bukkit.plugin.Plugin import java.util.concurrent.ConcurrentHashMap +import net.kyori.adventure.text.Component as AdventureComponent +import net.minecraft.network.chat.Component as MinecraftComponent /** * PacketLoreListener is a class that implements PacketListenerAbstract and is responsible for @@ -28,7 +31,20 @@ object PacketLoreListener : PacketListener { private val loreHandlers = ConcurrentHashMap() private val loreHandlersGlobal = ConcurrentHashMap>() + @Volatile + private var loreHandlerSnapshot: Array> = emptyArray() + + @Volatile + private var loreHandlerGlobalSnapshot: Array>> = + emptyArray() + private val ORIGINAL_LORE_KEY = key("original_lore") + private val ORIGINAL_LORE_KEY_STRING = ORIGINAL_LORE_KEY.asString() + + private val ITALIC_DECORATION = TextDecoration.ITALIC + private val ITALIC_STATE_FALSE = TextDecoration.State.FALSE + + private fun hasAnyHandlers(): Boolean = loreHandlerSnapshot.isNotEmpty() || loreHandlerGlobalSnapshot.isNotEmpty() @ServerboundListener fun onPacketReceive(event: ServerboundSetCreativeModeSlotPacket) { @@ -37,16 +53,26 @@ object PacketLoreListener : PacketListener { @ClientboundListener fun onWindowItem(event: ClientboundContainerSetContentPacket): ClientboundContainerSetContentPacket { + if (!hasAnyHandlers()) return event + + val items = event.items + val updatedItems = ObjectArrayList(items.size) + for (i in items.indices) { + updatedItems.add(makeUpdatedItemStack(items[i].copy())) + } + return ClientboundContainerSetContentPacket( event.containerId(), event.stateId(), - event.items.map { makeUpdatedItemStack(it.copy()) }, + items, makeUpdatedItemStack(event.carriedItem().copy()) ) } @ClientboundListener fun onSetSlotPacket(event: ClientboundContainerSetSlotPacket): ClientboundContainerSetSlotPacket { + if (!hasAnyHandlers()) return event + return ClientboundContainerSetSlotPacket( event.containerId, event.stateId, @@ -57,6 +83,8 @@ object PacketLoreListener : PacketListener { @ClientboundListener fun onSetPlayerInventoryPacket(event: ClientboundSetPlayerInventoryPacket): ClientboundSetPlayerInventoryPacket { + if (!hasAnyHandlers()) return event + return ClientboundSetPlayerInventoryPacket( event.slot(), makeUpdatedItemStack(event.contents.copy()) @@ -65,6 +93,8 @@ object PacketLoreListener : PacketListener { @ClientboundListener fun onSetCursorItemPacket(event: ClientboundSetCursorItemPacket): ClientboundSetCursorItemPacket { + if (!hasAnyHandlers()) return event + return ClientboundSetCursorItemPacket(makeUpdatedItemStack(event.contents.copy())) } @@ -72,32 +102,54 @@ object PacketLoreListener : PacketListener { item: ItemStack, ): ItemStack { if (item.isEmpty) return item - if (loreHandlers.isEmpty() && loreHandlersGlobal.isEmpty()) return item - val bukkitStack = item.asBukkitMirror() - val pdc = bukkitStack.persistentDataContainer + // One volatile read + val handlerEntries = loreHandlerSnapshot + val globalEntries = loreHandlerGlobalSnapshot + val nmsLore = item.getOrDefault(DataComponents.LORE, ItemLore.EMPTY) val lines = nmsLore.lines - val mutableLore = lines.mapTo(mutableObjectListOf(lines.size)) { it.toBukkit() } - loreHandlers.forEach { (identifier, handler) -> - if (pdc.has(identifier)) { - handler.handleLore(mutableLore, pdc, bukkitStack) + val mutableLore = mutableObjectListOf(lines.size) + for (i in lines.indices) { + mutableLore.add(lines[i].toBukkit()) + } + + val bukkitStack = item.asBukkitMirror() + val pdc = bukkitStack.persistentDataContainer + + var anyHandlerRan = false + if (handlerEntries.isNotEmpty()) { + for ((key, handler) in handlerEntries) { + if (pdc.has(key)) { + handler.handleLore(mutableLore, pdc, bukkitStack) + anyHandlerRan = true + } } } - loreHandlersGlobal.forEach { (plugin, handlers) -> - if (plugin.isEnabled) { - handlers.forEach { it.handleLore(mutableLore, pdc, bukkitStack) } + if (globalEntries.isNotEmpty()) { + for ((plugin, handlers) in globalEntries) { + if (plugin.isEnabled) { + for (handler in handlers) { + handler.handleLore(mutableLore, pdc, bukkitStack) + anyHandlerRan = true + } + } } } - val updatedNmsLore = ItemLore( - mutableLore.asSequence() - .map { it.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE) } - .map { it.toNms() } - .toList() - ) + if (!anyHandlerRan) return item + + val updatedLines = ObjectArrayList(mutableLore.size) + for (i in mutableLore.indices) { + val component = mutableLore[i] + updatedLines.add( + component.decorationIfAbsent(ITALIC_DECORATION, ITALIC_STATE_FALSE).toNms() + ) + } + + val updatedNmsLore = ItemLore(updatedLines) if (updatedNmsLore == nmsLore) { return item @@ -105,7 +157,7 @@ object PacketLoreListener : PacketListener { item.set(DataComponents.LORE, updatedNmsLore) CustomData.update(DataComponents.CUSTOM_DATA, item) { tag -> - tag.store(ORIGINAL_LORE_KEY.asString(), ItemLore.CODEC, nmsLore) + tag.store(ORIGINAL_LORE_KEY_STRING, ItemLore.CODEC, nmsLore) } return item @@ -115,29 +167,38 @@ object PacketLoreListener : PacketListener { stack: ItemStack, ): ItemStack { CustomData.update(DataComponents.CUSTOM_DATA, stack) { tag -> - val originalLore = tag.read(ORIGINAL_LORE_KEY.asString(), ItemLore.CODEC) + val originalLore = tag.read(ORIGINAL_LORE_KEY_STRING, ItemLore.CODEC) originalLore.ifPresent { lore -> stack.set(DataComponents.LORE, lore) - tag.remove(ORIGINAL_LORE_KEY.asString()) + tag.remove(ORIGINAL_LORE_KEY_STRING) } } return stack } + private fun rebuildSnapshots() { + loreHandlerSnapshot = loreHandlers.entries.toTypedArray() + loreHandlerGlobalSnapshot = loreHandlersGlobal.entries.toTypedArray() + } + fun register(identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler) { loreHandlers[identifier] = listener + rebuildSnapshots() } fun register(plugin: Plugin, listener: SurfBukkitPacketLoreHandler) { loreHandlersGlobal.computeIfAbsent(plugin) { ConcurrentHashMap.newKeySet() }.add(listener) + rebuildSnapshots() } fun unregister(identifier: NamespacedKey) { loreHandlers.remove(identifier) + rebuildSnapshots() } fun unregister(plugin: Plugin) { loreHandlersGlobal.remove(plugin) + rebuildSnapshots() } } From 3e5c9b6c27c5dbe367d5399ba9d041306721b03e Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 15 Mar 2026 12:13:58 +0100 Subject: [PATCH 2/5] feat: enhance PacketLoreListener with plugin disable handling and optimize lore handler registration --- .../bukkit/api/packet/SurfBukkitPacketApi.kt | 19 +- .../impl/packet/SurfBukkitPacketApiImpl.kt | 3 +- .../bukkit/server/packet/PacketApiLoader.kt | 5 + .../server/packet/lore/PacketLoreListener.kt | 292 +++++++++++++----- .../lore/PluginDisablePacketLoreListener.kt | 13 + 5 files changed, 257 insertions(+), 75 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PluginDisablePacketLoreListener.kt diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt index d142341a..f8443b6e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt @@ -2,6 +2,7 @@ package dev.slne.surf.surfapi.bukkit.api.packet import dev.slne.surf.surfapi.bukkit.api.packet.lore.SurfBukkitPacketLoreHandler import dev.slne.surf.surfapi.bukkit.api.packet.lore.SurfBukkitPacketLoreHandlerSimple +import dev.slne.surf.surfapi.bukkit.api.util.getCallingPlugin import dev.slne.surf.surfapi.core.api.util.requiredService import org.bukkit.NamespacedKey import org.bukkit.plugin.Plugin @@ -35,6 +36,14 @@ interface SurfBukkitPacketApi { fun registerPacketLoreListener( identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler + ) { + registerPacketLoreListener(getCallingPlugin(), identifier, listener) + } + + fun registerPacketLoreListener( + plugin: Plugin, + identifier: NamespacedKey, + listener: SurfBukkitPacketLoreHandler ) /** @@ -49,7 +58,15 @@ interface SurfBukkitPacketApi { identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandlerSimple ) { - registerPacketLoreListener(identifier, listener as SurfBukkitPacketLoreHandler) + registerPacketLoreListener(getCallingPlugin(), identifier, listener) + } + + fun registerPacketLoreListener( + plugin: Plugin, + identifier: NamespacedKey, + listener: SurfBukkitPacketLoreHandlerSimple + ) { + registerPacketLoreListener(plugin, identifier, listener as SurfBukkitPacketLoreHandler) } /** diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/SurfBukkitPacketApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/SurfBukkitPacketApiImpl.kt index d631412f..861985f1 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/SurfBukkitPacketApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/SurfBukkitPacketApiImpl.kt @@ -15,10 +15,11 @@ class SurfBukkitPacketApiImpl : SurfBukkitPacketApi { } override fun registerPacketLoreListener( + plugin: Plugin, identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler ) { - PacketLoreListener.register(identifier, listener) + PacketLoreListener.register(plugin, identifier, listener) } override fun registerPacketLoreListenerGlobal( diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/PacketApiLoader.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/PacketApiLoader.kt index 1e5ddc67..171a4e47 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/PacketApiLoader.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/PacketApiLoader.kt @@ -1,11 +1,14 @@ package dev.slne.surf.surfapi.bukkit.server.packet import com.github.retrooper.packetevents.PacketEvents +import dev.slne.surf.surfapi.bukkit.api.event.register +import dev.slne.surf.surfapi.bukkit.api.event.unregister import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution import dev.slne.surf.surfapi.bukkit.api.packet.listener.packetListenerApi import dev.slne.surf.surfapi.bukkit.server.impl.glow.GlowingPacketListener import dev.slne.surf.surfapi.bukkit.server.packet.listener.PlayerChannelInjector import dev.slne.surf.surfapi.bukkit.server.packet.lore.PacketLoreListener +import dev.slne.surf.surfapi.bukkit.server.packet.lore.PluginDisablePacketLoreListener import dev.slne.surf.surfapi.bukkit.server.plugin import dev.slne.surf.surfapi.core.api.extensions.packetEvents import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder @@ -23,12 +26,14 @@ object PacketApiLoader { packetListenerApi.registerListeners(GlowingPacketListener) PlayerChannelInjector.register() + PluginDisablePacketLoreListener.register() } @OptIn(NmsUseWithCaution::class) fun onDisable() { packetEvents.terminate() packetListenerApi.unregisterListeners(PacketLoreListener) + PluginDisablePacketLoreListener.unregister() } private fun setupPacketEvents() { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt index b3b27776..f5854b8d 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt @@ -8,8 +8,9 @@ import dev.slne.surf.surfapi.bukkit.api.packet.lore.SurfBukkitPacketLoreHandler import dev.slne.surf.surfapi.bukkit.api.util.key import dev.slne.surf.surfapi.bukkit.server.nms.toBukkit import dev.slne.surf.surfapi.bukkit.server.nms.toNms -import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import net.kyori.adventure.text.format.TextDecoration import net.minecraft.core.component.DataComponents import net.minecraft.network.protocol.game.* @@ -18,8 +19,6 @@ import net.minecraft.world.item.component.CustomData import net.minecraft.world.item.component.ItemLore import org.bukkit.NamespacedKey import org.bukkit.plugin.Plugin -import java.util.concurrent.ConcurrentHashMap -import net.kyori.adventure.text.Component as AdventureComponent import net.minecraft.network.chat.Component as MinecraftComponent /** @@ -28,23 +27,22 @@ import net.minecraft.network.chat.Component as MinecraftComponent */ @OptIn(NmsUseWithCaution::class) object PacketLoreListener : PacketListener { - private val loreHandlers = ConcurrentHashMap() - private val loreHandlersGlobal = ConcurrentHashMap>() + private val globalHandlersByPlugin = + Object2ObjectLinkedOpenHashMap>() + + private val keyedHandlersByPlugin = + Object2ObjectLinkedOpenHashMap>() @Volatile - private var loreHandlerSnapshot: Array> = emptyArray() + private var keyedHandlersSnapshot: Map = emptyMap() @Volatile - private var loreHandlerGlobalSnapshot: Array>> = - emptyArray() + private var globalHandlersSnapshot: Array = emptyArray() private val ORIGINAL_LORE_KEY = key("original_lore") private val ORIGINAL_LORE_KEY_STRING = ORIGINAL_LORE_KEY.asString() - private val ITALIC_DECORATION = TextDecoration.ITALIC - private val ITALIC_STATE_FALSE = TextDecoration.State.FALSE - - private fun hasAnyHandlers(): Boolean = loreHandlerSnapshot.isNotEmpty() || loreHandlerGlobalSnapshot.isNotEmpty() + private fun hasAnyHandlers(): Boolean = keyedHandlersSnapshot.isNotEmpty() || globalHandlersSnapshot.isNotEmpty() @ServerboundListener fun onPacketReceive(event: ServerboundSetCreativeModeSlotPacket) { @@ -55,109 +53,164 @@ object PacketLoreListener : PacketListener { fun onWindowItem(event: ClientboundContainerSetContentPacket): ClientboundContainerSetContentPacket { if (!hasAnyHandlers()) return event - val items = event.items - val updatedItems = ObjectArrayList(items.size) - for (i in items.indices) { - updatedItems.add(makeUpdatedItemStack(items[i].copy())) + val sourceItems = event.items + val updatedItems = ObjectArrayList(sourceItems.size) + + var changed = false + + for (i in sourceItems.indices) { + val original = sourceItems[i] + val updated = makeUpdatedItemStack(original) + + if (updated !== original) { + changed = true + } + + updatedItems.add(updated) + } + + val originalCarried = event.carriedItem() + val updatedCarried = makeUpdatedItemStack(originalCarried) + + if (updatedCarried !== originalCarried) { + changed = true + } + + if (!changed) { + return event } return ClientboundContainerSetContentPacket( event.containerId(), event.stateId(), - items, - makeUpdatedItemStack(event.carriedItem().copy()) + updatedItems, + updatedCarried ) } @ClientboundListener fun onSetSlotPacket(event: ClientboundContainerSetSlotPacket): ClientboundContainerSetSlotPacket { - if (!hasAnyHandlers()) return event + val original = event.item + val updated = makeUpdatedItemStack(original) + + if (updated === original) { + return event + } return ClientboundContainerSetSlotPacket( event.containerId, event.stateId, event.slot, - makeUpdatedItemStack(event.item.copy()) + updated ) } @ClientboundListener fun onSetPlayerInventoryPacket(event: ClientboundSetPlayerInventoryPacket): ClientboundSetPlayerInventoryPacket { - if (!hasAnyHandlers()) return event + val original = event.contents + val updated = makeUpdatedItemStack(original) + + if (updated === original) { + return event + } return ClientboundSetPlayerInventoryPacket( event.slot(), - makeUpdatedItemStack(event.contents.copy()) + updated ) } @ClientboundListener fun onSetCursorItemPacket(event: ClientboundSetCursorItemPacket): ClientboundSetCursorItemPacket { - if (!hasAnyHandlers()) return event + val original = event.contents + val updated = makeUpdatedItemStack(original) - return ClientboundSetCursorItemPacket(makeUpdatedItemStack(event.contents.copy())) + if (updated === original) { + return event + } + + return ClientboundSetCursorItemPacket(updated) } + /** + * Returns the original item if: + * - item is empty + * - no handlers exist + * - no keyed handler matches and no global handler exists + * - handlers run but lore result is identical + * + * Only copies the stack if at least one handler will actually run. + */ private fun makeUpdatedItemStack( - item: ItemStack, + original: ItemStack, ): ItemStack { - if (item.isEmpty) return item + if (original.isEmpty) return original // One volatile read - val handlerEntries = loreHandlerSnapshot - val globalEntries = loreHandlerGlobalSnapshot + val keyedSnapshot = keyedHandlersSnapshot + val globalSnapshot = globalHandlersSnapshot - val nmsLore = item.getOrDefault(DataComponents.LORE, ItemLore.EMPTY) - val lines = nmsLore.lines + if (keyedSnapshot.isEmpty() && globalSnapshot.isEmpty()) { + return original + } + + /* + * We need Bukkit PDC for the current handler API. + * But we only use the original mirror to cheaply determine + * whether any keyed handlers actually match. + */ + val originalBukkitStack = original.asBukkitMirror() + val originalPdc = originalBukkitStack.persistentDataContainer + + val matchingKeyedHandlers = resolveMatchingKeyedHandlers( + originalPdc.keys, + keyedSnapshot + ) - val mutableLore = mutableObjectListOf(lines.size) - for (i in lines.indices) { - mutableLore.add(lines[i].toBukkit()) + if (matchingKeyedHandlers.isEmpty() && globalSnapshot.isEmpty()) { + return original } + /* + * From here on, we know that at least one handler will run. + * Only now create a copy. + */ + val item = original.copy() val bukkitStack = item.asBukkitMirror() val pdc = bukkitStack.persistentDataContainer - var anyHandlerRan = false - if (handlerEntries.isNotEmpty()) { - for ((key, handler) in handlerEntries) { - if (pdc.has(key)) { - handler.handleLore(mutableLore, pdc, bukkitStack) - anyHandlerRan = true - } - } - } + val originalLore = item.getOrDefault(DataComponents.LORE, ItemLore.EMPTY) + val originalLines = originalLore.lines - if (globalEntries.isNotEmpty()) { - for ((plugin, handlers) in globalEntries) { - if (plugin.isEnabled) { - for (handler in handlers) { - handler.handleLore(mutableLore, pdc, bukkitStack) - anyHandlerRan = true - } - } - } + val mutableLore = originalLines.mapTo( + ObjectArrayList(originalLines.size) + ) { it.toBukkit() } + + for (i in matchingKeyedHandlers.indices) { + matchingKeyedHandlers[i].handleLore(mutableLore, pdc, bukkitStack) } - if (!anyHandlerRan) return item + for (i in globalSnapshot.indices) { + globalSnapshot[i].handleLore(mutableLore, pdc, bukkitStack) + } val updatedLines = ObjectArrayList(mutableLore.size) for (i in mutableLore.indices) { - val component = mutableLore[i] - updatedLines.add( - component.decorationIfAbsent(ITALIC_DECORATION, ITALIC_STATE_FALSE).toNms() - ) + val line = mutableLore[i].decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE) + updatedLines.add(line.toNms()) } - val updatedNmsLore = ItemLore(updatedLines) + val updatedLore = ItemLore(updatedLines) - if (updatedNmsLore == nmsLore) { - return item + if (updatedLore == originalLore) { + return original } - item.set(DataComponents.LORE, updatedNmsLore) + item.set(DataComponents.LORE, updatedLore) CustomData.update(DataComponents.CUSTOM_DATA, item) { tag -> - tag.store(ORIGINAL_LORE_KEY_STRING, ItemLore.CODEC, nmsLore) + if (!tag.contains(ORIGINAL_LORE_KEY_STRING)) { + tag.store(ORIGINAL_LORE_KEY_STRING, ItemLore.CODEC, originalLore) + } } return item @@ -166,6 +219,15 @@ object PacketLoreListener : PacketListener { private fun makeCleanItemStack( stack: ItemStack, ): ItemStack { + if (stack.isEmpty) { + return stack + } + + val customData = stack.get(DataComponents.CUSTOM_DATA) ?: return stack + if (!customData.contains(ORIGINAL_LORE_KEY_STRING)) { + return stack + } + CustomData.update(DataComponents.CUSTOM_DATA, stack) { tag -> val originalLore = tag.read(ORIGINAL_LORE_KEY_STRING, ItemLore.CODEC) originalLore.ifPresent { lore -> @@ -177,28 +239,112 @@ object PacketLoreListener : PacketListener { return stack } - private fun rebuildSnapshots() { - loreHandlerSnapshot = loreHandlers.entries.toTypedArray() - loreHandlerGlobalSnapshot = loreHandlersGlobal.entries.toTypedArray() + private fun resolveMatchingKeyedHandlers( + itemKeys: Set, + keyedSnapshot: Map, + ): List { + if (itemKeys.isEmpty() || keyedSnapshot.isEmpty()) { + return emptyList() + } + + var result: ObjectArrayList? = null + for (key in itemKeys) { + val handler = keyedSnapshot[key] ?: continue + + if (result == null) { + result = ObjectArrayList(2) + } + + result.add(handler) + } + + return result ?: emptyList() } - fun register(identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler) { - loreHandlers[identifier] = listener - rebuildSnapshots() + fun register(plugin: Plugin, identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler) { + synchronized(this) { + check(!keyedHandlersSnapshot.containsKey(identifier)) { + "A PacketLore handler for $identifier is already registered!" + } + + val handlers = keyedHandlersByPlugin.computeIfAbsent(plugin) { Object2ObjectLinkedOpenHashMap() } + + val previous = handlers.putIfAbsent(identifier, listener) + check(previous == null) { + "A PacketLore handler for $identifier is already registered for plugin ${plugin.name}!" + } + + val newSnapshot = Object2ObjectLinkedOpenHashMap(keyedHandlersSnapshot) + newSnapshot[identifier] = listener + keyedHandlersSnapshot = newSnapshot + } } fun register(plugin: Plugin, listener: SurfBukkitPacketLoreHandler) { - loreHandlersGlobal.computeIfAbsent(plugin) { ConcurrentHashMap.newKeySet() }.add(listener) - rebuildSnapshots() + synchronized(this) { + val handlers = globalHandlersByPlugin.computeIfAbsent(plugin) { ObjectLinkedOpenHashSet() } + if (handlers.add(listener)) { + rebuildGlobalHandlersSnapshot() + } else { + error("A PacketLore handler identical to the provided one (${listener.javaClass.name}) is already registered for plugin ${plugin.name}!") + } + } } fun unregister(identifier: NamespacedKey) { - loreHandlers.remove(identifier) - rebuildSnapshots() + synchronized(this) { + if (!keyedHandlersSnapshot.containsKey(identifier)) { + return + } + + var emptyPlugin: Plugin? = null + + for ((plugin, handlers) in keyedHandlersByPlugin) { + if (handlers.remove(identifier) != null) { + if (handlers.isEmpty()) { + emptyPlugin = plugin + } + break + } + } + + if (emptyPlugin != null) { + keyedHandlersByPlugin.remove(emptyPlugin) + } + + val newSnapshot = Object2ObjectLinkedOpenHashMap(keyedHandlersSnapshot) + newSnapshot.remove(identifier) + keyedHandlersSnapshot = newSnapshot + } } fun unregister(plugin: Plugin) { - loreHandlersGlobal.remove(plugin) - rebuildSnapshots() + synchronized(this) { + val removedGlobal = globalHandlersByPlugin.remove(plugin) != null + if (removedGlobal) { + rebuildGlobalHandlersSnapshot() + } + + val removedKeyed = keyedHandlersByPlugin.remove(plugin) + if (removedKeyed != null) { + val newSnapshot = Object2ObjectLinkedOpenHashMap(keyedHandlersSnapshot) + for (identifier in removedKeyed.keys) { + newSnapshot.remove(identifier) + } + keyedHandlersSnapshot = newSnapshot + } + } + } + + private fun rebuildGlobalHandlersSnapshot() { + val snapshot = ObjectArrayList() + + globalHandlersByPlugin.object2ObjectEntrySet().fastForEach { (plugin, handlers) -> + if (plugin.isEnabled) { + snapshot.addAll(handlers) + } + } + + globalHandlersSnapshot = snapshot.toTypedArray() } } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PluginDisablePacketLoreListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PluginDisablePacketLoreListener.kt new file mode 100644 index 00000000..5955a63c --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PluginDisablePacketLoreListener.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.surfapi.bukkit.server.packet.lore + +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.server.PluginDisableEvent + +object PluginDisablePacketLoreListener : Listener { + + @EventHandler + fun onPluginDisable(event: PluginDisableEvent) { + PacketLoreListener.unregister(event.plugin) + } +} \ No newline at end of file From 6724ad34ccd6593547896037cbf8599b96617946 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 15 Mar 2026 12:16:12 +0100 Subject: [PATCH 3/5] chore: update version to 1.21.11-2.66.0 and bump abi --- gradle.properties | 2 +- .../surf-api-bukkit-api/api/surf-api-bukkit-api.api | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1a170ea0..c1f28a83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.65.0 +version=1.21.11-2.66.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false diff --git a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api index ac48d12b..a48e023a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api +++ b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api @@ -2155,8 +2155,10 @@ public abstract interface class dev/slne/surf/surfapi/bukkit/api/nms/listener/pa public abstract interface class dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi { public static final field Companion Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi$Companion; public static fun getInstance ()Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi; - public abstract fun registerPacketLoreListener (Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandler;)V + public fun registerPacketLoreListener (Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandler;)V public fun registerPacketLoreListener (Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V + public abstract fun registerPacketLoreListener (Lorg/bukkit/plugin/Plugin;Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandler;)V + public fun registerPacketLoreListener (Lorg/bukkit/plugin/Plugin;Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V public abstract fun registerPacketLoreListenerGlobal (Lorg/bukkit/plugin/Plugin;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandler;)V public fun registerPacketLoreListenerGlobal (Lorg/bukkit/plugin/Plugin;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V public abstract fun unregisterPacketLoreListener (Lorg/bukkit/NamespacedKey;)V @@ -2168,7 +2170,9 @@ public final class dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi$C } public final class dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi$DefaultImpls { + public static fun registerPacketLoreListener (Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi;Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandler;)V public static fun registerPacketLoreListener (Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi;Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V + public static fun registerPacketLoreListener (Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi;Lorg/bukkit/plugin/Plugin;Lorg/bukkit/NamespacedKey;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V public static fun registerPacketLoreListenerGlobal (Ldev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi;Lorg/bukkit/plugin/Plugin;Ldev/slne/surf/surfapi/bukkit/api/packet/lore/SurfBukkitPacketLoreHandlerSimple;)V } From a5501adf426e2e2a1666406512d3df60a379d9b7 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 15 Mar 2026 12:21:47 +0100 Subject: [PATCH 4/5] feat: optimize matching keyed handlers resolution in PacketLoreListener --- .../server/packet/lore/PacketLoreListener.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt index f5854b8d..758543ea 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/packet/lore/PacketLoreListener.kt @@ -11,6 +11,7 @@ import dev.slne.surf.surfapi.bukkit.server.nms.toNms import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import it.unimi.dsi.fastutil.objects.ObjectLists import net.kyori.adventure.text.format.TextDecoration import net.minecraft.core.component.DataComponents import net.minecraft.network.protocol.game.* @@ -155,17 +156,20 @@ object PacketLoreListener : PacketListener { } /* - * We need Bukkit PDC for the current handler API. - * But we only use the original mirror to cheaply determine - * whether any keyed handlers actually match. + * We need Bukkit PDC for the current handler API, but only when there + * are keyed handlers to consider. The original mirror is used to cheaply + * determine whether any keyed handlers actually match. */ - val originalBukkitStack = original.asBukkitMirror() - val originalPdc = originalBukkitStack.persistentDataContainer - - val matchingKeyedHandlers = resolveMatchingKeyedHandlers( - originalPdc.keys, - keyedSnapshot - ) + val matchingKeyedHandlers = if (keyedSnapshot.isNotEmpty()) { + val originalBukkitStack = original.asBukkitMirror() + val originalPdc = originalBukkitStack.persistentDataContainer + resolveMatchingKeyedHandlers( + originalPdc.keys, + keyedSnapshot + ) + } else { + ObjectLists.emptyList() + } if (matchingKeyedHandlers.isEmpty() && globalSnapshot.isEmpty()) { return original From ac53c1c308a97d9ba0481ed824b9512f66ccfa67 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 15 Mar 2026 12:32:09 +0100 Subject: [PATCH 5/5] feat: update SurfBukkitPacketApi to prefer explicit plugin parameter for lore listener registration --- .../bukkit/api/packet/SurfBukkitPacketApi.kt | 119 ++++++++++-------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt index f8443b6e..549a8ec6 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/packet/SurfBukkitPacketApi.kt @@ -10,36 +10,48 @@ import org.bukkit.plugin.Plugin /** * The SurfBukkitPacketApi interface extends packet handling capabilities for Bukkit environments. * - * It provides methods for registering and unregistering lore listeners, enabling dynamic modification - * of item stack lore based on custom logic. It also includes global registration methods for listening - * to all items and utilities for managing these listeners efficiently. + * Provides methods for registering and unregistering lore listeners, enabling dynamic modification + * of item stack lore based on custom logic. Includes global registration for all items and utilities + * for managing these listeners efficiently. * - * This API allows developers to enhance item lore dynamically, providing a flexible system for plugins - * that need to alter or interact with lore without directly modifying the underlying item stack data. + * Prefer the overloads that accept an explicit [Plugin] parameter over the deprecated ones, + * as automatic caller-plugin detection via `getCallingPlugin` is unreliable across different + * call-site configurations. */ interface SurfBukkitPacketApi { /** - * Registers a listener for modifying the lore of a specific item stack identified by the given key. - * - * @param identifier A unique key representing the item to listen for. Must not be null. - * @param listener The listener that modifies the lore of the item stack. Must not be null. - * - * Example Usage: - * ``` - * val key = NamespacedKey("myplugin", "custom_item") - * surfBukkitPacketApi.registerPacketLoreListener(key, SurfBukkitPacketLoreHandler { lore, _, _ -> - * lore.add(Component.text("Special Lore!")) - * }) - * ``` + * Registers a listener for modifying the lore of a specific item stack identified by [identifier]. + * + * @param identifier A unique key representing the item to listen for. + * @param listener The listener that modifies the lore of the item stack. + * + * @deprecated Automatic plugin detection via `getCallingPlugin` is unreliable. Use + * [registerPacketLoreListener(Plugin, NamespacedKey, SurfBukkitPacketLoreHandler)] instead + * and pass your plugin instance explicitly. */ + @Deprecated( + message = "Automatic plugin detection is unreliable. Pass your plugin instance explicitly.", + replaceWith = ReplaceWith("registerPacketLoreListener(plugin, identifier, listener)") + ) fun registerPacketLoreListener( identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandler ) { - registerPacketLoreListener(getCallingPlugin(), identifier, listener) + registerPacketLoreListener(getCallingPlugin(2), identifier, listener) } + /** + * Registers a listener for modifying the lore of a specific item stack identified by [identifier]. + * + * This is the preferred overload. The [plugin] reference is used to properly manage the + * listener lifecycle — all listeners registered under a plugin are automatically cleaned up + * when [unregisterPacketLoreListener(Plugin)] is called. + * + * @param plugin The plugin registering the listener. Used for lifecycle management. + * @param identifier A unique key representing the item to listen for. + * @param listener The listener that modifies the lore of the item stack. + */ fun registerPacketLoreListener( plugin: Plugin, identifier: NamespacedKey, @@ -47,20 +59,39 @@ interface SurfBukkitPacketApi { ) /** - * Registers a simplified packet lore listener for a specific item. + * Registers a simplified listener for modifying the lore of a specific item stack identified by [identifier]. + * + * Delegates to [registerPacketLoreListener(Plugin, NamespacedKey, SurfBukkitPacketLoreHandler)]. * - * @param identifier A unique key representing the item to listen for. Must not be null. - * @param listener The simplified packet lore listener that focuses solely on modifying the lore list. + * @param identifier A unique key representing the item to listen for. + * @param listener The simplified lore listener that focuses solely on modifying the lore list. * - * This method delegates to the standard [registerPacketLoreListener] implementation. + * @deprecated Automatic plugin detection is unreliable. Use + * [registerPacketLoreListener(Plugin, NamespacedKey, SurfBukkitPacketLoreHandlerSimple)] instead + * and pass your plugin instance explicitly. */ + @Deprecated( + message = "Automatic plugin detection is unreliable. Pass your plugin instance explicitly.", + replaceWith = ReplaceWith("registerPacketLoreListener(plugin, identifier, listener)") + ) fun registerPacketLoreListener( identifier: NamespacedKey, listener: SurfBukkitPacketLoreHandlerSimple ) { - registerPacketLoreListener(getCallingPlugin(), identifier, listener) + registerPacketLoreListener(getCallingPlugin(2), identifier, listener) } + /** + * Registers a simplified listener for modifying the lore of a specific item stack identified by [identifier]. + * + * This is the preferred overload. Delegates to + * [registerPacketLoreListener(Plugin, NamespacedKey, SurfBukkitPacketLoreHandler)]. + * The [plugin] reference is used to properly manage the listener lifecycle. + * + * @param plugin The plugin registering the listener. Used for lifecycle management. + * @param identifier A unique key representing the item to listen for. + * @param listener The simplified lore listener that focuses solely on modifying the lore list. + */ fun registerPacketLoreListener( plugin: Plugin, identifier: NamespacedKey, @@ -70,17 +101,14 @@ interface SurfBukkitPacketApi { } /** - * Registers a packet lore listener globally to handle lore modifications for all items. + * Registers a lore listener globally to handle lore modifications for all items. * - * @param plugin The plugin registering the listener. Used to manage lifecycle and cleanup. - * @param listener The lore listener to handle lore modifications globally. + * Unlike the key-based overloads, this listener fires for every item stack regardless of + * its identifier. Use this only when you genuinely need to intercept all items, as it has + * a broader performance impact. * - * Example Usage: - * ``` - * surfBukkitPacketApi.registerPacketLoreListenerGlobal(myPlugin, SurfBukkitPacketLoreHandler { lore, _, _ -> - * lore.add(Component.text("Global Lore Modification")) - * }) - * ``` + * @param plugin The plugin registering the listener. Used for lifecycle management. + * @param listener The lore listener to handle lore modifications globally. */ fun registerPacketLoreListenerGlobal( plugin: Plugin, @@ -88,12 +116,12 @@ interface SurfBukkitPacketApi { ) /** - * Registers a simplified packet lore listener globally for all items. + * Registers a simplified lore listener globally for all items. * - * @param plugin The plugin registering the listener. Used for proper cleanup during plugin shutdown. - * @param listener The simplified lore listener that focuses solely on the lore list. + * Delegates to [registerPacketLoreListenerGlobal(Plugin, SurfBukkitPacketLoreHandler)]. * - * This method delegates to the standard [registerPacketLoreListenerGlobal] implementation. + * @param plugin The plugin registering the listener. Used for lifecycle management. + * @param listener The simplified lore listener that focuses solely on the lore list. */ fun registerPacketLoreListenerGlobal( plugin: Plugin, @@ -103,26 +131,19 @@ interface SurfBukkitPacketApi { } /** - * Unregisters a previously registered packet lore listener identified by the given key. + * Unregisters the packet lore listener associated with the given [identifier]. * * @param identifier The key identifying the listener to unregister. - * - * Example Usage: - * ``` - * surfBukkitPacketApi.unregisterPacketLoreListener(NamespacedKey("myplugin", "custom_item")) - * ``` */ fun unregisterPacketLoreListener(identifier: NamespacedKey) /** - * Unregisters all packet lore listeners associated with the given plugin. + * Unregisters all packet lore listeners associated with the given [plugin]. * - * @param plugin The plugin whose listeners should be unregistered. + * Call this during plugin shutdown to ensure all listeners registered under this plugin + * are properly cleaned up. * - * Example Usage: - * ``` - * surfBukkitPacketApi.unregisterPacketLoreListener(myPlugin) - * ``` + * @param plugin The plugin whose listeners should be unregistered. */ fun unregisterPacketLoreListener(plugin: Plugin) @@ -131,5 +152,3 @@ interface SurfBukkitPacketApi { val instance = requiredService() } } - -val surfBukkitPacketApi get() = SurfBukkitPacketApi.instance