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