bin/削除・設定の調整

This commit is contained in:
Keisuke Hirata 2025-12-20 06:59:06 +09:00
parent 167daab8cd
commit 33f3b4f878
18 changed files with 51 additions and 2235 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ build/
*.iml
.idea/
out/
bin

View File

@ -1,100 +0,0 @@
package net.hareworks.hcu.landsector
import net.hareworks.hcu.core.Main
import net.hareworks.hcu.core.player.PlayerIdService
import net.hareworks.hcu.landsector.command.LandSectorCommand
import net.hareworks.hcu.landsector.listener.SectorListener
import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService
import org.bukkit.plugin.java.JavaPlugin
class LandSectorPlugin : JavaPlugin() {
companion object {
lateinit var instance: LandSectorPlugin
private set
}
var sectorService: SectorService? = null
var selectionService: SelectionService? = null
override fun onEnable() {
instance = this
saveDefaultConfig()
selectionService = SelectionService()
// Register commands
LandSectorCommand(this).register()
// Defer service init
server.scheduler.runTaskLater(this, Runnable {
initializeServices()
}, 60L)
logger.info("LandSector plugin has been enabled!")
}
private fun initializeServices() {
val db = try {
Main.instance.exposedDatabase()
} catch (e: Exception) {
null
}
if (db == null) {
logger.severe("hcu-core database not ready.")
return
}
val landsPlugin = server.pluginManager.getPlugin("lands") as? net.hareworks.hcu.lands.App
val landService = landsPlugin?.landService
if (landService == null) {
logger.severe("Lands plugin or service not ready.")
return
}
val service = SectorService(this, db, landService)
try {
service.init()
} catch (e: Exception) {
logger.severe("Failed to init DB: ${e.message}")
return
}
this.sectorService = service
val pIdService = server.servicesManager.load(PlayerIdService::class.java)
if (pIdService == null) {
logger.severe("PlayerIdService not found")
return
}
val selService = selectionService
if (selService != null) {
val factionService = net.hareworks.hcu.faction.service.FactionService()
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService, factionService), this)
server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
// Schedule visualization task
net.hareworks.hcu.landsector.task.SelectionVisualizerTask(this, selService, service).runTaskTimer(this, 0L, 4L)
} else {
logger.severe("SelectionService not initialized!")
}
// Schedule auto-save every 5 minutes
server.scheduler.runTaskTimerAsynchronously(this, Runnable {
service.flushChanges()
}, 6000L, 6000L)
// Schedule rotation task
net.hareworks.hcu.landsector.task.SectorRotationTask(this).runTaskTimer(this, 1L, 1L)
logger.info("LandSector initialized with services.")
}
override fun onDisable() {
sectorService?.flushChanges()
logger.info("LandSector plugin has been disabled!")
}
}

View File

