FakeEntity化

This commit is contained in:
Keisuke Hirata 2026-03-30 19:38:18 +09:00
parent b985131011
commit bd6aa1743f
23 changed files with 906 additions and 291 deletions

View File

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

View File

@ -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

View File

@ -1,3 +1,10 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://repo.papermc.io/repository/maven-public/")
}
}
rootProject.name = "GhostDisplays"
includeBuild("kommand-lib")

View File

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

View File

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

View File

@ -1,30 +1,30 @@
package net.hareworks.ghostdisplays.api
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
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 destroyAll()
}

View File

@ -0,0 +1,7 @@
package net.hareworks.ghostdisplays.api
enum class DisplayType {
TEXT,
BLOCK,
ITEM
}

View File

@ -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 {

View File

@ -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 =

View File

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

View File

@ -1,19 +1,18 @@
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.InteractionOptions
import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController
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,32 +20,50 @@ internal class DefaultDisplayService(
private val plugin: JavaPlugin,
private val registry: DisplayRegistry
) : DisplayService {
private val controllers = CopyOnWriteArraySet<BaseDisplayController<out Display>>()
private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
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 destroyAll() {
@ -54,50 +71,25 @@ internal class DefaultDisplayService(
controllers.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()
}
}
}

View File

@ -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) {
@ -125,53 +163,66 @@ internal class BaseDisplayController<T : Display>(
}
}
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()
refreshAudience()
}
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

View File

@ -1,35 +1,50 @@
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.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 interactionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
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, entityId, isAttack ->
if (isAttack) return@InteractionPacketListener
val fromInteraction = interactionControllers[entityId]
val fromDisplay = displayControllers[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)
}
}
packetListener.injectAll()
plugin.server.pluginManager.registerEvents(packetListener, plugin)
}
fun register(controller: BaseDisplayController) {
displayControllers[controller.fakeDisplay.entityId] = controller
controller.fakeInteraction?.let {
interactionControllers[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 {
interactionControllers.remove(it.entityId, controller)
}
}
@ -40,14 +55,6 @@ internal class DisplayRegistry : Listener {
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)
}
@EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) {
controllersSnapshot().forEach { it.handlePlayerQuit(event.player) }
@ -68,12 +75,7 @@ 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 controllersSnapshot(): Collection<BaseDisplayController> =
displayControllers.values.toSet()
private fun refreshAudiences(player: Player) {
@ -83,15 +85,12 @@ internal class DisplayRegistry : Listener {
}
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
players.forEach { player ->
activeControllers.forEach { controller ->
controller.refreshAudience(player)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
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 width: Float = 0.8f
var height: Float = 0.8f
var responsive: Boolean = true
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,94 @@
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 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.plugin.java.JavaPlugin
internal class InteractionPacketListener(
private val plugin: JavaPlugin,
private val onInteract: (player: Player, entityId: Int, isAttack: Boolean) -> 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 isAttack = isAttackAction(msg)
onInteract(player, entityId, isAttack)
} catch (ex: Throwable) {
plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex)
}
}
super.channelRead(ctx, msg)
}
}
}
private fun isAttackAction(packet: ServerboundInteractPacket): Boolean {
var attack = false
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 }
})
return attack
}
}