Fake-Interaction
This commit is contained in:
parent
bd6aa1743f
commit
d917475e7d
46
README.md
46
README.md
|
|
@ -5,9 +5,9 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
|
||||||
## 提供機能
|
## 提供機能
|
||||||
|
|
||||||
- `DisplayService` による Text/Block/Item Display の生成 API
|
- `DisplayService` による Text/Block/Item Display の生成 API
|
||||||
- `DisplayController` を介した `Player#showEntity` / `hideEntity` の参照カウント管理
|
- `DisplayController` を介したパケットベースの表示・非表示制御(参照カウント管理)
|
||||||
- `AudiencePredicate`/`AudiencePredicates` による可視対象の自動同期(ログイン・ワールド移動・リスポーンで再評価)
|
- `AudiencePredicate`/`AudiencePredicates` による可視対象の自動同期(ログイン・ワールド移動・リスポーンで再評価)
|
||||||
- Interaction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー
|
- FakeInteraction エンティティを用いたクリック判定サポートと、優先度付きクリックハンドラー
|
||||||
- まとめて `destroyAll()` できるリソース管理
|
- まとめて `destroyAll()` できるリソース管理
|
||||||
|
|
||||||
## スタンドアロンでの使い方
|
## スタンドアロンでの使い方
|
||||||
|
|
@ -19,7 +19,7 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
|
||||||
| `/ghostdisplay create text <id>` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 |
|
| `/ghostdisplay create text <id>` | プレイヤー視点の約 1.5 ブロック先に TextDisplay を生成し、即座にチャット編集モードへ。次に送信したメッセージ(または `cancel`)が内容になります。 |
|
||||||
| `/ghostdisplay create block <id> <blockstate>` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 |
|
| `/ghostdisplay create block <id> <blockstate>` | BlockDisplay を生成します。`oak_planks[facing=north]` のような `BlockData` 文字列を指定してください。 |
|
||||||
| `/ghostdisplay create item <id> <material>` | ItemDisplay を生成します。`minecraft:stick` などのアイテム ID を受け付けます。 |
|
| `/ghostdisplay create item <id> <material>` | ItemDisplay を生成します。`minecraft:stick` などのアイテム ID を受け付けます。 |
|
||||||
| `/ghostdisplay text edit <id>` | 既存の TextDisplay をチャット入力で編集。`cancel` で破棄。単語のみで良い場合は `/ghostdisplay text set <id> Hello_World` のように `_` をスペースに変換して即時更新できます。 |
|
| `/ghostdisplay text set <id> <content>` | TextDisplay のテキストを即時更新します。`_` はスペースに、`\n` は改行に変換されます。 |
|
||||||
| `/ghostdisplay viewer add|remove <id> <player/@a>` | 任意のプレイヤーまたはセレクターを表示対象に追加/削除します。`/ghostdisplay viewer clear <id>` で全員解除。 |
|
| `/ghostdisplay viewer add|remove <id> <player/@a>` | 任意のプレイヤーまたはセレクターを表示対象に追加/削除します。`/ghostdisplay viewer clear <id>` で全員解除。 |
|
||||||
| `/ghostdisplay audience permission add <id> <permission>` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 |
|
| `/ghostdisplay audience permission add <id> <permission>` | 指定パーミッションを持つプレイヤーに自動表示 (`remove` で解除)。 |
|
||||||
| `/ghostdisplay audience near set <id> <radius>` | Display 周囲の半径プレイヤーへ自動表示 (`audience clear` で全自動表示を解除)。 |
|
| `/ghostdisplay audience near set <id> <radius>` | Display 周囲の半径プレイヤーへ自動表示 (`audience clear` で全自動表示を解除)。 |
|
||||||
|
|
@ -28,11 +28,43 @@ Paper 1.21.10 向けの不可視ディスプレイ制御ライブラリです。
|
||||||
|
|
||||||
## 技術的ポイント
|
## 技術的ポイント
|
||||||
|
|
||||||
- Paper 標準 API の `setVisibleByDefault(false)` + `showEntity`/`hideEntity` を採用し、ProtocolLib に依存しない設計
|
- **NMS パケットベースの FakeEntity 方式** — サーバー上に実エンティティを生成せず、クライアントへのパケット送信のみで表示を実現。エンティティティック・コリジョン・永続化のオーバーヘッドがゼロ
|
||||||
- Display / Interaction は `setPersistent(false)` でスポーンし、サーバーリロードやチャンクアンロードに強い
|
- パケット送信はスレッドセーフなため、Folia のリージョンスケジューリングに依存しないシンプルな設計
|
||||||
|
- クリック検知は Netty パイプラインで `ServerboundInteractPacket` を傍受し、FakeInteraction の entityId にマッチしたらハンドラーを発火
|
||||||
- Predicate ごとにアクティブなプレイヤー集合を持つため、複数条件での重複表示にも対応
|
- Predicate ごとにアクティブなプレイヤー集合を持つため、複数条件での重複表示にも対応
|
||||||
- クリック検知は `PlayerInteractEntityEvent` を介して Display/Interaction 双方から観測し、ハンドラーは Kotlin DSL で登録可能
|
- `paperweight-userdev` による NMS アクセス(1.21.11 対応)
|
||||||
- Kotlin 2.2 + ShadowJar + plugin-yml 生成を使い、`./gradlew build`でそのまま Paper に投入可能なJarを生成
|
|
||||||
|
## API 利用例
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val service: DisplayService = GhostDisplaysPlugin.service()
|
||||||
|
|
||||||
|
// TextDisplay を生成
|
||||||
|
val controller = service.createTextDisplay(location) {
|
||||||
|
text = Component.text("Hello")
|
||||||
|
billboard = Display.Billboard.CENTER
|
||||||
|
viewRange = 0.5f
|
||||||
|
}
|
||||||
|
controller.show(player)
|
||||||
|
|
||||||
|
// テキスト更新
|
||||||
|
controller.updateText { it.text = Component.text("Updated") }
|
||||||
|
|
||||||
|
// 共通プロパティ更新
|
||||||
|
controller.updateDisplay { it.billboard = Display.Billboard.FIXED }
|
||||||
|
|
||||||
|
// テレポート
|
||||||
|
controller.teleport(newLocation)
|
||||||
|
|
||||||
|
// Audience ルール
|
||||||
|
controller.addAudienceRule(AudiencePredicates.near(location, 30.0), AudienceAction.ADD)
|
||||||
|
|
||||||
|
// クリックハンドラー
|
||||||
|
controller.onClick { ctx -> ctx.player.sendMessage("Clicked!") }
|
||||||
|
|
||||||
|
// 破棄
|
||||||
|
controller.destroy()
|
||||||
|
```
|
||||||
|
|
||||||
## 同梱ライブラリ
|
## 同梱ライブラリ
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package net.hareworks.ghostdisplays.api
|
package net.hareworks.ghostdisplays.api
|
||||||
|
|
||||||
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
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.FakeItemDisplay
|
||||||
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
|
import net.hareworks.ghostdisplays.internal.fake.FakeTextDisplay
|
||||||
import org.bukkit.Location
|
import org.bukkit.Location
|
||||||
|
|
@ -26,5 +27,10 @@ interface DisplayService {
|
||||||
builder: FakeItemDisplay.() -> Unit = {}
|
builder: FakeItemDisplay.() -> Unit = {}
|
||||||
): DisplayController
|
): DisplayController
|
||||||
|
|
||||||
|
fun createInteraction(
|
||||||
|
location: Location,
|
||||||
|
builder: FakeInteraction.() -> Unit = {}
|
||||||
|
): InteractionController
|
||||||
|
|
||||||
fun destroyAll()
|
fun destroyAll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package net.hareworks.ghostdisplays.api
|
||||||
|
|
||||||
|
import net.hareworks.ghostdisplays.api.audience.AudienceAction
|
||||||
|
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
|
||||||
|
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
||||||
|
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
||||||
|
import org.bukkit.Location
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface InteractionController {
|
||||||
|
val entityId: Int
|
||||||
|
val key: String?
|
||||||
|
val location: Location
|
||||||
|
|
||||||
|
fun show(player: Player)
|
||||||
|
fun hide(player: Player)
|
||||||
|
fun isViewing(playerId: UUID): Boolean
|
||||||
|
fun viewerIds(): Set<UUID>
|
||||||
|
|
||||||
|
fun updateInteraction(mutator: (FakeInteraction) -> Unit)
|
||||||
|
fun teleport(location: Location)
|
||||||
|
|
||||||
|
fun setBaseVisibility(visible: Boolean)
|
||||||
|
fun addAudience(predicate: AudiencePredicate): HandlerRegistration
|
||||||
|
fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration
|
||||||
|
fun clearAudienceRules()
|
||||||
|
fun refreshAudience(target: Player? = null)
|
||||||
|
fun needsPeriodicUpdate(): Boolean
|
||||||
|
|
||||||
|
fun destroy()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package net.hareworks.ghostdisplays.api.event
|
||||||
|
|
||||||
|
import net.hareworks.ghostdisplays.api.InteractionController
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.Cancellable
|
||||||
|
import org.bukkit.event.Event
|
||||||
|
import org.bukkit.event.HandlerList
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
import org.bukkit.util.Vector
|
||||||
|
|
||||||
|
class FakeInteractionClickEvent(
|
||||||
|
val player: Player,
|
||||||
|
val interactionController: InteractionController,
|
||||||
|
val interactionEntityId: Int,
|
||||||
|
val clickType: ClickType,
|
||||||
|
val hand: EquipmentSlot?,
|
||||||
|
val clickPosition: Vector?
|
||||||
|
) : Event(), Cancellable {
|
||||||
|
|
||||||
|
val isLeftClick: Boolean get() = clickType == ClickType.LEFT
|
||||||
|
val isRightClick: Boolean get() = clickType != ClickType.LEFT
|
||||||
|
|
||||||
|
private var cancelled = false
|
||||||
|
|
||||||
|
override fun isCancelled(): Boolean = cancelled
|
||||||
|
override fun setCancelled(cancel: Boolean) { cancelled = cancel }
|
||||||
|
|
||||||
|
override fun getHandlers(): HandlerList = handlerList
|
||||||
|
|
||||||
|
enum class ClickType {
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
RIGHT_AT
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
val handlerList = HandlerList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@ import java.util.UUID
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
import net.hareworks.ghostdisplays.api.DisplayController
|
import net.hareworks.ghostdisplays.api.DisplayController
|
||||||
import net.hareworks.ghostdisplays.api.DisplayService
|
import net.hareworks.ghostdisplays.api.DisplayService
|
||||||
|
import net.hareworks.ghostdisplays.api.InteractionController
|
||||||
import net.hareworks.ghostdisplays.api.InteractionOptions
|
import net.hareworks.ghostdisplays.api.InteractionOptions
|
||||||
import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController
|
import net.hareworks.ghostdisplays.internal.controller.BaseDisplayController
|
||||||
|
import net.hareworks.ghostdisplays.internal.controller.BaseInteractionController
|
||||||
import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry
|
import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry
|
||||||
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
import net.hareworks.ghostdisplays.internal.fake.FakeBlockDisplay
|
||||||
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
||||||
|
|
@ -21,6 +23,7 @@ internal class DefaultDisplayService(
|
||||||
private val registry: DisplayRegistry
|
private val registry: DisplayRegistry
|
||||||
) : DisplayService {
|
) : DisplayService {
|
||||||
private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
|
private val controllers = CopyOnWriteArraySet<BaseDisplayController>()
|
||||||
|
private val interactionControllers = CopyOnWriteArraySet<BaseInteractionController>()
|
||||||
|
|
||||||
override fun createTextDisplay(
|
override fun createTextDisplay(
|
||||||
location: Location,
|
location: Location,
|
||||||
|
|
@ -66,9 +69,27 @@ internal class DefaultDisplayService(
|
||||||
return register(fake, location, interaction)
|
return register(fake, location, interaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createInteraction(
|
||||||
|
location: Location,
|
||||||
|
builder: FakeInteraction.() -> Unit
|
||||||
|
): InteractionController {
|
||||||
|
val fake = FakeInteraction(
|
||||||
|
entityId = DisplayPacketFactory.nextEntityId(),
|
||||||
|
uuid = UUID.randomUUID(),
|
||||||
|
location = location.clone()
|
||||||
|
)
|
||||||
|
builder(fake)
|
||||||
|
val controller = BaseInteractionController(plugin, fake, registry)
|
||||||
|
interactionControllers += controller
|
||||||
|
registry.registerInteraction(controller)
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
override fun destroyAll() {
|
override fun destroyAll() {
|
||||||
controllers.forEach { it.destroy() }
|
controllers.forEach { it.destroy() }
|
||||||
controllers.clear()
|
controllers.clear()
|
||||||
|
interactionControllers.forEach { it.destroy() }
|
||||||
|
interactionControllers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun register(
|
private fun register(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
package net.hareworks.ghostdisplays.internal.controller
|
||||||
|
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import net.hareworks.ghostdisplays.api.InteractionController
|
||||||
|
import net.hareworks.ghostdisplays.api.audience.AudienceAction
|
||||||
|
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
|
||||||
|
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
||||||
|
import net.hareworks.ghostdisplays.internal.fake.FakeInteraction
|
||||||
|
import net.hareworks.ghostdisplays.internal.nms.DisplayPacketFactory
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Location
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
internal class BaseInteractionController(
|
||||||
|
private val plugin: JavaPlugin,
|
||||||
|
val fakeInteraction: FakeInteraction,
|
||||||
|
private val registry: DisplayRegistry
|
||||||
|
) : InteractionController {
|
||||||
|
|
||||||
|
private val destroyed = AtomicBoolean(false)
|
||||||
|
private val viewerCounts = ConcurrentHashMap<UUID, Int>()
|
||||||
|
|
||||||
|
private var baseVisibility: Boolean = false
|
||||||
|
private val audienceRules = CopyOnWriteArrayList<RuleEntry>()
|
||||||
|
private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>()
|
||||||
|
private var hasDynamicRules: Boolean = false
|
||||||
|
|
||||||
|
override val entityId: Int get() = fakeInteraction.entityId
|
||||||
|
override val key: String? get() = fakeInteraction.key
|
||||||
|
override val location: Location get() = fakeInteraction.location.clone()
|
||||||
|
|
||||||
|
override fun needsPeriodicUpdate(): Boolean = hasDynamicRules
|
||||||
|
|
||||||
|
override fun show(player: Player) {
|
||||||
|
val uuid = player.uniqueId
|
||||||
|
val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1
|
||||||
|
if (newCount == 1) {
|
||||||
|
DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createSpawnBundle(fakeInteraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hide(player: Player) {
|
||||||
|
val uuid = player.uniqueId
|
||||||
|
val current = viewerCounts[uuid] ?: return
|
||||||
|
if (current <= 1) {
|
||||||
|
viewerCounts.remove(uuid)
|
||||||
|
DisplayPacketFactory.sendPacket(player, DisplayPacketFactory.createRemovePacket(fakeInteraction.entityId))
|
||||||
|
} else {
|
||||||
|
viewerCounts[uuid] = current - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isViewing(playerId: UUID): Boolean = viewerCounts.containsKey(playerId)
|
||||||
|
override fun viewerIds(): Set<UUID> = HashSet(viewerCounts.keys)
|
||||||
|
|
||||||
|
override fun updateInteraction(mutator: (FakeInteraction) -> Unit) {
|
||||||
|
mutator(fakeInteraction)
|
||||||
|
broadcastMetadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun teleport(location: Location) {
|
||||||
|
fakeInteraction.location = location.clone()
|
||||||
|
val viewers = onlineViewers()
|
||||||
|
if (viewers.isEmpty()) return
|
||||||
|
val packet = DisplayPacketFactory.createTeleportPacket(fakeInteraction)
|
||||||
|
viewers.forEach { DisplayPacketFactory.sendPacket(it, packet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
if (!destroyed.compareAndSet(false, true)) return
|
||||||
|
registry.unregisterInteraction(this)
|
||||||
|
val viewers = onlineViewers()
|
||||||
|
if (viewers.isNotEmpty()) {
|
||||||
|
val removePacket = DisplayPacketFactory.createRemovePacket(fakeInteraction.entityId)
|
||||||
|
viewers.forEach { DisplayPacketFactory.sendPacket(it, removePacket) }
|
||||||
|
}
|
||||||
|
viewerCounts.clear()
|
||||||
|
audienceRules.clear()
|
||||||
|
autoVisiblePlayers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBaseVisibility(visible: Boolean) {
|
||||||
|
this.baseVisibility = visible
|
||||||
|
refreshAudience()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAudience(predicate: AudiencePredicate): HandlerRegistration {
|
||||||
|
return addAudienceRule(predicate, AudienceAction.ADD)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration {
|
||||||
|
val entry = RuleEntry(predicate, action)
|
||||||
|
audienceRules.add(entry)
|
||||||
|
updateDynamicStatus()
|
||||||
|
refreshAudience()
|
||||||
|
return HandlerRegistration {
|
||||||
|
audienceRules.remove(entry)
|
||||||
|
updateDynamicStatus()
|
||||||
|
refreshAudience()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearAudienceRules() {
|
||||||
|
audienceRules.clear()
|
||||||
|
refreshAudience()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refreshAudience(target: Player?) {
|
||||||
|
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers()
|
||||||
|
if (players.isEmpty()) return
|
||||||
|
|
||||||
|
players.forEach { player ->
|
||||||
|
val uuid = player.uniqueId
|
||||||
|
val shouldBeVisible = evaluateVisibility(player)
|
||||||
|
val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid)
|
||||||
|
|
||||||
|
if (shouldBeVisible && !isCurrentlyAutoVisible) {
|
||||||
|
autoVisiblePlayers.add(uuid)
|
||||||
|
show(player)
|
||||||
|
} else if (!shouldBeVisible && isCurrentlyAutoVisible) {
|
||||||
|
autoVisiblePlayers.remove(uuid)
|
||||||
|
hide(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun handlePlayerQuit(player: Player) {
|
||||||
|
val uuid = player.uniqueId
|
||||||
|
viewerCounts.remove(uuid)
|
||||||
|
autoVisiblePlayers.remove(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastMetadata() {
|
||||||
|
val viewers = onlineViewers()
|
||||||
|
if (viewers.isEmpty()) return
|
||||||
|
val packet = DisplayPacketFactory.createMetadataPacket(fakeInteraction)
|
||||||
|
viewers.forEach { DisplayPacketFactory.sendPacket(it, packet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onlineViewers(): List<Player> =
|
||||||
|
viewerCounts.keys.mapNotNull { Bukkit.getPlayer(it) }
|
||||||
|
|
||||||
|
private fun updateDynamicStatus() {
|
||||||
|
hasDynamicRules = audienceRules.any { it.predicate.isDynamic() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateVisibility(player: Player): Boolean {
|
||||||
|
var visible = baseVisibility
|
||||||
|
for (rule in audienceRules) {
|
||||||
|
val matches = runCatching { rule.predicate.test(player) }.getOrDefault(false)
|
||||||
|
if (matches) {
|
||||||
|
when (rule.action) {
|
||||||
|
AudienceAction.ADD -> visible = true
|
||||||
|
AudienceAction.EXCLUDE -> visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RuleEntry(
|
||||||
|
val predicate: AudiencePredicate,
|
||||||
|
val action: AudienceAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package net.hareworks.ghostdisplays.internal.controller
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import net.hareworks.ghostdisplays.api.click.ClickSurface
|
import net.hareworks.ghostdisplays.api.click.ClickSurface
|
||||||
|
import net.hareworks.ghostdisplays.api.event.FakeInteractionClickEvent
|
||||||
|
import net.hareworks.ghostdisplays.internal.nms.InteractAction
|
||||||
import net.hareworks.ghostdisplays.internal.nms.InteractionPacketListener
|
import net.hareworks.ghostdisplays.internal.nms.InteractionPacketListener
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
|
|
@ -15,19 +17,37 @@ import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
internal class DisplayRegistry : Listener {
|
internal class DisplayRegistry : Listener {
|
||||||
private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
private val displayControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
||||||
private val interactionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
private val displayInteractionControllers = ConcurrentHashMap<Int, BaseDisplayController>()
|
||||||
|
private val standaloneInteractionControllers = ConcurrentHashMap<Int, BaseInteractionController>()
|
||||||
private lateinit var packetListener: InteractionPacketListener
|
private lateinit var packetListener: InteractionPacketListener
|
||||||
|
|
||||||
fun initialize(plugin: JavaPlugin) {
|
fun initialize(plugin: JavaPlugin) {
|
||||||
packetListener = InteractionPacketListener(plugin) { player, entityId, isAttack ->
|
packetListener = InteractionPacketListener(plugin) { player, data ->
|
||||||
if (isAttack) return@InteractionPacketListener
|
// スタンドアロンInteractionを先にチェック → Bukkitイベント発火
|
||||||
val fromInteraction = interactionControllers[entityId]
|
val standalone = standaloneInteractionControllers[data.entityId]
|
||||||
val fromDisplay = displayControllers[entityId]
|
if (standalone != null) {
|
||||||
|
val clickType = when (data.action) {
|
||||||
|
InteractAction.ATTACK -> FakeInteractionClickEvent.ClickType.LEFT
|
||||||
|
InteractAction.INTERACT -> FakeInteractionClickEvent.ClickType.RIGHT
|
||||||
|
InteractAction.INTERACT_AT -> FakeInteractionClickEvent.ClickType.RIGHT_AT
|
||||||
|
}
|
||||||
|
val loc = standalone.fakeInteraction.location
|
||||||
|
plugin.server.regionScheduler.execute(plugin, loc) {
|
||||||
|
val event = FakeInteractionClickEvent(player, standalone, data.entityId, clickType, data.hand, data.clickPosition)
|
||||||
|
Bukkit.getPluginManager().callEvent(event)
|
||||||
|
}
|
||||||
|
return@InteractionPacketListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display付属Interactionの場合は既存のクリックハンドラ(右クリックのみ)
|
||||||
|
if (data.action == InteractAction.ATTACK) return@InteractionPacketListener
|
||||||
|
val fromInteraction = displayInteractionControllers[data.entityId]
|
||||||
|
val fromDisplay = displayControllers[data.entityId]
|
||||||
val controller = fromInteraction ?: fromDisplay ?: return@InteractionPacketListener
|
val controller = fromInteraction ?: fromDisplay ?: return@InteractionPacketListener
|
||||||
val surface = if (fromInteraction != null) ClickSurface.INTERACTION else ClickSurface.DISPLAY
|
val surface = if (fromInteraction != null) ClickSurface.INTERACTION else ClickSurface.DISPLAY
|
||||||
val loc = controller.fakeDisplay.location
|
val loc = controller.fakeDisplay.location
|
||||||
plugin.server.regionScheduler.execute(plugin, loc) {
|
plugin.server.regionScheduler.execute(plugin, loc) {
|
||||||
controller.handleClick(player, entityId, surface)
|
controller.handleClick(player, data.entityId, surface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
packetListener.injectAll()
|
packetListener.injectAll()
|
||||||
|
|
@ -37,27 +57,37 @@ internal class DisplayRegistry : Listener {
|
||||||
fun register(controller: BaseDisplayController) {
|
fun register(controller: BaseDisplayController) {
|
||||||
displayControllers[controller.fakeDisplay.entityId] = controller
|
displayControllers[controller.fakeDisplay.entityId] = controller
|
||||||
controller.fakeInteraction?.let {
|
controller.fakeInteraction?.let {
|
||||||
interactionControllers[it.entityId] = controller
|
displayInteractionControllers[it.entityId] = controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregister(controller: BaseDisplayController) {
|
fun unregister(controller: BaseDisplayController) {
|
||||||
displayControllers.remove(controller.fakeDisplay.entityId, controller)
|
displayControllers.remove(controller.fakeDisplay.entityId, controller)
|
||||||
controller.fakeInteraction?.let {
|
controller.fakeInteraction?.let {
|
||||||
interactionControllers.remove(it.entityId, controller)
|
displayInteractionControllers.remove(it.entityId, controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun controllerCount(): Int = displayControllers.size
|
fun registerInteraction(controller: BaseInteractionController) {
|
||||||
|
standaloneInteractionControllers[controller.fakeInteraction.entityId] = controller
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterInteraction(controller: BaseInteractionController) {
|
||||||
|
standaloneInteractionControllers.remove(controller.fakeInteraction.entityId, controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun controllerCount(): Int = displayControllers.size + standaloneInteractionControllers.size
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
displayControllers.clear()
|
displayControllers.clear()
|
||||||
interactionControllers.clear()
|
displayInteractionControllers.clear()
|
||||||
|
standaloneInteractionControllers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
fun onPlayerQuit(event: PlayerQuitEvent) {
|
fun onPlayerQuit(event: PlayerQuitEvent) {
|
||||||
controllersSnapshot().forEach { it.handlePlayerQuit(event.player) }
|
displayControllersSnapshot().forEach { it.handlePlayerQuit(event.player) }
|
||||||
|
standaloneInteractionsSnapshot().forEach { it.handlePlayerQuit(event.player) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
|
|
@ -75,26 +105,28 @@ internal class DisplayRegistry : Listener {
|
||||||
refreshAudiences(event.player)
|
refreshAudiences(event.player)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun controllersSnapshot(): Collection<BaseDisplayController> =
|
private fun displayControllersSnapshot(): Collection<BaseDisplayController> =
|
||||||
displayControllers.values.toSet()
|
displayControllers.values.toSet()
|
||||||
|
|
||||||
|
private fun standaloneInteractionsSnapshot(): Collection<BaseInteractionController> =
|
||||||
|
standaloneInteractionControllers.values.toSet()
|
||||||
|
|
||||||
private fun refreshAudiences(player: Player) {
|
private fun refreshAudiences(player: Player) {
|
||||||
val controllers = controllersSnapshot()
|
displayControllersSnapshot().forEach { it.refreshAudience(player) }
|
||||||
if (controllers.isEmpty()) return
|
standaloneInteractionsSnapshot().forEach { it.refreshAudience(player) }
|
||||||
controllers.forEach { it.refreshAudience(player) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAudiences() {
|
fun updateAudiences() {
|
||||||
val players = Bukkit.getOnlinePlayers()
|
val players = Bukkit.getOnlinePlayers()
|
||||||
if (players.isEmpty()) return
|
if (players.isEmpty()) return
|
||||||
val controllers = controllersSnapshot()
|
|
||||||
if (controllers.isEmpty()) return
|
val activeDisplays = displayControllersSnapshot().filter { it.needsPeriodicUpdate() }
|
||||||
val activeControllers = controllers.filter { it.needsPeriodicUpdate() }
|
val activeInteractions = standaloneInteractionsSnapshot().filter { it.needsPeriodicUpdate() }
|
||||||
if (activeControllers.isEmpty()) return
|
if (activeDisplays.isEmpty() && activeInteractions.isEmpty()) return
|
||||||
|
|
||||||
players.forEach { player ->
|
players.forEach { player ->
|
||||||
activeControllers.forEach { controller ->
|
activeDisplays.forEach { it.refreshAudience(player) }
|
||||||
controller.refreshAudience(player)
|
activeInteractions.forEach { it.refreshAudience(player) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class FakeInteraction(
|
||||||
location: Location
|
location: Location
|
||||||
) : FakeEntity(entityId, uuid, location) {
|
) : FakeEntity(entityId, uuid, location) {
|
||||||
|
|
||||||
|
var key: String? = null
|
||||||
var width: Float = 0.8f
|
var width: Float = 0.8f
|
||||||
var height: Float = 0.8f
|
var height: Float = 0.8f
|
||||||
var responsive: Boolean = true
|
var responsive: Boolean = true
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,32 @@ import io.netty.channel.ChannelHandlerContext
|
||||||
import java.lang.invoke.MethodHandles
|
import java.lang.invoke.MethodHandles
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import net.minecraft.network.protocol.game.ServerboundInteractPacket
|
import net.minecraft.network.protocol.game.ServerboundInteractPacket
|
||||||
|
import net.minecraft.world.InteractionHand
|
||||||
import org.bukkit.craftbukkit.entity.CraftPlayer
|
import org.bukkit.craftbukkit.entity.CraftPlayer
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.Listener
|
import org.bukkit.event.Listener
|
||||||
import org.bukkit.event.player.PlayerJoinEvent
|
import org.bukkit.event.player.PlayerJoinEvent
|
||||||
import org.bukkit.event.player.PlayerQuitEvent
|
import org.bukkit.event.player.PlayerQuitEvent
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
internal data class InteractData(
|
||||||
|
val entityId: Int,
|
||||||
|
val action: InteractAction,
|
||||||
|
val hand: EquipmentSlot?,
|
||||||
|
val clickPosition: org.bukkit.util.Vector?
|
||||||
|
)
|
||||||
|
|
||||||
|
internal enum class InteractAction {
|
||||||
|
ATTACK,
|
||||||
|
INTERACT,
|
||||||
|
INTERACT_AT
|
||||||
|
}
|
||||||
|
|
||||||
internal class InteractionPacketListener(
|
internal class InteractionPacketListener(
|
||||||
private val plugin: JavaPlugin,
|
private val plugin: JavaPlugin,
|
||||||
private val onInteract: (player: Player, entityId: Int, isAttack: Boolean) -> Unit
|
private val onInteract: (player: Player, data: InteractData) -> Unit
|
||||||
) : Listener {
|
) : Listener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -71,8 +86,8 @@ internal class InteractionPacketListener(
|
||||||
if (msg is ServerboundInteractPacket) {
|
if (msg is ServerboundInteractPacket) {
|
||||||
try {
|
try {
|
||||||
val entityId = ENTITY_ID_GETTER.invoke(msg) as Int
|
val entityId = ENTITY_ID_GETTER.invoke(msg) as Int
|
||||||
val isAttack = isAttackAction(msg)
|
val data = extractAction(entityId, msg)
|
||||||
onInteract(player, entityId, isAttack)
|
onInteract(player, data)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex)
|
plugin.logger.log(Level.WARNING, "Failed to handle interact packet", ex)
|
||||||
}
|
}
|
||||||
|
|
@ -82,13 +97,34 @@ internal class InteractionPacketListener(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAttackAction(packet: ServerboundInteractPacket): Boolean {
|
private fun extractAction(entityId: Int, packet: ServerboundInteractPacket): InteractData {
|
||||||
var attack = false
|
var action = InteractAction.ATTACK
|
||||||
|
var hand: EquipmentSlot? = null
|
||||||
|
var clickPos: org.bukkit.util.Vector? = null
|
||||||
|
|
||||||
packet.dispatch(object : ServerboundInteractPacket.Handler {
|
packet.dispatch(object : ServerboundInteractPacket.Handler {
|
||||||
override fun onInteraction(hand: net.minecraft.world.InteractionHand) {}
|
override fun onInteraction(nmsHand: InteractionHand) {
|
||||||
override fun onInteraction(hand: net.minecraft.world.InteractionHand, pos: net.minecraft.world.phys.Vec3) {}
|
action = InteractAction.INTERACT
|
||||||
override fun onAttack() { attack = true }
|
hand = toBukkit(nmsHand)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInteraction(nmsHand: InteractionHand, pos: net.minecraft.world.phys.Vec3) {
|
||||||
|
action = InteractAction.INTERACT_AT
|
||||||
|
hand = toBukkit(nmsHand)
|
||||||
|
clickPos = org.bukkit.util.Vector(pos.x, pos.y, pos.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttack() {
|
||||||
|
action = InteractAction.ATTACK
|
||||||
|
hand = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return attack
|
|
||||||
|
return InteractData(entityId, action, hand, clickPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toBukkit(hand: InteractionHand): EquipmentSlot = when (hand) {
|
||||||
|
InteractionHand.MAIN_HAND -> EquipmentSlot.HAND
|
||||||
|
InteractionHand.OFF_HAND -> EquipmentSlot.OFF_HAND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user