@ -1,257 +0,0 @@
package net.hareworks.hcu.landsector.command
import net.hareworks.hcu.landsector.LandSectorPlugin
import net.hareworks.hcu.landsector.model.Sector
import net.hareworks.kommand_lib.kommand
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.entity.Player
import org.bukkit.entity.Shulker
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType
class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
fun register() {
kommand(landSectorPlugin) {
command("landsector") {
// Admin Commands
literal("reload") {
condition { it.isOp }
executes {
landSectorPlugin.reloadConfig()
sender.sendMessage(Component.text("Configuration reloaded.", NamedTextColor.GREEN))
}
}
literal("give") {
condition { it.isOp }
executes {
val player = sender as? Player ?: return@executes
val item = ItemStack(Material.BEDROCK)
val meta = item.itemMeta
meta.displayName(Component.text("Sector Core", NamedTextColor.LIGHT_PURPLE))
meta.persistentDataContainer.set(
NamespacedKey(landSectorPlugin, "component"),
PersistentDataType.STRING,
"sector_core"
)
item.itemMeta = meta
player.inventory.addItem(item)
sender.sendMessage(Component.text("Gave 1 Sector Core.", NamedTextColor.GREEN))
}
}
literal("list") {
condition { it.isOp }
executes {
val player = sender as? Player ?: return@executes
val service = landSectorPlugin.sectorService
if (service == null) {
sender.sendMessage(Component.text("SectorService not ready.", NamedTextColor.RED))
return@executes
}
val worldName = player.world.name
val loc = player.location
val sectors = service.getAllSectors(worldName)
.sortedBy { sector ->
val sx = sector.x + 0.5
val sy = sector.y + 1.0
val sz = sector.z + 0.5
(loc.x - sx) * (loc.x - sx) +
(loc.y - sy) * (loc.y - sy) +
(loc.z - sz) * (loc.z - sz)
}
sender.sendMessage(Component.text("=== Sector List (${sectors.size}) ===", NamedTextColor.GOLD))
sectors.forEach { sector ->
val distSq = (loc.x - (sector.x + 0.5)) * (loc.x - (sector.x + 0.5)) +
(loc.y - (sector.y + 1.0)) * (loc.y - (sector.y + 1.0)) +
(loc.z - (sector.z + 0.5)) * (loc.z - (sector.z + 0.5))
val dist = Math.sqrt(distSq).toInt()
sender.sendMessage(
Component.text()
.append(Component.text("#${sector.id} ", NamedTextColor.YELLOW))
.append(Component.text("(${sector.x}, ${sector.y}, ${sector.z}) ", NamedTextColor.GRAY))
.append(Component.text("HP: ${sector.hp} ", NamedTextColor.RED))
.append(Component.text("- ${dist}m", NamedTextColor.AQUA))
.build()
)
}
}
}
literal("delete") {
condition { it.isOp }
integer("id") {
executes {
val id = argument<Int>("id")
val service = landSectorPlugin.sectorService
if (service == null) {
sender.sendMessage(Component.text("SectorService not ready.", NamedTextColor.RED))
return@executes
}
val sector = service.deleteSector(id)
if (sector == null) {
sender.sendMessage(Component.text("Sector #$id not found.", NamedTextColor.RED))
} else {
// Physical removal
val world = Bukkit.getWorld(sector.world)
if (world != null) {
val x = sector.x
val y = sector.y
val z = sector.z
val blockBase = world.getBlockAt(x, y - 1, z)
val blockTop = world.getBlockAt(x, y + 1, z)
if (blockBase.type == Material.DEEPSLATE_TILE_SLAB) blockBase.type = Material.AIR
if (blockTop.type == Material.DEEPSLATE_TILE_SLAB) blockTop.type = Material.AIR
// Remove All Entities with this sector ID
val center = Location(world, x + 0.5, y.toDouble(), z + 0.5)
if (center.chunk.isLoaded) {
val sectorKey = NamespacedKey(landSectorPlugin, "sector_id")
center.chunk.entities.forEach { entity ->
if (entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val sId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
if (sId == id) {
entity.remove()
}
}
}
}
}
sender.sendMessage(Component.text("Sector #$id deleted.", NamedTextColor.GREEN))
}
}
}
}
// 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))
}
}
}
}
}
}
}
}
}
}
private fun giveTool(player: Player, sectorId: Int?) {
val item = ItemStack(Material.FLINT)
val meta = item.itemMeta
meta.displayName(Component.text("Range Selection Tool" + (if (sectorId != null) " (#$sectorId)" else ""), NamedTextColor.AQUA))
meta.persistentDataContainer.set(
NamespacedKey(landSectorPlugin, "component"),
PersistentDataType.STRING,
"land_sector_tool"
)
if (sectorId != null) {
meta.persistentDataContainer.set(
NamespacedKey(landSectorPlugin, "sector_id"),
PersistentDataType.INTEGER,
sectorId
)
}
meta.lore(listOf(
Component.text("Left Click: Switch Mode", NamedTextColor.GRAY),
Component.text("Right Click: Select Position", NamedTextColor.GRAY),
Component.text("Sneaking acts as modifier", NamedTextColor.DARK_GRAY)
) + if (sectorId != null) listOf(Component.text("Linked to Sector #$sectorId", NamedTextColor.GOLD)) else emptyList())
item.itemMeta = meta
player.inventory.addItem(item)
player.sendMessage(Component.text("Gave Range Selection Tool${if (sectorId != null) " for Sector #$sectorId" else ""}.", NamedTextColor.GREEN))
}
}

View File

@ -1,16 +0,0 @@
package net.hareworks.hcu.landsector.database
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.hareworks.hcu.lands.model.LandData
import org.jetbrains.exposed.v1.core.Table
object SectorDraftsTable : Table("sector_drafts") {
val sectorId = integer("sector_id").references(SectorsTable.id)
val data = text("data").transform(
{ json -> Json.decodeFromString(LandData.serializer(), json) },
{ value -> Json.encodeToString(LandData.serializer(), value) }
)
override val primaryKey = PrimaryKey(sectorId)
}

View File

@ -1,17 +0,0 @@
package net.hareworks.hcu.landsector.database
import org.jetbrains.exposed.v1.core.Table
object SectorsTable : Table("land_sectors") {
val id = integer("id").autoIncrement()
val ownerActorId = integer("owner_actor_id")
val world = varchar("world", 64)
val x = integer("x")
val y = integer("y")
val z = integer("z")
val hp = integer("hp").default(1000)
val landId = integer("land_id").nullable()
override val primaryKey = PrimaryKey(id)
}

View File

@ -1,491 +0,0 @@
package net.hareworks.hcu.landsector.listener
import net.hareworks.hcu.landsector.LandSectorPlugin
import net.hareworks.hcu.landsector.service.SectorService
import net.hareworks.hcu.core.player.PlayerIdService
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.EntityType
import org.bukkit.entity.Shulker
import org.bukkit.attribute.Attribute
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.world.ChunkLoadEvent
import org.bukkit.entity.Player
import org.bukkit.entity.BlockDisplay
import org.bukkit.persistence.PersistentDataType
import org.bukkit.util.Transformation
import org.joml.Vector3f
import org.joml.Quaternionf
import org.bukkit.block.data.type.Slab
import net.hareworks.hcu.landsector.service.SelectionService
import net.kyori.adventure.text.event.ClickEvent
import org.bukkit.event.player.PlayerInteractEntityEvent
import net.hareworks.hcu.faction.service.FactionService
import net.hareworks.hcu.faction.database.schema.FactionRole
class SectorListener(
private val plugin: LandSectorPlugin,
private val sectorService: SectorService,
private val playerIdService: PlayerIdService,
private val selectionService: SelectionService,
private val factionService: FactionService
) : Listener {
@EventHandler
fun onBreak(event: BlockBreakEvent) {
val block = event.block
val loc = block.location
val player = event.player
if (sectorService.isSectorBlock(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
player.sendMessage(Component.text("You cannot break Sector Core blocks!", NamedTextColor.RED))
event.isCancelled = true
}
}
@EventHandler
fun onPlace(event: BlockPlaceEvent) {
val block = event.block
val loc = block.location
val player = event.player
// Global protection check
if (sectorService.isSectorArea(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
player.sendMessage(Component.text("This area is protected by a Sector Core.", NamedTextColor.RED))
event.isCancelled = true
return
}
val item = event.itemInHand
val meta = item.itemMeta ?: return
val key = NamespacedKey(plugin, "component")
if (meta.persistentDataContainer.get(key, PersistentDataType.STRING) != "sector_core") {
return
}
val playerEntry = playerIdService.find(player.uniqueId)
if (playerEntry == null) {
player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
event.isCancelled = true
return
}
// ... (rest of the code)
// Check 3x3x3 space availability (relative to placed block base)
val baseLoc = block.location
for (dx in -1..1) {
for (dy in 0..2) {
for (dz in -1..1) {
if (dx == 0 && dy == 0 && dz == 0) continue // Skip the placed block itself
val checkLoc = baseLoc.clone().add(dx.toDouble(), dy.toDouble(), dz.toDouble())
if (!checkLoc.block.type.isAir) {
player.sendMessage(Component.text("Not enough space! Need 3x3x3 free area.", NamedTextColor.RED))
event.isCancelled = true
return
}
}
}
}
// Define Center Location (This will be the DB coordinates)
val centerLoc = baseLoc.clone().add(0.0, 1.0, 0.0) // y+1 from base
val sector = sectorService.createSector(
playerEntry.actorId,
player.world.name,
centerLoc.blockX,
centerLoc.blockY,
centerLoc.blockZ
)
if (sector == null) {
player.sendMessage(Component.text("Failed to create sector.", NamedTextColor.RED))
event.isCancelled = true
return
}
// Create Visuals
val sectorKey = NamespacedKey(plugin, "sector_id")
val visualCenter = centerLoc.clone().add(0.5, 0.0, 0.5) // Center of the block space
// 1. Command Block (Center + 0.5y) -> Matches old base+1.5
val cbLoc = visualCenter.clone().add(0.0, 0.5, 0.0)
val cb = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
cb.block = Material.COMMAND_BLOCK.createBlockData {
(it as org.bukkit.block.data.Directional).facing = org.bukkit.block.BlockFace.UP
}
cb.transformation = Transformation(
Vector3f(-0.25f, -0.25f, -0.25f),
Quaternionf(0f, 0f, 0f, 1f),
Vector3f(0.5f, 0.5f, 0.5f),
Quaternionf(0f, 0f, 0f, 1f)
)
cb.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 2. Tinted Glass (Center + 0.5y) -> Matches old base+1.5
val glass = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
glass.block = Material.TINTED_GLASS.createBlockData()
glass.transformation = Transformation(
Vector3f(-0.4375f, -0.4375f, -0.4375f),
Quaternionf(0f, 0f, 0f, 1f),
Vector3f(0.875f, 0.875f, 0.875f),
Quaternionf(0f, 0f, 0f, 1f)
)
glass.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 3. Top Cauldron (Center + 1.3125y) -> Matches old base+2.3125
val topCauldronLoc = visualCenter.clone().add(0.0, 1.3125, 0.0)
val topCauldron = player.world.spawnEntity(topCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
topCauldron.block = Material.CAULDRON.createBlockData()
topCauldron.transformation = Transformation(
Vector3f(0.4990f, -0.4990f, 0.4990f),
Quaternionf(0f, 1f, 0f, 0f),
Vector3f(0.9980f, 0.9980f, 0.9980f),
Quaternionf(0f, 0f, 0f, 1f)
)
topCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 4. Bottom Cauldron (Center - 0.3125y) -> Matches old base+0.6875
val botCauldronLoc = visualCenter.clone().add(0.0, -0.3125, 0.0)
val botCauldron = player.world.spawnEntity(botCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
botCauldron.block = Material.CAULDRON.createBlockData()
botCauldron.transformation = Transformation(
Vector3f(-0.4990f, 0.4990f, 0.4990f),
Quaternionf(1f, 0f, 0f, 0f),
Vector3f(0.9980f, 0.9980f, 0.9980f),
Quaternionf(0f, 0f, 0f, 1f)
)
botCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// Spawn Shulker (Hitbox) at Center
val shulkerLoc = visualCenter.clone() // Center is exactly y+1 from base
val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
shulker.setAI(false)
shulker.isInvisible = true
// shulker.isInvulnerable = true // REMOVED to allow damage
val param = shulker.getAttribute(Attribute.MAX_HEALTH)
param?.baseValue = 1000.0
shulker.health = 1000.0
shulker.maximumNoDamageTicks = 0
// Tag Shulker with Sector ID
shulker.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// Place Blocks (Base and Top)
// Base is at center.y - 1 (The placed block)
// Top is at center.y + 1
block.type = Material.DEEPSLATE_TILE_SLAB
val bottomSlab = block.blockData as Slab
bottomSlab.type = Slab.Type.BOTTOM
block.blockData = bottomSlab
val topBlock = centerLoc.clone().add(0.0, 1.0, 0.0).block
topBlock.type = Material.DEEPSLATE_TILE_SLAB
val topSlab = topBlock.blockData as Slab
topSlab.type = Slab.Type.TOP
topBlock.blockData = topSlab
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
}
@EventHandler
fun onInteractEntity(event: PlayerInteractEntityEvent) {
if (event.hand != org.bukkit.inventory.EquipmentSlot.HAND) return
val entity = event.rightClicked
if (entity !is Shulker) return
val sectorKey = NamespacedKey(plugin, "sector_id")
if (!entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) return
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
val player = event.player
// Ownership Check
val pEntry = playerIdService.find(player.uniqueId)
if (pEntry == null) return
val sector = sectorService.getSector(sectorId) ?: return
// Resolve Owner Name
var ownerName = "Unknown"
// Try getting faction name
val factionName = factionService.getFactionName(sector.ownerActorId)
if (factionName != null) {
ownerName = "Faction: $factionName"
} else {
// Assume player. Ideally resolve UUID from ActorID, but for now ID or fallback
// We don't have direct Player Actor ID -> Name cache here easily without querying DB or UUID service
// Just show ID for now to be safe, or "Player #ID"
ownerName = "Player #${sector.ownerActorId}"
}
val canManage = if (sector.ownerActorId == pEntry.actorId) {
true
} else {
// Or if owner is my faction and I am EXEC/OWNER
val myFaction = factionService.getFactionOfPlayer(player.uniqueId)
if (myFaction == sector.ownerActorId) {
val myRole = factionService.getRole(myFaction, player.uniqueId)
myRole == FactionRole.OWNER || myRole == FactionRole.EXEC
} else {
false
}
}
if (!canManage) {
player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED))
return
}
// Create Written Book for UI
val book = org.bukkit.inventory.ItemStack(Material.WRITTEN_BOOK)
val meta = book.itemMeta as org.bukkit.inventory.meta.BookMeta
meta.title(Component.text("Sector Manager"))
meta.author(Component.text("System"))
// Build Page Content
val content = Component.text()
.append(Component.text("Sector Core @ ${sector.x},${sector.y},${sector.z}\n", NamedTextColor.BLACK))
.append(Component.text("Owner: $ownerName\n\n", NamedTextColor.DARK_GRAY))
// Transfer Check
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 operation transfer $sectorId $myFaction"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction")))
)
}
}
// Actions Row 1
// 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 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 operation givetool $sectorId"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to get selection tool")))
)
// Ranges List
val ranges = sectorService.getRanges(sectorId)
if (ranges.isEmpty()) {
content.append(Component.text("Parts: None", NamedTextColor.GRAY))
} else {
content.append(Component.text("Parts:", NamedTextColor.BLACK))
ranges.forEach { range ->
content.append(Component.text("\n"))
content.append(
Component.text("[x] ", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector operation range delete $sectorId ${range.id}"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range")))
)
val info = if (range.type == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
"(${range.x1},${range.y1},${range.z1})~(${range.x2},${range.y2},${range.z2})"
} else {
// Cylinder: x1,y1,z1 is Center. x2=R, y2=Bottom, z2=Top
val h = 1 + range.y2 + range.z2
"Cyl @(${range.x1},${range.y1},${range.z1}) R:${range.x2} H:$h"
}
content.append(Component.text(info, NamedTextColor.DARK_GRAY))
}
}
meta.addPages(content.build())
book.itemMeta = meta
player.openBook(book)
event.isCancelled = true
}
@EventHandler
fun onDamage(event: EntityDamageEvent) {
val entity = event.entity
if (entity !is Shulker) return
val sectorKey = NamespacedKey(plugin, "sector_id")
if (!entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) return
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
// Prevent vanilla death logic but show damage effect
// We restore health to max after processing
val damage = event.finalDamage
val maxHealth = entity.getAttribute(Attribute.MAX_HEALTH)?.value ?: 1000.0
// Cancel the event so it doesn't actually die in vanilla terms,
// OR let it happen but force health reset.
// Cancelling prevents the red flash in some versions/cases, so setting damage to 0 is often better
// if we want the effect. But resetting health is robust.
val oldHp = sectorService.getHealth(sectorId) ?: return // Get current health from service
val newHp = sectorService.reduceHealth(sectorId, damage.toInt())
entity.health = maxHealth
entity.noDamageTicks = 0 // Disable invulnerability
if (newHp != null) {
val loc = entity.location
val world = entity.world
// Sound Logic
val threshold = 100 // 10% of 1000
val crossedThreshold = (oldHp / threshold) > (newHp / threshold)
if (crossedThreshold) {
world.playSound(loc, Sound.BLOCK_ANVIL_PLACE, 1.0f, 0.5f)
} else {
world.playSound(loc, Sound.BLOCK_STONE_BREAK, 1.0f, 1.0f)
}
world.spawnParticle(Particle.BLOCK, loc.add(0.0, 0.5, 0.0), 20, 0.3, 0.3, 0.3, Material.BEDROCK.createBlockData())
if (event is EntityDamageByEntityEvent && event.damager is Player) {
val player = event.damager as Player
val percent = newHp.toDouble() / maxHealth
val color = when {
percent > 0.5 -> NamedTextColor.GREEN
percent > 0.2 -> NamedTextColor.YELLOW
else -> NamedTextColor.RED
}
val progressBar = createProgressBar(newHp, maxHealth.toInt(), color)
val message = Component.text()
.append(Component.text("Sector Core ", NamedTextColor.GOLD))
.append(progressBar)
.append(Component.text(" "))
.append(Component.text(newHp, color))
.append(Component.text("/", NamedTextColor.GRAY))
.append(Component.text(maxHealth.toInt(), NamedTextColor.GRAY))
.build()
player.sendActionBar(message)
}
if (newHp <= 0) {
// Destruction
entity.remove()
// Remove other visuals in chunk
val chunk = entity.chunk
chunk.entities.forEach { ent ->
val sId = ent.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
if (sId == sectorId) ent.remove()
}
// Remove blocks.
val x = loc.blockX
val y = loc.blockY
val z = loc.blockZ
val base = world.getBlockAt(x, y - 1, z)
val top = world.getBlockAt(x, y + 1, z)
if (base.type == Material.DEEPSLATE_TILE_SLAB) base.type = Material.AIR
if (top.type == Material.DEEPSLATE_TILE_SLAB) top.type = Material.AIR
world.dropItemNaturally(loc, org.bukkit.inventory.ItemStack(Material.DEEPSLATE_TILE_SLAB, 2))
}
}
}
private fun createProgressBar(current: Int, max: Int, color: NamedTextColor): Component {
val totalBars = 20
val percent = current.toDouble() / max.toDouble()
val filledBars = (totalBars * percent).toInt().coerceIn(0, totalBars)
val emptyBars = totalBars - filledBars
return Component.text()
.append(Component.text("[", NamedTextColor.DARK_GRAY))
.append(Component.text("|".repeat(filledBars), color))
.append(Component.text("|".repeat(emptyBars), NamedTextColor.GRAY))
.append(Component.text("]", NamedTextColor.DARK_GRAY))
.build()
}
@EventHandler
fun onChunkLoad(event: ChunkLoadEvent) {
if (event.isNewChunk) return // Optimization: New chunks won't have old sectors to cleanup
val chunk = event.chunk
val entities = chunk.entities
val sectorKey = NamespacedKey(plugin, "sector_id")
entities.forEach { entity ->
if (entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return@forEach
// Check existance async
plugin.server.scheduler.runTaskAsynchronously(plugin, Runnable {
val exists = sectorService.exists(sectorId)
if (!exists) {
plugin.server.scheduler.runTask(plugin, Runnable {
// Re-verify ent validity
if (entity.isValid) {
entity.remove()
// We should also clean up blocks, but that requires knowing where they are relative to entity
val loc = entity.location
val world = entity.world
val x = loc.blockX
val y = loc.blockY
val z = loc.blockZ
// Entity is at x, y, z
// Base is y-1, Top is y+1
val base = world.getBlockAt(x, y - 1, z)
val top = world.getBlockAt(x, y + 1, z)
if (base.type == Material.DEEPSLATE_TILE_SLAB) base.type = Material.AIR
if (top.type == Material.DEEPSLATE_TILE_SLAB) top.type = Material.AIR
plugin.logger.info("Removed orphaned sector artifacts for ID $sectorId at $x,$y,$z")
}
})
}
})
}
}
}
}

