From 167daab8cd3df153188d2eccabe770d1f203f359 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Dec 2025 06:25:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=9F=E5=9C=B0=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=83=BB=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hcu/landsector/LandSectorPlugin.kt | 4 +- .../landsector/command/LandSectorCommand.kt | 173 ++++++++++-------- .../hcu/landsector/listener/SectorListener.kt | 39 ++-- .../hcu/landsector/service/SectorService.kt | 153 +++++++++++++++- .../hcu/landsector/LandSectorPlugin.kt | 4 +- .../landsector/command/LandSectorCommand.kt | 173 ++++++++++-------- .../hcu/landsector/listener/SectorListener.kt | 39 ++-- .../hcu/landsector/service/SectorService.kt | 153 +++++++++++++++- src/main/resources/config.yml | 6 + 9 files changed, 554 insertions(+), 190 deletions(-) create mode 100644 src/main/resources/config.yml diff --git a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt index 1ae9756..a2abbc5 100644 --- a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -21,6 +21,8 @@ class LandSectorPlugin : JavaPlugin() { override fun onEnable() { instance = this + saveDefaultConfig() + selectionService = SelectionService() // Register commands @@ -53,7 +55,7 @@ class LandSectorPlugin : JavaPlugin() { return } - val service = SectorService(db, landService) + val service = SectorService(this, db, landService) try { service.init() } catch (e: Exception) { diff --git a/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt b/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt index 35e26d5..8c91d6a 100644 --- a/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt +++ b/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt @@ -18,51 +18,17 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { fun register() { kommand(landSectorPlugin) { command("landsector") { - literal("givetool") { + // Admin Commands + literal("reload") { + condition { it.isOp } executes { - val player = sender as? Player ?: return@executes - giveTool(player, null) - } - integer("sectorId") { - executes { - val player = sender as? Player ?: return@executes - val id = argument("sectorId") - giveTool(player, id) - } - } - } - - literal("activate") { - integer("sectorId") { - executes { - val id = argument("sectorId") - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.activateSector(id)) { - sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) - } - } - } - } - - literal("cancel") { - integer("sectorId") { - executes { - val id = argument("sectorId") - sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) - // TODO: Cancel logic - val player = sender as? Player - if (player != null) { - landSectorPlugin.selectionService?.clearSelection(player.uniqueId) - sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED)) - } - } + landSectorPlugin.reloadConfig() + sender.sendMessage(Component.text("Configuration reloaded.", NamedTextColor.GREEN)) } } literal("give") { + condition { it.isOp } executes { val player = sender as? Player ?: return@executes @@ -82,6 +48,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } literal("list") { + condition { it.isOp } executes { val player = sender as? Player ?: return@executes val service = landSectorPlugin.sectorService @@ -123,46 +90,8 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } - literal("transfer") { - integer("sectorId") { - integer("targetActorId") { - executes { - val sectorId = argument("sectorId") - val targetActorId = argument("targetActorId") - val player = sender as? Player ?: return@executes - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.transferSector(sectorId, targetActorId)) { - sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED)) - } - } - } - } - } - - literal("range") { - literal("delete") { - integer("sectorId") { - integer("rangeIndex") { - executes { - val sectorId = argument("sectorId") - val rangeIndex = argument("rangeIndex") - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.deleteRange(sectorId, rangeIndex)) { - sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED)) - } - } - } - } - } - } - literal("delete") { + condition { it.isOp } integer("id") { executes { val id = argument("id") @@ -209,6 +138,92 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } } + + // User Operations + literal("operation") { + literal("givetool") { + executes { + val player = sender as? Player ?: return@executes + giveTool(player, null) + } + integer("sectorId") { + executes { + val player = sender as? Player ?: return@executes + val id = argument("sectorId") + giveTool(player, id) + } + } + } + + literal("activate") { + integer("sectorId") { + executes { + val id = argument("sectorId") + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.activateSector(id)) { + sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) + } + } + } + } + + literal("cancel") { + integer("sectorId") { + executes { + val id = argument("sectorId") + sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) + // TODO: Cancel logic + val player = sender as? Player + if (player != null) { + landSectorPlugin.selectionService?.clearSelection(player.uniqueId) + sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED)) + } + } + } + } + + literal("transfer") { + integer("sectorId") { + integer("targetActorId") { + executes { + val sectorId = argument("sectorId") + val targetActorId = argument("targetActorId") + val player = sender as? Player ?: return@executes + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.transferSector(sectorId, targetActorId)) { + sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED)) + } + } + } + } + } + + literal("range") { + literal("delete") { + integer("sectorId") { + integer("rangeIndex") { + executes { + val sectorId = argument("sectorId") + val rangeIndex = argument("rangeIndex") + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.deleteRange(sectorId, rangeIndex)) { + sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED)) + } + } + } + } + } + } + } } } } diff --git a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt index b2bdb2b..8396c59 100644 --- a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -267,32 +267,43 @@ class SectorListener( val myFaction = factionService.getFactionOfPlayer(player.uniqueId) if (myFaction != null && sector.ownerActorId != myFaction) { val myRole = factionService.getRole(myFaction, player.uniqueId) - if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) { - content.append( - Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD) - .clickEvent(ClickEvent.runCommand("/landsector transfer $sectorId $myFaction")) - .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction"))) - ) + if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) { + content.append( + Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD) + .clickEvent(ClickEvent.runCommand("/landsector operation transfer $sectorId $myFaction")) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction"))) + ) } } // Actions Row 1 - content.append( - Component.text("[Activate]", NamedTextColor.DARK_GREEN) - .clickEvent(ClickEvent.runCommand("/landsector activate $sectorId")) - .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to activate sector"))) - ) + // Check activation + val activationResult = sectorService.checkActivationConditions(sectorId) + + if (activationResult.canActivate) { + content.append( + Component.text("[Activate]", NamedTextColor.DARK_GREEN) + .clickEvent(ClickEvent.runCommand("/landsector operation activate $sectorId")) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to activate sector"))) + ) + } else { + val reasons = activationResult.reasons.joinToString("\n") { "- $it" } + content.append( + Component.text("[Activate]", NamedTextColor.GRAY) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Cannot Activate:\n$reasons", NamedTextColor.RED))) + ) + } content.append(Component.text(" ")) content.append( Component.text("[Destroy]\n", NamedTextColor.RED) - .clickEvent(ClickEvent.runCommand("/landsector cancel $sectorId")) + .clickEvent(ClickEvent.runCommand("/landsector operation cancel $sectorId")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to destroy sector"))) ) // Actions Row 2 content.append( Component.text("[Get Tool]\n\n", NamedTextColor.DARK_AQUA) - .clickEvent(ClickEvent.runCommand("/landsector givetool $sectorId")) + .clickEvent(ClickEvent.runCommand("/landsector operation givetool $sectorId")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to get selection tool"))) ) @@ -306,7 +317,7 @@ class SectorListener( content.append(Component.text("\n")) content.append( Component.text("[x] ", NamedTextColor.RED) - .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) + .clickEvent(ClickEvent.runCommand("/landsector operation range delete $sectorId ${range.id}")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) ) diff --git a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt index b83205d..05525da 100644 --- a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt @@ -1,5 +1,6 @@ package net.hareworks.hcu.landsector.service +import net.hareworks.hcu.landsector.LandSectorPlugin import net.hareworks.hcu.landsector.database.SectorDraftsTable import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.model.Sector @@ -18,9 +19,22 @@ import org.jetbrains.exposed.v1.jdbc.andWhere import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.transactions.transaction import java.util.concurrent.ConcurrentHashMap +import kotlin.math.PI +import kotlin.math.pow +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.ceil +import kotlin.math.sqrt // ... class definition ... -class SectorService(private val database: Database, private val landService: net.hareworks.hcu.lands.service.LandService) { +class SectorService( + private val plugin: LandSectorPlugin, + private val database: Database, + private val landService: net.hareworks.hcu.lands.service.LandService +) { + + data class ActivationResult(val canActivate: Boolean, val reasons: List) private val hpCache = ConcurrentHashMap() private val dirtySet = ConcurrentHashMap.newKeySet() @@ -68,6 +82,143 @@ class SectorService(private val database: Database, private val landService: net } } + fun checkActivationConditions(sectorId: Int): ActivationResult { + val sector = getSector(sectorId) ?: return ActivationResult(false, listOf("Sector not found")) + val draft = draftLands[sectorId] ?: return ActivationResult(false, listOf("No activation draft found")) + if (draft.data.parts.isEmpty()) return ActivationResult(false, listOf("No land selected")) + + val reasons = mutableListOf() + val config = plugin.config + + // 1. Volume Check + val minVolume = config.getInt("activation.min-volume", 1000) + val currentVolume = draft.data.parts.sumOf { getVolume(it) } + if (currentVolume < minVolume) { + reasons.add("Volume too small: $currentVolume < $minVolume") + } + + // 2. Range Check (Core Protection) + val coreRadius = config.getDouble("activation.core-protection.radius", 5.0) + val coreHeight = config.getInt("activation.core-protection.height", 10) + + // Define required Cylinder around sector core + // Center: sector.x, sector.y, sector.z. + val yMin = sector.y - (coreHeight / 2) + val yMax = sector.y + (coreHeight + 1) / 2 - 1 + + if (!isCylinderCovered(sector.x, sector.y, sector.z, coreRadius, yMin, yMax, draft.data.parts)) { + reasons.add("Land must cover core area (R:$coreRadius, H:$coreHeight around core)") + } + + // 3. Distance Check + val minDist = config.getInt("activation.distance-from-others", 20) + val activeSectors = sectorsCache.values.filter { + it.world == sector.world && it.landId != null && it.id != sector.id + } + + for (otherSector in activeSectors) { + val otherLand = landService.getLand(otherSector.landId!!) ?: continue + if (isTooClose(draft.data.parts, otherLand.data.parts, minDist)) { + reasons.add("Too close to Sector ${otherSector.id} (Min $minDist blocks)") + break + } + } + + return ActivationResult(reasons.isEmpty(), reasons) + } + + private fun getVolume(shape: Shape): Long { + return when (shape) { + is Shape.Cuboid -> { + val dx = abs(shape.x2 - shape.x1) + 1L + val dy = abs(shape.y2 - shape.y1) + 1L + val dz = abs(shape.z2 - shape.z1) + 1L + dx * dy * dz + } + is Shape.Cylinder -> { + val r = shape.radius + val h = (shape.bottomHeight + shape.topHeight + 1).toLong() + (Math.PI * r * r).toLong() * h // Approximate + } + } + } + + private fun isCylinderCovered(cx: Int, cy: Int, cz: Int, r: Double, yMin: Int, yMax: Int, shapes: List): Boolean { + val rInt = ceil(r).toInt() + val rSq = r * r + + for (y in yMin..yMax) { + for (x in -rInt..rInt) { + for (z in -rInt..rInt) { + if ((x*x + z*z).toDouble() <= rSq) { + val gx = cx + x + val gz = cz + z + if (shapes.none { contains(it, gx, y, gz) }) { + return false + } + } + } + } + } + return true + } + + private fun contains(shape: Shape, x: Int, y: Int, z: Int): Boolean { + return when (shape) { + is Shape.Cuboid -> { + x >= min(shape.x1, shape.x2) && x <= max(shape.x1, shape.x2) && + y >= min(shape.y1, shape.y2) && y <= max(shape.y1, shape.y2) && + z >= min(shape.z1, shape.z2) && z <= max(shape.z1, shape.z2) + } + is Shape.Cylinder -> { + if (y < shape.y - shape.bottomHeight || y > shape.y + shape.topHeight) return false + val dx = x - shape.x + val dz = z - shape.z + (dx*dx + dz*dz) <= (shape.radius * shape.radius) + } + } + } + + private fun isTooClose(shapes1: List, shapes2: List, minDist: Int): Boolean { + for (s1 in shapes1) { + for (s2 in shapes2) { + if (getDistance(s1, s2) < minDist) return true + } + } + return false + } + + private fun getDistance(s1: Shape, s2: Shape): Double { + val bb1 = getBounds(s1) + val bb2 = getBounds(s2) + + val dx = max(0, max(bb1.minX - bb2.maxX, bb2.minX - bb1.maxX)) + val dy = max(0, max(bb1.minY - bb2.maxY, bb2.minY - bb1.maxY)) + val dz = max(0, max(bb1.minZ - bb2.maxZ, bb2.minZ - bb1.maxZ)) + + return sqrt((dx*dx + dy*dy + dz*dz).toDouble()) + } + + data class Bounds(val minX: Int, val maxX: Int, val minY: Int, val maxY: Int, val minZ: Int, val maxZ: Int) + + private fun getBounds(shape: Shape): Bounds { + return when (shape) { + is Shape.Cuboid -> Bounds( + min(shape.x1, shape.x2), max(shape.x1, shape.x2), + min(shape.y1, shape.y2), max(shape.y1, shape.y2), + min(shape.z1, shape.z2), max(shape.z1, shape.z2) + ) + is Shape.Cylinder -> { + val r = ceil(shape.radius).toInt() + Bounds( + shape.x - r, shape.x + r, + shape.y - shape.bottomHeight, shape.y + shape.topHeight, + shape.z - r, shape.z + r + ) + } + } + } + fun createSector(ownerActorId: Int, world: String, x: Int, y: Int, z: Int): Sector? { return transaction(database) { val id = SectorsTable.insert { diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt index 1ae9756..a2abbc5 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -21,6 +21,8 @@ class LandSectorPlugin : JavaPlugin() { override fun onEnable() { instance = this + saveDefaultConfig() + selectionService = SelectionService() // Register commands @@ -53,7 +55,7 @@ class LandSectorPlugin : JavaPlugin() { return } - val service = SectorService(db, landService) + val service = SectorService(this, db, landService) try { service.init() } catch (e: Exception) { diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt b/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt index 35e26d5..8c91d6a 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt @@ -18,51 +18,17 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { fun register() { kommand(landSectorPlugin) { command("landsector") { - literal("givetool") { + // Admin Commands + literal("reload") { + condition { it.isOp } executes { - val player = sender as? Player ?: return@executes - giveTool(player, null) - } - integer("sectorId") { - executes { - val player = sender as? Player ?: return@executes - val id = argument("sectorId") - giveTool(player, id) - } - } - } - - literal("activate") { - integer("sectorId") { - executes { - val id = argument("sectorId") - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.activateSector(id)) { - sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) - } - } - } - } - - literal("cancel") { - integer("sectorId") { - executes { - val id = argument("sectorId") - sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) - // TODO: Cancel logic - val player = sender as? Player - if (player != null) { - landSectorPlugin.selectionService?.clearSelection(player.uniqueId) - sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED)) - } - } + landSectorPlugin.reloadConfig() + sender.sendMessage(Component.text("Configuration reloaded.", NamedTextColor.GREEN)) } } literal("give") { + condition { it.isOp } executes { val player = sender as? Player ?: return@executes @@ -82,6 +48,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } literal("list") { + condition { it.isOp } executes { val player = sender as? Player ?: return@executes val service = landSectorPlugin.sectorService @@ -123,46 +90,8 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } - literal("transfer") { - integer("sectorId") { - integer("targetActorId") { - executes { - val sectorId = argument("sectorId") - val targetActorId = argument("targetActorId") - val player = sender as? Player ?: return@executes - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.transferSector(sectorId, targetActorId)) { - sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED)) - } - } - } - } - } - - literal("range") { - literal("delete") { - integer("sectorId") { - integer("rangeIndex") { - executes { - val sectorId = argument("sectorId") - val rangeIndex = argument("rangeIndex") - val service = landSectorPlugin.sectorService ?: return@executes - - if (service.deleteRange(sectorId, rangeIndex)) { - sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN)) - } else { - sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED)) - } - } - } - } - } - } - literal("delete") { + condition { it.isOp } integer("id") { executes { val id = argument("id") @@ -209,6 +138,92 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } } + + // User Operations + literal("operation") { + literal("givetool") { + executes { + val player = sender as? Player ?: return@executes + giveTool(player, null) + } + integer("sectorId") { + executes { + val player = sender as? Player ?: return@executes + val id = argument("sectorId") + giveTool(player, id) + } + } + } + + literal("activate") { + integer("sectorId") { + executes { + val id = argument("sectorId") + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.activateSector(id)) { + sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) + } + } + } + } + + literal("cancel") { + integer("sectorId") { + executes { + val id = argument("sectorId") + sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) + // TODO: Cancel logic + val player = sender as? Player + if (player != null) { + landSectorPlugin.selectionService?.clearSelection(player.uniqueId) + sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED)) + } + } + } + } + + literal("transfer") { + integer("sectorId") { + integer("targetActorId") { + executes { + val sectorId = argument("sectorId") + val targetActorId = argument("targetActorId") + val player = sender as? Player ?: return@executes + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.transferSector(sectorId, targetActorId)) { + sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED)) + } + } + } + } + } + + literal("range") { + literal("delete") { + integer("sectorId") { + integer("rangeIndex") { + executes { + val sectorId = argument("sectorId") + val rangeIndex = argument("rangeIndex") + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.deleteRange(sectorId, rangeIndex)) { + sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED)) + } + } + } + } + } + } + } } } } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt index b2bdb2b..8396c59 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -267,32 +267,43 @@ class SectorListener( val myFaction = factionService.getFactionOfPlayer(player.uniqueId) if (myFaction != null && sector.ownerActorId != myFaction) { val myRole = factionService.getRole(myFaction, player.uniqueId) - if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) { - content.append( - Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD) - .clickEvent(ClickEvent.runCommand("/landsector transfer $sectorId $myFaction")) - .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction"))) - ) + if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) { + content.append( + Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD) + .clickEvent(ClickEvent.runCommand("/landsector operation transfer $sectorId $myFaction")) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction"))) + ) } } // Actions Row 1 - content.append( - Component.text("[Activate]", NamedTextColor.DARK_GREEN) - .clickEvent(ClickEvent.runCommand("/landsector activate $sectorId")) - .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to activate sector"))) - ) + // Check activation + val activationResult = sectorService.checkActivationConditions(sectorId) + + if (activationResult.canActivate) { + content.append( + Component.text("[Activate]", NamedTextColor.DARK_GREEN) + .clickEvent(ClickEvent.runCommand("/landsector operation activate $sectorId")) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to activate sector"))) + ) + } else { + val reasons = activationResult.reasons.joinToString("\n") { "- $it" } + content.append( + Component.text("[Activate]", NamedTextColor.GRAY) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Cannot Activate:\n$reasons", NamedTextColor.RED))) + ) + } content.append(Component.text(" ")) content.append( Component.text("[Destroy]\n", NamedTextColor.RED) - .clickEvent(ClickEvent.runCommand("/landsector cancel $sectorId")) + .clickEvent(ClickEvent.runCommand("/landsector operation cancel $sectorId")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to destroy sector"))) ) // Actions Row 2 content.append( Component.text("[Get Tool]\n\n", NamedTextColor.DARK_AQUA) - .clickEvent(ClickEvent.runCommand("/landsector givetool $sectorId")) + .clickEvent(ClickEvent.runCommand("/landsector operation givetool $sectorId")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to get selection tool"))) ) @@ -306,7 +317,7 @@ class SectorListener( content.append(Component.text("\n")) content.append( Component.text("[x] ", NamedTextColor.RED) - .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) + .clickEvent(ClickEvent.runCommand("/landsector operation range delete $sectorId ${range.id}")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) ) diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt index b83205d..05525da 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt @@ -1,5 +1,6 @@ package net.hareworks.hcu.landsector.service +import net.hareworks.hcu.landsector.LandSectorPlugin import net.hareworks.hcu.landsector.database.SectorDraftsTable import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.model.Sector @@ -18,9 +19,22 @@ import org.jetbrains.exposed.v1.jdbc.andWhere import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.transactions.transaction import java.util.concurrent.ConcurrentHashMap +import kotlin.math.PI +import kotlin.math.pow +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.ceil +import kotlin.math.sqrt // ... class definition ... -class SectorService(private val database: Database, private val landService: net.hareworks.hcu.lands.service.LandService) { +class SectorService( + private val plugin: LandSectorPlugin, + private val database: Database, + private val landService: net.hareworks.hcu.lands.service.LandService +) { + + data class ActivationResult(val canActivate: Boolean, val reasons: List) private val hpCache = ConcurrentHashMap() private val dirtySet = ConcurrentHashMap.newKeySet() @@ -68,6 +82,143 @@ class SectorService(private val database: Database, private val landService: net } } + fun checkActivationConditions(sectorId: Int): ActivationResult { + val sector = getSector(sectorId) ?: return ActivationResult(false, listOf("Sector not found")) + val draft = draftLands[sectorId] ?: return ActivationResult(false, listOf("No activation draft found")) + if (draft.data.parts.isEmpty()) return ActivationResult(false, listOf("No land selected")) + + val reasons = mutableListOf() + val config = plugin.config + + // 1. Volume Check + val minVolume = config.getInt("activation.min-volume", 1000) + val currentVolume = draft.data.parts.sumOf { getVolume(it) } + if (currentVolume < minVolume) { + reasons.add("Volume too small: $currentVolume < $minVolume") + } + + // 2. Range Check (Core Protection) + val coreRadius = config.getDouble("activation.core-protection.radius", 5.0) + val coreHeight = config.getInt("activation.core-protection.height", 10) + + // Define required Cylinder around sector core + // Center: sector.x, sector.y, sector.z. + val yMin = sector.y - (coreHeight / 2) + val yMax = sector.y + (coreHeight + 1) / 2 - 1 + + if (!isCylinderCovered(sector.x, sector.y, sector.z, coreRadius, yMin, yMax, draft.data.parts)) { + reasons.add("Land must cover core area (R:$coreRadius, H:$coreHeight around core)") + } + + // 3. Distance Check + val minDist = config.getInt("activation.distance-from-others", 20) + val activeSectors = sectorsCache.values.filter { + it.world == sector.world && it.landId != null && it.id != sector.id + } + + for (otherSector in activeSectors) { + val otherLand = landService.getLand(otherSector.landId!!) ?: continue + if (isTooClose(draft.data.parts, otherLand.data.parts, minDist)) { + reasons.add("Too close to Sector ${otherSector.id} (Min $minDist blocks)") + break + } + } + + return ActivationResult(reasons.isEmpty(), reasons) + } + + private fun getVolume(shape: Shape): Long { + return when (shape) { + is Shape.Cuboid -> { + val dx = abs(shape.x2 - shape.x1) + 1L + val dy = abs(shape.y2 - shape.y1) + 1L + val dz = abs(shape.z2 - shape.z1) + 1L + dx * dy * dz + } + is Shape.Cylinder -> { + val r = shape.radius + val h = (shape.bottomHeight + shape.topHeight + 1).toLong() + (Math.PI * r * r).toLong() * h // Approximate + } + } + } + + private fun isCylinderCovered(cx: Int, cy: Int, cz: Int, r: Double, yMin: Int, yMax: Int, shapes: List): Boolean { + val rInt = ceil(r).toInt() + val rSq = r * r + + for (y in yMin..yMax) { + for (x in -rInt..rInt) { + for (z in -rInt..rInt) { + if ((x*x + z*z).toDouble() <= rSq) { + val gx = cx + x + val gz = cz + z + if (shapes.none { contains(it, gx, y, gz) }) { + return false + } + } + } + } + } + return true + } + + private fun contains(shape: Shape, x: Int, y: Int, z: Int): Boolean { + return when (shape) { + is Shape.Cuboid -> { + x >= min(shape.x1, shape.x2) && x <= max(shape.x1, shape.x2) && + y >= min(shape.y1, shape.y2) && y <= max(shape.y1, shape.y2) && + z >= min(shape.z1, shape.z2) && z <= max(shape.z1, shape.z2) + } + is Shape.Cylinder -> { + if (y < shape.y - shape.bottomHeight || y > shape.y + shape.topHeight) return false + val dx = x - shape.x + val dz = z - shape.z + (dx*dx + dz*dz) <= (shape.radius * shape.radius) + } + } + } + + private fun isTooClose(shapes1: List, shapes2: List, minDist: Int): Boolean { + for (s1 in shapes1) { + for (s2 in shapes2) { + if (getDistance(s1, s2) < minDist) return true + } + } + return false + } + + private fun getDistance(s1: Shape, s2: Shape): Double { + val bb1 = getBounds(s1) + val bb2 = getBounds(s2) + + val dx = max(0, max(bb1.minX - bb2.maxX, bb2.minX - bb1.maxX)) + val dy = max(0, max(bb1.minY - bb2.maxY, bb2.minY - bb1.maxY)) + val dz = max(0, max(bb1.minZ - bb2.maxZ, bb2.minZ - bb1.maxZ)) + + return sqrt((dx*dx + dy*dy + dz*dz).toDouble()) + } + + data class Bounds(val minX: Int, val maxX: Int, val minY: Int, val maxY: Int, val minZ: Int, val maxZ: Int) + + private fun getBounds(shape: Shape): Bounds { + return when (shape) { + is Shape.Cuboid -> Bounds( + min(shape.x1, shape.x2), max(shape.x1, shape.x2), + min(shape.y1, shape.y2), max(shape.y1, shape.y2), + min(shape.z1, shape.z2), max(shape.z1, shape.z2) + ) + is Shape.Cylinder -> { + val r = ceil(shape.radius).toInt() + Bounds( + shape.x - r, shape.x + r, + shape.y - shape.bottomHeight, shape.y + shape.topHeight, + shape.z - r, shape.z + r + ) + } + } + } + fun createSector(ownerActorId: Int, world: String, x: Int, y: Int, z: Int): Sector? { return transaction(database) { val id = SectorsTable.insert { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..0be84b1 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,6 @@ +activation: + min-volume: 1000 + distance-from-others: 10 + core-protection: + radius: 5.0 + height: 10