feat: 最適化とアウトライン表示
This commit is contained in:
parent
0ed06e789f
commit
576005aada
|
|
@ -21,16 +21,7 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
override fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||||
return landService.getAllLands()
|
return landService.getLandAt(world, x, y, z)
|
||||||
.filter { it.world == world }
|
|
||||||
.firstOrNull { land ->
|
|
||||||
land.data.parts.any { shape ->
|
|
||||||
when (shape) {
|
|
||||||
is net.hareworks.hcu.lands.model.Shape.Cuboid -> shape.contains(x, y, z)
|
|
||||||
is net.hareworks.hcu.lands.model.Shape.Cylinder -> shape.contains(x, y, z)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLand(name: String): Land? {
|
override fun getLand(name: String): Land? {
|
||||||
|
|
|
||||||
198
src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt
Normal file
198
src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
package net.hareworks.hcu.lands.index
|
||||||
|
|
||||||
|
import net.hareworks.hcu.lands.model.Land
|
||||||
|
import net.hareworks.hcu.lands.model.Shape
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk-based spatial index for fast land lookups.
|
||||||
|
*
|
||||||
|
* This index maintains a mapping from chunk coordinates to lands that overlap those chunks.
|
||||||
|
* This allows for O(1) chunk lookup followed by O(k) land checks where k is the number of
|
||||||
|
* lands in that chunk (typically 1-10).
|
||||||
|
*/
|
||||||
|
class LandIndex {
|
||||||
|
|
||||||
|
// Chunk -> Set of lands that overlap this chunk
|
||||||
|
private val chunkIndex = ConcurrentHashMap<ChunkKey, MutableSet<Land>>()
|
||||||
|
|
||||||
|
// Land -> its bounding box (cached)
|
||||||
|
private val boundingBoxCache = ConcurrentHashMap<String, BoundingBox>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the entire index from a collection of lands.
|
||||||
|
* This should be called when the plugin initializes or when bulk changes occur.
|
||||||
|
*/
|
||||||
|
fun rebuild(lands: Collection<Land>) {
|
||||||
|
chunkIndex.clear()
|
||||||
|
boundingBoxCache.clear()
|
||||||
|
|
||||||
|
for (land in lands) {
|
||||||
|
addLand(land)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a land to the index.
|
||||||
|
*/
|
||||||
|
fun addLand(land: Land) {
|
||||||
|
val bbox = calculateBoundingBox(land)
|
||||||
|
boundingBoxCache[land.name] = bbox
|
||||||
|
|
||||||
|
val chunks = bbox.getAffectedChunks(land.world)
|
||||||
|
for (chunk in chunks) {
|
||||||
|
chunkIndex.getOrPut(chunk) { ConcurrentHashMap.newKeySet() }.add(land)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a land from the index.
|
||||||
|
*/
|
||||||
|
fun removeLand(land: Land) {
|
||||||
|
val bbox = boundingBoxCache.remove(land.name) ?: return
|
||||||
|
|
||||||
|
val chunks = bbox.getAffectedChunks(land.world)
|
||||||
|
for (chunk in chunks) {
|
||||||
|
// Use removeIf to ensure we remove the land even if its data has changed
|
||||||
|
// (since equals/hashCode would be different for modified data classes)
|
||||||
|
chunkIndex[chunk]?.removeIf { it.name == land.name }
|
||||||
|
|
||||||
|
if (chunkIndex[chunk]?.isEmpty() == true) {
|
||||||
|
chunkIndex.remove(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a land in the index (removes old, adds new).
|
||||||
|
*/
|
||||||
|
fun updateLand(land: Land) {
|
||||||
|
removeLand(land)
|
||||||
|
addLand(land)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the land at the given coordinates.
|
||||||
|
*
|
||||||
|
* @return The land containing the coordinates, or null if none found
|
||||||
|
*/
|
||||||
|
fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||||
|
val chunkKey = ChunkKey.fromCoords(world, x, z)
|
||||||
|
val candidates = chunkIndex[chunkKey] ?: return null
|
||||||
|
|
||||||
|
// Check each candidate land in this chunk
|
||||||
|
for (land in candidates) {
|
||||||
|
// Early exit: check bounding box first
|
||||||
|
val bbox = boundingBoxCache[land.name]
|
||||||
|
if (bbox != null && !bbox.contains(x, y, z)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed shape check
|
||||||
|
if (land.data.parts.any { shape -> checkShapeContains(shape, x, y, z) }) {
|
||||||
|
return land
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all lands that overlap with the given chunk.
|
||||||
|
*/
|
||||||
|
fun getLandsInChunk(world: String, chunkX: Int, chunkZ: Int): Set<Land> {
|
||||||
|
val chunkKey = ChunkKey(world, chunkX, chunkZ)
|
||||||
|
return chunkIndex[chunkKey]?.toSet() ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all lands in the given world.
|
||||||
|
*/
|
||||||
|
fun getLandsInWorld(world: String): Set<Land> {
|
||||||
|
return chunkIndex.entries
|
||||||
|
.filter { it.key.world == world }
|
||||||
|
.flatMap { it.value }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bounding box for a land.
|
||||||
|
*/
|
||||||
|
private fun calculateBoundingBox(land: Land): BoundingBox {
|
||||||
|
if (land.data.parts.isEmpty()) {
|
||||||
|
// Empty land, use center block as bounding box
|
||||||
|
return 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
|
||||||
|
|
||||||
|
for (shape in land.data.parts) {
|
||||||
|
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 -> {
|
||||||
|
// Use actual radius (radius + 0.5) for bounding box
|
||||||
|
val actualRadius = shape.radius + 0.5
|
||||||
|
val r = actualRadius.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 BoundingBox(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a shape contains the given coordinates.
|
||||||
|
*/
|
||||||
|
private fun checkShapeContains(shape: Shape, x: Double, y: Double, z: Double): Boolean {
|
||||||
|
return when (shape) {
|
||||||
|
is Shape.Cuboid -> shape.contains(x, y, z)
|
||||||
|
is Shape.Cylinder -> shape.contains(x, y, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets statistics about the index.
|
||||||
|
*/
|
||||||
|
fun getStats(): IndexStats {
|
||||||
|
val totalChunks = chunkIndex.size
|
||||||
|
val totalLands = boundingBoxCache.size
|
||||||
|
val avgLandsPerChunk = if (totalChunks > 0) {
|
||||||
|
chunkIndex.values.sumOf { it.size }.toDouble() / totalChunks
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return IndexStats(
|
||||||
|
totalChunks = totalChunks,
|
||||||
|
totalLands = totalLands,
|
||||||
|
avgLandsPerChunk = avgLandsPerChunk
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics about the land index.
|
||||||
|
*/
|
||||||
|
data class IndexStats(
|
||||||
|
val totalChunks: Int,
|
||||||
|
val totalLands: Int,
|
||||||
|
val avgLandsPerChunk: Double
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package net.hareworks.hcu.lands.index
|
||||||
|
|
||||||
|
import net.hareworks.hcu.lands.model.Land
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a chunk coordinate key for indexing.
|
||||||
|
*/
|
||||||
|
data class ChunkKey(
|
||||||
|
val world: String,
|
||||||
|
val chunkX: Int,
|
||||||
|
val chunkZ: Int
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates a ChunkKey from block coordinates.
|
||||||
|
*/
|
||||||
|
fun fromBlockCoords(world: String, blockX: Int, blockZ: Int): ChunkKey {
|
||||||
|
return ChunkKey(world, blockX shr 4, blockZ shr 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ChunkKey from double coordinates.
|
||||||
|
*/
|
||||||
|
fun fromCoords(world: String, x: Double, z: Double): ChunkKey {
|
||||||
|
return ChunkKey(world, x.toInt() shr 4, z.toInt() shr 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a 3D bounding box for spatial queries.
|
||||||
|
*/
|
||||||
|
data class BoundingBox(
|
||||||
|
val minX: Int,
|
||||||
|
val minY: Int,
|
||||||
|
val minZ: Int,
|
||||||
|
val maxX: Int,
|
||||||
|
val maxY: Int,
|
||||||
|
val maxZ: Int
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Checks if this bounding box contains the given coordinates.
|
||||||
|
*/
|
||||||
|
fun contains(x: Double, y: Double, z: Double): Boolean {
|
||||||
|
return x >= minX && x <= maxX + 1.0 &&
|
||||||
|
y >= minY && y <= maxY + 1.0 &&
|
||||||
|
z >= minZ && z <= maxZ + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all chunk keys that this bounding box overlaps.
|
||||||
|
*/
|
||||||
|
fun getAffectedChunks(world: String): Set<ChunkKey> {
|
||||||
|
val chunks = mutableSetOf<ChunkKey>()
|
||||||
|
val minChunkX = minX shr 4
|
||||||
|
val maxChunkX = maxX shr 4
|
||||||
|
val minChunkZ = minZ shr 4
|
||||||
|
val maxChunkZ = maxZ shr 4
|
||||||
|
|
||||||
|
for (chunkX in minChunkX..maxChunkX) {
|
||||||
|
for (chunkZ in minChunkZ..maxChunkZ) {
|
||||||
|
chunks.add(ChunkKey(world, chunkX, chunkZ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,9 +67,13 @@ sealed class Shape {
|
||||||
val maxY = centerY + 1.0 + topHeight
|
val maxY = centerY + 1.0 + topHeight
|
||||||
|
|
||||||
if (ty < minY || ty > maxY) return false
|
if (ty < minY || ty > 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 dx = tx - centerX
|
||||||
val dz = tz - centerZ
|
val dz = tz - centerZ
|
||||||
return (dx * dx + dz * dz) <= (radius * radius)
|
return (dx * dx + dz * dz) <= (actualRadius * actualRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,13 +96,16 @@ sealed class Shape {
|
||||||
val outline = mutableSetOf<Triple<Int, Int, Int>>()
|
val outline = mutableSetOf<Triple<Int, Int, Int>>()
|
||||||
val centerX = x + 0.5
|
val centerX = x + 0.5
|
||||||
val centerZ = z + 0.5
|
val centerZ = z + 0.5
|
||||||
val radiusSquared = radius * radius
|
|
||||||
|
// Use actual radius (radius + 0.5) for block-aligned judgment
|
||||||
|
val actualRadius = radius + 0.5
|
||||||
|
val radiusSquared = actualRadius * actualRadius
|
||||||
|
|
||||||
// Calculate the bounding box for the cylinder
|
// Calculate the bounding box for the cylinder
|
||||||
val minBlockX = (x - radius - 1).toInt()
|
val minBlockX = (x - actualRadius - 1).toInt()
|
||||||
val maxBlockX = (x + radius + 1).toInt()
|
val maxBlockX = (x + actualRadius + 1).toInt()
|
||||||
val minBlockZ = (z - radius - 1).toInt()
|
val minBlockZ = (z - actualRadius - 1).toInt()
|
||||||
val maxBlockZ = (z + radius + 1).toInt()
|
val maxBlockZ = (z + actualRadius + 1).toInt()
|
||||||
|
|
||||||
// For each Y level in the cylinder
|
// For each Y level in the cylinder
|
||||||
val minY = y - bottomHeight
|
val minY = y - bottomHeight
|
||||||
|
|
@ -108,10 +115,8 @@ sealed class Shape {
|
||||||
// Check each block in the XZ plane
|
// Check each block in the XZ plane
|
||||||
for (blockX in minBlockX..maxBlockX) {
|
for (blockX in minBlockX..maxBlockX) {
|
||||||
for (blockZ in minBlockZ..maxBlockZ) {
|
for (blockZ in minBlockZ..maxBlockZ) {
|
||||||
// Check if this block is inside the cylinder
|
// Check if this block center is inside the cylinder
|
||||||
val dx = (blockX + 0.5) - centerX
|
val isInside = contains(blockX + 0.5, blockY + 0.5, blockZ + 0.5)
|
||||||
val dz = (blockZ + 0.5) - centerZ
|
|
||||||
val isInside = (dx * dx + dz * dz) <= radiusSquared
|
|
||||||
|
|
||||||
if (isInside) {
|
if (isInside) {
|
||||||
// Check if any adjacent block is outside (making this an edge block)
|
// Check if any adjacent block is outside (making this an edge block)
|
||||||
|
|
@ -121,9 +126,7 @@ sealed class Shape {
|
||||||
Pair(blockX, blockZ - 1),
|
Pair(blockX, blockZ - 1),
|
||||||
Pair(blockX, blockZ + 1)
|
Pair(blockX, blockZ + 1)
|
||||||
).any { (nx, nz) ->
|
).any { (nx, nz) ->
|
||||||
val ndx = (nx + 0.5) - centerX
|
!contains(nx + 0.5, blockY + 0.5, nz + 0.5)
|
||||||
val ndz = (nz + 0.5) - centerZ
|
|
||||||
(ndx * ndx + ndz * ndz) > radiusSquared
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOutsideNeighbor) {
|
if (hasOutsideNeighbor) {
|
||||||
|
|
@ -136,6 +139,70 @@ sealed class Shape {
|
||||||
|
|
||||||
return outline
|
return outline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a set of blocks that form the outline of the blockified cylinder.
|
||||||
|
* Optimized algorithm: O(R) instead of O(R^2).
|
||||||
|
* Calculates boundary blocks by scanning X/Z axes instead of checking all blocks area.
|
||||||
|
*/
|
||||||
|
fun getOptimizedFaceOutline(): Set<Pair<Int, Int>> {
|
||||||
|
val outline = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
val actualRadius = radius + 0.5
|
||||||
|
val radiusSq = actualRadius * actualRadius
|
||||||
|
val rInt = actualRadius.toInt() + 1
|
||||||
|
|
||||||
|
// 1. Scan X axis to find Z boundaries (Top/Bottom edges)
|
||||||
|
for (dx in -rInt..rInt) {
|
||||||
|
val bx = x + dx
|
||||||
|
// Calculate distance from center X
|
||||||
|
// Block center is at bx + 0.5. Relative to cylinder center (x + 0.5)
|
||||||
|
// dist = (bx + 0.5) - (x + 0.5) = bx - x = dx
|
||||||
|
// Wait, logic check:
|
||||||
|
// Cylinder center: cx = x + 0.5
|
||||||
|
// Block center: bcx = bx + 0.5
|
||||||
|
// diff = bcx - cx = bx - x = dx
|
||||||
|
// So (dx)^2 + (dz)^2 <= R^2
|
||||||
|
// We need to verify if this dx is valid geometrically with +0.5 offset considerations handled by contains()
|
||||||
|
// Let's rely on calculation consistent with contains():
|
||||||
|
// (bx + 0.5 - (x + 0.5))^2 + ... = dx^2 + dz^2
|
||||||
|
|
||||||
|
// Correction: The contains() logic uses:
|
||||||
|
// val dx = tx - centerX
|
||||||
|
// tx is block coord + 0.5, centerX is x + 0.5.
|
||||||
|
// So dx = (bx + 0.5) - (x + 0.5) = bx - x.
|
||||||
|
// It works simply with integers.
|
||||||
|
|
||||||
|
val term = radiusSq - dx * dx
|
||||||
|
if (term < 0) continue
|
||||||
|
|
||||||
|
val maxDistZ = kotlin.math.sqrt(term)
|
||||||
|
// We want largest integer dz such that dz^2 <= term
|
||||||
|
// Since dz is integer difference (bz - z), dz can be simply floor(maxDistZ) and ceil(-maxDistZ)
|
||||||
|
// Let's verify: if dz = 4.5, integer dz can be 4.
|
||||||
|
|
||||||
|
val maxDz = kotlin.math.floor(maxDistZ).toInt()
|
||||||
|
val minDz = -maxDz // Symmetry
|
||||||
|
|
||||||
|
outline.add(Pair(bx, z + maxDz))
|
||||||
|
outline.add(Pair(bx, z + minDz))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Scan Z axis to find X boundaries (Left/Right edges)
|
||||||
|
for (dz in -rInt..rInt) {
|
||||||
|
val term = radiusSq - dz * dz
|
||||||
|
if (term < 0) continue
|
||||||
|
|
||||||
|
val maxDistX = kotlin.math.sqrt(term)
|
||||||
|
val maxDx = kotlin.math.floor(maxDistX).toInt()
|
||||||
|
val minDx = -maxDx
|
||||||
|
|
||||||
|
val bz = z + dz
|
||||||
|
outline.add(Pair(x + maxDx, bz))
|
||||||
|
outline.add(Pair(x + minDx, bz))
|
||||||
|
}
|
||||||
|
|
||||||
|
return outline
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package net.hareworks.hcu.lands.service
|
package net.hareworks.hcu.lands.service
|
||||||
|
|
||||||
import net.hareworks.hcu.lands.database.LandsTable
|
import net.hareworks.hcu.lands.database.LandsTable
|
||||||
|
import net.hareworks.hcu.lands.index.LandIndex
|
||||||
import net.hareworks.hcu.lands.model.Land
|
import net.hareworks.hcu.lands.model.Land
|
||||||
import net.hareworks.hcu.lands.model.LandData
|
import net.hareworks.hcu.lands.model.LandData
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
|
@ -16,6 +17,7 @@ import org.bukkit.Bukkit
|
||||||
|
|
||||||
class LandService(private val database: Database) {
|
class LandService(private val database: Database) {
|
||||||
private val cache = ConcurrentHashMap<String, Land>()
|
private val cache = ConcurrentHashMap<String, Land>()
|
||||||
|
private val spatialIndex = LandIndex()
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
|
|
@ -35,6 +37,9 @@ class LandService(private val database: Database) {
|
||||||
)
|
)
|
||||||
cache[land.name] = land
|
cache[land.name] = land
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rebuild spatial index
|
||||||
|
spatialIndex.rebuild(cache.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createLand(name: String, actorId: Int, world: String): Boolean {
|
fun createLand(name: String, actorId: Int, world: String): Boolean {
|
||||||
|
|
@ -48,8 +53,10 @@ class LandService(private val database: Database) {
|
||||||
it[LandsTable.data] = LandData()
|
it[LandsTable.data] = LandData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update cache efficiently
|
// Update cache and index
|
||||||
cache[name] = Land(name, actorId, world, LandData())
|
val land = Land(name, actorId, world, LandData())
|
||||||
|
cache[name] = land
|
||||||
|
spatialIndex.addLand(land)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,20 +72,52 @@ class LandService(private val database: Database) {
|
||||||
it[LandsTable.data] = newData
|
it[LandsTable.data] = newData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache[name] = land.copy(data = newData)
|
|
||||||
|
val updatedLand = land.copy(data = newData)
|
||||||
|
cache[name] = updatedLand
|
||||||
|
|
||||||
|
// Update spatial index
|
||||||
|
spatialIndex.updateLand(updatedLand)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLand(name: String): Boolean {
|
fun deleteLand(name: String): Boolean {
|
||||||
if (!cache.containsKey(name)) return false
|
if (!cache.containsKey(name)) return false
|
||||||
|
val land = cache[name]!!
|
||||||
|
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
LandsTable.deleteWhere { LandsTable.name eq name }
|
LandsTable.deleteWhere { LandsTable.name eq name }
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.remove(name)
|
cache.remove(name)
|
||||||
|
|
||||||
|
// Remove from spatial index
|
||||||
|
spatialIndex.removeLand(land)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLand(name: String): Land? = cache[name]
|
fun getLand(name: String): Land? = cache[name]
|
||||||
|
|
||||||
fun getAllLands(): Collection<Land> = cache.values
|
fun getAllLands(): Collection<Land> = cache.values
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the land at the specified coordinates using the spatial index.
|
||||||
|
* This is much faster than iterating through all lands.
|
||||||
|
*/
|
||||||
|
fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||||
|
return spatialIndex.getLandAt(world, x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all lands in the specified chunk.
|
||||||
|
*/
|
||||||
|
fun getLandsInChunk(world: String, chunkX: Int, chunkZ: Int): Set<Land> {
|
||||||
|
return spatialIndex.getLandsInChunk(world, chunkX, chunkZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets statistics about the spatial index.
|
||||||
|
*/
|
||||||
|
fun getIndexStats() = spatialIndex.getStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,24 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
Bukkit.getOnlinePlayers().forEach { p ->
|
Bukkit.getOnlinePlayers().forEach { p ->
|
||||||
if (LandsVisualizer.isEnabled(p)) {
|
if (LandsVisualizer.isEnabled(p)) {
|
||||||
landService.getAllLands().forEach { land ->
|
val pChunkX = p.location.blockX shr 4
|
||||||
if (land.world == p.world.name) {
|
val pChunkZ = p.location.blockZ shr 4
|
||||||
// Optimization: Check distance to Land center/bounds?
|
val radiusInChunks = 2 // 32 blocks radius approx
|
||||||
// For now basic iteration
|
|
||||||
land.data.parts.forEach { shape ->
|
// Collect lands from nearby chunks using spatial index
|
||||||
drawShape(p, shape)
|
val nearbyLands = mutableSetOf<net.hareworks.hcu.lands.model.Land>()
|
||||||
}
|
|
||||||
|
for (cx in (pChunkX - radiusInChunks)..(pChunkX + radiusInChunks)) {
|
||||||
|
for (cz in (pChunkZ - radiusInChunks)..(pChunkZ + radiusInChunks)) {
|
||||||
|
nearbyLands.addAll(landService.getLandsInChunk(p.world.name, cx, cz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nearbyLands.forEach { land ->
|
||||||
|
// 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 ->
|
||||||
|
drawShape(p, shape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +112,9 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
val centerX = cylinder.x + 0.5
|
val centerX = cylinder.x + 0.5
|
||||||
val centerZ = cylinder.z + 0.5
|
val centerZ = cylinder.z + 0.5
|
||||||
|
|
||||||
|
// Use actual radius (radius + 0.5) for visualization
|
||||||
|
val actualRadius = cylinder.radius + 0.5
|
||||||
|
|
||||||
// Y coordinates use block boundaries (no +0.5 offset)
|
// Y coordinates use block boundaries (no +0.5 offset)
|
||||||
val bottomY = cylinder.y.toDouble() - cylinder.bottomHeight
|
val bottomY = cylinder.y.toDouble() - cylinder.bottomHeight
|
||||||
val topY = cylinder.y.toDouble() + 1.0 + cylinder.topHeight
|
val topY = cylinder.y.toDouble() + 1.0 + cylinder.topHeight
|
||||||
|
|
@ -113,10 +127,10 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
for (i in 0 until segments) {
|
for (i in 0 until segments) {
|
||||||
val angle1 = i * step
|
val angle1 = i * step
|
||||||
val angle2 = (i + 1) * step
|
val angle2 = (i + 1) * step
|
||||||
val x1 = centerX + cos(angle1) * cylinder.radius
|
val x1 = centerX + cos(angle1) * actualRadius
|
||||||
val z1 = centerZ + sin(angle1) * cylinder.radius
|
val z1 = centerZ + sin(angle1) * actualRadius
|
||||||
val x2 = centerX + cos(angle2) * cylinder.radius
|
val x2 = centerX + cos(angle2) * actualRadius
|
||||||
val z2 = centerZ + sin(angle2) * cylinder.radius
|
val z2 = centerZ + sin(angle2) * actualRadius
|
||||||
|
|
||||||
drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25)
|
drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25)
|
||||||
}
|
}
|
||||||
|
|
@ -125,10 +139,10 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
for (i in 0 until segments) {
|
for (i in 0 until segments) {
|
||||||
val angle1 = i * step
|
val angle1 = i * step
|
||||||
val angle2 = (i + 1) * step
|
val angle2 = (i + 1) * step
|
||||||
val x1 = centerX + cos(angle1) * cylinder.radius
|
val x1 = centerX + cos(angle1) * actualRadius
|
||||||
val z1 = centerZ + sin(angle1) * cylinder.radius
|
val z1 = centerZ + sin(angle1) * actualRadius
|
||||||
val x2 = centerX + cos(angle2) * cylinder.radius
|
val x2 = centerX + cos(angle2) * actualRadius
|
||||||
val z2 = centerZ + sin(angle2) * cylinder.radius
|
val z2 = centerZ + sin(angle2) * actualRadius
|
||||||
|
|
||||||
drawLine(player, x1, topY, z1, x2, topY, z2, edgeColor, 0.25)
|
drawLine(player, x1, topY, z1, x2, topY, z2, edgeColor, 0.25)
|
||||||
}
|
}
|
||||||
|
|
@ -137,19 +151,69 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
||||||
val verticalSegments = 8
|
val verticalSegments = 8
|
||||||
for (i in 0 until verticalSegments) {
|
for (i in 0 until verticalSegments) {
|
||||||
val angle = i * (Math.PI * 2) / verticalSegments
|
val angle = i * (Math.PI * 2) / verticalSegments
|
||||||
val x = centerX + cos(angle) * cylinder.radius
|
val x = centerX + cos(angle) * actualRadius
|
||||||
val z = centerZ + sin(angle) * cylinder.radius
|
val z = centerZ + sin(angle) * actualRadius
|
||||||
|
|
||||||
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
|
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw blockified outline
|
// Draw top face outline in red (outer edges only)
|
||||||
val blockifiedOutline = cylinder.getBlockifiedOutline()
|
// Optimized: Calculate outline once for both faces as they share the same XZ shape
|
||||||
val blockColor = Particle.DustOptions(Color.fromRGB(150, 220, 255), 0.5f)
|
val faceOutline = cylinder.getOptimizedFaceOutline()
|
||||||
|
val redColor = Particle.DustOptions(Color.RED, 1.0f)
|
||||||
|
val topFaceY = cylinder.y + cylinder.topHeight
|
||||||
|
|
||||||
for ((bx, by, bz) in blockifiedOutline) {
|
drawFaceOutline(player, faceOutline, topFaceY + 1.0, redColor, cylinder)
|
||||||
// Draw a subtle outline for each block in the blockified cylinder
|
|
||||||
drawBlockOutline(player, bx, by, bz, Color.fromRGB(150, 220, 255))
|
// Draw bottom face outline in red (outer edges only)
|
||||||
|
val bottomFaceY = cylinder.y - cylinder.bottomHeight
|
||||||
|
|
||||||
|
drawFaceOutline(player, faceOutline, bottomFaceY.toDouble(), redColor, cylinder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws only the outer edges of a face outline (excluding inner holes).
|
||||||
|
*/
|
||||||
|
private fun drawFaceOutline(
|
||||||
|
player: Player,
|
||||||
|
faceBlocks: Set<Pair<Int, Int>>,
|
||||||
|
y: Double,
|
||||||
|
dustOptions: Particle.DustOptions,
|
||||||
|
cylinder: Shape.Cylinder
|
||||||
|
) {
|
||||||
|
// Use the cylinder's vertical center for the 'contains' check to ensure we are within Y bounds
|
||||||
|
// This effectively checks if the neighbor is outside the radius laterally
|
||||||
|
val checkY = cylinder.y + 0.5
|
||||||
|
|
||||||
|
// For each block in the face, check which edges face outward (not toward inner holes)
|
||||||
|
for ((bx, bz) in faceBlocks) {
|
||||||
|
// Check each of the 4 edges
|
||||||
|
// An edge should be drawn if the adjacent block is NOT in the face
|
||||||
|
// AND is outside the cylinder (not an inner hole)
|
||||||
|
|
||||||
|
// Top edge (z- direction)
|
||||||
|
val topNeighbor = Pair(bx, bz - 1)
|
||||||
|
if (!faceBlocks.contains(topNeighbor) && !cylinder.contains(topNeighbor.first + 0.5, checkY, topNeighbor.second + 0.5)) {
|
||||||
|
drawLine(player, bx.toDouble(), y, bz.toDouble(), bx + 1.0, y, bz.toDouble(), dustOptions, 0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right edge (x+ direction)
|
||||||
|
val rightNeighbor = Pair(bx + 1, bz)
|
||||||
|
if (!faceBlocks.contains(rightNeighbor) && !cylinder.contains(rightNeighbor.first + 0.5, checkY, rightNeighbor.second + 0.5)) {
|
||||||
|
drawLine(player, bx + 1.0, y, bz.toDouble(), bx + 1.0, y, bz + 1.0, dustOptions, 0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom edge (z+ direction)
|
||||||
|
val bottomNeighbor = Pair(bx, bz + 1)
|
||||||
|
if (!faceBlocks.contains(bottomNeighbor) && !cylinder.contains(bottomNeighbor.first + 0.5, checkY, bottomNeighbor.second + 0.5)) {
|
||||||
|
drawLine(player, bx + 1.0, y, bz + 1.0, bx.toDouble(), y, bz + 1.0, dustOptions, 0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left edge (x- direction)
|
||||||
|
val leftNeighbor = Pair(bx - 1, bz)
|
||||||
|
if (!faceBlocks.contains(leftNeighbor) && !cylinder.contains(leftNeighbor.first + 0.5, checkY, leftNeighbor.second + 0.5)) {
|
||||||
|
drawLine(player, bx.toDouble(), y, bz + 1.0, bx.toDouble(), y, bz.toDouble(), dustOptions, 0.25)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user