Fake-Interaction

This commit is contained in:
Keisuke Hirata 2026-03-31 10:40:46 +09:00
parent bd6aa1743f
commit d917475e7d
9 changed files with 408 additions and 38 deletions

View File

@ -5,9 +5,9 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
## 提供機能 ## 提供機能
- `DisplayService` による Text/Block/Item Display の生成 API - `DisplayService` による Text/Block/Item Display の生成 API
- `DisplayController` を介した `Player#showEntity` / `hideEntity` の参照カウント管理 - `DisplayController` を介したパケットベースの表示・非表示制御(参照カウント管理)
- `AudiencePredicate`/`AudiencePredicates` による可視対象の自動同期(ログイン・ワールド移動・リスポーンで再評価) - `AudiencePredicate`/`AudiencePredicates` による可視対象の自動同期(ログイン・ワールド移動・リスポーンで再評価)
- Interaction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー - FakeInteraction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー
- まとめて `destroyAll()` できるリソース管理 - まとめて `destroyAll()` できるリソース管理
## スタンドアロンでの使い方 ## スタンドアロンでの使い方
@ -19,7 +19,7 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
| `/ghostdisplay create text <id>` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 | | `/ghostdisplay create text <id>` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 |
| `/ghostdisplay create block <id> <blockstate>` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 | | `/ghostdisplay create block <id> <blockstate>` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 |
| `/ghostdisplay create item <id> <material>` | ItemDisplay を生成します。`minecraft:stick` などのアイテム ID を受け付けます。 | | `/ghostdisplay create item <id> <material>` | ItemDisplay を生成します。`minecraft:stick` などのアイテム ID を受け付けます。 |
| `/ghostdisplay text edit <id>` | 既存の TextDisplay をチャット入力で編集。`cancel` で破棄。単語のみで良い場合は `/ghostdisplay text set <id> Hello_World` のように `_` をスペースに変換して即時更新できます。 | | `/ghostdisplay text set <id> <content>` | TextDisplay のテキストを即時更新します。`_` はスペースに、`\n` は改行に変換されます。 |
| `/ghostdisplay viewer add|remove <id> <player/@a>` | 任意のプレイヤーまたはセレクターを表示対象に追加/削除します。`/ghostdisplay viewer clear <id>` で全員解除。 | | `/ghostdisplay viewer add|remove <id> <player/@a>` | 任意のプレイヤーまたはセレクターを表示対象に追加/削除します。`/ghostdisplay viewer clear <id>` で全員解除。 |
| `/ghostdisplay audience permission add <id> <permission>` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 | | `/ghostdisplay audience permission add <id> <permission>` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 |
| `/ghostdisplay audience near set <id> <radius>` | Display 周囲の半径プレイヤーへ自動表示 (`audience clear` で全自動表示を解除)。 | | `/ghostdisplay audience near set <id> <radius>` | Display 周囲の半径プレイヤーへ自動表示 (`audience clear` で全自動表示を解除)。 |
@ -28,11 +28,43 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
## 技術的ポイント ## 技術的ポイント
- Paper 標準 API の `setVisibleByDefault(false)` + `showEntity`/`hideEntity` を採用し、ProtocolLib に依存しない設計 - **NMS パケットベースの FakeEntity 方式** — サーバー上に実エンティティを生成せず、クライアントへのパケット送信のみで表示を実現。エンティティティック・コリジョン・永続化のオーバーヘッドがゼロ
- Display / Interaction は `setPersistent(false)` でスポーンし、サーバーリロードやチャンクアンロードに強い - パケット送信はスレッドセーフなため、Folia のリージョンスケジューリングに依存しないシンプルな設計
- クリック検知は Netty パイプラインで `ServerboundInteractPacket` を傍受し、FakeInteraction の entityId にマッチしたらハンドラーを発火
- Predicate ごとにアクティブなプレイヤー集合を持つため、複数条件での重複表示にも対応 - Predicate ごとにアクティブなプレイヤー集合を持つため、複数条件での重複表示にも対応
- クリック検知は `PlayerInteractEntityEvent` を介して Display/Interaction 双方から観測し、ハンドラーは Kotlin DSL で登録可能 - `paperweight-userdev` による NMS アクセス1.21.11 対応)
- Kotlin 2.2 + ShadowJar + plugin-yml 生成を使い、`./gradlew build`でそのまま Paper に投入可能なJarを生成
## 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()
```
## 同梱ライブラリ ## 同梱ライブラリ

View File

@ -1,6 +1,7 @@
package net.hareworks.ghostdisplays.api package net.hareworks.ghostdisplays.api
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay 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.FakeItemDisplay
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
import org.bukkit.Location import org.bukkit.Location
@ -26,5 +27,10 @@ interface DisplayService {
builder: FakeItemDisplay.() -> Unit = {} builder: FakeItemDisplay.() -> Unit = {}
): DisplayController ): DisplayController
fun createInteraction(
location: Location,
builder: FakeInteraction.() -> Unit = {}
): InteractionController
fun destroyAll() fun destroyAll()
} }

View File

@ -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<UUID>
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()
}

View File

@ -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()
}
}

View File

@ -4,8 +4,10 @@ import java.util.UUID
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.DisplayController
import net.hareworks.ghostdisplays.api.DisplayService import net.hareworks.ghostdisplays.api.DisplayService
import net.hareworks.ghostdisplays.api.InteractionController
import net.hareworks.ghostdisplays.api.InteractionOptions import net.hareworks.ghostdisplays.api.InteractionOptions
import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController 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.controller.DisplayRegistry
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
@ -21,6 +23,7 @@ internal class DefaultDisplayService(
private val registry: DisplayRegistry private val registry: DisplayRegistry
) : DisplayService { ) : DisplayService {
private val controllers = CopyOnWriteArraySet<BaseDisplayController>() private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
private val interactionControllers = CopyOnWriteArraySet<BaseInteractionController>()
override fun createTextDisplay( override fun createTextDisplay(
location: Location, location: Location,
@ -66,9 +69,27 @@ internal class DefaultDisplayService(
return register(fake, location, interaction) 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() { override fun destroyAll() {
controllers.forEach { it.destroy() } controllers.forEach { it.destroy() }
controllers.clear() controllers.clear()
interactionControllers.forEach { it.destroy() }
interactionControllers.clear()
} }
private fun register( private fun register(

View File

@ -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<UUID, Int>()
private var baseVisibility: Boolean = false
private val audienceRules = CopyOnWriteArrayList<RuleEntry>()
private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>()
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<UUID> = 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<Player> =
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
)
}

View File

@ -2,6 +2,8 @@ package net.hareworks.ghostdisplays.internal.controller
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import net.hareworks.ghostdisplays.api.click.ClickSurface 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 net.hareworks.ghostdisplays.internal.nms.InteractionPacketListener
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.entity.Player import org.bukkit.entity.Player
@ -15,19 +17,37 @@ import org.bukkit.plugin.java.JavaPlugin
internal class DisplayRegistry : Listener { internal class DisplayRegistry : Listener {
private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>() private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>()
private val interactionControllers = ConcurrentHashMap<Int, BaseDisplayController>() private val displayInteractionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
private val standaloneInteractionControllers = ConcurrentHashMap<Int, BaseInteractionController>()
private lateinit var packetListener: InteractionPacketListener private lateinit var packetListener: InteractionPacketListener
fun initialize(plugin: JavaPlugin) { fun initialize(plugin: JavaPlugin) {
packetListener = InteractionPacketListener(plugin) { player, entityId, isAttack -> packetListener = InteractionPacketListener(plugin) { player, data ->
if (isAttack) return@InteractionPacketListener // スタンドアロンInteractionを先にチェック → Bukkitイベント発火
val fromInteraction = interactionControllers[entityId] val standalone = standaloneInteractionControllers[data.entityId]
val fromDisplay = displayControllers[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 controller = fromInteraction ?: fromDisplay ?: return@InteractionPacketListener
val surface = if (fromInteraction != null) ClickSurface.INTERACTION else ClickSurface.DISPLAY val surface = if (fromInteraction != null) ClickSurface.INTERACTION else ClickSurface.DISPLAY
val loc = controller.fakeDisplay.location val loc = controller.fakeDisplay.location
plugin.server.regionScheduler.execute(plugin, loc) { plugin.server.regionScheduler.execute(plugin, loc) {
controller.handleClick(player, entityId, surface) controller.handleClick(player, data.entityId, surface)
} }
} }
packetListener.injectAll() packetListener.injectAll()
@ -37,27 +57,37 @@ internal class DisplayRegistry : Listener {
fun register(controller: BaseDisplayController) { fun register(controller: BaseDisplayController) {
displayControllers[controller.fakeDisplay.entityId] = controller displayControllers[controller.fakeDisplay.entityId] = controller
controller.fakeInteraction?.let { controller.fakeInteraction?.let {
interactionControllers[it.entityId] = controller displayInteractionControllers[it.entityId] = controller
} }
} }
fun unregister(controller: BaseDisplayController) { fun unregister(controller: BaseDisplayController) {
displayControllers.remove(controller.fakeDisplay.entityId, controller) displayControllers.remove(controller.fakeDisplay.entityId, controller)
controller.fakeInteraction?.let { 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() { fun shutdown() {
displayControllers.clear() displayControllers.clear()
interactionControllers.clear() displayInteractionControllers.clear()
standaloneInteractionControllers.clear()
} }
@EventHandler @EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) { fun onPlayerQuit(event: PlayerQuitEvent) {
controllersSnapshot().forEach { it.handlePlayerQuit(event.player) } displayControllersSnapshot().forEach { it.handlePlayerQuit(event.player) }
standaloneInteractionsSnapshot().forEach { it.handlePlayerQuit(event.player) }
} }
@EventHandler @EventHandler
@ -75,26 +105,28 @@ internal class DisplayRegistry : Listener {
refreshAudiences(event.player) refreshAudiences(event.player)
} }
private fun controllersSnapshot(): Collection<BaseDisplayController> = private fun displayControllersSnapshot(): Collection<BaseDisplayController> =
displayControllers.values.toSet() displayControllers.values.toSet()
private fun standaloneInteractionsSnapshot(): Collection<BaseInteractionController> =
standaloneInteractionControllers.values.toSet()
private fun refreshAudiences(player: Player) { private fun refreshAudiences(player: Player) {
val controllers = controllersSnapshot() displayControllersSnapshot().forEach { it.refreshAudience(player) }
if (controllers.isEmpty()) return standaloneInteractionsSnapshot().forEach { it.refreshAudience(player) }
controllers.forEach { it.refreshAudience(player) }
} }
fun updateAudiences() { fun updateAudiences() {
val players = Bukkit.getOnlinePlayers() val players = Bukkit.getOnlinePlayers()
if (players.isEmpty()) return if (players.isEmpty()) return
val controllers = controllersSnapshot()
if (controllers.isEmpty()) return val activeDisplays = displayControllersSnapshot().filter { it.needsPeriodicUpdate() }
val activeControllers = controllers.filter { it.needsPeriodicUpdate() } val activeInteractions = standaloneInteractionsSnapshot().filter { it.needsPeriodicUpdate() }
if (activeControllers.isEmpty()) return if (activeDisplays.isEmpty() && activeInteractions.isEmpty()) return
players.forEach { player -> players.forEach { player ->
activeControllers.forEach { controller -> activeDisplays.forEach { it.refreshAudience(player) }
controller.refreshAudience(player) activeInteractions.forEach { it.refreshAudience(player) }
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ class FakeInteraction(
location: Location location: Location
) : FakeEntity(entityId, uuid, location) { ) : FakeEntity(entityId, uuid, location) {
var key: String? = null
var width: Float = 0.8f var width: Float = 0.8f
var height: Float = 0.8f var height: Float = 0.8f
var responsive: Boolean = true var responsive: Boolean = true

View File

@ -5,17 +5,32 @@ import io.netty.channel.ChannelHandlerContext
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
import java.util.logging.Level import java.util.logging.Level
import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.network.protocol.game.ServerboundInteractPacket
import net.minecraft.world.InteractionHand
import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.craftbukkit.entity.CraftPlayer
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.plugin.java.JavaPlugin 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( internal class InteractionPacketListener(
private val plugin: JavaPlugin, private val plugin: JavaPlugin,
private val onInteract: (player: Player, entityId: Int, isAttack: Boolean) -> Unit private val onInteract: (player: Player, data: InteractData) -> Unit
) : Listener { ) : Listener {
companion object { companion object {
@ -71,8 +86,8 @@ internal class InteractionPacketListener(
if (msg is ServerboundInteractPacket) { if (msg is ServerboundInteractPacket) {
try { try {
val entityId = ENTITY_ID_GETTER.invoke(msg) as Int val entityId = ENTITY_ID_GETTER.invoke(msg) as Int
val isAttack = isAttackAction(msg) val data = extractAction(entityId, msg)
onInteract(player, entityId, isAttack) onInteract(player, data)
} catch (ex: Throwable) { } catch (ex: Throwable) {
plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex) plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex)
} }
@ -82,13 +97,34 @@ internal class InteractionPacketListener(
} }
} }
private fun isAttackAction(packet: ServerboundInteractPacket): Boolean { private fun extractAction(entityId: Int, packet: ServerboundInteractPacket): InteractData {
var attack = false var action = InteractAction.ATTACK
var hand: EquipmentSlot? = null
var clickPos: org.bukkit.util.Vector? = null
packet.dispatch(object : ServerboundInteractPacket.Handler { packet.dispatch(object : ServerboundInteractPacket.Handler {
override fun onInteraction(hand: net.minecraft.world.InteractionHand) {} override fun onInteraction(nmsHand: InteractionHand) {
override fun onInteraction(hand: net.minecraft.world.InteractionHand, pos: net.minecraft.world.phys.Vec3) {} action = InteractAction.INTERACT
override fun onAttack() { attack = true } 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
} }
} }