feat: info
This commit is contained in:
parent
32e4641e24
commit
1c809b3def
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
GhostDisplays
Submodule
1
GhostDisplays
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit be85d83428eff52a749747fc495c4bda0e82d035
|
||||
|
|
@ -2,5 +2,5 @@ rootProject.name = "faction"
|
|||
includeBuild("hcu-core")
|
||||
includeBuild("hcu-core/kommand-lib")
|
||||
includeBuild("hcu-core/kommand-lib/permits-lib")
|
||||
|
||||
includeBuild("GhostDisplays")
|
||||
|
||||
|
|
|
|||
|
|
@ -385,6 +385,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 {
|
||||
val player = sender as? Player ?: return@executes
|
||||
|
|
@ -426,7 +454,8 @@ class LandsCommand(
|
|||
}
|
||||
|
||||
service.modifyLand(land.id) { data ->
|
||||
action(data.parts)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
action(data.parts as MutableList<Shape>)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int>()
|
||||
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<Shape> = mutableListOf()
|
||||
val parts: List<Shape> = 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,9 +300,55 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a coordinate to block integer (floor)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -67,6 +67,20 @@ class LandService(private val database: Database) {
|
|||
return true
|
||||
}
|
||||
|
||||
private val volumeCache = ConcurrentHashMap<Int, Long>()
|
||||
|
||||
fun getAsyncVolume(land: Land): java.util.concurrent.CompletableFuture<Long> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user