diff --git a/.gitmodules b/.gitmodules index 0f5eec0..6b79d95 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = hcu-core url = git@gitea.hareworks.net:hcu/hcu-core.git branch = master +[submodule "GhostDisplays"] + path = GhostDisplays + url = git@gitea.hareworks.net:Hare/GhostDisplays.git diff --git a/GhostDisplays b/GhostDisplays new file mode 160000 index 0000000..be85d83 --- /dev/null +++ b/GhostDisplays @@ -0,0 +1 @@ +Subproject commit be85d83428eff52a749747fc495c4bda0e82d035 diff --git a/settings.gradle.kts b/settings.gradle.kts index 548fb23..f8e8494 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,5 +2,5 @@ rootProject.name = "faction" includeBuild("hcu-core") includeBuild("hcu-core/kommand-lib") includeBuild("hcu-core/kommand-lib/permits-lib") - +includeBuild("GhostDisplays") diff --git a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt index 1e734f1..53e768b 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt @@ -384,6 +384,34 @@ class LandsCommand( } } } + + literal("info") { + string("name") { + executes { + val input: String = argument("name") + val land = resolveLand(sender, input) ?: return@executes + printLandInfo(sender, land, app) + } + } + } + + literal("info-here") { + executes { + val player = sender as? Player ?: return@executes + val service = app.landService + if (service == null) { + sender.sendMessage(Component.text("Service not ready.", NamedTextColor.RED)) + return@executes + } + + val land = service.getLandAt(player.world.name, player.location.x, player.location.y, player.location.z) + if (land == null) { + sender.sendMessage(Component.text("There is no land at your position.", NamedTextColor.RED)) + } else { + printLandInfo(sender, land, app) + } + } + } literal("visualize") { executes { @@ -426,7 +454,8 @@ class LandsCommand( } service.modifyLand(land.id) { data -> - action(data.parts) + @Suppress("UNCHECKED_CAST") + action(data.parts as MutableList) } } @@ -472,4 +501,30 @@ class LandsCommand( return null } + + private fun printLandInfo(sender: CommandSender, land: Land, app: App) { + sender.sendMessage(Component.text("----- Land Info -----", NamedTextColor.GOLD)) + sender.sendMessage(Component.text("Name: ", NamedTextColor.GRAY).append(Component.text(land.name, NamedTextColor.WHITE))) + sender.sendMessage(Component.text("ID: ", NamedTextColor.GRAY).append(Component.text(land.id, NamedTextColor.WHITE))) + sender.sendMessage(Component.text("Owner (Actor ID): ", NamedTextColor.GRAY).append(Component.text(land.actorId, NamedTextColor.WHITE))) + sender.sendMessage(Component.text("World: ", NamedTextColor.GRAY).append(Component.text(land.world, NamedTextColor.WHITE))) + + sender.sendMessage(Component.text("Calculating total volume...", NamedTextColor.GRAY)) + app.landService?.getAsyncVolume(land)?.thenAccept { totalVolume -> + sender.sendMessage(Component.text("Total Blocks (Exact): ", NamedTextColor.GRAY).append(Component.text(totalVolume, NamedTextColor.WHITE))) + }?.exceptionally { ex -> + sender.sendMessage(Component.text("Error calculating volume: ${ex.message}", NamedTextColor.RED)) + null + } + + sender.sendMessage(Component.text("Parts (${land.data.parts.size}):", NamedTextColor.GRAY)) + land.data.parts.forEachIndexed { index, shape -> + val description = when (shape) { + is Shape.Cuboid -> "Cuboid (Size: ${shape.volume()})" + is Shape.Cylinder -> "Cylinder (R=${shape.radius}, H=${shape.totalHeight()})" + } + sender.sendMessage(Component.text(" $index: $description", NamedTextColor.WHITE)) + } + sender.sendMessage(Component.text("---------------------", NamedTextColor.GOLD)) + } } 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 4557c55..3beecc3 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt @@ -10,11 +10,100 @@ data class Land( val actorId: Int, val world: String, val data: LandData -) +) { + fun totalVolume(): Long = data.parts.sumOf { it.volume() } + + fun calculateExactVolume(): Long { + if (data.parts.isEmpty()) return 0 + if (data.parts.size == 1) return data.parts[0].volume() + + // 1. Collect all Y boundaries to split into vertical slices + val yBoundaries = sortedSetOf() + data.parts.forEach { shape -> + when (shape) { + is Shape.Cuboid -> { + yBoundaries.add(shape.minY()) + yBoundaries.add(shape.maxY() + 1) + } + is Shape.Cylinder -> { + yBoundaries.add(shape.y - shape.bottomHeight) + yBoundaries.add(shape.y + shape.topHeight + 1) + } + } + } + + var totalVolume = 0L + val yList = yBoundaries.toList() + + // 2. Iterate through Y intervals (slices) + for (i in 0 until yList.size - 1) { + val yStart = yList[i] + val yEnd = yList[i+1] // Exclusive + val height = (yEnd - yStart).toLong() + + // Use generating a midpoint Y for checking containment + // (Actually just yStart + 0.5 is sufficient as shapes are block-aligned) + val checkY = yStart.toDouble() + 0.5 + + // Filter shapes active in this Y slice + val activeShapes = data.parts.filter { shape -> + when (shape) { + is Shape.Cuboid -> checkY >= shape.minY() && checkY <= shape.maxY() + 1.0 + is Shape.Cylinder -> checkY >= (shape.y - shape.bottomHeight) && checkY <= (shape.y + shape.topHeight + 1.0) + } + } + + if (activeShapes.isEmpty()) continue + + // 3. Scan XZ plane for this interval + // Determine bounding box for active shapes to limit scan area + var minX = Int.MAX_VALUE + var minZ = Int.MAX_VALUE + var maxX = Int.MIN_VALUE + var maxZ = Int.MIN_VALUE + + activeShapes.forEach { shape -> + when (shape) { + is Shape.Cuboid -> { + minX = minOf(minX, shape.minX()) + maxX = maxOf(maxX, shape.maxX()) + 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) + minZ = minOf(minZ, shape.z - r) + maxZ = maxOf(maxZ, shape.z + r) + } + } + } + + var area = 0L + // Iterate all blocks in the bounding box of this slice + for (x in minX..maxX) { + for (z in minZ..maxZ) { + val cx = x + 0.5 + val cz = z + 0.5 + + // If any shape contains this block, it contributes to area + // We pass checkY which is valid for all activeShapes + if (activeShapes.any { it.contains(cx, checkY, cz) }) { + area++ + } + } + } + totalVolume += area * height + } + + return totalVolume + } +} @Serializable data class LandData( - val parts: MutableList = mutableListOf() + val parts: List = emptyList() ) @Serializable @@ -32,7 +121,7 @@ sealed class Shape { fun minZ() = minOf(z1, z2) fun maxZ() = maxOf(z1, z2) - fun contains(x: Double, y: Double, z: Double): Boolean { + override fun contains(x: Double, y: Double, z: Double): Boolean { // Add 0.5 to block coordinates for center-based comparison return x >= minX() + 0.5 && x <= maxX() + 0.5 && y >= minY() + 0.5 && y <= maxY() + 0.5 && @@ -47,6 +136,13 @@ sealed class Shape { val maxCorner = Triple(maxX(), maxY(), maxZ()) return minCorner to maxCorner } + + override fun volume(): Long { + val width = (maxX() - minX() + 1).toLong() + val height = (maxY() - minY() + 1).toLong() + val depth = (maxZ() - minZ() + 1).toLong() + return width * height * depth + } } @Serializable @@ -57,23 +153,23 @@ sealed class Shape { val bottomHeight: Int, // Height extending downward from center block val topHeight: Int // Height extending upward from center block ) : Shape() { - fun contains(tx: Double, ty: Double, tz: Double): Boolean { + override fun contains(x: Double, y: Double, z: Double): Boolean { // Add 0.5 to block coordinates for center-based comparison - val centerX = x + 0.5 - val centerY = y + 0.5 - val centerZ = z + 0.5 + val centerX = this.x + 0.5 + val centerY = this.y + 0.5 + val centerZ = this.z + 0.5 // Total height includes center block (1) + bottomHeight + topHeight val minY = centerY - bottomHeight val maxY = centerY + 1.0 + topHeight - if (ty < minY || ty > maxY) return false + if (y < minY || y > maxY) return false // Add 0.5 to radius for block-aligned judgment // This ensures that a radius of 4 includes all blocks within 4 blocks from center val actualRadius = radius + 0.5 - val dx = tx - centerX - val dz = tz - centerZ + val dx = x - centerX + val dz = z - centerZ return (dx * dx + dz * dz) <= (actualRadius * actualRadius) } @@ -204,7 +300,53 @@ sealed class Shape { return outline } + + override fun volume(): Long { + // Optimized O(R) calculation using quadrant symmetry + // We calculate one quadrant (x > 0, z > 0) and multiply by 4. + // Then we add the axes (x=0 line and z=0 line) separately. + + val actualRadius = radius + 0.5 + val radiusSq = actualRadius * actualRadius + val rInt = actualRadius.toInt() + 1 + + var quadrantBlocks = 0L + + // Loop for x from 1 to rInt (Positive X) + for (dx in 1..rInt) { + val remainingSq = radiusSq - (dx * dx) + if (remainingSq < 0) break // Should not happen with loop bounds, but safe + + // Max integer dz for this dx such that dx^2 + dz^2 <= r^2 + // We only count z > 0 here + val maxDz = kotlin.math.floor(kotlin.math.sqrt(remainingSq)).toLong() + + if (maxDz > 0) { + quadrantBlocks += maxDz + } + } + + // Blocks on axes (including center) + // Center row (z=0): covers x from -maxX to +maxX + // Center col (x=0): covers z from -maxZ to +maxZ (excluding center to avoid double count? No let's do it cleanly) + + // Let's count clearer: + // 1. Center block (0,0): 1 + // 2. Axes (excluding center): + // Length of radius along axis (excluding center) is simply floor(actualRadius) + // So 4 arms of length floor(actualRadius) + val rFloor = kotlin.math.floor(actualRadius).toLong() + val axisBlocks = 1 + (4 * rFloor) + + // Total base area = (Quadrant * 4) + Axes + val baseArea = (quadrantBlocks * 4) + axisBlocks + + return baseArea * totalHeight() + } } + + abstract fun contains(x: Double, y: Double, z: Double): Boolean + abstract fun volume(): Long } /** diff --git a/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt b/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt index 110f14d..31af859 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt @@ -67,6 +67,20 @@ class LandService(private val database: Database) { return true } + private val volumeCache = ConcurrentHashMap() + + fun getAsyncVolume(land: Land): java.util.concurrent.CompletableFuture { + if (volumeCache.containsKey(land.id)) { + return java.util.concurrent.CompletableFuture.completedFuture(volumeCache[land.id]) + } + + return java.util.concurrent.CompletableFuture.supplyAsync { + val vol = land.calculateExactVolume() + volumeCache[land.id] = vol + vol + } + } + fun modifyLand(id: Int, modifier: (LandData) -> Unit): Boolean { val land = cache[id] ?: return false val newParts = ArrayList(land.data.parts) @@ -83,6 +97,9 @@ class LandService(private val database: Database) { val updatedLand = land.copy(data = newData) cache[id] = updatedLand + // Invalidate volume cache + volumeCache.remove(id) + // Update spatial index spatialIndex.updateLand(updatedLand) return true @@ -97,6 +114,7 @@ class LandService(private val database: Database) { } cache.remove(id) + volumeCache.remove(id) // Remove from spatial index spatialIndex.removeLand(land) 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 66e7a0f..93fbb3f 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt @@ -44,6 +44,12 @@ class VisualizerTask(private val landService: LandService) : Runnable { } nearbyLands.forEach { land -> + // Verify that this is the current version of the land + val currentLand = landService.getLand(land.id) + if (currentLand == null || currentLand != land) { + return@forEach + } + // Further distance check could be added here if needed, // but chunk-based filtering is already efficient enough for visualization logic land.data.parts.forEach { shape ->