View File

@ -1,204 +0,0 @@
package net.hareworks.hcu.landsector.listener
import net.hareworks.hcu.landsector.LandSectorPlugin
import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService
import net.hareworks.hcu.landsector.model.SelectionMode
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.Action
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.persistence.PersistentDataType
import org.bukkit.inventory.EquipmentSlot
class SelectionListener(
private val plugin: LandSectorPlugin,
private val selectionService: SelectionService,
private val sectorService: SectorService
) : Listener {
@EventHandler
fun onInteract(event: PlayerInteractEvent) {
val item = event.item ?: return
if (item.type != Material.FLINT) return
val key = NamespacedKey(plugin, "component")
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) != "land_sector_tool") {
return
}
// Only Main Hand to avoid double fire
if (event.hand != EquipmentSlot.HAND) return
val player = event.player
val selection = selectionService.getSelection(player.uniqueId)
// Left Click: Cancel only
if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
event.isCancelled = true
return
}
// Right Click: Set Points
if (event.action == Action.RIGHT_CLICK_BLOCK || event.action == Action.RIGHT_CLICK_AIR) {
event.isCancelled = true // Prevent placing or item usage
val targetBlock = event.clickedBlock ?: player.rayTraceBlocks(100.0)?.hitBlock
if (targetBlock == null) return
val loc = targetBlock.location
if (selection.point1 == null) {
// Set Point 1
selection.point1 = loc
// selection.isCenterMode is persistent, so we don't set it here based on sneaking
selection.point2 = null
player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN))
player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 2.0f)
} else {
// Set Point 2
if (selection.point2 != null) {
// Resetting, treat as P1
selection.point1 = loc
selection.point2 = null
player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN))
player.playSound(player.location, org.bukkit.Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.5f)
} else {
selection.point2 = loc
// Check for sector ID
val itemStack = event.item
val sectorKey = NamespacedKey(plugin, "sector_id")
if (itemStack != null && itemStack.itemMeta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val sId = itemStack.itemMeta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)!!
val p1 = selection.point1!!
val p2 = selection.point2!!
// Add Range
val range = sectorService.addRange(
sId,
selection.mode,
p1.blockX, p1.blockY, p1.blockZ,
p2.blockX, p2.blockY, p2.blockZ,
selection.isCenterMode
)
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN))
player.playSound(player.location, org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 2.0f)
// Clear selection
selection.point1 = null
selection.point2 = null
} else {
player.sendMessage(Component.text("Position 2 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}", NamedTextColor.GREEN))
// Show Cost / Area
player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA))
player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD))
player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 1.0f)
}
}
}
}
}
@EventHandler
fun onDrop(event: org.bukkit.event.player.PlayerDropItemEvent) {
val item = event.itemDrop.itemStack
val key = NamespacedKey(plugin, "component")
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") {
event.isCancelled = true
val player = event.player
val selection = selectionService.getSelection(player.uniqueId)
if (player.isSneaking) {
// Toggle Center/Corner Mode
selection.isCenterMode = !selection.isCenterMode
player.sendMessage(Component.text("Selection Modifier: ${if (selection.isCenterMode) "Center/Base Mode" else "Corner/Symmetric Mode"}", NamedTextColor.GOLD))
player.playSound(player.location, org.bukkit.Sound.BLOCK_COMPARATOR_CLICK, 1.0f, 1.0f)
} else {
// Toggle Shape Mode
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
player.sendMessage(Component.text("Shape: ${selection.mode}", NamedTextColor.YELLOW))
player.playSound(player.location, org.bukkit.Sound.ITEM_BOOK_PAGE_TURN, 1.0f, 1.0f)
// Reset selection on mode switch
selection.point1 = null
selection.point2 = null
}
}
}
@EventHandler
fun onInventoryClick(event: org.bukkit.event.inventory.InventoryClickEvent) {
val player = event.whoClicked as? org.bukkit.entity.Player ?: return
val clickedInv = event.clickedInventory
val action = event.action
fun isTool(stack: org.bukkit.inventory.ItemStack?): Boolean {
if (stack == null || stack.type != Material.FLINT) return false
val key = NamespacedKey(plugin, "component")
return stack.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
}
// 1. Prevent dropping via inventory
if (action.name.startsWith("DROP")) {
if (isTool(event.currentItem) || isTool(event.cursor)) {
event.isCancelled = true
player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
return
}
}
// 2. Prevent interaction with tool in external inventories (Top Inventory)
// If clicking inside an inventory that is NOT the player's inventory
if (clickedInv != null && clickedInv != player.inventory) {
// If trying to place tool (Cursor has tool)
if (isTool(event.cursor)) {
event.isCancelled = true
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
return
}
// If trying to swap tool from hotbar (Number key)
if (event.click == org.bukkit.event.inventory.ClickType.NUMBER_KEY) {
val swappedItem = player.inventory.getItem(event.hotbarButton)
if (isTool(swappedItem)) {
event.isCancelled = true
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
return
}
}
// If trying to take tool IS in external inventory? (Shouldn't happen, but prevent taking just in case)
// This safeguards if somehow it got there.
if (isTool(event.currentItem)) {
event.isCancelled = true
// Allow them to break the item? No, just block interaction.
return
}
}
// 3. Prevent Shift-Clicking tool FROM player inventory TO external inventory
if (clickedInv == player.inventory && event.isShiftClick) {
if (isTool(event.currentItem)) {
// If top inventory is NOT Crafting/Creative (Personal), block transfer
val topType = event.view.topInventory.type
if (topType != org.bukkit.event.inventory.InventoryType.CRAFTING &&
topType != org.bukkit.event.inventory.InventoryType.CREATIVE) {
event.isCancelled = true
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
return
}
}
}
}
}

