diff --git a/build.gradle.kts b/build.gradle.kts index 0dab403..fe56de9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) + } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da..2a84e18 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 6e03ddf..e3a1099 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://repo.papermc.io/repository/maven-public/") + } +} + rootProject.name = "GhostDisplays" includeBuild("kommand-lib") diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt b/src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt index 2384d33..1a0f859 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt @@ -24,6 +24,7 @@ class GhostDisplaysPlugin : JavaPlugin() { miniMessage = MiniMessage.miniMessage() displayRegistry = DisplayRegistry().also { server.pluginManager.registerEvents(it, this) + it.initialize(this) } serviceImpl = DefaultDisplayService(this, displayRegistry) displayManager = DisplayManager(this, serviceImpl, miniMessage) @@ -34,7 +35,7 @@ class GhostDisplaysPlugin : JavaPlugin() { instance = this logger.info("GhostDisplays ready: ${displayRegistry.controllerCount()} controllers active.") - + server.globalRegionScheduler.runAtFixedRate(this, { _ -> displayRegistry.updateAudiences() }, 20L, 20L) diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt index dfa7bbd..efb5b57 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt @@ -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 { - 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 - 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() diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt index fa58539..48d0c33 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt @@ -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 + builder: FakeTextDisplay.() -> Unit = {} + ): DisplayController fun createBlockDisplay( location: Location, interaction: InteractionOptions = InteractionOptions.Disabled, - builder: BlockDisplay.() -> Unit = {} - ): DisplayController + builder: FakeBlockDisplay.() -> Unit = {} + ): DisplayController fun createItemDisplay( location: Location, itemStack: ItemStack, interaction: InteractionOptions = InteractionOptions.Disabled, - builder: ItemDisplay.() -> Unit = {} - ): DisplayController + builder: FakeItemDisplay.() -> Unit = {} + ): DisplayController fun destroyAll() } diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayType.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayType.kt new file mode 100644 index 0000000..03d58ec --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayType.kt @@ -0,0 +1,7 @@ +package net.hareworks.ghostdisplays.api + +enum class DisplayType { + TEXT, + BLOCK, + ITEM +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt index 7a83906..139d4b3 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt @@ -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 { diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt b/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt index 0e5c6e3..ee5fdf5 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt @@ -464,7 +464,7 @@ object CommandRegistrar { val id = argument("id") val targets = argument>("targets") var count = 0 - targets.forEach { + targets.forEach { if (manager.removePlayerAudience(id, it.uniqueId)) count++ } sender.success("Removed $count/${targets.size} player(s) from audience of '$id'.") diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt b/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt index 1f68e0f..1af6261 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt @@ -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>() + private val displays = ConcurrentHashMap() 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 { "${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> = displays.values.sortedBy { it.id } + fun listDisplays(): List = 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 = diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt index 0bc8a3f..95599f4 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt @@ -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( +sealed class ManagedDisplay( val id: String, val kind: DisplayKind, - val controller: DisplayController, + val controller: DisplayController, location: Location, val createdAt: Instant, val createdBy: UUID? @@ -61,29 +57,29 @@ sealed class ManagedDisplay( class Text( id: String, - controller: DisplayController, + controller: DisplayController, location: Location, createdAt: Instant, createdBy: UUID?, var rawContent: String, var component: Component - ) : ManagedDisplay(id, DisplayKind.TEXT, controller, location, createdAt, createdBy) + ) : ManagedDisplay(id, DisplayKind.TEXT, controller, location, createdAt, createdBy) class Block( id: String, - controller: DisplayController, + controller: DisplayController, location: Location, createdAt: Instant, createdBy: UUID?, var blockData: BlockData - ) : ManagedDisplay(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy) + ) : ManagedDisplay(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy) class Item( id: String, - controller: DisplayController, + controller: DisplayController, location: Location, createdAt: Instant, createdBy: UUID?, var itemStack: ItemStack - ) : ManagedDisplay(id, DisplayKind.ITEM, controller, location, createdAt, createdBy) + ) : ManagedDisplay(id, DisplayKind.ITEM, controller, location, createdAt, createdBy) } diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt index 13237c9..23f76c0 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt @@ -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>() + private val controllers = CopyOnWriteArraySet() override fun createTextDisplay( location: Location, interaction: InteractionOptions, - builder: TextDisplay.() -> Unit - ): DisplayController = 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 = 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 = 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 spawnDisplay( + private fun register( + fake: net.hareworks.ghostdisplays.internal.fake.FakeDisplay, location: Location, - type: Class, - interactionOptions: InteractionOptions, - afterSpawn: (T) -> Unit - ): DisplayController { - 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 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() - plugin.server.regionScheduler.run(plugin, location) { _ -> future.complete(action()) } - future.join() - } - } } diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt index 20890a4..ec90756 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt @@ -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( +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 { +) : DisplayController { + private val destroyed = AtomicBoolean(false) private val viewerCounts = ConcurrentHashMap() private val handlers = CopyOnWriteArrayList() - - // Audience Management - // Audience Management + private var baseVisibility: Boolean = false private val audienceRules = CopyOnWriteArrayList() private val autoVisiblePlayers = ConcurrentHashMap.newKeySet() - - // 最適化: 定期更新が必要なルールがあるか 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( override fun viewerIds(): Set = 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( override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration { val entry = HandlerEntry(priority, handler) handlers += entry - return HandlerRegistration { - handlers.remove(entry) - } + return HandlerRegistration { handlers.remove(entry) } } override fun setBaseVisibility(visible: Boolean) { @@ -124,23 +162,6 @@ internal class BaseDisplayController( refreshAudience() } } - - private fun updateDynamicStatus() { - // 簡易判定: クラス名に "Near" が含まれる、または特定の実装であることを期待する - // ただし現状はラムダなので名前判定は不安定。 - // AudiencePredicate 側にマーカーインターフェースをつけるのが正しいが、 - // 今回は「ユーザー定義のPredicate」も含めて、動的かどうかわからない。 - // -> AudiencePredicates.near() が返すオブジェクトに特徴を持たせる。 - hasDynamicRules = audienceRules.any { isDynamicPredicate(it.predicate) } - } - - private fun isDynamicPredicate(predicate: AudiencePredicate): Boolean { - // AudiencePredicates.near が返すクラスの実装詳細に依存するか、 - // AudiencePredicate にプロパティを追加する。 - // ここでは一旦、toString() 等で識別するか?いや、安全ではない。 - // API変更: AudiencePredicate に default isDynamic() を追加する。 - return predicate.isDynamic() - } override fun clearAudienceRules() { audienceRules.clear() @@ -148,30 +169,60 @@ internal class BaseDisplayController( } 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 = + 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( 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 callSync(action: () -> R): R { - return if (Bukkit.isOwnedByCurrentRegion(display)) { - action() - } else { - val future = CompletableFuture() - display.scheduler.run(plugin, { future.complete(action()) }, { future.cancel(false) }) - future.join() - } - } - private data class RuleEntry( val predicate: AudiencePredicate, val action: AudienceAction diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt index 348da8c..0c9e546 100644 --- a/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt @@ -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>() - private val interactionControllers = ConcurrentHashMap>() + private val displayControllers = ConcurrentHashMap() + private val interactionControllers = ConcurrentHashMap() + private lateinit var packetListener: InteractionPacketListener - fun register(controller: BaseDisplayController) { - 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) { - 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? { - return displayControllers[entity.uniqueId] - ?: interactionControllers[entity.uniqueId] - } - - private fun controllersSnapshot(): Collection> = + private fun controllersSnapshot(): Collection = 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) diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeBlockDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeBlockDisplay.kt new file mode 100644 index 0000000..4cb9ae0 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeBlockDisplay.kt @@ -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") +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeDisplay.kt new file mode 100644 index 0000000..aab1734 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeDisplay.kt @@ -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 +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeEntity.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeEntity.kt new file mode 100644 index 0000000..f3656d3 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeEntity.kt @@ -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 +) diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt new file mode 100644 index 0000000..6cf183a --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeInteraction.kt @@ -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 +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeItemDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeItemDisplay.kt new file mode 100644 index 0000000..1048557 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeItemDisplay.kt @@ -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 +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeTextDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeTextDisplay.kt new file mode 100644 index 0000000..65b947e --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/fake/FakeTextDisplay.kt @@ -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 + } +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/DisplayPacketFactory.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/DisplayPacketFactory.kt new file mode 100644 index 0000000..b5f70bf --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/DisplayPacketFactory.kt @@ -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> { + val values = mutableListOf>() + 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> { + 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> { + val nmsBlockState = (fake.blockData as CraftBlockData).state + return listOf( + SynchedEntityData.DataValue.create(EntityDataFields.BLOCK_STATE, nmsBlockState) + ) + } + + private fun buildItemMetadata(fake: FakeItemDisplay): List> { + 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> { + 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) + } +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/EntityDataFields.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/EntityDataFields.kt new file mode 100644 index 0000000..40d8657 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/EntityDataFields.kt @@ -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 = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val INTERPOLATION_DURATION: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val TELEPORT_INTERPOLATION_DURATION: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_POS_ROT_INTERPOLATION_DURATION_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val TRANSLATION: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_TRANSLATION_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val SCALE: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_SCALE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val LEFT_ROTATION: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_LEFT_ROTATION_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val RIGHT_ROTATION: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_RIGHT_ROTATION_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val BILLBOARD: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_BILLBOARD_RENDER_CONSTRAINTS_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val BRIGHTNESS_OVERRIDE: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_BRIGHTNESS_OVERRIDE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val VIEW_RANGE: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_VIEW_RANGE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val SHADOW_RADIUS: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_SHADOW_RADIUS_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val SHADOW_STRENGTH: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_SHADOW_STRENGTH_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val DISPLAY_WIDTH: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_WIDTH_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val DISPLAY_HEIGHT: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_HEIGHT_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val GLOW_COLOR_OVERRIDE: EntityDataAccessor = + displayLookup.findStaticGetter( + NmsDisplay::class.java, + "DATA_GLOW_COLOR_OVERRIDE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + // --- TextDisplay --- + + private val textDisplayLookup = MethodHandles.privateLookupIn( + NmsDisplay.TextDisplay::class.java, MethodHandles.lookup() + ) + + val TEXT: EntityDataAccessor = + textDisplayLookup.findStaticGetter( + NmsDisplay.TextDisplay::class.java, + "DATA_TEXT_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val LINE_WIDTH: EntityDataAccessor = + textDisplayLookup.findStaticGetter( + NmsDisplay.TextDisplay::class.java, + "DATA_LINE_WIDTH_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val BACKGROUND_COLOR: EntityDataAccessor = + textDisplayLookup.findStaticGetter( + NmsDisplay.TextDisplay::class.java, + "DATA_BACKGROUND_COLOR_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val TEXT_OPACITY: EntityDataAccessor = + textDisplayLookup.findStaticGetter( + NmsDisplay.TextDisplay::class.java, + "DATA_TEXT_OPACITY_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val STYLE_FLAGS: EntityDataAccessor = + textDisplayLookup.findStaticGetter( + NmsDisplay.TextDisplay::class.java, + "DATA_STYLE_FLAGS_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + // --- BlockDisplay --- + + private val blockDisplayLookup = MethodHandles.privateLookupIn( + NmsDisplay.BlockDisplay::class.java, MethodHandles.lookup() + ) + + val BLOCK_STATE: EntityDataAccessor = + blockDisplayLookup.findStaticGetter( + NmsDisplay.BlockDisplay::class.java, + "DATA_BLOCK_STATE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + // --- ItemDisplay --- + + private val itemDisplayLookup = MethodHandles.privateLookupIn( + NmsDisplay.ItemDisplay::class.java, MethodHandles.lookup() + ) + + val ITEM_STACK: EntityDataAccessor = + itemDisplayLookup.findStaticGetter( + NmsDisplay.ItemDisplay::class.java, + "DATA_ITEM_STACK_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val ITEM_DISPLAY_TRANSFORM: EntityDataAccessor = + itemDisplayLookup.findStaticGetter( + NmsDisplay.ItemDisplay::class.java, + "DATA_ITEM_DISPLAY_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + // --- Interaction --- + + private val interactionLookup = MethodHandles.privateLookupIn( + NmsInteraction::class.java, MethodHandles.lookup() + ) + + val INTERACTION_WIDTH: EntityDataAccessor = + interactionLookup.findStaticGetter( + NmsInteraction::class.java, + "DATA_WIDTH_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val INTERACTION_HEIGHT: EntityDataAccessor = + interactionLookup.findStaticGetter( + NmsInteraction::class.java, + "DATA_HEIGHT_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor + + val INTERACTION_RESPONSE: EntityDataAccessor = + interactionLookup.findStaticGetter( + NmsInteraction::class.java, + "DATA_RESPONSE_ID", + EntityDataAccessor::class.java + ).invoke() as EntityDataAccessor +} diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt new file mode 100644 index 0000000..663ff2a --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/nms/InteractionPacketListener.kt @@ -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 + } +}