feat: 土地条件追加・コマンド変更

This commit is contained in:
Keisuke Hirata 2025-12-20 06:25:09 +09:00
parent 3c6b644607
commit 167daab8cd
9 changed files with 554 additions and 190 deletions

View File

@ -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) {

View File

@ -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<Int>("sectorId")
giveTool(player, id)
}
}
}
literal("activate") {
integer("sectorId") {
executes {
val id = argument<Int>("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<Int>("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<Int>("sectorId")
val targetActorId = argument<Int>("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<Int>("sectorId")
val rangeIndex = argument<Int>("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<Int>("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<Int>("sectorId")
giveTool(player, id)
}
}
}
literal("activate") {
integer("sectorId") {
executes {
val id = argument<Int>("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<Int>("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<Int>("sectorId")
val targetActorId = argument<Int>("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<Int>("sectorId")
val rangeIndex = argument<Int>("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))
}
}
}
}
}
}
}
}
}
}

View File

@ -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")))
)

View File

@ -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<String>)
private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
@ -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<String>()
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<Shape>): 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<Shape>, shapes2: List<Shape>, 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 {

View File

@ -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) {

View File

@ -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<Int>("sectorId")
giveTool(player, id)
}
}
}
literal("activate") {
integer("sectorId") {
executes {
val id = argument<Int>("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<Int>("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<Int>("sectorId")
val targetActorId = argument<Int>("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<Int>("sectorId")
val rangeIndex = argument<Int>("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<Int>("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<Int>("sectorId")
giveTool(player, id)
}
}
}
literal("activate") {
integer("sectorId") {
executes {
val id = argument<Int>("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<Int>("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<Int>("sectorId")
val targetActorId = argument<Int>("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<Int>("sectorId")
val rangeIndex = argument<Int>("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))
}
}
}
}
}
}
}
}
}
}

View File

@ -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")))
)

View File

@ -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<String>)
private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
@ -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<String>()
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<Shape>): 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<Shape>, shapes2: List<Shape>, 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 {

View File

@ -0,0 +1,6 @@
activation:
min-volume: 1000
distance-from-others: 10
core-protection:
radius: 5.0
height: 10