View File

@ -1,12 +0,0 @@
package net.hareworks.hcu.landsector.model
data class Sector(
val id: Int,
val ownerActorId: Int,
val world: String,
val x: Int,
val y: Int,
val z: Int,
val hp: Int,
val landId: Int? = null
)

View File

@ -1,14 +0,0 @@
package net.hareworks.hcu.landsector.model
data class SectorRange(
val id: Int,
val sectorId: Int,
val type: SelectionMode,
val x1: Int,
val y1: Int,
val z1: Int,
val x2: Int,
val y2: Int,
val z2: Int,
val isSneaking: Boolean
)

View File

@ -1,15 +0,0 @@
package net.hareworks.hcu.landsector.model
import org.bukkit.Location
enum class SelectionMode {
CUBOID,
CYLINDER
}
data class SelectionData(
var mode: SelectionMode = SelectionMode.CUBOID,
var point1: Location? = null,
var point2: Location? = null,
var isCenterMode: Boolean = false // Replaces old p1Sneaking logic with explicit toggle
)

View File

@ -1,637 +0,0 @@
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
import net.hareworks.hcu.landsector.model.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode
import net.hareworks.hcu.lands.model.LandData
import net.hareworks.hcu.lands.model.Shape
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.select
import org.jetbrains.exposed.v1.jdbc.update
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 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>()
private val sectorsCache = ConcurrentHashMap<Int, Sector>()
// Draft storage: SectorId -> Land (Draft)
private val draftLands = ConcurrentHashMap<Int, net.hareworks.hcu.lands.model.Land>()
fun init() {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorDraftsTable)
// Load Sectors
SectorsTable.selectAll().forEach {
val id = it[SectorsTable.id]
val sector = Sector(
id,
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
it[SectorsTable.hp],
it[SectorsTable.landId]
)
sectorsCache[id] = sector
}
// Load Drafts
SectorDraftsTable.selectAll().forEach {
val sId = it[SectorDraftsTable.sectorId]
val data = it[SectorDraftsTable.data]
val sector = sectorsCache[sId]
if (sector != null && sector.landId == null) {
draftLands[sId] = net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sId",
actorId = sector.ownerActorId,
world = sector.world,
data = data
)
}
}
}
}
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 {
it[SectorsTable.ownerActorId] = ownerActorId
it[SectorsTable.world] = world
it[SectorsTable.x] = x
it[SectorsTable.y] = y
it[SectorsTable.z] = z
it[SectorsTable.hp] = 1000
it[SectorsTable.landId] = null
}[SectorsTable.id]
hpCache[id] = 1000
val sector = Sector(id, ownerActorId, world, x, y, z, 1000, null)
sectorsCache[id] = sector
// Initialize draft land
val draftLand = net.hareworks.hcu.lands.model.Land(
id = -1, // Dummy ID
name = "Draft Sector $id",
actorId = ownerActorId,
world = world,
data = net.hareworks.hcu.lands.model.LandData()
)
draftLands[id] = draftLand
// Persist draft
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = id
it[SectorDraftsTable.data] = draftLand.data
}
sector
}
}
fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? {
// Optimized to use cache
return sectorsCache.values.firstOrNull {
it.world == world && it.x == x && it.y == y && it.z == z
}
}
fun getSector(id: Int): Sector? {
return sectorsCache[id] ?: transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()?.let {
Sector(
it[SectorsTable.id],
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
it[SectorsTable.hp],
it[SectorsTable.landId]
)
}
}
}
fun getHealth(id: Int): Int? {
// Try get from cache
var currentHp = hpCache[id]
// If not in cache, load from DB
if (currentHp == null) {
val sector = sectorsCache[id] ?: return null
currentHp = sector.hp
hpCache[id] = currentHp
}
return currentHp
}
fun reduceHealth(id: Int, amount: Int): Int? {
// Try get from cache
var currentHp = hpCache[id]
// If not in cache, try to init from sectorsCache
if (currentHp == null) {
val sector = sectorsCache[id] ?: return null
currentHp = sector.hp
hpCache[id] = currentHp
}
val newHp = (currentHp!! - amount).coerceAtLeast(0)
hpCache[id] = newHp
dirtySet.add(id)
return newHp
}
fun flushChanges() {
if (dirtySet.isEmpty()) return
// Take a snapshot of keys to update
val toUpdate = dirtySet.toMutableSet()
dirtySet.removeAll(toUpdate) // Clear them from dirty set so we don't re-save unless modified again
if (toUpdate.isEmpty()) return
transaction(database) {
toUpdate.forEach { id ->
val hp = hpCache[id] ?: return@forEach
SectorsTable.update({ SectorsTable.id eq id }) {
it[SectorsTable.hp] = hp
}
}
}
}
fun deleteSector(id: Int): Sector? {
val sector = transaction(database) {
val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null
SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq id }
SectorsTable.deleteWhere { SectorsTable.id eq id }
Sector(
record[SectorsTable.id],
record[SectorsTable.ownerActorId],
record[SectorsTable.world],
record[SectorsTable.x],
record[SectorsTable.y],
record[SectorsTable.z],
record[SectorsTable.hp],
record[SectorsTable.landId]
)
}
if (sector != null) {
hpCache.remove(id)
dirtySet.remove(id)
sectorsCache.remove(id)
draftLands.remove(id)
if (sector.landId != null) {
landService.deleteLand(sector.landId)
}
}
return sector
}
fun isSectorBlock(world: String, x: Int, y: Int, z: Int): Boolean {
// Sector blocks are at center.y - 1 (Base) and center.y + 1 (Top)
// Center is at sector.y
return sectorsCache.values.any {
it.world == world &&
it.x == x &&
it.z == z &&
(it.y - 1 == y || it.y + 1 == y)
}
}
fun isSectorArea(world: String, x: Int, y: Int, z: Int): Boolean {
// Check 3x3x3 around center (sector.x, sector.y, sector.z)
return sectorsCache.values.any {
it.world == world &&
x >= it.x - 1 && x <= it.x + 1 &&
y >= it.y - 1 && y <= it.y + 1 &&
z >= it.z - 1 && z <= it.z + 1
}
}
fun exists(id: Int): Boolean {
if (hpCache.containsKey(id)) return true
return transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.count() > 0
}
}
fun getAllSectorsList(): List<Sector> {
return transaction(database) {
SectorsTable.selectAll().map {
val id = it[SectorsTable.id]
val cachedHp = hpCache[id]
val hp = cachedHp ?: it[SectorsTable.hp]
Sector(
id,
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
hp,
it[SectorsTable.landId]
)
}
}
}
fun getAllSectors(world: String): List<Sector> {
return transaction(database) {
SectorsTable.selectAll()
.andWhere { SectorsTable.world eq world }
.map {
// Update HP from cache if exists
val id = it[SectorsTable.id]
val cachedHp = hpCache[id]
val hp = cachedHp ?: it[SectorsTable.hp]
Sector(
id,
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
hp,
it[SectorsTable.landId]
)
}
}
}
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): Any? {
val sector = getSector(sectorId) ?: return null
// Cannot edit if already confirmed
if (sector.landId != null) return null
val draft = draftLands.computeIfAbsent(sectorId) {
net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sectorId",
actorId = sector.ownerActorId,
world = sector.world,
data = net.hareworks.hcu.lands.model.LandData()
)
}
val shape = when (mode) {
SelectionMode.CUBOID -> {
net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2)
}
SelectionMode.CYLINDER -> {
// Convert bounds to cylinder params
// Approximating from bounding box
val minX = minOf(x1, x2)
val maxX = maxOf(x1, x2)
val minZ = minOf(z1, z2)
val maxZ = maxOf(z1, z2)
val minY = minOf(y1, y2)
val maxY = maxOf(y1, y2)
val cx = (minX + maxX) / 2
val cz = (minZ + maxZ) / 2
val cy = (minY + maxY) / 2
// Radius is max dist from center to edge of bounding box in X or Z
val radiusX = (maxX - minX) / 2.0
val radiusZ = (maxZ - minZ) / 2.0
val radius = maxOf(radiusX, radiusZ)
val height = maxY - minY + 1
val bottom = (cy - minY)
val top = (maxY - cy)
net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top)
}
else -> return null
}
// Use LandService modify pattern or direct replacement since stored in local map
val newParts = draft.data.parts.toMutableList()
newParts.add(shape)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
// Check if exists first? Or upsert if supported easily.
// We know it exists from creation, but safety check.
val count = SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
if (count == 0) {
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = sectorId
it[SectorDraftsTable.data] = newDraft.data
}
}
}
return shape
}
fun getRanges(sectorId: Int): List<SectorRange> {
val sector = getSector(sectorId) ?: return emptyList()
val land = if (sector.landId != null) {
landService.getLand(sector.landId)
} else {
draftLands[sectorId]
} ?: return emptyList()
// Convert Shapes back to SectorRanges for UI compatibility
return land.data.parts.mapIndexed { index, shape ->
when (shape) {
is net.hareworks.hcu.lands.model.Shape.Cuboid -> {
SectorRange(
index, // Use index as ID for UI
sectorId,
SelectionMode.CUBOID,
shape.x1, shape.y1, shape.z1,
shape.x2, shape.y2, shape.z2,
false
)
}
is net.hareworks.hcu.lands.model.Shape.Cylinder -> {
SectorRange(
index,
sectorId,
SelectionMode.CYLINDER,
shape.x, shape.y, shape.z,
shape.radius.toInt(), shape.bottomHeight, shape.topHeight,
false
)
}
// Default fallback for unknown shapes if any (shouldn't happen with current limited types)
else -> SectorRange(index, sectorId, SelectionMode.CUBOID, 0,0,0,0,0,0, false)
}
}
}
fun getSectorShapes(sectorId: Int): List<net.hareworks.hcu.lands.model.Shape> {
val sector = getSector(sectorId) ?: return emptyList()
val land = if (sector.landId != null) {
landService.getLand(sector.landId)
} else {
draftLands[sectorId]
} ?: return emptyList()
return land.data.parts
}
fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Cannot edit confirmed
val draft = draftLands[sectorId] ?: return false
if (rangeIndex < 0 || rangeIndex >= draft.data.parts.size) return false
val newParts = draft.data.parts.toMutableList()
newParts.removeAt(rangeIndex)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
}
return true
}
fun activateSector(sectorId: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Already active
val draft = draftLands[sectorId] ?: return false
if (draft.data.parts.isEmpty()) return false
val landName = "Sector_${sectorId}_${System.currentTimeMillis()}"
if (landService.createLand(landName, draft.actorId, draft.world)) {
// Find the created land to get ID
val lands = landService.findLandsByName(landName)
val createdLand = lands.firstOrNull() ?: return false
// Update parts
landService.modifyLand(createdLand.id) { data ->
data.copy(parts = draft.data.parts)
}
// Update Sector DB & Delete Draft
transaction(database) {
SectorsTable.update({ SectorsTable.id eq sectorId }) {
it[SectorsTable.landId] = createdLand.id
}
SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq sectorId }
}
// Update cache & Clear draft
sectorsCache[sectorId] = sector.copy(landId = createdLand.id)
draftLands.remove(sectorId)
return true
}
return false
}
fun transferSector(id: Int, newOwnerActorId: Int): Boolean {
return transaction(database) {
val updated = SectorsTable.update({ SectorsTable.id eq id }) {
it[ownerActorId] = newOwnerActorId
}
if (updated > 0) {
// Update cache
val current = sectorsCache[id]
if (current != null) {
sectorsCache[id] = current.copy(ownerActorId = newOwnerActorId)
}
true
} else {
false
}
}
}
}

