feat: info
This commit is contained in:
parent
32e4641e24
commit
1c809b3def
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -2,3 +2,6 @@
|
||||||
path = hcu-core
|
path = hcu-core
|
||||||
url = git@gitea.hareworks.net:hcu/hcu-core.git
|
url = git@gitea.hareworks.net:hcu/hcu-core.git
|
||||||
branch = master
|
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")
|
||||||
includeBuild("hcu-core/kommand-lib")
|
includeBuild("hcu-core/kommand-lib")
|
||||||
includeBuild("hcu-core/kommand-lib/permits-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") {
|
literal("visualize") {
|
||||||
executes {
|
executes {
|
||||||
val player = sender as? Player ?: return@executes
|
val player = sender as? Player ?: return@executes
|
||||||
|
|
@ -426,7 +454,8 @@ class LandsCommand(
|
||||||
}
|
}
|
||||||
|
|
||||||
service.modifyLand(land.id) { data ->
|
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
|
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 actorId: Int,
|
||||||
val world: String,
|
val world: String,
|
||||||
val data: LandData
|
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
|
@Serializable
|
||||||
data class LandData(
|
data class LandData(
|
||||||
val parts: MutableList<Shape> = mutableListOf()
|
val parts: List<Shape> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -32,7 +121,7 @@ sealed class Shape {
|
||||||
fun minZ() = minOf(z1, z2)
|
fun minZ() = minOf(z1, z2)
|
||||||
fun maxZ() = maxOf(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
|
// Add 0.5 to block coordinates for center-based comparison
|
||||||
return x >= minX() + 0.5 && x <= maxX() + 0.5 &&
|
return x >= minX() + 0.5 && x <= maxX() + 0.5 &&
|
||||||
y >= minY() + 0.5 && y <= maxY() + 0.5 &&
|
y >= minY() + 0.5 && y <= maxY() + 0.5 &&
|
||||||
|
|
@ -47,6 +136,13 @@ sealed class Shape {
|
||||||
val maxCorner = Triple(maxX(), maxY(), maxZ())
|
val maxCorner = Triple(maxX(), maxY(), maxZ())
|
||||||
return minCorner to maxCorner
|
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
|
@Serializable
|
||||||
|
|
@ -57,23 +153,23 @@ sealed class Shape {
|
||||||
val bottomHeight: Int, // Height extending downward from center block
|
val bottomHeight: Int, // Height extending downward from center block
|
||||||
val topHeight: Int // Height extending upward from center block
|
val topHeight: Int // Height extending upward from center block
|
||||||
) : Shape() {
|
) : 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
|
// Add 0.5 to block coordinates for center-based comparison
|
||||||
val centerX = x + 0.5
|
val centerX = this.x + 0.5
|
||||||
val centerY = y + 0.5
|
val centerY = this.y + 0.5
|
||||||
val centerZ = z + 0.5
|
val centerZ = this.z + 0.5
|
||||||
|
|
||||||
// Total height includes center block (1) + bottomHeight + topHeight
|
// Total height includes center block (1) + bottomHeight + topHeight
|
||||||
val minY = centerY - bottomHeight
|
val minY = centerY - bottomHeight
|
||||||
val maxY = centerY + 1.0 + topHeight
|
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
|
// Add 0.5 to radius for block-aligned judgment
|
||||||
// This ensures that a radius of 4 includes all blocks within 4 blocks from center
|
// This ensures that a radius of 4 includes all blocks within 4 blocks from center
|
||||||
val actualRadius = radius + 0.5
|
val actualRadius = radius + 0.5
|
||||||
val dx = tx - centerX
|
val dx = x - centerX
|
||||||
val dz = tz - centerZ
|
val dz = z - centerZ
|
||||||
return (dx * dx + dz * dz) <= (actualRadius * actualRadius)
|
return (dx * dx + dz * dz) <= (actualRadius * actualRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,9 +300,55 @@ sealed class Shape {
|
||||||
|
|
||||||
return outline
|
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)
|
* Normalizes a coordinate to block integer (floor)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ class LandService(private val database: Database) {
|
||||||
return true
|
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 {
|
fun modifyLand(id: Int, modifier: (LandData) -> Unit): Boolean {
|
||||||
val land = cache[id] ?: return false
|
val land = cache[id] ?: return false
|
||||||
val newParts = ArrayList(land.data.parts)
|
val newParts = ArrayList(land.data.parts)
|
||||||
|
|
@ -83,6 +97,9 @@ class LandService(private val database: Database) {
|
||||||
val updatedLand = land.copy(data = newData)
|
val updatedLand = land.copy(data = newData)
|
||||||
cache[id] = updatedLand
|
cache[id] = updatedLand
|
||||||
|
|
||||||
|
// Invalidate volume cache
|
||||||
|
volumeCache.remove(id)
|
||||||
|
|
||||||
// Update spatial index
|
// Update spatial index
|
||||||
spatialIndex.updateLand(updatedLand)
|
spatialIndex.updateLand(updatedLand)
|
||||||
return true
|
return true
|
||||||
|
|
@ -97,6 +114,7 @@ class LandService(private val database: Database) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.remove(id)
|
cache.remove(id)
|
||||||
|
volumeCache.remove(id)
|
||||||
|
|
||||||
// Remove from spatial index
|
// Remove from spatial index
|
||||||
spatialIndex.removeLand(land)
|
spatialIndex.removeLand(land)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
nearbyLands.forEach { land ->
|
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,
|
// Further distance check could be added here if needed,
|
||||||
// but chunk-based filtering is already efficient enough for visualization logic
|
// but chunk-based filtering is already efficient enough for visualization logic
|
||||||
land.data.parts.forEach { shape ->
|
land.data.parts.forEach { shape ->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user