diff --git a/docs/api.md b/docs/api.md index bfb303d..f16d757 100644 --- a/docs/api.md +++ b/docs/api.md @@ -41,6 +41,10 @@ check(api.apiVersion == 2) val plot = api.getPlotAt(player.world.name, player.location.blockX, player.location.blockZ) val owned = api.getOwnedPlots(player.uniqueId) val accessible = api.getAccessiblePlots(player.uniqueId) + +// Exact containment for a building/arena footprint. Unlike corner checks this also rejects holes. +val fits = plot?.containsArea(player.world.name, minX, minZ, maxX, maxZ) == true +val roundArenaFits = plot?.containsCircle(player.world.name, centerX, centerZ, diameter) == true ``` Java: diff --git a/src/main/kotlin/pl/syntaxdevteam/plotsx/api/PlotsXApi.kt b/src/main/kotlin/pl/syntaxdevteam/plotsx/api/PlotsXApi.kt index 8722504..95a1d17 100644 --- a/src/main/kotlin/pl/syntaxdevteam/plotsx/api/PlotsXApi.kt +++ b/src/main/kotlin/pl/syntaxdevteam/plotsx/api/PlotsXApi.kt @@ -66,6 +66,61 @@ data class PlotSnapshot @JvmOverloads constructor(val id: Int, val owner: UUID, (if (radius == null) chunks.any { it.x == Math.floorDiv(x, 16) && it.z == Math.floorDiv(z, 16) } else (kotlin.math.abs(x.toLong() - this.x) <= radius.toLong() && kotlin.math.abs(z.toLong() - this.z) <= radius.toLong()) || extensions.any { it.contains(x, z) }) + + /** + * Returns true only when every block column in the inclusive rectangle belongs to this plot. + * This is intentionally stricter than checking the four corners: chunk plots and joined classic + * segments may be non-rectangular and can contain holes between otherwise valid corners. + */ + fun containsArea(world: String, minX: Int, minZ: Int, maxX: Int, maxZ: Int): Boolean { + if (!this.world.equals(world, true) || minX > maxX || minZ > maxZ) return false + if (radius == null) { + val claimed = chunks.toHashSet() + for (chunkX in Math.floorDiv(minX, 16)..Math.floorDiv(maxX, 16)) { + for (chunkZ in Math.floorDiv(minZ, 16)..Math.floorDiv(maxZ, 16)) { + if (ChunkSnapshot(chunkX, chunkZ) !in claimed) return false + } + } + return true + } + + // A rectangle covered by a union of square regions has no gaps iff each horizontal span is + // fully covered on every row. Avoid iterating every block in large declared areas. + for (blockZ in minZ..maxZ) { + val intervals = buildList { + fun addRegion(centerX: Int, centerZ: Int, regionRadius: Int) { + if (blockZ.toLong() !in centerZ.toLong() - regionRadius..centerZ.toLong() + regionRadius) return + add(centerX.toLong() - regionRadius to centerX.toLong() + regionRadius) + } + addRegion(x, z, requireNotNull(radius)) + extensions.forEach { addRegion(it.x, it.z, it.radius) } + }.sortedBy { it.first } + var coveredUntil = minX.toLong() - 1 + for ((start, end) in intervals) { + if (start > coveredUntil + 1) break + if (end > coveredUntil) coveredUntil = end + if (coveredUntil >= maxX) break + } + if (coveredUntil < maxX) return false + } + return true + } + + /** Exact filled-circle containment. Diameter is expressed in block columns around the centre. */ + fun containsCircle(world: String, centerX: Int, centerZ: Int, diameter: Int): Boolean { + if (diameter <= 0) return false + val radius = diameter / 2.0 + val minZ = kotlin.math.ceil(centerZ - radius + 0.5).toInt() + val maxZ = kotlin.math.floor(centerZ + radius - 0.5).toInt() + for (blockZ in minZ..maxZ) { + val dz = blockZ + 0.5 - (centerZ + 0.5) + val halfSpan = kotlin.math.sqrt(radius * radius - dz * dz) + val minX = kotlin.math.ceil(centerX + 0.5 - halfSpan).toInt() + val maxX = kotlin.math.floor(centerX + 0.5 + halfSpan).toInt() + if (minX <= maxX && !containsArea(world, minX, blockZ, maxX, blockZ)) return false + } + return true + } } /** API v2: complete 16 x 16 block columns; radius is null for chunk plots. */ diff --git a/src/test/kotlin/pl/syntaxdevteam/plotsx/databases/PlotGeometryTest.kt b/src/test/kotlin/pl/syntaxdevteam/plotsx/databases/PlotGeometryTest.kt index 58bc41e..705fe56 100644 --- a/src/test/kotlin/pl/syntaxdevteam/plotsx/databases/PlotGeometryTest.kt +++ b/src/test/kotlin/pl/syntaxdevteam/plotsx/databases/PlotGeometryTest.kt @@ -2,11 +2,28 @@ package pl.syntaxdevteam.plotsx.databases import org.junit.Assert.* import org.junit.Test +import pl.syntaxdevteam.plotsx.api.ChunkSnapshot import pl.syntaxdevteam.plotsx.api.PlotRegionSnapshot import pl.syntaxdevteam.plotsx.api.PlotSnapshot import java.util.UUID class PlotGeometryTest { + + @Test + fun `api snapshot requires the complete requested area to be claimed`() { + val owner = UUID.randomUUID() + val chunkPlot = PlotSnapshot(1, owner, "world", 0, 64, 0, null, "chunks", 0, + chunks = listOf(ChunkSnapshot(0, 0), ChunkSnapshot(1, 0))) + assertTrue(chunkPlot.containsArea("world", 2, 2, 29, 13)) + assertFalse(chunkPlot.containsArea("world", 2, 2, 29, 18)) + + val joined = PlotSnapshot(2, owner, "world", 0, 64, 0, 2, "joined", 0, + extensions = listOf(PlotRegionSnapshot(5, 0, 2))) + assertTrue(joined.containsArea("world", -2, -2, 7, 2)) + assertFalse(joined.containsArea("world", -2, -3, 7, 2)) + assertTrue(joined.containsCircle("world", 0, 0, 5)) + assertFalse(joined.containsCircle("world", 0, 0, 7)) + } private val base = PlotData(1, UUID.randomUUID(), 0, 0, 64, 16, "world", "Home", 0) @Test fun `north and east form L without claiming missing corner`() {