From d917475e7d006f54b32c5b0de2ae8ba40225ad6c Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 31 Mar 2026 10:40:46 +0900 Subject: [PATCH] Fake-Interaction --- README.md | 46 ++++- .../ghostdisplays/api/DisplayService.kt | 6 + .../api/InteractionController.kt | 32 ++++ .../api/event/FakeInteractionClickEvent.kt | 40 +++++ .../internal/DefaultDisplayService.kt | 21 +++ .../controller/BaseInteractionController.kt | 170 ++++++++++++++++++ .../internal/controller/DisplayRegistry.kt | 76 +++++--- .../internal/fake/FakeInteraction.kt | 1 + .../internal/nms/InteractionPacketListener.kt | 54 +++++- 9 files changed, 408 insertions(+), 38 deletions(-) create mode 100644 src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionController.kt create mode 100644 src/main/kotlin/net/hareworks/ghostdisplays/api/event/FakeInteractionClickEvent.kt create mode 100644 src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseInteractionController.kt diff --git a/README.md b/README.md index c078cf7..7c5da9f 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。 ## 提供機能 - `DisplayService` による Text/Block/Item Display の生成 API -- `DisplayController` を介した `Player#showEntity` / `hideEntity` の参照カウント管理 +- `DisplayController` を介したパケットベースの表示・非表示制御(参照カウント管理) - `AudiencePredicate`/`AudiencePredicates` による可視対象の自動同期(ログイン・ワールド移動・リスポーンで再評価) -- Interaction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー +- FakeInteraction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー - まとめて `destroyAll()` できるリソース管理 ## スタンドアロンでの使い方 @@ -19,7 +19,7 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。 | `/ghostdisplay create text ` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 | | `/ghostdisplay create block ` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 | | `/ghostdisplay create item ` | ItemDisplay を生成します。`minecraft:stick` などのアイテム ID を受け付けます。 | -| `/ghostdisplay text edit ` | 既存の TextDisplay をチャット入力で編集。`cancel` で破棄。単語のみで良い場合は `/ghostdisplay text set Hello_World` のように `_` をスペースに変換して即時更新できます。 | +| `/ghostdisplay text set ` | TextDisplay のテキストを即時更新します。`_` はスペースに、`\n` は改行に変換されます。 | | `/ghostdisplay viewer add|remove ` | 任意のプレイヤーまたはセレクターを表示対象に追加/削除します。`/ghostdisplay viewer clear ` で全員解除。 | | `/ghostdisplay audience permission add ` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 | | `/ghostdisplay audience near set ` | Display 周囲の半径プレイヤーへ自動表示 (`audience clear` で全自動表示を解除)。 | @@ -28,11 +28,43 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。 ## 技術的ポイント -- Paper 標準 API の `setVisibleByDefault(false)` + `showEntity`/`hideEntity` を採用し、ProtocolLib に依存しない設計 -- Display / Interaction は `setPersistent(false)` でスポーンし、サーバーリロードやチャンクアンロードに強い +- **NMS パケットベースの FakeEntity 方式** — サーバー上に実エンティティを生成せず、クライアントへのパケット送信のみで表示を実現。エンティティティック・コリジョン・永続化のオーバーヘッドがゼロ +- パケット送信はスレッドセーフなため、Folia のリージョンスケジューリングに依存しないシンプルな設計 +- クリック検知は Netty パイプラインで `ServerboundInteractPacket` を傍受し、FakeInteraction の entityId にマッチしたらハンドラーを発火 - Predicate ごとにアクティブなプレイヤー集合を持つため、複数条件での重複表示にも対応 -- クリック検知は `PlayerInteractEntityEvent` を介して Display/Interaction 双方から観測し、ハンドラーは Kotlin DSL で登録可能 -- Kotlin 2.2 + ShadowJar + plugin-yml 生成を使い、`./gradlew build`でそのまま Paper に投入可能なJarを生成 +- `paperweight-userdev` による NMS アクセス(1.21.11 対応) + +## API 利用例 + +```kotlin +val service: DisplayService = GhostDisplaysPlugin.service() + +// TextDisplay を生成 +val controller = service.createTextDisplay(location) { + text = Component.text("Hello") + billboard = Display.Billboard.CENTER + viewRange = 0.5f +} +controller.show(player) + +// テキスト更新 +controller.updateText { it.text = Component.text("Updated") } + +// 共通プロパティ更新 +controller.updateDisplay { it.billboard = Display.Billboard.FIXED } + +// テレポート +controller.teleport(newLocation) + +// Audience ルール +controller.addAudienceRule(AudiencePredicates.near(location, 30.0), AudienceAction.ADD) + +// クリックハンドラー +controller.onClick { ctx -> ctx.player.sendMessage("Clicked!") } + +// 破棄 +controller.destroy() +``` ## 同梱ライブラリ diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt index 48d0c33..9327b4f 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt @@ -1,6 +1,7 @@ package net.hareworks.ghostdisplays.api import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay +import net.hareworks.ghostdisplays.internal.fake.FakeInteraction import net.hareworks.ghostdisplays.internal.fake.FakeItemDisplay import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay import org.bukkit.Location @@ -26,5 +27,10 @@ interface DisplayService { builder: FakeItemDisplay.() -> Unit = {} ): DisplayController + fun createInteraction( + location: Location, + builder: FakeInteraction.() -> Unit = {} + ): InteractionController + fun destroyAll() } diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionController.kt new file mode 100644 index 0000000..f8e5c51 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionController.kt @@ -0,0 +1,32 @@ +package net.hareworks.ghostdisplays.api + +import net.hareworks.ghostdisplays.api.audience.AudienceAction +import net.hareworks.ghostdisplays.api.audience.AudiencePredicate +import net.hareworks.ghostdisplays.api.click.HandlerRegistration +import net.hareworks.ghostdisplays.internal.fake.FakeInteraction +import org.bukkit.Location +import org.bukkit.entity.Player +import java.util.UUID + +interface InteractionController { + val entityId: Int + val key: String? + val location: Location + + fun show(player: Player) + fun hide(player: Player) + fun isViewing(playerId: UUID): Boolean + fun viewerIds(): Set + + fun updateInteraction(mutator: (FakeInteraction) -> Unit) + fun teleport(location: Location) + + fun setBaseVisibility(visible: Boolean) + fun addAudience(predicate: AudiencePredicate): HandlerRegistration + fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration + fun clearAudienceRules() + fun refreshAudience(target: Player? = null) + fun needsPeriodicUpdate(): Boolean + + fun destroy() +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/event/FakeInteractionClickEvent.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/event/FakeInteractionClickEvent.kt new file mode 100644 index 0000000..076d271 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/event/FakeInteractionClickEvent.kt @@ -0,0 +1,40 @@ +package net.hareworks.ghostdisplays.api.event + +import net.hareworks.ghostdisplays.api.InteractionController +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.util.Vector + +class FakeInteractionClickEvent( + val player: Player, + val interactionController: InteractionController, + val interactionEntityId: Int, + val clickType: ClickType, + val hand: EquipmentSlot?, + val clickPosition: Vector? +) : Event(), Cancellable { + + val isLeftClick: Boolean get() = clickType == ClickType.LEFT + val isRightClick: Boolean get() = clickType != ClickType.LEFT + + private var cancelled = false + + override fun isCancelled(): Boolean = cancelled + override fun setCancelled(cancel: Boolean) { cancelled = cancel } + + override fun getHandlers(): HandlerList = handlerList + + enum class ClickType { + LEFT, + RIGHT, + RIGHT_AT + } + + companion object { + @JvmStatic + val handlerList = HandlerList() + } +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt index 23f76c0..5ed177b 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt @@ -4,8 +4,10 @@ import java.util.UUID import java.util.concurrent.CopyOnWriteArraySet import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.DisplayService +import net.hareworks.ghostdisplays.api.InteractionController import net.hareworks.ghostdisplays.api.InteractionOptions import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController +import net.hareworks.ghostdisplays.internal.controller.BaseInteractionController import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay import net.hareworks.ghostdisplays.internal.fake.FakeInteraction @@ -21,6 +23,7 @@ internal class DefaultDisplayService( private val registry: DisplayRegistry ) : DisplayService { private val controllers = CopyOnWriteArraySet() + private val interactionControllers = CopyOnWriteArraySet() override fun createTextDisplay( location: Location, @@ -66,9 +69,27 @@ internal class DefaultDisplayService( return register(fake, location, interaction) } + override fun createInteraction( + location: Location, + builder: FakeInteraction.() -> Unit + ): InteractionController { + val fake = FakeInteraction( + entityId = DisplayPacketFactory.nextEntityId(), + uuid = UUID.randomUUID(), + location = location.clone() + ) + builder(fake) + val controller = BaseInteractionController(plugin, fake, registry) + interactionControllers += controller + registry.registerInteraction(controller) + return controller + } + override fun destroyAll() { controllers.forEach { it.destroy() } controllers.clear() + interactionControllers.forEach { it.destroy() } + interactionControllers.clear() } private fun register( diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseInteractionController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseInteractionController.kt new file mode 100644 index 0000000..99bbd72 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseInteractionController.kt @@ -0,0 +1,170 @@ +package net.hareworks.ghostdisplays.internal.controller + +import java.util.HashSet +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import net.hareworks.ghostdisplays.api.InteractionController +import net.hareworks.ghostdisplays.api.audience.AudienceAction +import net.hareworks.ghostdisplays.api.audience.AudiencePredicate +import net.hareworks.ghostdisplays.api.click.HandlerRegistration +import net.hareworks.ghostdisplays.internal.fake.FakeInteraction +import net.hareworks.ghostdisplays.internal.nms.DisplayPacketFactory +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin + +internal class BaseInteractionController( + private val plugin: JavaPlugin, + val fakeInteraction: FakeInteraction, + private val registry: DisplayRegistry +) : InteractionController { + + private val destroyed = AtomicBoolean(false) + private val viewerCounts = ConcurrentHashMap() + + private var baseVisibility: Boolean = false + private val audienceRules = CopyOnWriteArrayList() + private val autoVisiblePlayers = ConcurrentHashMap.newKeySet() + private var hasDynamicRules: Boolean = false + + override val entityId: Int get() = fakeInteraction.entityId + override val key: String? get() = fakeInteraction.key + override val location: Location get() = fakeInteraction.location.clone() + + override fun needsPeriodicUpdate(): Boolean = hasDynamicRules + + override fun show(player: Player) { + val uuid = player.uniqueId + val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1 + if (newCount == 1) { + DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(fakeInteraction)) + } + } + + override fun hide(player: Player) { + val uuid = player.uniqueId + val current = viewerCounts[uuid] ?: return + if (current <= 1) { + viewerCounts.remove(uuid) + DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createRemovePacket(fakeInteraction.entityId)) + } else { + viewerCounts[uuid] = current - 1 + } + } + + override fun isViewing(playerId: UUID): Boolean = viewerCounts.containsKey(playerId) + override fun viewerIds(): Set = HashSet(viewerCounts.keys) + + override fun updateInteraction(mutator: (FakeInteraction) -> Unit) { + mutator(fakeInteraction) + broadcastMetadata() + } + + override fun teleport(location: Location) { + fakeInteraction.location = location.clone() + val viewers = onlineViewers() + if (viewers.isEmpty()) return + val packet = DisplayPacketFactory.createTeleportPacket(fakeInteraction) + viewers.forEach { DisplayPacketFactory.sendPacket(it, packet) } + } + + override fun destroy() { + if (!destroyed.compareAndSet(false, true)) return + registry.unregisterInteraction(this) + val viewers = onlineViewers() + if (viewers.isNotEmpty()) { + val removePacket = DisplayPacketFactory.createRemovePacket(fakeInteraction.entityId) + viewers.forEach { DisplayPacketFactory.sendPacket(it, removePacket) } + } + viewerCounts.clear() + audienceRules.clear() + autoVisiblePlayers.clear() + } + + override fun setBaseVisibility(visible: Boolean) { + this.baseVisibility = visible + refreshAudience() + } + + override fun addAudience(predicate: AudiencePredicate): HandlerRegistration { + return addAudienceRule(predicate, AudienceAction.ADD) + } + + override fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration { + val entry = RuleEntry(predicate, action) + audienceRules.add(entry) + updateDynamicStatus() + refreshAudience() + return HandlerRegistration { + audienceRules.remove(entry) + updateDynamicStatus() + refreshAudience() + } + } + + override fun clearAudienceRules() { + audienceRules.clear() + refreshAudience() + } + + override fun refreshAudience(target: Player?) { + val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers() + if (players.isEmpty()) return + + players.forEach { player -> + val uuid = player.uniqueId + val shouldBeVisible = evaluateVisibility(player) + val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid) + + if (shouldBeVisible && !isCurrentlyAutoVisible) { + autoVisiblePlayers.add(uuid) + show(player) + } else if (!shouldBeVisible && isCurrentlyAutoVisible) { + autoVisiblePlayers.remove(uuid) + hide(player) + } + } + } + + internal fun handlePlayerQuit(player: Player) { + val uuid = player.uniqueId + viewerCounts.remove(uuid) + autoVisiblePlayers.remove(uuid) + } + + private fun broadcastMetadata() { + val viewers = onlineViewers() + if (viewers.isEmpty()) return + val packet = DisplayPacketFactory.createMetadataPacket(fakeInteraction) + viewers.forEach { DisplayPacketFactory.sendPacket(it, packet) } + } + + private fun onlineViewers(): List = + viewerCounts.keys.mapNotNull { Bukkit.getPlayer(it) } + + private fun updateDynamicStatus() { + hasDynamicRules = audienceRules.any { it.predicate.isDynamic() } + } + + private fun evaluateVisibility(player: Player): Boolean { + var visible = baseVisibility + for (rule in audienceRules) { + val matches = runCatching { rule.predicate.test(player) }.getOrDefault(false) + if (matches) { + when (rule.action) { + AudienceAction.ADD -> visible = true + AudienceAction.EXCLUDE -> visible = false + } + } + } + return visible + } + + private data class RuleEntry( + val predicate: AudiencePredicate, + val action: AudienceAction + ) +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt index 0c9e546..ee50200 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt @@ -2,6 +2,8 @@ package net.hareworks.ghostdisplays.internal.controller import java.util.concurrent.ConcurrentHashMap import net.hareworks.ghostdisplays.api.click.ClickSurface +import net.hareworks.ghostdisplays.api.event.FakeInteractionClickEvent +import net.hareworks.ghostdisplays.internal.nms.InteractAction import net.hareworks.ghostdisplays.internal.nms.InteractionPacketListener import org.bukkit.Bukkit import org.bukkit.entity.Player @@ -15,19 +17,37 @@ import org.bukkit.plugin.java.JavaPlugin internal class DisplayRegistry : Listener { private val displayControllers = ConcurrentHashMap() - private val interactionControllers = ConcurrentHashMap() + private val displayInteractionControllers = ConcurrentHashMap() + private val standaloneInteractionControllers = ConcurrentHashMap() private lateinit var packetListener: InteractionPacketListener fun initialize(plugin: JavaPlugin) { - packetListener = InteractionPacketListener(plugin) { player, entityId, isAttack -> - if (isAttack) return@InteractionPacketListener - val fromInteraction = interactionControllers[entityId] - val fromDisplay = displayControllers[entityId] + packetListener = InteractionPacketListener(plugin) { player, data -> + // スタンドアロンInteractionを先にチェック → Bukkitイベント発火 + val standalone = standaloneInteractionControllers[data.entityId] + if (standalone != null) { + val clickType = when (data.action) { + InteractAction.ATTACK -> FakeInteractionClickEvent.ClickType.LEFT + InteractAction.INTERACT -> FakeInteractionClickEvent.ClickType.RIGHT + InteractAction.INTERACT_AT -> FakeInteractionClickEvent.ClickType.RIGHT_AT + } + val loc = standalone.fakeInteraction.location + plugin.server.regionScheduler.execute(plugin, loc) { + val event = FakeInteractionClickEvent(player, standalone, data.entityId, clickType, data.hand, data.clickPosition) + Bukkit.getPluginManager().callEvent(event) + } + return@InteractionPacketListener + } + + // Display付属Interactionの場合は既存のクリックハンドラ(右クリックのみ) + if (data.action == InteractAction.ATTACK) return@InteractionPacketListener + val fromInteraction = displayInteractionControllers[data.entityId] + val fromDisplay = displayControllers[data.entityId] val controller = fromInteraction ?: fromDisplay ?: return@InteractionPacketListener val surface = if (fromInteraction != null) ClickSurface.INTERACTION else ClickSurface.DISPLAY val loc = controller.fakeDisplay.location plugin.server.regionScheduler.execute(plugin, loc) { - controller.handleClick(player, entityId, surface) + controller.handleClick(player, data.entityId, surface) } } packetListener.injectAll() @@ -37,27 +57,37 @@ internal class DisplayRegistry : Listener { fun register(controller: BaseDisplayController) { displayControllers[controller.fakeDisplay.entityId] = controller controller.fakeInteraction?.let { - interactionControllers[it.entityId] = controller + displayInteractionControllers[it.entityId] = controller } } fun unregister(controller: BaseDisplayController) { displayControllers.remove(controller.fakeDisplay.entityId, controller) controller.fakeInteraction?.let { - interactionControllers.remove(it.entityId, controller) + displayInteractionControllers.remove(it.entityId, controller) } } - fun controllerCount(): Int = displayControllers.size + fun registerInteraction(controller: BaseInteractionController) { + standaloneInteractionControllers[controller.fakeInteraction.entityId] = controller + } + + fun unregisterInteraction(controller: BaseInteractionController) { + standaloneInteractionControllers.remove(controller.fakeInteraction.entityId, controller) + } + + fun controllerCount(): Int = displayControllers.size + standaloneInteractionControllers.size fun shutdown() { displayControllers.clear() - interactionControllers.clear() + displayInteractionControllers.clear() + standaloneInteractionControllers.clear() } @EventHandler fun onPlayerQuit(event: PlayerQuitEvent) { - controllersSnapshot().forEach { it.handlePlayerQuit(event.player) } + displayControllersSnapshot().forEach { it.handlePlayerQuit(event.player) } + standaloneInteractionsSnapshot().forEach { it.handlePlayerQuit(event.player) } } @EventHandler @@ -75,26 +105,28 @@ internal class DisplayRegistry : Listener { refreshAudiences(event.player) } - private fun controllersSnapshot(): Collection = + private fun displayControllersSnapshot(): Collection = displayControllers.values.toSet() + private fun standaloneInteractionsSnapshot(): Collection = + standaloneInteractionControllers.values.toSet() + private fun refreshAudiences(player: Player) { - val controllers = controllersSnapshot() - if (controllers.isEmpty()) return - controllers.forEach { it.refreshAudience(player) } + displayControllersSnapshot().forEach { it.refreshAudience(player) } + standaloneInteractionsSnapshot().forEach { it.refreshAudience(player) } } fun updateAudiences() { val players = Bukkit.getOnlinePlayers() if (players.isEmpty()) return - val controllers = controllersSnapshot() - if (controllers.isEmpty()) return - val activeControllers = controllers.filter { it.needsPeriodicUpdate() } - if (activeControllers.isEmpty()) return + + val activeDisplays = displayControllersSnapshot().filter { it.needsPeriodicUpdate() } + val activeInteractions = standaloneInteractionsSnapshot().filter { it.needsPeriodicUpdate() } + if (activeDisplays.isEmpty() && activeInteractions.isEmpty()) return + players.forEach { player -> - activeControllers.forEach { controller -> - controller.refreshAudience(player) - } + activeDisplays.forEach { it.refreshAudience(player) } + activeInteractions.forEach { it.refreshAudience(player) } } } } diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt index 6cf183a..2fc6d6e 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt @@ -9,6 +9,7 @@ class FakeInteraction( location: Location ) : FakeEntity(entityId, uuid, location) { + var key: String? = null var width: Float = 0.8f var height: Float = 0.8f var responsive: Boolean = true diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt index 663ff2a..43cedb0 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt @@ -5,17 +5,32 @@ import io.netty.channel.ChannelHandlerContext import java.lang.invoke.MethodHandles import java.util.logging.Level import net.minecraft.network.protocol.game.ServerboundInteractPacket +import net.minecraft.world.InteractionHand import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.inventory.EquipmentSlot import org.bukkit.plugin.java.JavaPlugin +internal data class InteractData( + val entityId: Int, + val action: InteractAction, + val hand: EquipmentSlot?, + val clickPosition: org.bukkit.util.Vector? +) + +internal enum class InteractAction { + ATTACK, + INTERACT, + INTERACT_AT +} + internal class InteractionPacketListener( private val plugin: JavaPlugin, - private val onInteract: (player: Player, entityId: Int, isAttack: Boolean) -> Unit + private val onInteract: (player: Player, data: InteractData) -> Unit ) : Listener { companion object { @@ -71,8 +86,8 @@ internal class InteractionPacketListener( if (msg is ServerboundInteractPacket) { try { val entityId = ENTITY_ID_GETTER.invoke(msg) as Int - val isAttack = isAttackAction(msg) - onInteract(player, entityId, isAttack) + val data = extractAction(entityId, msg) + onInteract(player, data) } catch (ex: Throwable) { plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex) } @@ -82,13 +97,34 @@ internal class InteractionPacketListener( } } - private fun isAttackAction(packet: ServerboundInteractPacket): Boolean { - var attack = false + private fun extractAction(entityId: Int, packet: ServerboundInteractPacket): InteractData { + var action = InteractAction.ATTACK + var hand: EquipmentSlot? = null + var clickPos: org.bukkit.util.Vector? = null + packet.dispatch(object : ServerboundInteractPacket.Handler { - override fun onInteraction(hand: net.minecraft.world.InteractionHand) {} - override fun onInteraction(hand: net.minecraft.world.InteractionHand, pos: net.minecraft.world.phys.Vec3) {} - override fun onAttack() { attack = true } + override fun onInteraction(nmsHand: InteractionHand) { + action = InteractAction.INTERACT + hand = toBukkit(nmsHand) + } + + override fun onInteraction(nmsHand: InteractionHand, pos: net.minecraft.world.phys.Vec3) { + action = InteractAction.INTERACT_AT + hand = toBukkit(nmsHand) + clickPos = org.bukkit.util.Vector(pos.x, pos.y, pos.z) + } + + override fun onAttack() { + action = InteractAction.ATTACK + hand = null + } }) - return attack + + return InteractData(entityId, action, hand, clickPos) + } + + private fun toBukkit(hand: InteractionHand): EquipmentSlot = when (hand) { + InteractionHand.MAIN_HAND -> EquipmentSlot.HAND + InteractionHand.OFF_HAND -> EquipmentSlot.OFF_HAND } }