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" kotlin("jvm") version "2.2.21"
id("de.eldoria.plugin-yml.paper") version "0.8.0" id("de.eldoria.plugin-yml.paper") version "0.8.0"
id("com.gradleup.shadow") version "9.2.2" id("com.gradleup.shadow") version "9.2.2"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
} }
repositories { repositories {
mavenCentral() mavenCentral()
@ -16,7 +17,7 @@ repositories {
dependencies { dependencies {
compileOnly("me.clip:placeholderapi:2.11.6") 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") compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
implementation("net.hareworks:kommand-lib:1.1") implementation("net.hareworks:kommand-lib:1.1")
implementation("net.hareworks:permits-lib:1.1") implementation("net.hareworks:permits-lib:1.1")
@ -62,4 +63,7 @@ tasks {
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7")) exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7"))
} }
} }
assemble {
dependsOn(reobfJar)
}
} }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

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

View File

@ -24,6 +24,7 @@ class GhostDisplaysPlugin : JavaPlugin() {
miniMessage = MiniMessage.miniMessage() miniMessage = MiniMessage.miniMessage()
displayRegistry = DisplayRegistry().also { displayRegistry = DisplayRegistry().also {
server.pluginManager.registerEvents(it, this) server.pluginManager.registerEvents(it, this)
it.initialize(this)
} }
serviceImpl = DefaultDisplayService(this, displayRegistry) serviceImpl = DefaultDisplayService(this, displayRegistry)
displayManager = DisplayManager(this, serviceImpl, miniMessage) 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.ClickPriority
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
import net.hareworks.ghostdisplays.api.click.HandlerRegistration import net.hareworks.ghostdisplays.api.click.HandlerRegistration
import org.bukkit.entity.Display import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
import org.bukkit.entity.Interaction 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 org.bukkit.entity.Player
import java.util.UUID import java.util.UUID
interface DisplayController<T : Display> { interface DisplayController {
val display: T val entityId: Int
val interaction: Interaction? val displayType: DisplayType
val location: Location
fun show(player: Player) fun show(player: Player)
fun hide(player: Player) fun hide(player: Player)
fun isViewing(playerId: UUID): Boolean fun isViewing(playerId: UUID): Boolean
fun viewerIds(): Set<UUID> 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) fun setBaseVisibility(visible: Boolean)
/**
* 従来の簡易メソッドAction=ADD として登録します
*/
fun addAudience(predicate: AudiencePredicate): HandlerRegistration fun addAudience(predicate: AudiencePredicate): HandlerRegistration
/**
* ルールを追加します評価順序は追加順です
*/
fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration
fun clearAudienceRules() fun clearAudienceRules()
fun refreshAudience(target: Player? = null) fun refreshAudience(target: Player? = null)
/**
* 定期的な更新チェックが必要かどうかを返します
* trueの場合サーバーのtick毎または定期タスクにrefreshAudienceが呼び出されます
*/
fun needsPeriodicUpdate(): Boolean = false fun needsPeriodicUpdate(): Boolean = false
fun destroy() fun destroy()

View File

@ -1,30 +1,30 @@
package net.hareworks.ghostdisplays.api 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.Location
import org.bukkit.entity.BlockDisplay
import org.bukkit.entity.ItemDisplay
import org.bukkit.entity.TextDisplay
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
interface DisplayService { interface DisplayService {
fun createTextDisplay( fun createTextDisplay(
location: Location, location: Location,
interaction: InteractionOptions = InteractionOptions.Disabled, interaction: InteractionOptions = InteractionOptions.Disabled,
builder: TextDisplay.() -> Unit = {} builder: FakeTextDisplay.() -> Unit = {}
): DisplayController<TextDisplay> ): DisplayController
fun createBlockDisplay( fun createBlockDisplay(
location: Location, location: Location,
interaction: InteractionOptions = InteractionOptions.Disabled, interaction: InteractionOptions = InteractionOptions.Disabled,
builder: BlockDisplay.() -> Unit = {} builder: FakeBlockDisplay.() -> Unit = {}
): DisplayController<BlockDisplay> ): DisplayController
fun createItemDisplay( fun createItemDisplay(
location: Location, location: Location,
itemStack: ItemStack, itemStack: ItemStack,
interaction: InteractionOptions = InteractionOptions.Disabled, interaction: InteractionOptions = InteractionOptions.Disabled,
builder: ItemDisplay.() -> Unit = {} builder: FakeItemDisplay.() -> Unit = {}
): DisplayController<ItemDisplay> ): DisplayController
fun destroyAll() 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 package net.hareworks.ghostdisplays.api.click
import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.DisplayController
import org.bukkit.entity.Entity
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.inventory.EquipmentSlot import org.bukkit.inventory.EquipmentSlot
enum class ClickPriority { enum class ClickPriority {
@ -20,10 +18,10 @@ enum class ClickSurface {
data class DisplayClickContext( data class DisplayClickContext(
val player: Player, val player: Player,
val hand: EquipmentSlot?, val hand: EquipmentSlot?,
val clickedEntity: Entity, val entityId: Int,
val surface: ClickSurface, val surface: ClickSurface,
val controller: DisplayController<*>, val controller: DisplayController,
val event: PlayerInteractEntityEvent var cancelled: Boolean = false
) )
fun interface DisplayClickHandler { 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.InteractionOptions
import net.hareworks.ghostdisplays.api.audience.AudienceAction import net.hareworks.ghostdisplays.api.audience.AudienceAction
import net.hareworks.ghostdisplays.api.audience.AudiencePredicates 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.Component
import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.MiniMessage
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.Location import org.bukkit.Location
import org.bukkit.block.data.BlockData 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.Player
import org.bukkit.entity.TextDisplay
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -28,7 +23,7 @@ class DisplayManager(
private val service: DisplayService, private val service: DisplayService,
private val miniMessage: MiniMessage 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 idPattern = Regex("^[a-z0-9_\\-]{1,32}$")
private val logger: Logger = plugin.logger private val logger: Logger = plugin.logger
private val plainSerializer = PlainTextComponentSerializer.plainText() private val plainSerializer = PlainTextComponentSerializer.plainText()
@ -39,9 +34,7 @@ class DisplayManager(
val safeLocation = location.clone() val safeLocation = location.clone()
val controller = service.createTextDisplay(safeLocation, INTERACTION_DEFAULT) val controller = service.createTextDisplay(safeLocation, INTERACTION_DEFAULT)
val component = parseMiniMessage(initialContent.ifBlank { "<gray>${normalized}" }, creator) val component = parseMiniMessage(initialContent.ifBlank { "<gray>${normalized}" }, creator)
controller.applyEntityUpdate { textDisplay -> controller.updateText { it.text = component }
textDisplay.text(component)
}
val managed = ManagedDisplay.Text( val managed = ManagedDisplay.Text(
id = normalized, id = normalized,
controller = controller, controller = controller,
@ -61,7 +54,7 @@ class DisplayManager(
ensureIdAvailable(normalized) ensureIdAvailable(normalized)
val safeLocation = location.clone() val safeLocation = location.clone()
val controller = service.createBlockDisplay(safeLocation, INTERACTION_DEFAULT) val controller = service.createBlockDisplay(safeLocation, INTERACTION_DEFAULT)
controller.applyEntityUpdate { it.block = blockData.clone() } controller.updateBlock { it.blockData = blockData.clone() }
val managed = ManagedDisplay.Block( val managed = ManagedDisplay.Block(
id = normalized, id = normalized,
controller = controller, controller = controller,
@ -80,7 +73,6 @@ class DisplayManager(
ensureIdAvailable(normalized) ensureIdAvailable(normalized)
val safeLocation = location.clone() val safeLocation = location.clone()
val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT) val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT)
controller.applyEntityUpdate { it.setItemStack(itemStack.clone()) }
val managed = ManagedDisplay.Item( val managed = ManagedDisplay.Item(
id = normalized, id = normalized,
controller = controller, controller = controller,
@ -94,14 +86,14 @@ class DisplayManager(
return managed 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 { fun updateText(id: String, content: String, playerContext: Player? = null): Component {
val display = requireText(id) val display = requireText(id)
val component = parseMiniMessage(content, playerContext) val component = parseMiniMessage(content, playerContext)
display.controller.applyEntityUpdate { it.text(component) } display.controller.updateText { it.text = component }
display.rawContent = content display.rawContent = content
display.component = component display.component = component
return component return component
@ -109,14 +101,14 @@ class DisplayManager(
fun updateBlock(id: String, blockData: BlockData) { fun updateBlock(id: String, blockData: BlockData) {
val display = requireBlock(id) val display = requireBlock(id)
display.controller.applyEntityUpdate { it.block = blockData.clone() } display.controller.updateBlock { it.blockData = blockData.clone() }
display.blockData = blockData.clone() display.blockData = blockData.clone()
} }
fun updateItem(id: String, itemStack: ItemStack) { fun updateItem(id: String, itemStack: ItemStack) {
val display = requireItem(id) val display = requireItem(id)
val clone = itemStack.clone() val clone = itemStack.clone()
display.controller.applyEntityUpdate { it.setItemStack(clone) } display.controller.updateItem { it.itemStack = clone }
display.itemStack = clone display.itemStack = clone
} }
@ -219,14 +211,14 @@ class DisplayManager(
displays.clear() displays.clear()
} }
fun describeContent(display: ManagedDisplay<*>): String = fun describeContent(display: ManagedDisplay): String =
when (display) { when (display) {
is ManagedDisplay.Text -> plainSerializer.serialize(display.component) is ManagedDisplay.Text -> plainSerializer.serialize(display.component)
is ManagedDisplay.Block -> display.blockData.asString is ManagedDisplay.Block -> display.blockData.asString
is ManagedDisplay.Item -> display.itemStack.type.name.lowercase() 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.") displays[normalizeId(id)] ?: throw DisplayOperationException("Display '$id' does not exist.")
private fun requireText(id: String): ManagedDisplay.Text = 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 net.kyori.adventure.text.Component
import org.bukkit.Location import org.bukkit.Location
import org.bukkit.block.data.BlockData 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 import org.bukkit.inventory.ItemStack
enum class DisplayKind { enum class DisplayKind {
@ -30,10 +26,10 @@ data class AudienceBinding(
} }
} }
sealed class ManagedDisplay<T : Display>( sealed class ManagedDisplay(
val id: String, val id: String,
val kind: DisplayKind, val kind: DisplayKind,
val controller: DisplayController<T>, val controller: DisplayController,
location: Location, location: Location,
val createdAt: Instant, val createdAt: Instant,
val createdBy: UUID? val createdBy: UUID?
@ -61,29 +57,29 @@ sealed class ManagedDisplay<T : Display>(
class Text( class Text(
id: String, id: String,
controller: DisplayController<TextDisplay>, controller: DisplayController,
location: Location, location: Location,
createdAt: Instant, createdAt: Instant,
createdBy: UUID?, createdBy: UUID?,
var rawContent: String, var rawContent: String,
var component: Component var component: Component
) : ManagedDisplay<TextDisplay>(id, DisplayKind.TEXT, controller, location, createdAt, createdBy) ) : ManagedDisplay(id, DisplayKind.TEXT, controller, location, createdAt, createdBy)
class Block( class Block(
id: String, id: String,
controller: DisplayController<BlockDisplay>, controller: DisplayController,
location: Location, location: Location,
createdAt: Instant, createdAt: Instant,
createdBy: UUID?, createdBy: UUID?,
var blockData: BlockData var blockData: BlockData
) : ManagedDisplay<BlockDisplay>(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy) ) : ManagedDisplay(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy)
class Item( class Item(
id: String, id: String,
controller: DisplayController<ItemDisplay>, controller: DisplayController,
location: Location, location: Location,
createdAt: Instant, createdAt: Instant,
createdBy: UUID?, createdBy: UUID?,
var itemStack: ItemStack 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 package net.hareworks.ghostdisplays.internal
import java.util.concurrent.CompletableFuture import java.util.UUID
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.DisplayController
import net.hareworks.ghostdisplays.api.DisplayService import net.hareworks.ghostdisplays.api.DisplayService
import net.hareworks.ghostdisplays.api.InteractionOptions import net.hareworks.ghostdisplays.api.InteractionOptions
import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController
import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry 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.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.inventory.ItemStack
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -21,32 +20,50 @@ internal class DefaultDisplayService(
private val plugin: JavaPlugin, private val plugin: JavaPlugin,
private val registry: DisplayRegistry private val registry: DisplayRegistry
) : DisplayService { ) : DisplayService {
private val controllers = CopyOnWriteArraySet<BaseDisplayController<out Display>>() private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
override fun createTextDisplay( override fun createTextDisplay(
location: Location, location: Location,
interaction: InteractionOptions, interaction: InteractionOptions,
builder: TextDisplay.() -> Unit builder: FakeTextDisplay.() -> Unit
): DisplayController<TextDisplay> = spawnDisplay(location, TextDisplay::class.java, interaction) { ): DisplayController {
builder(it) val fake = FakeTextDisplay(
entityId = DisplayPacketFactory.nextEntityId(),
uuid = UUID.randomUUID(),
location = location.clone()
)
builder(fake)
return register(fake, location, interaction)
} }
override fun createBlockDisplay( override fun createBlockDisplay(
location: Location, location: Location,
interaction: InteractionOptions, interaction: InteractionOptions,
builder: BlockDisplay.() -> Unit builder: FakeBlockDisplay.() -> Unit
): DisplayController<BlockDisplay> = spawnDisplay(location, BlockDisplay::class.java, interaction) { ): DisplayController {
builder(it) val fake = FakeBlockDisplay(
entityId = DisplayPacketFactory.nextEntityId(),
uuid = UUID.randomUUID(),
location = location.clone()
)
builder(fake)
return register(fake, location, interaction)
} }
override fun createItemDisplay( override fun createItemDisplay(
location: Location, location: Location,
itemStack: ItemStack, itemStack: ItemStack,
interaction: InteractionOptions, interaction: InteractionOptions,
builder: ItemDisplay.() -> Unit builder: FakeItemDisplay.() -> Unit
): DisplayController<ItemDisplay> = spawnDisplay(location, ItemDisplay::class.java, interaction) { ): DisplayController {
it.setItemStack(itemStack.clone()) val fake = FakeItemDisplay(
builder(it) entityId = DisplayPacketFactory.nextEntityId(),
uuid = UUID.randomUUID(),
location = location.clone()
)
fake.itemStack = itemStack.clone()
builder(fake)
return register(fake, location, interaction)
} }
override fun destroyAll() { override fun destroyAll() {
@ -54,50 +71,25 @@ internal class DefaultDisplayService(
controllers.clear() controllers.clear()
} }
private fun <T : Display> spawnDisplay( private fun register(
fake: net.hareworks.ghostdisplays.internal.fake.FakeDisplay,
location: Location, location: Location,
type: Class<T>, interactionOptions: InteractionOptions
interactionOptions: InteractionOptions, ): DisplayController {
afterSpawn: (T) -> Unit val fakeInteraction = if (interactionOptions.enabled) {
): DisplayController<T> { FakeInteraction(
val entity = callSync(location) { entityId = DisplayPacketFactory.nextEntityId(),
val world = location.world ?: throw IllegalArgumentException("Location must have a world") uuid = UUID.randomUUID(),
world.spawn(location, type) { spawned -> location = location.clone()
spawned.setPersistent(false) ).apply {
spawned.setVisibleByDefault(false) width = interactionOptions.effectiveWidth().toFloat()
height = interactionOptions.effectiveHeight().toFloat()
responsive = interactionOptions.responsive
} }
} } else null
callSync(location) { afterSpawn(entity) } val controller = BaseDisplayController(plugin, fake, fakeInteraction, registry)
val interaction = if (interactionOptions.enabled) spawnInteraction(location, interactionOptions) else null
val controller = BaseDisplayController(plugin, entity, interaction, registry)
controllers += controller controllers += controller
registry.register(controller) registry.register(controller)
return 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.HashSet
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import net.hareworks.ghostdisplays.api.DisplayController 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.AudienceAction
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
import net.hareworks.ghostdisplays.api.click.ClickPriority 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.DisplayClickContext
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
import net.hareworks.ghostdisplays.api.click.HandlerRegistration 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.Bukkit
import org.bukkit.entity.Display import org.bukkit.Location
import org.bukkit.entity.Entity
import org.bukkit.entity.Interaction
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.inventory.EquipmentSlot
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
internal class BaseDisplayController<T : Display>( internal class BaseDisplayController(
private val plugin: JavaPlugin, private val plugin: JavaPlugin,
override val display: T, val fakeDisplay: FakeDisplay,
override val interaction: Interaction?, val fakeInteraction: FakeInteraction?,
private val registry: DisplayRegistry private val registry: DisplayRegistry
) : DisplayController<T> { ) : DisplayController {
private val destroyed = AtomicBoolean(false) private val destroyed = AtomicBoolean(false)
private val viewerCounts = ConcurrentHashMap<UUID, Int>() private val viewerCounts = ConcurrentHashMap<UUID, Int>()
private val handlers = CopyOnWriteArrayList<HandlerEntry>() private val handlers = CopyOnWriteArrayList<HandlerEntry>()
// Audience Management
// Audience Management
private var baseVisibility: Boolean = false private var baseVisibility: Boolean = false
private val audienceRules = CopyOnWriteArrayList<RuleEntry>() private val audienceRules = CopyOnWriteArrayList<RuleEntry>()
private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>() private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>()
// 最適化: 定期更新が必要なルールがあるか
private var hasDynamicRules: Boolean = false 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 needsPeriodicUpdate(): Boolean = hasDynamicRules
override fun show(player: Player) { override fun show(player: Player) {
runSync { val uuid = player.uniqueId
val uuid = player.uniqueId val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1
val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1 if (newCount == 1) {
if (newCount == 1) { DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(fakeDisplay))
player.showEntity(plugin, display) fakeInteraction?.let {
interaction?.let { player.showEntity(plugin, it) } DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(it))
} }
} }
} }
override fun hide(player: Player) { override fun hide(player: Player) {
runSync { val uuid = player.uniqueId
val uuid = player.uniqueId val current = viewerCounts[uuid] ?: return
val current = viewerCounts[uuid] ?: return@runSync if (current <= 1) {
if (current <= 1) { viewerCounts.remove(uuid)
viewerCounts.remove(uuid) val ids = mutableListOf(fakeDisplay.entityId)
player.hideEntity(plugin, display) fakeInteraction?.let { ids.add(it.entityId) }
interaction?.let { player.hideEntity(plugin, it) } DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createRemovePacket(*ids.toIntArray()))
} else { } else {
viewerCounts[uuid] = current - 1 viewerCounts[uuid] = current - 1
}
} }
} }
@ -72,25 +84,53 @@ internal class BaseDisplayController<T : Display>(
override fun viewerIds(): Set<UUID> = HashSet(viewerCounts.keys) override fun viewerIds(): Set<UUID> = HashSet(viewerCounts.keys)
override fun applyEntityUpdate(mutator: (T) -> Unit) { override fun updateText(mutator: (FakeTextDisplay) -> Unit) {
callSync { check(fakeDisplay is FakeTextDisplay) { "Not a text display" }
mutator(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() { override fun destroy() {
if (!destroyed.compareAndSet(false, true)) return if (!destroyed.compareAndSet(false, true)) return
registry.unregister(this) registry.unregister(this)
val viewers = viewerIds().mapNotNull { Bukkit.getPlayer(it) } val viewers = onlineViewers()
runSync { if (viewers.isNotEmpty()) {
viewers.forEach { val ids = mutableListOf(fakeDisplay.entityId)
it.hideEntity(plugin, display) fakeInteraction?.let { ids.add(it.entityId) }
interaction?.let { interactionEntity -> it.hideEntity(plugin, interactionEntity) } val removePacket = DisplayPacketFactory.createRemovePacket(*ids.toIntArray())
} viewers.forEach { DisplayPacketFactory.sendPacket(it, removePacket) }
display.remove()
interaction?.remove()
viewerCounts.clear()
} }
viewerCounts.clear()
handlers.clear() handlers.clear()
audienceRules.clear() audienceRules.clear()
autoVisiblePlayers.clear() autoVisiblePlayers.clear()
@ -99,9 +139,7 @@ internal class BaseDisplayController<T : Display>(
override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration { override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration {
val entry = HandlerEntry(priority, handler) val entry = HandlerEntry(priority, handler)
handlers += entry handlers += entry
return HandlerRegistration { return HandlerRegistration { handlers.remove(entry) }
handlers.remove(entry)
}
} }
override fun setBaseVisibility(visible: Boolean) { 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() { override fun clearAudienceRules() {
audienceRules.clear() audienceRules.clear()
refreshAudience() refreshAudience()
} }
override fun refreshAudience(target: Player?) { 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) { players.forEach { player ->
runSync { val uuid = player.uniqueId
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers() val shouldBeVisible = evaluateVisibility(player)
if (players.isEmpty()) return@runSync val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid)
players.forEach { player -> if (shouldBeVisible && !isCurrentlyAutoVisible) {
val uuid = player.uniqueId autoVisiblePlayers.add(uuid)
val shouldBeVisible = evaluateVisibility(player) show(player)
val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid) } else if (!shouldBeVisible && isCurrentlyAutoVisible) {
autoVisiblePlayers.remove(uuid)
if (shouldBeVisible && !isCurrentlyAutoVisible) { hide(player)
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 { private fun evaluateVisibility(player: Player): Boolean {
var visible = baseVisibility var visible = baseVisibility
for (rule in audienceRules) { for (rule in audienceRules) {
@ -186,50 +237,6 @@ internal class BaseDisplayController<T : Display>(
return visible 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( private data class RuleEntry(
val predicate: AudiencePredicate, val predicate: AudiencePredicate,
val action: AudienceAction val action: AudienceAction

View File

@ -1,35 +1,50 @@
package net.hareworks.ghostdisplays.internal.controller package net.hareworks.ghostdisplays.internal.controller
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import net.hareworks.ghostdisplays.api.click.ClickSurface import net.hareworks.ghostdisplays.api.click.ClickSurface
import org.bukkit.entity.Display import net.hareworks.ghostdisplays.internal.nms.InteractionPacketListener
import org.bukkit.entity.Entity import org.bukkit.Bukkit
import org.bukkit.entity.Interaction
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerChangedWorldEvent import org.bukkit.event.player.PlayerChangedWorldEvent
import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.event.player.PlayerRespawnEvent import org.bukkit.event.player.PlayerRespawnEvent
import org.bukkit.plugin.java.JavaPlugin
internal class DisplayRegistry : Listener { internal class DisplayRegistry : Listener {
private val displayControllers = ConcurrentHashMap<UUID, BaseDisplayController<out Display>>() private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>()
private val interactionControllers = ConcurrentHashMap<UUID, BaseDisplayController<out Display>>() private val interactionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
private lateinit var packetListener: InteractionPacketListener
fun register(controller: BaseDisplayController<out Display>) { fun initialize(plugin: JavaPlugin) {
displayControllers[controller.display.uniqueId] = controller packetListener = InteractionPacketListener(plugin) { player, entityId, isAttack ->
controller.interaction?.let { if (isAttack) return@InteractionPacketListener
interactionControllers[it.uniqueId] = controller 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>) { fun unregister(controller: BaseDisplayController) {
displayControllers.remove(controller.display.uniqueId, controller) displayControllers.remove(controller.fakeDisplay.entityId, controller)
controller.interaction?.let { controller.fakeInteraction?.let {
interactionControllers.remove(it.uniqueId, controller) interactionControllers.remove(it.entityId, controller)
} }
} }
@ -40,14 +55,6 @@ internal class DisplayRegistry : Listener {
interactionControllers.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)
}
@EventHandler @EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) { fun onPlayerQuit(event: PlayerQuitEvent) {
controllersSnapshot().forEach { it.handlePlayerQuit(event.player) } controllersSnapshot().forEach { it.handlePlayerQuit(event.player) }
@ -68,12 +75,7 @@ internal class DisplayRegistry : Listener {
refreshAudiences(event.player) refreshAudiences(event.player)
} }
private fun lookupController(entity: Entity): BaseDisplayController<out Display>? { private fun controllersSnapshot(): Collection<BaseDisplayController> =
return displayControllers[entity.uniqueId]
?: interactionControllers[entity.uniqueId]
}
private fun controllersSnapshot(): Collection<BaseDisplayController<out Display>> =
displayControllers.values.toSet() displayControllers.values.toSet()
private fun refreshAudiences(player: Player) { private fun refreshAudiences(player: Player) {
@ -83,15 +85,12 @@ internal class DisplayRegistry : Listener {
} }
fun updateAudiences() { fun updateAudiences() {
val players = org.bukkit.Bukkit.getOnlinePlayers() val players = Bukkit.getOnlinePlayers()
if (players.isEmpty()) return if (players.isEmpty()) return
val controllers = controllersSnapshot() val controllers = controllersSnapshot()
if (controllers.isEmpty()) return if (controllers.isEmpty()) return
// 動的な評価が必要なコントローラーだけを抽出して評価する
val activeControllers = controllers.filter { it.needsPeriodicUpdate() } val activeControllers = controllers.filter { it.needsPeriodicUpdate() }
if (activeControllers.isEmpty()) return if (activeControllers.isEmpty()) return
players.forEach { player -> players.forEach { player ->
activeControllers.forEach { controller -> activeControllers.forEach { controller ->
controller.refreshAudience(player) 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
}
}