View File

@ -1,103 +0,0 @@
package net.hareworks.hcu.landsector.service
import net.hareworks.hcu.landsector.model.SelectionData
import org.bukkit.Location
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs
import kotlin.math.PI
import kotlin.math.ceil
class SelectionService {
private val selections = ConcurrentHashMap<UUID, SelectionData>()
fun getSelection(uuid: UUID): SelectionData {
return selections.computeIfAbsent(uuid) { SelectionData() }
}
fun clearSelection(uuid: UUID) {
selections.remove(uuid)
}
fun getAreaDetails(uuid: UUID): String {
val data = getSelection(uuid)
if (data.point1 == null || data.point2 == null) {
return "No complete selection."
}
val p1 = data.point1!!
val p2 = data.point2!!
return if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
val width: Int
val height: Int
val length: Int
if (!data.isCenterMode) {
// Normal: P1 to P2
width = abs(p1.blockX - p2.blockX) + 1
height = abs(p1.blockY - p2.blockY) + 1
length = abs(p1.blockZ - p2.blockZ) + 1
} else {
// Center Mode: P1 is center
val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ)
width = dx * 2 + 1
height = dy * 2 + 1
length = dz * 2 + 1
}
"Cuboid: ${width}x${height}x${length} (${width * height * length} blocks)"
} else {
// Cylinder
val dx = p1.blockX - p2.blockX
val dz = p1.blockZ - p2.blockZ
// Use ceil(dist - 0.5) to find the minimum integer radius R such that (dist <= R + 0.5)
// This ensures the point is included in the R+0.5 boundary without overshooting to the next integer.
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
val baseRadius = ceil(dist - 0.5)
val radius = baseRadius + 0.5
val totalHeight = if (!data.isCenterMode) {
// Normal: P1 center, symmetric height
val h = abs(p1.blockY - p2.blockY)
h * 2 + 1
} else {
// Center Mode: P1 base, P2 top (or vice versa, height is diff)
abs(p1.blockY - p2.blockY) + 1
}
val volume = PI * radius * radius * totalHeight
"Cylinder: r=${"%.0f".format(baseRadius)}, h=$totalHeight (~${ceil(volume).toInt()} blocks)"
}
}
fun getCost(uuid: UUID): Double {
val data = getSelection(uuid)
if (data.point1 == null || data.point2 == null) {
return 0.0
}
// Placeholder cost logic: 1 block = 1 unit?
// Need to refine based on actual volume
val p1 = data.point1!!
val p2 = data.point2!!
val volume = if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
val width = abs(p1.blockX - p2.blockX) + 1
val height = abs(p1.blockY - p2.blockY) + 1
val length = abs(p1.blockZ - p2.blockZ) + 1
(width * height * length).toDouble()
} else {
val dx = p1.blockX - p2.blockX
val dz = p1.blockZ - p2.blockZ
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
val baseRadius = ceil(dist - 0.5)
val radius = baseRadius + 0.5
val height = abs(p1.blockY - p2.blockY) + 1
PI * radius * radius * height
}
return volume * 0.5 // Example rate
}
}

