From e324a110e57553ae9d650e6973485f74a58402e6 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 9 Dec 2025 16:10:22 +0900 Subject: [PATCH] feat: update dependencies --- GhostDisplays | 2 +- build.gradle.kts | 18 ++- hcu-core | 2 +- settings.gradle.kts | 2 +- .../kotlin/net/hareworks/hcu/lands/App.kt | 12 +- .../net/hareworks/hcu/lands/model/Land.kt | 37 +++++ .../hcu/lands/task/VisualizerTask.kt | 142 ++++++++++++++++-- 7 files changed, 189 insertions(+), 26 deletions(-) diff --git a/GhostDisplays b/GhostDisplays index be85d83..5535c25 160000 --- a/GhostDisplays +++ b/GhostDisplays @@ -1 +1 @@ -Subproject commit be85d83428eff52a749747fc495c4bda0e82d035 +Subproject commit 5535c259494adcd33e659aae9a171c3ebf16e2cf diff --git a/build.gradle.kts b/build.gradle.kts index eb439df..92b758b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,15 +18,14 @@ val exposedVersion = "1.0.0-rc-4" dependencies { compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") - compileOnly("org.jetbrains.kotlin:kotlin-stdlib") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") { - exclude(group = "org.jetbrains.kotlin") - } + paperLibrary("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") + paperLibrary("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") compileOnly("com.michael-bull.kotlin-result:kotlin-result:2.1.0") compileOnly("net.hareworks.hcu:hcu-core") compileOnly("net.hareworks:kommand-lib") compileOnly("net.hareworks:permits-lib") + compileOnly("net.hareworks:GhostDisplays") compileOnly("net.kyori:adventure-api:4.25.0") compileOnly("net.kyori:adventure-text-minimessage:4.25.0") @@ -43,9 +42,10 @@ tasks { } shadowJar { archiveClassifier.set("min") - minimize() - dependencies { - exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib")) + minimize { + exclude(dependency("net.hareworks:GhostDisplays")) + exclude(dependency("net.hareworks:kommand-lib")) + exclude(dependency("net.hareworks:permits-lib")) } } } @@ -61,6 +61,10 @@ paper { required = true load = PaperPluginDescription.RelativeLoadOrder.BEFORE } + register("GhostDisplays") { + required = true + load = PaperPluginDescription.RelativeLoadOrder.BEFORE + } } authors = listOf( diff --git a/hcu-core b/hcu-core index 520a378..e2375c0 160000 --- a/hcu-core +++ b/hcu-core @@ -1 +1 @@ -Subproject commit 520a378cd5c3da6321305d07cc0b402b73292add +Subproject commit e2375c0876de9ba30be24af84d90443ea844a99b diff --git a/settings.gradle.kts b/settings.gradle.kts index f8e8494..f8bcc1f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "faction" +rootProject.name = "lands" includeBuild("hcu-core") includeBuild("hcu-core/kommand-lib") includeBuild("hcu-core/kommand-lib/permits-lib") diff --git a/src/main/kotlin/net/hareworks/hcu/lands/App.kt b/src/main/kotlin/net/hareworks/hcu/lands/App.kt index 511af66..f6f8ab0 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/App.kt @@ -58,6 +58,12 @@ class App : JavaPlugin() { this.playerIdService = pIdService this.actorIdentityService = actorService + // Load DisplayService (optional dependency) + val displayService = server.servicesManager.load(net.hareworks.ghostdisplays.api.DisplayService::class.java) + if (displayService == null) { + logger.warning("DisplayService not found. Land name displays will not be shown.") + } + // Register public API as a Bukkit service val landsAPI = net.hareworks.hcu.lands.api.LandsAPIImpl(service) server.servicesManager.register( @@ -67,8 +73,10 @@ class App : JavaPlugin() { org.bukkit.plugin.ServicePriority.Normal ) - // Run visualizer every 20 ticks (1 second) - server.scheduler.runTaskTimer(this, VisualizerTask(service), 20L, 10L) + // Run visualizer every 10 ticks (0.5 seconds) + val visualizerTask = VisualizerTask(service, displayService) + net.hareworks.hcu.lands.task.LandsVisualizer.setVisualizerTask(visualizerTask) + server.scheduler.runTaskTimer(this, visualizerTask, 20L, 10L) logger.info("Lands plugin initialized successfully.") logger.info("LandsAPI registered as a Bukkit service.") diff --git a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt index 3beecc3..cec8345 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt @@ -99,6 +99,43 @@ data class Land( return totalVolume } + + fun getBoundingBox(): net.hareworks.hcu.lands.index.BoundingBox { + if (data.parts.isEmpty()) { + return net.hareworks.hcu.lands.index.BoundingBox(0, 0, 0, 0, 0, 0) + } + + var minX = Int.MAX_VALUE + var minY = Int.MAX_VALUE + var minZ = Int.MAX_VALUE + var maxX = Int.MIN_VALUE + var maxY = Int.MIN_VALUE + var maxZ = Int.MIN_VALUE + + data.parts.forEach { shape -> + when (shape) { + is Shape.Cuboid -> { + minX = minOf(minX, shape.minX()) + maxX = maxOf(maxX, shape.maxX()) + minY = minOf(minY, shape.minY()) + maxY = maxOf(maxY, shape.maxY()) + minZ = minOf(minZ, shape.minZ()) + maxZ = maxOf(maxZ, shape.maxZ()) + } + is Shape.Cylinder -> { + val r = (shape.radius + 0.5).toInt() + 1 + minX = minOf(minX, shape.x - r) + maxX = maxOf(maxX, shape.x + r) + minY = minOf(minY, shape.y - shape.bottomHeight) + maxY = maxOf(maxY, shape.y + shape.topHeight) + minZ = minOf(minZ, shape.z - r) + maxZ = maxOf(maxZ, shape.z + r) + } + } + } + + return net.hareworks.hcu.lands.index.BoundingBox(minX, minY, minZ, maxX, maxY, maxZ) + } } @Serializable diff --git a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt index 93fbb3f..9127d5c 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt @@ -2,20 +2,32 @@ package net.hareworks.hcu.lands.task import net.hareworks.hcu.lands.model.Shape import net.hareworks.hcu.lands.service.LandService +import net.hareworks.ghostdisplays.api.DisplayController +import net.hareworks.ghostdisplays.api.DisplayService import org.bukkit.Bukkit import org.bukkit.Color +import org.bukkit.Location import org.bukkit.Particle import org.bukkit.entity.Player +import org.bukkit.entity.TextDisplay import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.abs import kotlin.math.cos import kotlin.math.sin object LandsVisualizer { private val enabledPlayers = mutableSetOf() + private var visualizerTask: VisualizerTask? = null + + fun setVisualizerTask(task: VisualizerTask) { + visualizerTask = task + } fun toggle(player: Player) { if (enabledPlayers.contains(player.uniqueId)) { enabledPlayers.remove(player.uniqueId) + visualizerTask?.cleanupDisplaysForPlayer(player.uniqueId) player.sendMessage("Visualization disabled.") } else { enabledPlayers.add(player.uniqueId) @@ -26,7 +38,12 @@ object LandsVisualizer { fun isEnabled(player: Player) = enabledPlayers.contains(player.uniqueId) } -class VisualizerTask(private val landService: LandService) : Runnable { +class VisualizerTask( + private val landService: LandService, + private val displayService: DisplayService? +) : Runnable { + private val logger = org.bukkit.Bukkit.getLogger() + private val displayControllers = ConcurrentHashMap>>() override fun run() { Bukkit.getOnlinePlayers().forEach { p -> if (LandsVisualizer.isEnabled(p)) { @@ -43,6 +60,30 @@ class VisualizerTask(private val landService: LandService) : Runnable { } } + // Manage TextDisplays for land names + if (displayService != null) { + val activeDisplays = displayControllers.getOrPut(p.uniqueId) { mutableMapOf() } + val nearbyLandIds = nearbyLands.map { it.id }.toSet() + + // Update displays for nearby lands + nearbyLands.forEach { land -> + val controller = activeDisplays.getOrPut(land.id) { + val newController = createDisplayForLand(p, land) + logger.info("[LandsDisplay] Created display for land '${land.name}' (ID: ${land.id}) for player ${p.name}") + newController + } + updateDisplayPosition(controller, p, land) + } + + // Remove displays for lands no longer nearby + val toRemove = activeDisplays.keys.filter { it !in nearbyLandIds } + toRemove.forEach { landId -> + activeDisplays[landId]?.destroy() + activeDisplays.remove(landId) + logger.info("[LandsDisplay] Removed display for land ID $landId for player ${p.name} (out of range)") + } + } + nearbyLands.forEach { land -> // Verify that this is the current version of the land val currentLand = landService.getLand(land.id) @@ -162,19 +203,6 @@ class VisualizerTask(private val landService: LandService) : Runnable { drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5) } - - // Draw top face outline in red (outer edges only) - // Optimized: Calculate outline once for both faces as they share the same XZ shape - val faceOutline = cylinder.getOptimizedFaceOutline() - val redColor = Particle.DustOptions(Color.RED, 1.0f) - val topFaceY = cylinder.y + cylinder.topHeight - - drawFaceOutline(player, faceOutline, topFaceY + 1.0, redColor, cylinder) - - // Draw bottom face outline in red (outer edges only) - val bottomFaceY = cylinder.y - cylinder.bottomHeight - - drawFaceOutline(player, faceOutline, bottomFaceY.toDouble(), redColor, cylinder) } /** @@ -275,4 +303,90 @@ class VisualizerTask(private val landService: LandService) : Runnable { player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, dustOptions) } } + + private fun createDisplayForLand(player: Player, land: net.hareworks.hcu.lands.model.Land): DisplayController { + val position = calculateFaceAttachmentPoint(player, land) + val controller = displayService!!.createTextDisplay(position) + controller.applyEntityUpdate { display -> + display.text(net.kyori.adventure.text.Component.text(land.name)) + display.billboard = org.bukkit.entity.Display.Billboard.VERTICAL + } + controller.show(player) + logger.info("[LandsDisplay] Display for '${land.name}' made visible to ${player.name} at ${position.blockX}, ${position.blockY}, ${position.blockZ}") + return controller + } + + private fun updateDisplayPosition(controller: DisplayController, player: Player, land: net.hareworks.hcu.lands.model.Land) { + val newPosition = calculateFaceAttachmentPoint(player, land) + val currentLoc = controller.display.location + + // Only update if position changed significantly (>0.5 blocks) + if (currentLoc.distanceSquared(newPosition) > 0.25) { + controller.applyEntityUpdate { display -> + display.teleport(newPosition) + } + } + } + + private fun calculateFaceAttachmentPoint(player: Player, land: net.hareworks.hcu.lands.model.Land): Location { + val bbox = land.getBoundingBox() + val px = player.location.x + val py = player.location.y + val pz = player.location.z + + // Check if player is within XZ footprint + val withinX = px >= bbox.minX && px <= bbox.maxX + 1.0 + val withinZ = pz >= bbox.minZ && pz <= bbox.maxZ + 1.0 + + if (withinX && withinZ) { + // Player is above or below the land → use top/bottom face + return if (py > bbox.maxY + 1.0) { + // Attach to top face + Location( + player.world, + px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0), + bbox.maxY + 1.1, + pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) + ) + } else { + // Attach to bottom face + Location( + player.world, + px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0), + bbox.minY - 0.1, + pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) + ) + } + } + + // Player is outside XZ footprint → use nearest vertical face + val clampedX = px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0) + val clampedY = py.coerceIn(bbox.minY.toDouble(), bbox.maxY + 1.0) + val clampedZ = pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) + + // Determine which face is nearest + val distToWest = abs(px - bbox.minX) + val distToEast = abs(px - (bbox.maxX + 1.0)) + val distToNorth = abs(pz - bbox.minZ) + val distToSouth = abs(pz - (bbox.maxZ + 1.0)) + + val minDist = minOf(distToWest, distToEast, distToNorth, distToSouth) + + return when (minDist) { + distToWest -> Location(player.world, bbox.minX - 0.1, clampedY, clampedZ) + distToEast -> Location(player.world, bbox.maxX + 1.1, clampedY, clampedZ) + distToNorth -> Location(player.world, clampedX, clampedY, bbox.minZ - 0.1) + else -> Location(player.world, clampedX, clampedY, bbox.maxZ + 1.1) + } + } + + fun cleanupDisplaysForPlayer(playerId: UUID) { + val count = displayControllers[playerId]?.size ?: 0 + displayControllers[playerId]?.values?.forEach { it.destroy() } + displayControllers.remove(playerId) + if (count > 0) { + val playerName = Bukkit.getPlayer(playerId)?.name ?: playerId.toString() + logger.info("[LandsDisplay] Cleaned up $count display(s) for player $playerName") + } + } }