Compare commits
2 Commits
b985131011
...
d917475e7d
| Author | SHA1 | Date | |
|---|---|---|---|
| d917475e7d | |||
| bd6aa1743f |
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()
|
||||
```
|
||||
|
||||
## 同梱ライブラリ
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ plugins {
|
|||
kotlin("jvm") version "2.2.21"
|
||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
@ -16,7 +17,7 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||
paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
|
||||
implementation("net.hareworks:kommand-lib:1.1")
|
||||
implementation("net.hareworks:permits-lib:1.1")
|
||||
|
|
@ -62,4 +63,7 @@ tasks {
|
|||
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7"))
|
||||
}
|
||||
}
|
||||
assemble {
|
||||
dependsOn(reobfJar)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "GhostDisplays"
|
||||
|
||||
includeBuild("kommand-lib")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class GhostDisplaysPlugin : JavaPlugin() {
|
|||
miniMessage = MiniMessage.miniMessage()
|
||||
displayRegistry = DisplayRegistry().also {
|
||||
server.pluginManager.registerEvents(it, this)
|
||||
it.initialize(this)
|
||||
}
|
||||
serviceImpl = DefaultDisplayService(this, displayRegistry)
|
||||
displayManager = DisplayManager(this, serviceImpl, miniMessage)
|
||||
|
|
@ -34,7 +35,7 @@ class GhostDisplaysPlugin : JavaPlugin() {
|
|||
|
||||
instance = this
|
||||
logger.info("GhostDisplays ready: ${displayRegistry.controllerCount()} controllers active.")
|
||||
|
||||
|
||||
server.globalRegionScheduler.runAtFixedRate(this, { _ ->
|
||||
displayRegistry.updateAudiences()
|
||||
}, 20L, 20L)
|
||||
|
|
|
|||
|
|
@ -5,49 +5,35 @@ import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
|
|||
import net.hareworks.ghostdisplays.api.click.ClickPriority
|
||||
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
|
||||
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.Interaction
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeItemDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.UUID
|
||||
|
||||
interface DisplayController<T : Display> {
|
||||
val display: T
|
||||
val interaction: Interaction?
|
||||
interface DisplayController {
|
||||
val entityId: Int
|
||||
val displayType: DisplayType
|
||||
val location: Location
|
||||
|
||||
fun show(player: Player)
|
||||
|
||||
fun hide(player: Player)
|
||||
|
||||
fun isViewing(playerId: UUID): Boolean
|
||||
|
||||
fun viewerIds(): Set<UUID>
|
||||
|
||||
fun applyEntityUpdate(mutator: (T) -> Unit)
|
||||
fun updateText(mutator: (FakeTextDisplay) -> Unit)
|
||||
fun updateBlock(mutator: (FakeBlockDisplay) -> Unit)
|
||||
fun updateItem(mutator: (FakeItemDisplay) -> Unit)
|
||||
fun updateDisplay(mutator: (FakeDisplay) -> Unit)
|
||||
fun teleport(location: Location)
|
||||
|
||||
/**
|
||||
* Displayの基本可視状態を設定します。
|
||||
* ルールにマッチしなかった場合のデフォルトの振る舞いとなります。
|
||||
*/
|
||||
fun setBaseVisibility(visible: Boolean)
|
||||
|
||||
/**
|
||||
* 従来の簡易メソッド。Action=ADD として登録します。
|
||||
*/
|
||||
fun addAudience(predicate: AudiencePredicate): HandlerRegistration
|
||||
|
||||
/**
|
||||
* ルールを追加します。評価順序は追加順です。
|
||||
*/
|
||||
fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration
|
||||
|
||||
fun clearAudienceRules()
|
||||
|
||||
fun refreshAudience(target: Player? = null)
|
||||
|
||||
/**
|
||||
* 定期的な更新チェックが必要かどうかを返します。
|
||||
* trueの場合、サーバーのtick毎(または定期タスク)にrefreshAudienceが呼び出されます。
|
||||
*/
|
||||
fun needsPeriodicUpdate(): Boolean = false
|
||||
|
||||
fun destroy()
|
||||
|
|
|
|||
|
|
@ -1,30 +1,36 @@
|
|||
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
|
||||
import org.bukkit.entity.BlockDisplay
|
||||
import org.bukkit.entity.ItemDisplay
|
||||
import org.bukkit.entity.TextDisplay
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
interface DisplayService {
|
||||
fun createTextDisplay(
|
||||
location: Location,
|
||||
interaction: InteractionOptions = InteractionOptions.Disabled,
|
||||
builder: TextDisplay.() -> Unit = {}
|
||||
): DisplayController<TextDisplay>
|
||||
builder: FakeTextDisplay.() -> Unit = {}
|
||||
): DisplayController
|
||||
|
||||
fun createBlockDisplay(
|
||||
location: Location,
|
||||
interaction: InteractionOptions = InteractionOptions.Disabled,
|
||||
builder: BlockDisplay.() -> Unit = {}
|
||||
): DisplayController<BlockDisplay>
|
||||
builder: FakeBlockDisplay.() -> Unit = {}
|
||||
): DisplayController
|
||||
|
||||
fun createItemDisplay(
|
||||
location: Location,
|
||||
itemStack: ItemStack,
|
||||
interaction: InteractionOptions = InteractionOptions.Disabled,
|
||||
builder: ItemDisplay.() -> Unit = {}
|
||||
): DisplayController<ItemDisplay>
|
||||
builder: FakeItemDisplay.() -> Unit = {}
|
||||
): DisplayController
|
||||
|
||||
fun createInteraction(
|
||||
location: Location,
|
||||
builder: FakeInteraction.() -> Unit = {}
|
||||
): InteractionController
|
||||
|
||||
fun destroyAll()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package net.hareworks.ghostdisplays.api
|
||||
|
||||
enum class DisplayType {
|
||||
TEXT,
|
||||
BLOCK,
|
||||
ITEM
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package net.hareworks.ghostdisplays.api.click
|
||||
|
||||
import net.hareworks.ghostdisplays.api.DisplayController
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent
|
||||
import org.bukkit.inventory.EquipmentSlot
|
||||
|
||||
enum class ClickPriority {
|
||||
|
|
@ -20,10 +18,10 @@ enum class ClickSurface {
|
|||
data class DisplayClickContext(
|
||||
val player: Player,
|
||||
val hand: EquipmentSlot?,
|
||||
val clickedEntity: Entity,
|
||||
val entityId: Int,
|
||||
val surface: ClickSurface,
|
||||
val controller: DisplayController<*>,
|
||||
val event: PlayerInteractEntityEvent
|
||||
val controller: DisplayController,
|
||||
var cancelled: Boolean = false
|
||||
)
|
||||
|
||||
fun interface DisplayClickHandler {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -464,7 +464,7 @@ object CommandRegistrar {
|
|||
val id = argument<String>("id")
|
||||
val targets = argument<List<Player>>("targets")
|
||||
var count = 0
|
||||
targets.forEach {
|
||||
targets.forEach {
|
||||
if (manager.removePlayerAudience(id, it.uniqueId)) count++
|
||||
}
|
||||
sender.success("Removed $count/${targets.size} player(s) from audience of '$id'.")
|
||||
|
|
|
|||
|
|
@ -8,18 +8,13 @@ import net.hareworks.ghostdisplays.api.DisplayService
|
|||
import net.hareworks.ghostdisplays.api.InteractionOptions
|
||||
import net.hareworks.ghostdisplays.api.audience.AudienceAction
|
||||
import net.hareworks.ghostdisplays.api.audience.AudiencePredicates
|
||||
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.block.data.BlockData
|
||||
import org.bukkit.entity.BlockDisplay
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.ItemDisplay
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.entity.TextDisplay
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
|
|
@ -28,7 +23,7 @@ class DisplayManager(
|
|||
private val service: DisplayService,
|
||||
private val miniMessage: MiniMessage
|
||||
) {
|
||||
private val displays = ConcurrentHashMap<String, ManagedDisplay<*>>()
|
||||
private val displays = ConcurrentHashMap<String, ManagedDisplay>()
|
||||
private val idPattern = Regex("^[a-z0-9_\\-]{1,32}$")
|
||||
private val logger: Logger = plugin.logger
|
||||
private val plainSerializer = PlainTextComponentSerializer.plainText()
|
||||
|
|
@ -39,9 +34,7 @@ class DisplayManager(
|
|||
val safeLocation = location.clone()
|
||||
val controller = service.createTextDisplay(safeLocation, INTERACTION_DEFAULT)
|
||||
val component = parseMiniMessage(initialContent.ifBlank { "<gray>${normalized}" }, creator)
|
||||
controller.applyEntityUpdate { textDisplay ->
|
||||
textDisplay.text(component)
|
||||
}
|
||||
controller.updateText { it.text = component }
|
||||
val managed = ManagedDisplay.Text(
|
||||
id = normalized,
|
||||
controller = controller,
|
||||
|
|
@ -61,7 +54,7 @@ class DisplayManager(
|
|||
ensureIdAvailable(normalized)
|
||||
val safeLocation = location.clone()
|
||||
val controller = service.createBlockDisplay(safeLocation, INTERACTION_DEFAULT)
|
||||
controller.applyEntityUpdate { it.block = blockData.clone() }
|
||||
controller.updateBlock { it.blockData = blockData.clone() }
|
||||
val managed = ManagedDisplay.Block(
|
||||
id = normalized,
|
||||
controller = controller,
|
||||
|
|
@ -80,7 +73,6 @@ class DisplayManager(
|
|||
ensureIdAvailable(normalized)
|
||||
val safeLocation = location.clone()
|
||||
val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT)
|
||||
controller.applyEntityUpdate { it.setItemStack(itemStack.clone()) }
|
||||
val managed = ManagedDisplay.Item(
|
||||
id = normalized,
|
||||
controller = controller,
|
||||
|
|
@ -94,14 +86,14 @@ class DisplayManager(
|
|||
return managed
|
||||
}
|
||||
|
||||
fun listDisplays(): List<ManagedDisplay<*>> = displays.values.sortedBy { it.id }
|
||||
fun listDisplays(): List<ManagedDisplay> = displays.values.sortedBy { it.id }
|
||||
|
||||
fun findDisplay(id: String): ManagedDisplay<*>? = displays[normalizeId(id)]
|
||||
fun findDisplay(id: String): ManagedDisplay? = displays[normalizeId(id)]
|
||||
|
||||
fun updateText(id: String, content: String, playerContext: Player? = null): Component {
|
||||
val display = requireText(id)
|
||||
val component = parseMiniMessage(content, playerContext)
|
||||
display.controller.applyEntityUpdate { it.text(component) }
|
||||
display.controller.updateText { it.text = component }
|
||||
display.rawContent = content
|
||||
display.component = component
|
||||
return component
|
||||
|
|
@ -109,14 +101,14 @@ class DisplayManager(
|
|||
|
||||
fun updateBlock(id: String, blockData: BlockData) {
|
||||
val display = requireBlock(id)
|
||||
display.controller.applyEntityUpdate { it.block = blockData.clone() }
|
||||
display.controller.updateBlock { it.blockData = blockData.clone() }
|
||||
display.blockData = blockData.clone()
|
||||
}
|
||||
|
||||
fun updateItem(id: String, itemStack: ItemStack) {
|
||||
val display = requireItem(id)
|
||||
val clone = itemStack.clone()
|
||||
display.controller.applyEntityUpdate { it.setItemStack(clone) }
|
||||
display.controller.updateItem { it.itemStack = clone }
|
||||
display.itemStack = clone
|
||||
}
|
||||
|
||||
|
|
@ -219,14 +211,14 @@ class DisplayManager(
|
|||
displays.clear()
|
||||
}
|
||||
|
||||
fun describeContent(display: ManagedDisplay<*>): String =
|
||||
fun describeContent(display: ManagedDisplay): String =
|
||||
when (display) {
|
||||
is ManagedDisplay.Text -> plainSerializer.serialize(display.component)
|
||||
is ManagedDisplay.Block -> display.blockData.asString
|
||||
is ManagedDisplay.Item -> display.itemStack.type.name.lowercase()
|
||||
}
|
||||
|
||||
private fun requireDisplay(id: String): ManagedDisplay<*> =
|
||||
private fun requireDisplay(id: String): ManagedDisplay =
|
||||
displays[normalizeId(id)] ?: throw DisplayOperationException("Display '$id' does not exist.")
|
||||
|
||||
private fun requireText(id: String): ManagedDisplay.Text =
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
|||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.block.data.BlockData
|
||||
import org.bukkit.entity.BlockDisplay
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.ItemDisplay
|
||||
import org.bukkit.entity.TextDisplay
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
enum class DisplayKind {
|
||||
|
|
@ -30,10 +26,10 @@ data class AudienceBinding(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class ManagedDisplay<T : Display>(
|
||||
sealed class ManagedDisplay(
|
||||
val id: String,
|
||||
val kind: DisplayKind,
|
||||
val controller: DisplayController<T>,
|
||||
val controller: DisplayController,
|
||||
location: Location,
|
||||
val createdAt: Instant,
|
||||
val createdBy: UUID?
|
||||
|
|
@ -61,29 +57,29 @@ sealed class ManagedDisplay<T : Display>(
|
|||
|
||||
class Text(
|
||||
id: String,
|
||||
controller: DisplayController<TextDisplay>,
|
||||
controller: DisplayController,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var rawContent: String,
|
||||
var component: Component
|
||||
) : ManagedDisplay<TextDisplay>(id, DisplayKind.TEXT, controller, location, createdAt, createdBy)
|
||||
) : ManagedDisplay(id, DisplayKind.TEXT, controller, location, createdAt, createdBy)
|
||||
|
||||
class Block(
|
||||
id: String,
|
||||
controller: DisplayController<BlockDisplay>,
|
||||
controller: DisplayController,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var blockData: BlockData
|
||||
) : ManagedDisplay<BlockDisplay>(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy)
|
||||
) : ManagedDisplay(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy)
|
||||
|
||||
class Item(
|
||||
id: String,
|
||||
controller: DisplayController<ItemDisplay>,
|
||||
controller: DisplayController,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var itemStack: ItemStack
|
||||
) : ManagedDisplay<ItemDisplay>(id, DisplayKind.ITEM, controller, location, createdAt, createdBy)
|
||||
) : ManagedDisplay(id, DisplayKind.ITEM, controller, location, createdAt, createdBy)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
package net.hareworks.ghostdisplays.internal
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
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 org.bukkit.Bukkit
|
||||
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 net.hareworks.ghostdisplays.internal.nms.DisplayPacketFactory
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.BlockDisplay
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.Interaction
|
||||
import org.bukkit.entity.ItemDisplay
|
||||
import org.bukkit.entity.TextDisplay
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
|
|
@ -21,83 +22,95 @@ internal class DefaultDisplayService(
|
|||
private val plugin: JavaPlugin,
|
||||
private val registry: DisplayRegistry
|
||||
) : DisplayService {
|
||||
private val controllers = CopyOnWriteArraySet<BaseDisplayController<out Display>>()
|
||||
private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
|
||||
private val interactionControllers = CopyOnWriteArraySet<BaseInteractionController>()
|
||||
|
||||
override fun createTextDisplay(
|
||||
location: Location,
|
||||
interaction: InteractionOptions,
|
||||
builder: TextDisplay.() -> Unit
|
||||
): DisplayController<TextDisplay> = spawnDisplay(location, TextDisplay::class.java, interaction) {
|
||||
builder(it)
|
||||
builder: FakeTextDisplay.() -> Unit
|
||||
): DisplayController {
|
||||
val fake = FakeTextDisplay(
|
||||
entityId = DisplayPacketFactory.nextEntityId(),
|
||||
uuid = UUID.randomUUID(),
|
||||
location = location.clone()
|
||||
)
|
||||
builder(fake)
|
||||
return register(fake, location, interaction)
|
||||
}
|
||||
|
||||
override fun createBlockDisplay(
|
||||
location: Location,
|
||||
interaction: InteractionOptions,
|
||||
builder: BlockDisplay.() -> Unit
|
||||
): DisplayController<BlockDisplay> = spawnDisplay(location, BlockDisplay::class.java, interaction) {
|
||||
builder(it)
|
||||
builder: FakeBlockDisplay.() -> Unit
|
||||
): DisplayController {
|
||||
val fake = FakeBlockDisplay(
|
||||
entityId = DisplayPacketFactory.nextEntityId(),
|
||||
uuid = UUID.randomUUID(),
|
||||
location = location.clone()
|
||||
)
|
||||
builder(fake)
|
||||
return register(fake, location, interaction)
|
||||
}
|
||||
|
||||
override fun createItemDisplay(
|
||||
location: Location,
|
||||
itemStack: ItemStack,
|
||||
interaction: InteractionOptions,
|
||||
builder: ItemDisplay.() -> Unit
|
||||
): DisplayController<ItemDisplay> = spawnDisplay(location, ItemDisplay::class.java, interaction) {
|
||||
it.setItemStack(itemStack.clone())
|
||||
builder(it)
|
||||
builder: FakeItemDisplay.() -> Unit
|
||||
): DisplayController {
|
||||
val fake = FakeItemDisplay(
|
||||
entityId = DisplayPacketFactory.nextEntityId(),
|
||||
uuid = UUID.randomUUID(),
|
||||
location = location.clone()
|
||||
)
|
||||
fake.itemStack = itemStack.clone()
|
||||
builder(fake)
|
||||
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 <T : Display> spawnDisplay(
|
||||
private fun register(
|
||||
fake: net.hareworks.ghostdisplays.internal.fake.FakeDisplay,
|
||||
location: Location,
|
||||
type: Class<T>,
|
||||
interactionOptions: InteractionOptions,
|
||||
afterSpawn: (T) -> Unit
|
||||
): DisplayController<T> {
|
||||
val entity = callSync(location) {
|
||||
val world = location.world ?: throw IllegalArgumentException("Location must have a world")
|
||||
world.spawn(location, type) { spawned ->
|
||||
spawned.setPersistent(false)
|
||||
spawned.setVisibleByDefault(false)
|
||||
interactionOptions: InteractionOptions
|
||||
): DisplayController {
|
||||
val fakeInteraction = if (interactionOptions.enabled) {
|
||||
FakeInteraction(
|
||||
entityId = DisplayPacketFactory.nextEntityId(),
|
||||
uuid = UUID.randomUUID(),
|
||||
location = location.clone()
|
||||
).apply {
|
||||
width = interactionOptions.effectiveWidth().toFloat()
|
||||
height = interactionOptions.effectiveHeight().toFloat()
|
||||
responsive = interactionOptions.responsive
|
||||
}
|
||||
}
|
||||
callSync(location) { afterSpawn(entity) }
|
||||
val interaction = if (interactionOptions.enabled) spawnInteraction(location, interactionOptions) else null
|
||||
val controller = BaseDisplayController(plugin, entity, interaction, registry)
|
||||
} else null
|
||||
val controller = BaseDisplayController(plugin, fake, fakeInteraction, registry)
|
||||
controllers += controller
|
||||
registry.register(controller)
|
||||
return controller
|
||||
}
|
||||
|
||||
private fun spawnInteraction(location: Location, options: InteractionOptions): Interaction =
|
||||
callSync(location) {
|
||||
val world = location.world ?: throw IllegalArgumentException("Location must have a world")
|
||||
world.spawn(location, Interaction::class.java) { interaction ->
|
||||
interaction.setPersistent(false)
|
||||
interaction.setInvisible(true)
|
||||
interaction.setVisibleByDefault(false)
|
||||
interaction.setResponsive(options.responsive)
|
||||
interaction.setInteractionWidth(options.effectiveWidth().toFloat())
|
||||
interaction.setInteractionHeight(options.effectiveHeight().toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> callSync(location: Location, action: () -> T): T {
|
||||
val world = location.world ?: throw IllegalArgumentException("Location has no world")
|
||||
val chunkX = location.blockX shr 4
|
||||
val chunkZ = location.blockZ shr 4
|
||||
return if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||
action()
|
||||
} else {
|
||||
val future = CompletableFuture<T>()
|
||||
plugin.server.regionScheduler.run(plugin, location) { _ -> future.complete(action()) }
|
||||
future.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package net.hareworks.ghostdisplays.internal.controller
|
|||
|
||||
import java.util.HashSet
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import net.hareworks.ghostdisplays.api.DisplayController
|
||||
import net.hareworks.ghostdisplays.api.DisplayType
|
||||
import net.hareworks.ghostdisplays.api.audience.AudienceAction
|
||||
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
|
||||
import net.hareworks.ghostdisplays.api.click.ClickPriority
|
||||
|
|
@ -14,57 +14,69 @@ import net.hareworks.ghostdisplays.api.click.ClickSurface
|
|||
import net.hareworks.ghostdisplays.api.click.DisplayClickContext
|
||||
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
|
||||
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeItemDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
|
||||
import net.hareworks.ghostdisplays.internal.nms.DisplayPacketFactory
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Interaction
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent
|
||||
import org.bukkit.inventory.EquipmentSlot
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
internal class BaseDisplayController<T : Display>(
|
||||
internal class BaseDisplayController(
|
||||
private val plugin: JavaPlugin,
|
||||
override val display: T,
|
||||
override val interaction: Interaction?,
|
||||
val fakeDisplay: FakeDisplay,
|
||||
val fakeInteraction: FakeInteraction?,
|
||||
private val registry: DisplayRegistry
|
||||
) : DisplayController<T> {
|
||||
) : DisplayController {
|
||||
|
||||
private val destroyed = AtomicBoolean(false)
|
||||
private val viewerCounts = ConcurrentHashMap<UUID, Int>()
|
||||
private val handlers = CopyOnWriteArrayList<HandlerEntry>()
|
||||
|
||||
// Audience Management
|
||||
// Audience Management
|
||||
|
||||
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() = fakeDisplay.entityId
|
||||
|
||||
override val displayType: DisplayType
|
||||
get() = when (fakeDisplay) {
|
||||
is FakeTextDisplay -> DisplayType.TEXT
|
||||
is FakeBlockDisplay -> DisplayType.BLOCK
|
||||
is FakeItemDisplay -> DisplayType.ITEM
|
||||
else -> DisplayType.BLOCK
|
||||
}
|
||||
|
||||
override val location: Location get() = fakeDisplay.location.clone()
|
||||
|
||||
override fun needsPeriodicUpdate(): Boolean = hasDynamicRules
|
||||
|
||||
override fun show(player: Player) {
|
||||
runSync {
|
||||
val uuid = player.uniqueId
|
||||
val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1
|
||||
if (newCount == 1) {
|
||||
player.showEntity(plugin, display)
|
||||
interaction?.let { player.showEntity(plugin, it) }
|
||||
val uuid = player.uniqueId
|
||||
val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1
|
||||
if (newCount == 1) {
|
||||
DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(fakeDisplay))
|
||||
fakeInteraction?.let {
|
||||
DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hide(player: Player) {
|
||||
runSync {
|
||||
val uuid = player.uniqueId
|
||||
val current = viewerCounts[uuid] ?: return@runSync
|
||||
if (current <= 1) {
|
||||
viewerCounts.remove(uuid)
|
||||
player.hideEntity(plugin, display)
|
||||
interaction?.let { player.hideEntity(plugin, it) }
|
||||
} else {
|
||||
viewerCounts[uuid] = current - 1
|
||||
}
|
||||
val uuid = player.uniqueId
|
||||
val current = viewerCounts[uuid] ?: return
|
||||
if (current <= 1) {
|
||||
viewerCounts.remove(uuid)
|
||||
val ids = mutableListOf(fakeDisplay.entityId)
|
||||
fakeInteraction?.let { ids.add(it.entityId) }
|
||||
DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createRemovePacket(*ids.toIntArray()))
|
||||
} else {
|
||||
viewerCounts[uuid] = current - 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,25 +84,53 @@ internal class BaseDisplayController<T : Display>(
|
|||
|
||||
override fun viewerIds(): Set<UUID> = HashSet(viewerCounts.keys)
|
||||
|
||||
override fun applyEntityUpdate(mutator: (T) -> Unit) {
|
||||
callSync {
|
||||
mutator(display)
|
||||
override fun updateText(mutator: (FakeTextDisplay) -> Unit) {
|
||||
check(fakeDisplay is FakeTextDisplay) { "Not a text display" }
|
||||
mutator(fakeDisplay as FakeTextDisplay)
|
||||
broadcastMetadata()
|
||||
}
|
||||
|
||||
override fun updateBlock(mutator: (FakeBlockDisplay) -> Unit) {
|
||||
check(fakeDisplay is FakeBlockDisplay) { "Not a block display" }
|
||||
mutator(fakeDisplay as FakeBlockDisplay)
|
||||
broadcastMetadata()
|
||||
}
|
||||
|
||||
override fun updateItem(mutator: (FakeItemDisplay) -> Unit) {
|
||||
check(fakeDisplay is FakeItemDisplay) { "Not an item display" }
|
||||
mutator(fakeDisplay as FakeItemDisplay)
|
||||
broadcastMetadata()
|
||||
}
|
||||
|
||||
override fun updateDisplay(mutator: (FakeDisplay) -> Unit) {
|
||||
mutator(fakeDisplay)
|
||||
broadcastMetadata()
|
||||
}
|
||||
|
||||
override fun teleport(location: Location) {
|
||||
fakeDisplay.location = location.clone()
|
||||
fakeInteraction?.let { it.location = location.clone() }
|
||||
val viewers = onlineViewers()
|
||||
if (viewers.isEmpty()) return
|
||||
val displayPacket = DisplayPacketFactory.createTeleportPacket(fakeDisplay)
|
||||
viewers.forEach { DisplayPacketFactory.sendPacket(it, displayPacket) }
|
||||
fakeInteraction?.let { interaction ->
|
||||
val interactionPacket = DisplayPacketFactory.createTeleportPacket(interaction)
|
||||
viewers.forEach { DisplayPacketFactory.sendPacket(it, interactionPacket) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
if (!destroyed.compareAndSet(false, true)) return
|
||||
registry.unregister(this)
|
||||
val viewers = viewerIds().mapNotNull { Bukkit.getPlayer(it) }
|
||||
runSync {
|
||||
viewers.forEach {
|
||||
it.hideEntity(plugin, display)
|
||||
interaction?.let { interactionEntity -> it.hideEntity(plugin, interactionEntity) }
|
||||
}
|
||||
display.remove()
|
||||
interaction?.remove()
|
||||
viewerCounts.clear()
|
||||
val viewers = onlineViewers()
|
||||
if (viewers.isNotEmpty()) {
|
||||
val ids = mutableListOf(fakeDisplay.entityId)
|
||||
fakeInteraction?.let { ids.add(it.entityId) }
|
||||
val removePacket = DisplayPacketFactory.createRemovePacket(*ids.toIntArray())
|
||||
viewers.forEach { DisplayPacketFactory.sendPacket(it, removePacket) }
|
||||
}
|
||||
viewerCounts.clear()
|
||||
handlers.clear()
|
||||
audienceRules.clear()
|
||||
autoVisiblePlayers.clear()
|
||||
|
|
@ -99,9 +139,7 @@ internal class BaseDisplayController<T : Display>(
|
|||
override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration {
|
||||
val entry = HandlerEntry(priority, handler)
|
||||
handlers += entry
|
||||
return HandlerRegistration {
|
||||
handlers.remove(entry)
|
||||
}
|
||||
return HandlerRegistration { handlers.remove(entry) }
|
||||
}
|
||||
|
||||
override fun setBaseVisibility(visible: Boolean) {
|
||||
|
|
@ -124,23 +162,6 @@ internal class BaseDisplayController<T : Display>(
|
|||
refreshAudience()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDynamicStatus() {
|
||||
// 簡易判定: クラス名に "Near" が含まれる、または特定の実装であることを期待する
|
||||
// ただし現状はラムダなので名前判定は不安定。
|
||||
// AudiencePredicate 側にマーカーインターフェースをつけるのが正しいが、
|
||||
// 今回は「ユーザー定義のPredicate」も含めて、動的かどうかわからない。
|
||||
// -> AudiencePredicates.near() が返すオブジェクトに特徴を持たせる。
|
||||
hasDynamicRules = audienceRules.any { isDynamicPredicate(it.predicate) }
|
||||
}
|
||||
|
||||
private fun isDynamicPredicate(predicate: AudiencePredicate): Boolean {
|
||||
// AudiencePredicates.near が返すクラスの実装詳細に依存するか、
|
||||
// AudiencePredicate にプロパティを追加する。
|
||||
// ここでは一旦、toString() 等で識別するか?いや、安全ではない。
|
||||
// API変更: AudiencePredicate に default isDynamic() を追加する。
|
||||
return predicate.isDynamic()
|
||||
}
|
||||
|
||||
override fun clearAudienceRules() {
|
||||
audienceRules.clear()
|
||||
|
|
@ -148,30 +169,60 @@ internal class BaseDisplayController<T : Display>(
|
|||
}
|
||||
|
||||
override fun refreshAudience(target: Player?) {
|
||||
refreshAudienceInternal(target)
|
||||
}
|
||||
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers()
|
||||
if (players.isEmpty()) return
|
||||
|
||||
private fun refreshAudienceInternal(target: Player? = null) {
|
||||
runSync {
|
||||
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers()
|
||||
if (players.isEmpty()) return@runSync
|
||||
players.forEach { player ->
|
||||
val uuid = player.uniqueId
|
||||
val shouldBeVisible = evaluateVisibility(player)
|
||||
val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid)
|
||||
|
||||
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)
|
||||
}
|
||||
if (shouldBeVisible && !isCurrentlyAutoVisible) {
|
||||
autoVisiblePlayers.add(uuid)
|
||||
show(player)
|
||||
} else if (!shouldBeVisible && isCurrentlyAutoVisible) {
|
||||
autoVisiblePlayers.remove(uuid)
|
||||
hide(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handleClick(player: Player, clickedEntityId: Int, surface: ClickSurface) {
|
||||
val sortedHandlers = handlers.sortedBy { it.priority.ordinal }
|
||||
if (sortedHandlers.isEmpty()) return
|
||||
val context = DisplayClickContext(
|
||||
player = player,
|
||||
hand = EquipmentSlot.HAND,
|
||||
entityId = clickedEntityId,
|
||||
surface = surface,
|
||||
controller = this
|
||||
)
|
||||
sortedHandlers.forEach { entry ->
|
||||
entry.handler.handle(context)
|
||||
if (context.cancelled) return
|
||||
}
|
||||
}
|
||||
|
||||
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(fakeDisplay)
|
||||
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) {
|
||||
|
|
@ -186,50 +237,6 @@ internal class BaseDisplayController<T : Display>(
|
|||
return visible
|
||||
}
|
||||
|
||||
internal fun handleClick(event: PlayerInteractEntityEvent, clicked: Entity, surface: ClickSurface) {
|
||||
val sortedHandlers = handlers.sortedBy { it.priority.ordinal }
|
||||
if (sortedHandlers.isEmpty()) return
|
||||
val context = DisplayClickContext(
|
||||
player = event.player,
|
||||
hand = event.hand,
|
||||
clickedEntity = clicked,
|
||||
surface = surface,
|
||||
controller = this,
|
||||
event = event
|
||||
)
|
||||
sortedHandlers.forEach { entry ->
|
||||
entry.handler.handle(context)
|
||||
if (event.isCancelled) return
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePlayerQuit(player: Player) {
|
||||
val uuid = player.uniqueId
|
||||
viewerCounts.remove(uuid)
|
||||
autoVisiblePlayers.remove(uuid)
|
||||
// Note: ref count logic doesn't strictly need persistent cleanup if we remove from counts,
|
||||
// but removing from autoVisiblePlayers ensures we don't think we're showing it if they rejoin?
|
||||
// Actually, if they quit, we should probably clear for them.
|
||||
}
|
||||
|
||||
private fun runSync(action: () -> Unit) {
|
||||
if (Bukkit.isOwnedByCurrentRegion(display)) {
|
||||
action()
|
||||
} else {
|
||||
display.scheduler.run(plugin, { action() }, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R> callSync(action: () -> R): R {
|
||||
return if (Bukkit.isOwnedByCurrentRegion(display)) {
|
||||
action()
|
||||
} else {
|
||||
val future = CompletableFuture<R>()
|
||||
display.scheduler.run(plugin, { future.complete(action()) }, { future.cancel(false) })
|
||||
future.join()
|
||||
}
|
||||
}
|
||||
|
||||
private data class RuleEntry(
|
||||
val predicate: AudiencePredicate,
|
||||
val action: AudienceAction
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -1,56 +1,93 @@
|
|||
package net.hareworks.ghostdisplays.internal.controller
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import net.hareworks.ghostdisplays.api.click.ClickSurface
|
||||
import org.bukkit.entity.Display
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Interaction
|
||||
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
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.event.player.PlayerQuitEvent
|
||||
import org.bukkit.event.player.PlayerRespawnEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
internal class DisplayRegistry : Listener {
|
||||
private val displayControllers = ConcurrentHashMap<UUID, BaseDisplayController<out Display>>()
|
||||
private val interactionControllers = ConcurrentHashMap<UUID, BaseDisplayController<out Display>>()
|
||||
private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
||||
private val displayInteractionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
||||
private val standaloneInteractionControllers = ConcurrentHashMap<Int, BaseInteractionController>()
|
||||
private lateinit var packetListener: InteractionPacketListener
|
||||
|
||||
fun register(controller: BaseDisplayController<out Display>) {
|
||||
displayControllers[controller.display.uniqueId] = controller
|
||||
controller.interaction?.let {
|
||||
interactionControllers[it.uniqueId] = controller
|
||||
fun initialize(plugin: JavaPlugin) {
|
||||
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, data.entityId, surface)
|
||||
}
|
||||
}
|
||||
packetListener.injectAll()
|
||||
plugin.server.pluginManager.registerEvents(packetListener, plugin)
|
||||
}
|
||||
|
||||
fun register(controller: BaseDisplayController) {
|
||||
displayControllers[controller.fakeDisplay.entityId] = controller
|
||||
controller.fakeInteraction?.let {
|
||||
displayInteractionControllers[it.entityId] = controller
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(controller: BaseDisplayController<out Display>) {
|
||||
displayControllers.remove(controller.display.uniqueId, controller)
|
||||
controller.interaction?.let {
|
||||
interactionControllers.remove(it.uniqueId, controller)
|
||||
fun unregister(controller: BaseDisplayController) {
|
||||
displayControllers.remove(controller.fakeDisplay.entityId, controller)
|
||||
controller.fakeInteraction?.let {
|
||||
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()
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerInteractEntity(event: PlayerInteractEntityEvent) {
|
||||
val entity = event.rightClicked
|
||||
val controller = lookupController(entity) ?: return
|
||||
val surface = if (entity is Interaction) ClickSurface.INTERACTION else ClickSurface.DISPLAY
|
||||
controller.handleClick(event, entity, surface)
|
||||
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
|
||||
|
|
@ -68,34 +105,28 @@ internal class DisplayRegistry : Listener {
|
|||
refreshAudiences(event.player)
|
||||
}
|
||||
|
||||
private fun lookupController(entity: Entity): BaseDisplayController<out Display>? {
|
||||
return displayControllers[entity.uniqueId]
|
||||
?: interactionControllers[entity.uniqueId]
|
||||
}
|
||||
|
||||
private fun controllersSnapshot(): Collection<BaseDisplayController<out Display>> =
|
||||
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 = org.bukkit.Bukkit.getOnlinePlayers()
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.block.data.BlockData
|
||||
|
||||
class FakeBlockDisplay(
|
||||
entityId: Int,
|
||||
uuid: UUID,
|
||||
location: Location
|
||||
) : FakeDisplay(entityId, uuid, location) {
|
||||
|
||||
var blockData: BlockData = Bukkit.createBlockData("minecraft:stone")
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Display.Billboard
|
||||
import org.bukkit.entity.Display.Brightness
|
||||
import org.bukkit.util.Transformation
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
open class FakeDisplay(
|
||||
entityId: Int,
|
||||
uuid: UUID,
|
||||
location: Location
|
||||
) : FakeEntity(entityId, uuid, location) {
|
||||
|
||||
var translation: Vector3f = Vector3f(0f, 0f, 0f)
|
||||
var scale: Vector3f = Vector3f(1f, 1f, 1f)
|
||||
var leftRotation: Quaternionf = Quaternionf()
|
||||
var rightRotation: Quaternionf = Quaternionf()
|
||||
|
||||
var transformation: Transformation
|
||||
get() = Transformation(translation, leftRotation, scale, rightRotation)
|
||||
set(value) {
|
||||
translation = value.translation
|
||||
leftRotation = value.leftRotation
|
||||
scale = value.scale
|
||||
rightRotation = value.rightRotation
|
||||
}
|
||||
|
||||
var interpolationDelay: Int = 0
|
||||
var interpolationDuration: Int = 0
|
||||
var teleportInterpolationDuration: Int = 0
|
||||
var billboard: Billboard = Billboard.FIXED
|
||||
var brightness: Brightness? = null
|
||||
var viewRange: Float = 1.0f
|
||||
var shadowRadius: Float = 0.0f
|
||||
var shadowStrength: Float = 1.0f
|
||||
var displayWidth: Float = 0.0f
|
||||
var displayHeight: Float = 0.0f
|
||||
var glowColorOverride: Int = -1
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import org.bukkit.Location
|
||||
|
||||
open class FakeEntity(
|
||||
val entityId: Int,
|
||||
val uuid: UUID,
|
||||
var location: Location
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import org.bukkit.Location
|
||||
|
||||
class FakeInteraction(
|
||||
entityId: Int,
|
||||
uuid: UUID,
|
||||
location: Location
|
||||
) : FakeEntity(entityId, uuid, location) {
|
||||
|
||||
var key: String? = null
|
||||
var width: Float = 0.8f
|
||||
var height: Float = 0.8f
|
||||
var responsive: Boolean = true
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.ItemDisplay.ItemDisplayTransform
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
class FakeItemDisplay(
|
||||
entityId: Int,
|
||||
uuid: UUID,
|
||||
location: Location
|
||||
) : FakeDisplay(entityId, uuid, location) {
|
||||
|
||||
var itemStack: ItemStack = ItemStack.empty()
|
||||
var itemDisplayTransform: ItemDisplayTransform = ItemDisplayTransform.NONE
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package net.hareworks.ghostdisplays.internal.fake
|
||||
|
||||
import java.util.UUID
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Location
|
||||
|
||||
class FakeTextDisplay(
|
||||
entityId: Int,
|
||||
uuid: UUID,
|
||||
location: Location
|
||||
) : FakeDisplay(entityId, uuid, location) {
|
||||
|
||||
var text: Component = Component.empty()
|
||||
var lineWidth: Int = 200
|
||||
var backgroundColor: Int = 0x40000000
|
||||
var textOpacity: Byte = -1
|
||||
var styleFlags: Byte = 0
|
||||
|
||||
companion object {
|
||||
const val FLAG_SHADOW: Byte = 0x01
|
||||
const val FLAG_SEE_THROUGH: Byte = 0x02
|
||||
const val FLAG_DEFAULT_BACKGROUND: Byte = 0x04
|
||||
const val FLAG_ALIGN_LEFT: Byte = 0x08
|
||||
const val FLAG_ALIGN_RIGHT: Byte = 0x10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package net.hareworks.ghostdisplays.internal.nms
|
||||
|
||||
import io.papermc.paper.adventure.PaperAdventure
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeEntity
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeItemDisplay
|
||||
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
|
||||
import net.minecraft.network.protocol.Packet
|
||||
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
|
||||
import net.minecraft.network.protocol.game.ClientboundBundlePacket
|
||||
import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket
|
||||
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
|
||||
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket
|
||||
import net.minecraft.network.syncher.SynchedEntityData
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.entity.PositionMoveRotation
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer
|
||||
import org.bukkit.craftbukkit.inventory.CraftItemStack
|
||||
import org.bukkit.entity.Display.Billboard
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal object DisplayPacketFactory {
|
||||
|
||||
fun nextEntityId(): Int = Entity.nextEntityId()
|
||||
|
||||
// --- Spawn packets ---
|
||||
|
||||
fun createSpawnPacket(fake: FakeDisplay): ClientboundAddEntityPacket {
|
||||
val entityType = when (fake) {
|
||||
is FakeTextDisplay -> EntityType.TEXT_DISPLAY
|
||||
is FakeBlockDisplay -> EntityType.BLOCK_DISPLAY
|
||||
is FakeItemDisplay -> EntityType.ITEM_DISPLAY
|
||||
else -> EntityType.BLOCK_DISPLAY
|
||||
}
|
||||
return ClientboundAddEntityPacket(
|
||||
fake.entityId,
|
||||
fake.uuid,
|
||||
fake.location.x, fake.location.y, fake.location.z,
|
||||
fake.location.pitch, fake.location.yaw,
|
||||
entityType,
|
||||
0,
|
||||
Vec3.ZERO,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
|
||||
fun createSpawnPacket(fake: FakeInteraction): ClientboundAddEntityPacket {
|
||||
return ClientboundAddEntityPacket(
|
||||
fake.entityId,
|
||||
fake.uuid,
|
||||
fake.location.x, fake.location.y, fake.location.z,
|
||||
0f, 0f,
|
||||
EntityType.INTERACTION,
|
||||
0,
|
||||
Vec3.ZERO,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
|
||||
// --- Metadata packets ---
|
||||
|
||||
fun createMetadataPacket(fake: FakeDisplay): ClientboundSetEntityDataPacket {
|
||||
val values = buildDisplayMetadata(fake)
|
||||
when (fake) {
|
||||
is FakeTextDisplay -> values.addAll(buildTextMetadata(fake))
|
||||
is FakeBlockDisplay -> values.addAll(buildBlockMetadata(fake))
|
||||
is FakeItemDisplay -> values.addAll(buildItemMetadata(fake))
|
||||
}
|
||||
return ClientboundSetEntityDataPacket(fake.entityId, values)
|
||||
}
|
||||
|
||||
fun createMetadataPacket(fake: FakeInteraction): ClientboundSetEntityDataPacket {
|
||||
val values = buildInteractionMetadata(fake)
|
||||
return ClientboundSetEntityDataPacket(fake.entityId, values)
|
||||
}
|
||||
|
||||
// --- Bundle (spawn + metadata) ---
|
||||
|
||||
fun createSpawnBundle(fake: FakeDisplay): ClientboundBundlePacket {
|
||||
return ClientboundBundlePacket(
|
||||
listOf(createSpawnPacket(fake), createMetadataPacket(fake))
|
||||
)
|
||||
}
|
||||
|
||||
fun createSpawnBundle(fake: FakeInteraction): ClientboundBundlePacket {
|
||||
return ClientboundBundlePacket(
|
||||
listOf(createSpawnPacket(fake), createMetadataPacket(fake))
|
||||
)
|
||||
}
|
||||
|
||||
// --- Remove ---
|
||||
|
||||
fun createRemovePacket(vararg entityIds: Int): ClientboundRemoveEntitiesPacket {
|
||||
return ClientboundRemoveEntitiesPacket(*entityIds)
|
||||
}
|
||||
|
||||
// --- Teleport ---
|
||||
|
||||
fun createTeleportPacket(fake: FakeEntity): ClientboundTeleportEntityPacket {
|
||||
val loc = fake.location
|
||||
return ClientboundTeleportEntityPacket(
|
||||
fake.entityId,
|
||||
PositionMoveRotation(Vec3(loc.x, loc.y, loc.z), Vec3.ZERO, loc.yaw, loc.pitch),
|
||||
java.util.Set.of(),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// --- Send ---
|
||||
|
||||
fun sendPacket(player: Player, packet: Packet<*>) {
|
||||
(player as CraftPlayer).handle.connection.send(packet)
|
||||
}
|
||||
|
||||
// --- Metadata builders ---
|
||||
|
||||
private fun buildDisplayMetadata(fake: FakeDisplay): MutableList<SynchedEntityData.DataValue<*>> {
|
||||
val values = mutableListOf<SynchedEntityData.DataValue<*>>()
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.TRANSLATION, fake.translation))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.SCALE, fake.scale))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.LEFT_ROTATION, fake.leftRotation))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.RIGHT_ROTATION, fake.rightRotation))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.INTERPOLATION_DELAY, fake.interpolationDelay))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.INTERPOLATION_DURATION, fake.interpolationDuration))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.TELEPORT_INTERPOLATION_DURATION, fake.teleportInterpolationDuration))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.BILLBOARD, billboardToByte(fake.billboard)))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.BRIGHTNESS_OVERRIDE, packBrightness(fake.brightness)))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.VIEW_RANGE, fake.viewRange))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.SHADOW_RADIUS, fake.shadowRadius))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.SHADOW_STRENGTH, fake.shadowStrength))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.DISPLAY_WIDTH, fake.displayWidth))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.DISPLAY_HEIGHT, fake.displayHeight))
|
||||
values.add(SynchedEntityData.DataValue.create(EntityDataFields.GLOW_COLOR_OVERRIDE, fake.glowColorOverride))
|
||||
return values
|
||||
}
|
||||
|
||||
private fun buildTextMetadata(fake: FakeTextDisplay): List<SynchedEntityData.DataValue<*>> {
|
||||
return listOf(
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.TEXT, PaperAdventure.asVanilla(fake.text)),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.LINE_WIDTH, fake.lineWidth),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.BACKGROUND_COLOR, fake.backgroundColor),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.TEXT_OPACITY, fake.textOpacity),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.STYLE_FLAGS, fake.styleFlags)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildBlockMetadata(fake: FakeBlockDisplay): List<SynchedEntityData.DataValue<*>> {
|
||||
val nmsBlockState = (fake.blockData as CraftBlockData).state
|
||||
return listOf(
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.BLOCK_STATE, nmsBlockState)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildItemMetadata(fake: FakeItemDisplay): List<SynchedEntityData.DataValue<*>> {
|
||||
val nmsItemStack = CraftItemStack.asNMSCopy(fake.itemStack)
|
||||
return listOf(
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.ITEM_STACK, nmsItemStack),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.ITEM_DISPLAY_TRANSFORM, fake.itemDisplayTransform.ordinal.toByte())
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildInteractionMetadata(fake: FakeInteraction): MutableList<SynchedEntityData.DataValue<*>> {
|
||||
return mutableListOf(
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.INTERACTION_WIDTH, fake.width),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.INTERACTION_HEIGHT, fake.height),
|
||||
SynchedEntityData.DataValue.create(EntityDataFields.INTERACTION_RESPONSE, fake.responsive)
|
||||
)
|
||||
}
|
||||
|
||||
private fun billboardToByte(billboard: Billboard): Byte = billboard.ordinal.toByte()
|
||||
|
||||
private fun packBrightness(brightness: org.bukkit.entity.Display.Brightness?): Int {
|
||||
if (brightness == null) return -1
|
||||
return brightness.blockLight or (brightness.skyLight shl 4)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
package net.hareworks.ghostdisplays.internal.nms
|
||||
|
||||
import java.lang.invoke.MethodHandles
|
||||
import net.minecraft.network.chat.Component as NmsComponent
|
||||
import net.minecraft.network.syncher.EntityDataAccessor
|
||||
import net.minecraft.world.entity.Display as NmsDisplay
|
||||
import net.minecraft.world.entity.Interaction as NmsInteraction
|
||||
import net.minecraft.world.item.ItemStack as NmsItemStack
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import org.joml.Quaternionf
|
||||
import org.joml.Vector3f
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal object EntityDataFields {
|
||||
|
||||
// --- Display (common) ---
|
||||
|
||||
private val displayLookup = MethodHandles.privateLookupIn(
|
||||
NmsDisplay::class.java, MethodHandles.lookup()
|
||||
)
|
||||
|
||||
val INTERPOLATION_DELAY: EntityDataAccessor<Int> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val INTERPOLATION_DURATION: EntityDataAccessor<Int> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val TELEPORT_INTERPOLATION_DURATION: EntityDataAccessor<Int> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_POS_ROT_INTERPOLATION_DURATION_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val TRANSLATION: EntityDataAccessor<Vector3f> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_TRANSLATION_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Vector3f>
|
||||
|
||||
val SCALE: EntityDataAccessor<Vector3f> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_SCALE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Vector3f>
|
||||
|
||||
val LEFT_ROTATION: EntityDataAccessor<Quaternionf> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_LEFT_ROTATION_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Quaternionf>
|
||||
|
||||
val RIGHT_ROTATION: EntityDataAccessor<Quaternionf> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_RIGHT_ROTATION_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Quaternionf>
|
||||
|
||||
val BILLBOARD: EntityDataAccessor<Byte> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_BILLBOARD_RENDER_CONSTRAINTS_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Byte>
|
||||
|
||||
val BRIGHTNESS_OVERRIDE: EntityDataAccessor<Int> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_BRIGHTNESS_OVERRIDE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val VIEW_RANGE: EntityDataAccessor<Float> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_VIEW_RANGE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val SHADOW_RADIUS: EntityDataAccessor<Float> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_SHADOW_RADIUS_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val SHADOW_STRENGTH: EntityDataAccessor<Float> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_SHADOW_STRENGTH_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val DISPLAY_WIDTH: EntityDataAccessor<Float> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_WIDTH_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val DISPLAY_HEIGHT: EntityDataAccessor<Float> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_HEIGHT_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val GLOW_COLOR_OVERRIDE: EntityDataAccessor<Int> =
|
||||
displayLookup.findStaticGetter(
|
||||
NmsDisplay::class.java,
|
||||
"DATA_GLOW_COLOR_OVERRIDE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
// --- TextDisplay ---
|
||||
|
||||
private val textDisplayLookup = MethodHandles.privateLookupIn(
|
||||
NmsDisplay.TextDisplay::class.java, MethodHandles.lookup()
|
||||
)
|
||||
|
||||
val TEXT: EntityDataAccessor<NmsComponent> =
|
||||
textDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.TextDisplay::class.java,
|
||||
"DATA_TEXT_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<NmsComponent>
|
||||
|
||||
val LINE_WIDTH: EntityDataAccessor<Int> =
|
||||
textDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.TextDisplay::class.java,
|
||||
"DATA_LINE_WIDTH_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val BACKGROUND_COLOR: EntityDataAccessor<Int> =
|
||||
textDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.TextDisplay::class.java,
|
||||
"DATA_BACKGROUND_COLOR_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Int>
|
||||
|
||||
val TEXT_OPACITY: EntityDataAccessor<Byte> =
|
||||
textDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.TextDisplay::class.java,
|
||||
"DATA_TEXT_OPACITY_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Byte>
|
||||
|
||||
val STYLE_FLAGS: EntityDataAccessor<Byte> =
|
||||
textDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.TextDisplay::class.java,
|
||||
"DATA_STYLE_FLAGS_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Byte>
|
||||
|
||||
// --- BlockDisplay ---
|
||||
|
||||
private val blockDisplayLookup = MethodHandles.privateLookupIn(
|
||||
NmsDisplay.BlockDisplay::class.java, MethodHandles.lookup()
|
||||
)
|
||||
|
||||
val BLOCK_STATE: EntityDataAccessor<BlockState> =
|
||||
blockDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.BlockDisplay::class.java,
|
||||
"DATA_BLOCK_STATE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<BlockState>
|
||||
|
||||
// --- ItemDisplay ---
|
||||
|
||||
private val itemDisplayLookup = MethodHandles.privateLookupIn(
|
||||
NmsDisplay.ItemDisplay::class.java, MethodHandles.lookup()
|
||||
)
|
||||
|
||||
val ITEM_STACK: EntityDataAccessor<NmsItemStack> =
|
||||
itemDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.ItemDisplay::class.java,
|
||||
"DATA_ITEM_STACK_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<NmsItemStack>
|
||||
|
||||
val ITEM_DISPLAY_TRANSFORM: EntityDataAccessor<Byte> =
|
||||
itemDisplayLookup.findStaticGetter(
|
||||
NmsDisplay.ItemDisplay::class.java,
|
||||
"DATA_ITEM_DISPLAY_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Byte>
|
||||
|
||||
// --- Interaction ---
|
||||
|
||||
private val interactionLookup = MethodHandles.privateLookupIn(
|
||||
NmsInteraction::class.java, MethodHandles.lookup()
|
||||
)
|
||||
|
||||
val INTERACTION_WIDTH: EntityDataAccessor<Float> =
|
||||
interactionLookup.findStaticGetter(
|
||||
NmsInteraction::class.java,
|
||||
"DATA_WIDTH_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val INTERACTION_HEIGHT: EntityDataAccessor<Float> =
|
||||
interactionLookup.findStaticGetter(
|
||||
NmsInteraction::class.java,
|
||||
"DATA_HEIGHT_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Float>
|
||||
|
||||
val INTERACTION_RESPONSE: EntityDataAccessor<Boolean> =
|
||||
interactionLookup.findStaticGetter(
|
||||
NmsInteraction::class.java,
|
||||
"DATA_RESPONSE_ID",
|
||||
EntityDataAccessor::class.java
|
||||
).invoke() as EntityDataAccessor<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package net.hareworks.ghostdisplays.internal.nms
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler
|
||||
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, data: InteractData) -> Unit
|
||||
) : Listener {
|
||||
|
||||
companion object {
|
||||
private const val HANDLER_NAME = "ghostdisplays_interact"
|
||||
|
||||
private val ENTITY_ID_GETTER = run {
|
||||
val lookup = MethodHandles.privateLookupIn(
|
||||
ServerboundInteractPacket::class.java,
|
||||
MethodHandles.lookup()
|
||||
)
|
||||
lookup.findGetter(
|
||||
ServerboundInteractPacket::class.java,
|
||||
"entityId",
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun injectAll() {
|
||||
plugin.server.onlinePlayers.forEach { inject(it) }
|
||||
}
|
||||
|
||||
fun inject(player: Player) {
|
||||
val channel = (player as CraftPlayer).handle.connection.connection.channel
|
||||
channel.eventLoop().execute {
|
||||
if (channel.pipeline().get(HANDLER_NAME) != null) return@execute
|
||||
channel.pipeline().addBefore("packet_handler", HANDLER_NAME, createHandler(player))
|
||||
}
|
||||
}
|
||||
|
||||
fun uninject(player: Player) {
|
||||
val channel = (player as CraftPlayer).handle.connection.connection.channel
|
||||
channel.eventLoop().execute {
|
||||
if (channel.pipeline().get(HANDLER_NAME) != null) {
|
||||
channel.pipeline().remove(HANDLER_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerJoin(event: PlayerJoinEvent) {
|
||||
inject(event.player)
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerQuit(event: PlayerQuitEvent) {
|
||||
uninject(event.player)
|
||||
}
|
||||
|
||||
private fun createHandler(player: Player): ChannelDuplexHandler {
|
||||
return object : ChannelDuplexHandler() {
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is ServerboundInteractPacket) {
|
||||
try {
|
||||
val entityId = ENTITY_ID_GETTER.invoke(msg) as Int
|
||||
val data = extractAction(entityId, msg)
|
||||
onInteract(player, data)
|
||||
} catch (ex: Throwable) {
|
||||
plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex)
|
||||
}
|
||||
}
|
||||
super.channelRead(ctx, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(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 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