View File

@ -1,88 +0,0 @@
package net.hareworks.hcu.landsector.task
import net.hareworks.hcu.landsector.LandSectorPlugin
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.entity.BlockDisplay
import org.bukkit.entity.EntityType
import org.bukkit.persistence.PersistentDataType
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.util.Transformation
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.PI
class SectorRotationTask(private val plugin: LandSectorPlugin) : BukkitRunnable() {
private val sectorKey = NamespacedKey(plugin, "sector_id")
private val scale = Vector3f(0.5f, 0.5f, 0.5f)
private val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // Scale 0.5 * Model Center (0.5, 0.5, 0.5)
override fun run() {
val sectors = plugin.sectorService?.getAllSectorsList() ?: return
// Bukkit.getLogger().info("Task running. Sectors count: ${sectors.size}")
sectors.forEach { sector ->
val world = Bukkit.getWorld(sector.world) ?: return@forEach
// Simple check if chunk is loaded to avoid loading chunks
val chunkX = sector.x shr 4
val chunkZ = sector.z shr 4
if (!world.isChunkLoaded(chunkX, chunkZ)) {
// Bukkit.getLogger().info("Chunk not loaded for sector ${sector.id}")
return@forEach
}
// Check entities at the core location
val centerLoc = org.bukkit.Location(world, sector.x + 0.5, sector.y + 0.5, sector.z + 0.5)
// Search radius expanded to 2.0 to ensure hit
val entities = world.getNearbyEntities(centerLoc, 2.0, 2.0, 2.0)
// Bukkit.getLogger().info("Checking sector ${sector.id} at $centerLoc. Found entities: ${entities.size}")
entities.forEach { entity ->
if (entity.type == EntityType.BLOCK_DISPLAY && entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val bd = entity as BlockDisplay
// Check if it's the command block
if (bd.block.material == Material.COMMAND_BLOCK) {
// Bukkit.getLogger().info("Rotater found target: ${bd.uniqueId}")
updateRotation(bd)
}
}
}
}
}
private fun updateRotation(bd: BlockDisplay) {
// Continuous rotation based on time: 1 full rotation every 4 seconds
val periodMs = 4000L
val time = System.currentTimeMillis()
val phase = (time % periodMs).toDouble() / periodMs.toDouble()
val angle = (phase * 2 * Math.PI).toFloat() // 0 to 2PI
// Log for debug (once per 100 ticks approx to avoid spam? No, just once to verify)
// Bukkit.getLogger().info("Rotating BD: angle=$angle")
// Rotation around Y axis
val rot = Quaternionf().rotateY(angle)
val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // The center in Scaled space
// T = - (R * C)
val rotatedOffset = Vector3f(centerOffset).rotate(rot)
val newTranslation = Vector3f(rotatedOffset).negate()
val newTrans = Transformation(
newTranslation,
rot,
scale,
Quaternionf(0f, 0f, 0f, 1f)
)
// Interpolation settings
bd.interpolationDuration = 3 // Short duration to match high frequency update
bd.interpolationDelay = -1 // Start immediately
bd.transformation = newTrans
}
}

