Fake-Interaction
This commit is contained in:
parent
bd6aa1743f
commit
d917475e7d
46
README.md
46
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 <id>` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 |
|
||||
| `/ghostdisplay create block <id> <blockstate>` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 |
|
||||
| `/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 audience permission add <id> <permission>` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 |
|
||||
| `/ghostdisplay audience near set <id> <radius>` | 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()
|
||||
```
|
||||
|
||||
## 同梱ライブラリ
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BaseDisplayController>()
|
||||
private val interactionControllers = CopyOnWriteArraySet<BaseInteractionController>()
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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<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
|
||||
|
||||
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<BaseDisplayController> =
|
||||
private fun displayControllersSnapshot(): Collection<BaseDisplayController> =
|
||||
displayControllers.values.toSet()
|
||||
|
||||
private fun standaloneInteractionsSnapshot(): Collection<BaseInteractionController> =
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user