View File

@ -1,229 +0,0 @@
package net.hareworks.hcu.landsector.task
import net.hareworks.hcu.landsector.model.SelectionMode
import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService
import net.hareworks.hcu.visualizer.GeometryVisualizer
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Bukkit
import org.bukkit.Color
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.entity.Player
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.persistence.PersistentDataType
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitRunnable
import kotlin.math.abs
class SelectionVisualizerTask(
private val plugin: JavaPlugin,
private val selectionService: SelectionService,
private val sectorService: SectorService
) : BukkitRunnable() {
override fun run() {
for (player in Bukkit.getOnlinePlayers()) {
visualize(player)
}
}
private fun visualize(player: Player) {
val key = NamespacedKey(plugin, "component")
val sectorKey = NamespacedKey(plugin, "sector_id")
// Find tool in inventory to get sector ID
var sectorId: Int? = null
var hasTool = false
// 1. Check held items first (Priority: Held ID > Inventory ID)
val heldItems = listOf(player.inventory.itemInMainHand, player.inventory.itemInOffHand)
for (item in heldItems) {
if (item.type == Material.FLINT) {
val meta = item.itemMeta
if (meta != null && meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") {
hasTool = true
if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
break // Found specific sector tool in hand, prioritize this.
}
}
}
}
// 2. If no specific sector ID found in hand, scan inventory for any linked tool
if (sectorId == null) {
for (item in player.inventory.contents) {
if (item != null && item.type == Material.FLINT) {
val meta = item.itemMeta ?: continue
if (meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") {
hasTool = true
if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
break // Found linked tool in inventory.
}
}
}
}
}
if (!hasTool) return
// 1. Visualize Existing Ranges (Green)
if (sectorId != null) {
// World Check
val sector = sectorService.getSector(sectorId)
if (sector != null && sector.world == player.world.name) {
val shapes = sectorService.getSectorShapes(sectorId)
val color = Color.fromRGB(0, 255, 0) // Green for saved
for (shape in shapes) {
when (shape) {
is net.hareworks.hcu.lands.model.Shape.Cuboid -> {
val minX = minOf(shape.x1, shape.x2).toDouble()
val minY = minOf(shape.y1, shape.y2).toDouble()
val minZ = minOf(shape.z1, shape.z2).toDouble()
val maxX = maxOf(shape.x1, shape.x2).toDouble() + 1.0
val maxY = maxOf(shape.y1, shape.y2).toDouble() + 1.0
val maxZ = maxOf(shape.z1, shape.z2).toDouble() + 1.0
GeometryVisualizer.drawCuboid(
player,
minX, minY, minZ,
maxX, maxY, maxZ,
color
)
}
is net.hareworks.hcu.lands.model.Shape.Cylinder -> {
val minY = (shape.y - shape.bottomHeight).toDouble()
val maxY = (shape.y + shape.topHeight + 1).toDouble()
GeometryVisualizer.drawCylinder(
player,
shape.x + 0.5, shape.y.toDouble(), shape.z + 0.5,
shape.radius.toInt(),
minY,
maxY,
color
)
}
}
}
}
}
// 2. Visualize Current Selection (Blue)
val selection = selectionService.getSelection(player.uniqueId)
val p1 = selection.point1 ?: return
var p2 = selection.point2
if (p2 == null) {
val target = player.getTargetBlockExact(30)
if (target != null) {
p2 = target.location
}
}
if (p2 == null) return
if (p1.world != p2.world) return
// ... (Existing Draw Logic for Selection) ...
// ... (Existing Draw Logic for Selection) ...
if (selection.mode == SelectionMode.CUBOID) {
var minX: Double
var minY: Double
var minZ: Double
var maxX: Double
var maxY: Double
var maxZ: Double
if (!selection.isCenterMode) {
// Normal: Corner to Corner (Diagonal)
minX = minOf(p1.blockX, p2.blockX).toDouble()
minY = minOf(p1.blockY, p2.blockY).toDouble()
minZ = minOf(p1.blockZ, p2.blockZ).toDouble()
maxX = maxOf(p1.blockX, p2.blockX).toDouble() + 1.0
maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0
maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0
} else {
// Center Mode: P1 is Center
val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ)
minX = (p1.blockX - dx).toDouble()
maxX = (p1.blockX + dx).toDouble() + 1.0
minY = (p1.blockY - dy).toDouble()
maxY = (p1.blockY + dy).toDouble() + 1.0
minZ = (p1.blockZ - dz).toDouble()
maxZ = (p1.blockZ + dz).toDouble() + 1.0
}
GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ)
} else {
val centerX: Double
val centerY: Double // This variable is actually unused in the logic below if we override args, but kept for clarity if needed
val centerZ: Double
val radius: Double
val minY: Double
val maxY: Double
val dx = p1.blockX - p2.blockX
val dz = p1.blockZ - p2.blockZ
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
radius = kotlin.math.ceil(dist - 0.5)
if (!selection.isCenterMode) {
// Normal: P1 is Center, Symmetric Height
centerX = p1.blockX + 0.5
centerY = p1.blockY + 0.5
centerZ = p1.blockZ + 0.5
val hDiff = abs(p1.blockY - p2.blockY)
val baseBlockY = p1.blockY - hDiff
val topBlockY = p1.blockY + hDiff
minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0
} else {
// Center/Base Mode: P1 is Base, P2 defines Height/Radius
centerX = p1.blockX + 0.5
centerZ = p1.blockZ + 0.5
val y1 = p1.blockY
val y2 = p2.blockY
val baseBlockY = minOf(y1, y2)
val topBlockY = maxOf(y1, y2)
minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0
}
// Draw Cylinder
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
// Draw Outlines (Surface)
val cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>()
val actualRadius = radius + 0.5
val radiusSq = actualRadius * actualRadius
val rInt = actualRadius.toInt() + 1
for (dx in -rInt..rInt) {
for (dz in -rInt..rInt) {
if (dx * dx + dz * dz <= radiusSq) {
blocks.add(Pair(cX + dx, cZ + dz))
}
}
}
val color = Color.fromRGB(100, 200, 255)
GeometryVisualizer.drawBlockSurfaceOutline(player, minY, blocks, { _, _, _ -> false }, color, minY)
GeometryVisualizer.drawBlockSurfaceOutline(player, maxY, blocks, { _, _, _ -> false }, color, maxY)
}
}
}
// Private helper extensions if needed for Shape
private fun net.hareworks.hcu.lands.model.Shape.worldName(): String { return "" } // Dummy, not used because we can't easily check

View File

@ -1,21 +0,0 @@
api-version: 1.21.10
name: landsector
version: 1.0-SNAPSHOT
main: net.hareworks.hcu.landsector.LandSectorPlugin
description: Land sector management plugin for HCU server
authors:
- Hare-K02
dependencies:
server:
faction:
load: BEFORE
required: true
join-classpath: true
hcu-core:
load: BEFORE
required: true
join-classpath: true
lands:
load: BEFORE
required: true
join-classpath: true

View File

@ -12,15 +12,22 @@ import org.bukkit.NamespacedKey
import org.bukkit.entity.Player
import org.bukkit.entity.Shulker
import org.bukkit.inventory.ItemStack
import org.bukkit.permissions.PermissionDefault
import org.bukkit.persistence.PersistentDataType
class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
fun register() {
kommand(landSectorPlugin) {
permissions {
namespace = "landsector"
rootSegment = "command"
defaultDescription { ctx -> "Allows /${ctx.commandName} (${ctx.path.joinToString(" ")})" }
}
command("landsector") {
// Admin Commands
literal("reload") {
condition { it.isOp }
permission { defaultValue = PermissionDefault.OP }
executes {
landSectorPlugin.reloadConfig()
sender.sendMessage(Component.text("Configuration reloaded.", NamedTextColor.GREEN))
@ -28,7 +35,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
}
literal("give") {
condition { it.isOp }
permission { defaultValue = PermissionDefault.OP }
executes {
val player = sender as? Player ?: return@executes
@ -48,7 +55,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
}
literal("list") {
condition { it.isOp }
permission { defaultValue = PermissionDefault.OP }
executes {
val player = sender as? Player ?: return@executes
val service = landSectorPlugin.sectorService
@ -91,7 +98,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
}
literal("delete") {
condition { it.isOp }
permission { defaultValue = PermissionDefault.OP }
integer("id") {
executes {
val id = argument<Int>("id")
@ -141,6 +148,7 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
// User Operations
literal("operation") {
permission { defaultValue = PermissionDefault.TRUE }
literal("givetool") {
executes {
val player = sender as? Player ?: return@executes

View File

@ -121,12 +121,26 @@ class SelectionListener(
if (player.isSneaking) {
// Toggle Center/Corner Mode
selection.isCenterMode = !selection.isCenterMode
player.sendMessage(Component.text("Selection Modifier: ${if (selection.isCenterMode) "Center/Base Mode" else "Corner/Symmetric Mode"}", NamedTextColor.GOLD))
val modifierName = if (selection.mode == SelectionMode.CUBOID) {
if (selection.isCenterMode) "Diagonal" else "Corner"
} else {
if (selection.isCenterMode) "Bottom" else "Center"
}
player.sendMessage(Component.text("Selection Modifier: $modifierName", NamedTextColor.GOLD))
player.playSound(player.location, org.bukkit.Sound.BLOCK_COMPARATOR_CLICK, 1.0f, 1.0f)
} else {
// Toggle Shape Mode
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
player.sendMessage(Component.text("Shape: ${selection.mode}", NamedTextColor.YELLOW))
val modifierName = if (selection.mode == SelectionMode.CUBOID) {
if (selection.isCenterMode) "Diagonal" else "Corner"
} else {
if (selection.isCenterMode) "Bottom" else "Center"
}
player.sendMessage(Component.text("Shape: ${selection.mode} ($modifierName)", NamedTextColor.YELLOW))
player.playSound(player.location, org.bukkit.Sound.ITEM_BOOK_PAGE_TURN, 1.0f, 1.0f)
// Reset selection on mode switch

View File

@ -103,8 +103,9 @@ class SectorService(
// 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 height is even, prioritize upper side
val yMin = sector.y - (coreHeight - 1) / 2
val yMax = sector.y + coreHeight / 2
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)")
@ -458,29 +459,25 @@ class SectorService(
net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2)
}
SelectionMode.CYLINDER -> {
// Convert bounds to cylinder params
// Approximating from bounding box
val minX = minOf(x1, x2)
val maxX = maxOf(x1, x2)
val minZ = minOf(z1, z2)
val maxZ = maxOf(z1, z2)
val minY = minOf(y1, y2)
val maxY = maxOf(y1, y2)
val dx = x2 - x1
val dz = z2 - z1
val dist = kotlin.math.sqrt((dx * dx + dz * dz).toDouble())
val radius = kotlin.math.ceil(dist - 0.5)
val cx = (minX + maxX) / 2
val cz = (minZ + maxZ) / 2
val cy = (minY + maxY) / 2
// isSneaking corresponds to isCenterMode
if (isSneaking) {
// Center/Base Mode: P1 is anchor, extends to P2 height
// P1 (y1) is the base reference point
val dy = y2 - y1
val bottom = if (dy < 0) -dy else 0
val top = if (dy > 0) dy else 0
// Radius is max dist from center to edge of bounding box in X or Z
val radiusX = (maxX - minX) / 2.0
val radiusZ = (maxZ - minZ) / 2.0
val radius = maxOf(radiusX, radiusZ)
val height = maxY - minY + 1
val bottom = (cy - minY)
val top = (maxY - cy)
net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top)
net.hareworks.hcu.lands.model.Shape.Cylinder(x1, y1, z1, radius, bottom, top)
} else {
// Corner/Symmetric Mode: P1 is Center, symmetric height to P2
val dy = kotlin.math.abs(y2 - y1)
net.hareworks.hcu.lands.model.Shape.Cylinder(x1, y1, z1, radius, dy, dy)
}
}
else -> return null
}