Compare commits

...

2 Commits

Author SHA1 Message Date
2621d28e67 WIP: BookUI 2025-12-20 00:52:45 +09:00
8f6f505cb4 feat: ビジュアライズと設定 2025-12-20 00:52:35 +09:00
24 changed files with 2153 additions and 43 deletions

2
Lands

@ -1 +1 @@
Subproject commit e24484bdea95f6dd1d9aeef538c01c0c7b9db1f1 Subproject commit d27057dc89e228ce120c5b5b4534b3c130e8480a

View File

@ -4,6 +4,7 @@ import net.hareworks.hcu.core.Main
import net.hareworks.hcu.core.player.PlayerIdService import net.hareworks.hcu.core.player.PlayerIdService
import net.hareworks.hcu.landsector.command.LandSectorCommand import net.hareworks.hcu.landsector.command.LandSectorCommand
import net.hareworks.hcu.landsector.listener.SectorListener import net.hareworks.hcu.landsector.listener.SectorListener
import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService import net.hareworks.hcu.landsector.service.SectorService
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -15,10 +16,13 @@ class LandSectorPlugin : JavaPlugin() {
} }
var sectorService: SectorService? = null var sectorService: SectorService? = null
var selectionService: SelectionService? = null
override fun onEnable() { override fun onEnable() {
instance = this instance = this
selectionService = SelectionService()
// Register commands // Register commands
LandSectorCommand(this).register() LandSectorCommand(this).register()
@ -57,12 +61,30 @@ class LandSectorPlugin : JavaPlugin() {
return return
} }
server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this) val selService = selectionService
if (selService != null) {
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), 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).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.") logger.info("LandSector initialized with services.")
} }
override fun onDisable() { override fun onDisable() {
sectorService?.flushChanges()
logger.info("LandSector plugin has been disabled!") logger.info("LandSector plugin has been disabled!")
} }
} }

View File

@ -1,19 +1,62 @@
package net.hareworks.hcu.landsector.command package net.hareworks.hcu.landsector.command
import net.hareworks.hcu.landsector.LandSectorPlugin import net.hareworks.hcu.landsector.LandSectorPlugin
import net.hareworks.hcu.landsector.model.Sector
import net.hareworks.kommand_lib.kommand import net.hareworks.kommand_lib.kommand
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.NamespacedKey import org.bukkit.NamespacedKey
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.entity.Shulker
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType import org.bukkit.persistence.PersistentDataType
class LandSectorCommand(private val plugin: LandSectorPlugin) { class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
fun register() { fun register() {
kommand(plugin) { kommand(landSectorPlugin) {
command("landsector") { command("landsector") {
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")
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
// TODO: Validate selection and lock in
}
}
}
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("give") { literal("give") {
executes { executes {
val player = sender as? Player ?: return@executes val player = sender as? Player ?: return@executes
@ -22,7 +65,7 @@ class LandSectorCommand(private val plugin: LandSectorPlugin) {
val meta = item.itemMeta val meta = item.itemMeta
meta.displayName(Component.text("Sector Core", NamedTextColor.LIGHT_PURPLE)) meta.displayName(Component.text("Sector Core", NamedTextColor.LIGHT_PURPLE))
meta.persistentDataContainer.set( meta.persistentDataContainer.set(
NamespacedKey(plugin, "component"), NamespacedKey(landSectorPlugin, "component"),
PersistentDataType.STRING, PersistentDataType.STRING,
"sector_core" "sector_core"
) )
@ -32,7 +75,124 @@ class LandSectorCommand(private val plugin: LandSectorPlugin) {
sender.sendMessage(Component.text("Gave 1 Sector Core.", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Gave 1 Sector Core.", NamedTextColor.GREEN))
} }
} }
literal("list") {
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") {
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))
}
}
}
}
} }
} }
} }
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

@ -9,6 +9,22 @@ object SectorsTable : Table("land_sectors") {
val x = integer("x") val x = integer("x")
val y = integer("y") val y = integer("y")
val z = integer("z") val z = integer("z")
val hp = integer("hp").default(1000)
override val primaryKey = PrimaryKey(id)
}
object SectorRangesTable : Table("land_sector_ranges") {
val id = integer("id").autoIncrement()
val sectorId = integer("sector_id").references(SectorsTable.id)
val type = varchar("type", 16)
val x1 = integer("x1")
val y1 = integer("y1")
val z1 = integer("z1")
val x2 = integer("x2")
val y2 = integer("y2")
val z2 = integer("z2")
val isSneaking = bool("is_sneaking")
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }

View File

@ -7,22 +7,62 @@ import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.NamespacedKey import org.bukkit.NamespacedKey
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.EntityType import org.bukkit.entity.EntityType
import org.bukkit.entity.Shulker import org.bukkit.entity.Shulker
import org.bukkit.attribute.Attribute import org.bukkit.attribute.Attribute
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent 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.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
class SectorListener( class SectorListener(
private val plugin: LandSectorPlugin, private val plugin: LandSectorPlugin,
private val sectorService: SectorService, private val sectorService: SectorService,
private val playerIdService: PlayerIdService private val playerIdService: PlayerIdService,
private val selectionService: SelectionService
) : Listener { ) : 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 @EventHandler
fun onPlace(event: BlockPlaceEvent) { 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 item = event.itemInHand
val meta = item.itemMeta ?: return val meta = item.itemMeta ?: return
val key = NamespacedKey(plugin, "component") val key = NamespacedKey(plugin, "component")
@ -31,50 +71,352 @@ class SectorListener(
return return
} }
val player = event.player
val playerEntry = playerIdService.find(player.uniqueId) val playerEntry = playerIdService.find(player.uniqueId)
if (playerEntry == null) { if (playerEntry == null) {
player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED)) player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
event.isCancelled = true event.isCancelled = true
return return
} }
val block = event.blockPlaced
val loc = block.location
// Space check // ... (rest of the code)
val above1 = block.getRelative(0, 1, 0)
val above2 = block.getRelative(0, 2, 0) // 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
}
}
}
}
if (!above1.type.isAir || !above2.type.isAir) { // Define Center Location (This will be the DB coordinates)
player.sendMessage(Component.text("Not enough space for Sector Core.", NamedTextColor.RED)) 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 event.isCancelled = true
return return
} }
// Spawn Shulker // Create Visuals
val shulkerLoc = loc.clone().add(0.5, 1.0, 0.5) 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 val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
shulker.setAI(false) shulker.setAI(false)
shulker.isInvulnerable = true
shulker.isInvisible = true shulker.isInvisible = true
// shulker.isInvulnerable = true // REMOVED to allow damage
val param = shulker.getAttribute(Attribute.MAX_HEALTH) val param = shulker.getAttribute(Attribute.MAX_HEALTH)
param?.baseValue = 1000.0 param?.baseValue = 1000.0
shulker.health = 1000.0 shulker.health = 1000.0
shulker.maximumNoDamageTicks = 0
// Tag Shulker with Sector ID
shulker.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// Place top bedrock // Place Blocks (Base and Top)
above2.type = Material.BEDROCK // Base is at center.y - 1 (The placed block)
// Top is at center.y + 1
// Record to DB (using Shulker pos as center) block.type = Material.DEEPSLATE_TILE_SLAB
sectorService.createSector( val bottomSlab = block.blockData as Slab
playerEntry.actorId, bottomSlab.type = Slab.Type.BOTTOM
player.world.name, block.blockData = bottomSlab
above1.x,
above1.y, val topBlock = centerLoc.clone().add(0.0, 1.0, 0.0).block
above1.z 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)) 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
if (sector.ownerActorId != pEntry.actorId) {
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\n", NamedTextColor.BLACK))
// 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")))
)
content.append(Component.text(" "))
content.append(
Component.text("[Destroy]\n", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector 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"))
.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- [${range.id}] ${range.type}", 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

@ -0,0 +1,188 @@
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: Switch Mode
if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
event.isCancelled = true // Prevent breaking block
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
player.sendMessage(Component.text("Mode switched to: ${selection.mode}", NamedTextColor.YELLOW))
// Reset selection on mode switch
selection.point1 = null
selection.point2 = null
selection.p1Sneaking = false
return
}
// Right Click: Set Points
if (event.action == Action.RIGHT_CLICK_BLOCK) {
event.isCancelled = true // Prevent placing
val clickedBlock = event.clickedBlock ?: return
val loc = clickedBlock.location
if (selection.point1 == null) {
// Set Point 1
selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null // Clear p2 just in case
player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
} else {
// Set Point 2
// If P2 is already set, reset to P1 new? standard standard is cyclic.
if (selection.point2 != null) {
// Resetting, treat as P1
selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null
player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
} 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.p1Sneaking
)
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN))
// 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))
}
}
}
}
}
@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
event.player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
}
}
@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

@ -6,5 +6,6 @@ data class Sector(
val world: String, val world: String,
val x: Int, val x: Int,
val y: Int, val y: Int,
val z: Int val z: Int,
val hp: Int
) )

View File

@ -0,0 +1,14 @@
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

@ -0,0 +1,15 @@
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 p1Sneaking: Boolean = false
)

View File

@ -1,20 +1,44 @@
package net.hareworks.hcu.landsector.service package net.hareworks.hcu.landsector.service
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
import net.hareworks.hcu.landsector.database.SectorRangesTable
import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.Sector
import net.hareworks.hcu.landsector.model.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll 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.andWhere
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.util.concurrent.ConcurrentHashMap
class SectorService(private val database: Database) { class SectorService(private val database: Database) {
private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
private val sectorsCache = ConcurrentHashMap<Int, Sector>()
fun init() { fun init() {
transaction(database) { transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable) SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable)
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]
)
sectorsCache[id] = sector
}
} }
} }
@ -26,29 +50,223 @@ class SectorService(private val database: Database) {
it[SectorsTable.x] = x it[SectorsTable.x] = x
it[SectorsTable.y] = y it[SectorsTable.y] = y
it[SectorsTable.z] = z it[SectorsTable.z] = z
it[SectorsTable.hp] = 1000
}[SectorsTable.id] }[SectorsTable.id]
Sector(id, ownerActorId, world, x, y, z) hpCache[id] = 1000
val sector = Sector(id, ownerActorId, world, x, y, z, 1000)
sectorsCache[id] = sector
sector
} }
} }
fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? { fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? {
return transaction(database) { // Optimized to use cache
SectorsTable.selectAll() return sectorsCache.values.firstOrNull {
.andWhere { SectorsTable.world eq world } it.world == world && it.x == x && it.y == y && it.z == z
.andWhere { SectorsTable.x eq x } }
.andWhere { SectorsTable.y eq y } }
.andWhere { SectorsTable.z eq z }
.map { fun getSector(id: Int): Sector? {
return sectorsCache[id] ?: transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()?.let {
Sector( Sector(
it[SectorsTable.id], it[SectorsTable.id],
it[SectorsTable.ownerActorId], it[SectorsTable.ownerActorId],
it[SectorsTable.world], it[SectorsTable.world],
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z] it[SectorsTable.z],
it[SectorsTable.hp]
) )
}.singleOrNull() }
}
}
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
SectorRangesTable.deleteWhere { SectorRangesTable.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]
)
}
if (sector != null) {
hpCache.remove(id)
dirtySet.remove(id)
sectorsCache.remove(id)
}
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
)
}
}
}
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
)
}
}
}
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange {
return transaction(database) {
val id = SectorRangesTable.insert {
it[SectorRangesTable.sectorId] = sectorId
it[SectorRangesTable.type] = mode.name
it[SectorRangesTable.x1] = x1
it[SectorRangesTable.y1] = y1
it[SectorRangesTable.z1] = z1
it[SectorRangesTable.x2] = x2
it[SectorRangesTable.y2] = y2
it[SectorRangesTable.z2] = z2
it[SectorRangesTable.isSneaking] = isSneaking
}[SectorRangesTable.id]
SectorRange(id, sectorId, mode, x1, y1, z1, x2, y2, z2, isSneaking)
}
}
fun getRanges(sectorId: Int): List<SectorRange> {
return transaction(database) {
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
SectorRange(
it[SectorRangesTable.id],
it[SectorRangesTable.sectorId],
SelectionMode.valueOf(it[SectorRangesTable.type]),
it[SectorRangesTable.x1],
it[SectorRangesTable.y1],
it[SectorRangesTable.z1],
it[SectorRangesTable.x2],
it[SectorRangesTable.y2],
it[SectorRangesTable.z2],
it[SectorRangesTable.isSneaking]
)
}
} }
} }
} }

View File

@ -0,0 +1,103 @@
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.p1Sneaking) {
// 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 {
// Sneak: 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.p1Sneaking) {
// Normal: P1 center, symmetric height
val h = abs(p1.blockY - p2.blockY)
h * 2 + 1
} else {
// Sneak: P1 base, P2 top
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

@ -0,0 +1,88 @@
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

@ -0,0 +1,195 @@
package net.hareworks.hcu.landsector.task
import net.hareworks.hcu.landsector.model.SelectionMode
import net.hareworks.hcu.landsector.service.SelectionService
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
) : BukkitRunnable() {
override fun run() {
for (player in Bukkit.getOnlinePlayers()) {
visualize(player)
}
}
private fun visualize(player: Player) {
// Check if holding tool
// Check if tool is in inventory
val key = NamespacedKey(plugin, "component")
val hasTool = player.inventory.contents.any { item ->
item != null && item.type == Material.FLINT &&
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
}
if (!hasTool) return
val selection = selectionService.getSelection(player.uniqueId)
val p1 = selection.point1 ?: return
// Determine P2: Either set in selection, or dynamic based on cursor if not set (or we show review for both?)
// If p2 is set, visualize that.
// If p2 is NOT set, visualize dynamic preview using target block.
var p2 = selection.point2
var isDynamic = false
if (p2 == null) {
val target = player.getTargetBlockExact(30)
if (target != null) {
p2 = target.location
isDynamic = true
}
}
if (p2 == null) return // No p2 and no target
// If different world, ignore
if (p1.world != p2.world) return
if (selection.mode == SelectionMode.CUBOID) {
// Cuboid visualization
var minX: Double
var minY: Double
var minZ: Double
var maxX: Double
var maxY: Double
var maxZ: Double
if (!selection.p1Sneaking) {
// Normal: corner to corner
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 {
// Sneak: P1 center, P2 defines radius
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 {
// Cylinder visualization
val centerX: Double
val centerY: Double
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.p1Sneaking) {
// Normal: P1 center, symmetric height based on P2.y diff
centerX = p1.blockX + 0.5
centerY = p1.blockY + 0.5 // Logic center
centerZ = p1.blockZ + 0.5
val hDiff = abs(p1.blockY - p2.blockY)
// Bottom and Top
// From center block center, go down hDiff blocks (and include center block?)
// If P1.y = 10, P2.y = 12. Diff 2.
// Blocks: 8, 9, 10, 11, 12. Range [8, 12]. Height 5. (2*2 + 1)
// Block coords:
val baseBlockY = p1.blockY - hDiff
val topBlockY = p1.blockY + hDiff
minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0
} else {
// Sneak: P1 base center. P2 top center.
centerX = p1.blockX + 0.5
centerZ = p1.blockZ + 0.5
// Only height from p2
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
}
// Calculate blocks for cylinder outline
val cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>()
// Add 0.5 to radius to encompass the full block width of the boundary blocks
val actualRadius = radius + 0.5
val radiusSq = actualRadius * actualRadius
val rInt = actualRadius.toInt() + 1
for (dx in -rInt..rInt) {
for (dz in -rInt..rInt) {
// Check if block center is within the radius
if (dx * dx + dz * dz <= radiusSq) {
blocks.add(Pair(cX + dx, cZ + dz))
}
}
}
// Draw wireframe circle (smooth) layered with block outline
// Wireframe uses the expanded radius to match the block outline visuals
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
val color = Color.fromRGB(100, 200, 255)
// Draw bottom surface outline
GeometryVisualizer.drawBlockSurfaceOutline(
player,
minY,
blocks,
{ _, _, _ -> false },
color,
minY
)
// Draw top surface outline
GeometryVisualizer.drawBlockSurfaceOutline(
player,
maxY,
blocks,
{ _, _, _ -> false },
color,
maxY
)
// Vertical edges for the outline?
// The current generic visualizer doesn't have a specific "drawVerticalConnectors" for this map-based approach easily accessible
// or we'd have to iterate edges.
// For now, surface outlines should be sufficient as requested.
}
}
}

View File

@ -32,6 +32,9 @@ dependencies {
compileOnly("net.hareworks:permits-lib") compileOnly("net.hareworks:permits-lib")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
// Visualizer Lib (Bundled & Relocated)
implementation("net.hareworks.hcu:visualizer-lib:1.0")
// Database (Exposed v1) // Database (Exposed v1)
compileOnly("org.jetbrains.exposed:exposed-core:$exposedVersion") compileOnly("org.jetbrains.exposed:exposed-core:$exposedVersion")
compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion") compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion")
@ -55,6 +58,8 @@ tasks {
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8")) exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8"))
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7")) exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7"))
} }
relocate("net.hareworks.hcu.visualizer", "net.hareworks.hcu.landsector.libs.visualizer")
} }
} }

View File

@ -4,6 +4,7 @@ import net.hareworks.hcu.core.Main
import net.hareworks.hcu.core.player.PlayerIdService import net.hareworks.hcu.core.player.PlayerIdService
import net.hareworks.hcu.landsector.command.LandSectorCommand import net.hareworks.hcu.landsector.command.LandSectorCommand
import net.hareworks.hcu.landsector.listener.SectorListener import net.hareworks.hcu.landsector.listener.SectorListener
import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService import net.hareworks.hcu.landsector.service.SectorService
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -15,10 +16,13 @@ class LandSectorPlugin : JavaPlugin() {
} }
var sectorService: SectorService? = null var sectorService: SectorService? = null
var selectionService: SelectionService? = null
override fun onEnable() { override fun onEnable() {
instance = this instance = this
selectionService = SelectionService()
// Register commands // Register commands
LandSectorCommand(this).register() LandSectorCommand(this).register()
@ -57,7 +61,16 @@ class LandSectorPlugin : JavaPlugin() {
return return
} }
server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this) val selService = selectionService
if (selService != null) {
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), 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).runTaskTimer(this, 0L, 4L)
} else {
logger.severe("SelectionService not initialized!")
}
// Schedule auto-save every 5 minutes // Schedule auto-save every 5 minutes
server.scheduler.runTaskTimerAsynchronously(this, Runnable { server.scheduler.runTaskTimerAsynchronously(this, Runnable {

View File

@ -18,6 +18,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
fun register() { fun register() {
kommand(landSectorPlugin) { kommand(landSectorPlugin) {
command("landsector") { command("landsector") {
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")
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
// TODO: Validate selection and lock in
}
}
}
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("give") { literal("give") {
executes { executes {
val player = sender as? Player ?: return@executes val player = sender as? Player ?: return@executes
@ -129,4 +168,31 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
} }
} }
} }
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

@ -13,3 +13,18 @@ object SectorsTable : Table("land_sectors") {
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
object SectorRangesTable : Table("land_sector_ranges") {
val id = integer("id").autoIncrement()
val sectorId = integer("sector_id").references(SectorsTable.id)
val type = varchar("type", 16)
val x1 = integer("x1")
val y1 = integer("y1")
val z1 = integer("z1")
val x2 = integer("x2")
val y2 = integer("y2")
val z2 = integer("z2")
val isSneaking = bool("is_sneaking")
override val primaryKey = PrimaryKey(id)
}

View File

@ -27,10 +27,15 @@ import org.joml.Vector3f
import org.joml.Quaternionf import org.joml.Quaternionf
import org.bukkit.block.data.type.Slab 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
class SectorListener( class SectorListener(
private val plugin: LandSectorPlugin, private val plugin: LandSectorPlugin,
private val sectorService: SectorService, private val sectorService: SectorService,
private val playerIdService: PlayerIdService private val playerIdService: PlayerIdService,
private val selectionService: SelectionService
) : Listener { ) : Listener {
@EventHandler @EventHandler
@ -194,6 +199,77 @@ class SectorListener(
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN)) 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
if (sector.ownerActorId != pEntry.actorId) {
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\n", NamedTextColor.BLACK))
// 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")))
)
content.append(Component.text(" "))
content.append(
Component.text("[Destroy]\n", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector 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"))
.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- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY))
}
}
meta.addPages(content.build())
book.itemMeta = meta
player.openBook(book)
event.isCancelled = true
}
@EventHandler @EventHandler
fun onDamage(event: EntityDamageEvent) { fun onDamage(event: EntityDamageEvent) {
val entity = event.entity val entity = event.entity

View File

@ -0,0 +1,188 @@
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: Switch Mode
if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
event.isCancelled = true // Prevent breaking block
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
player.sendMessage(Component.text("Mode switched to: ${selection.mode}", NamedTextColor.YELLOW))
// Reset selection on mode switch
selection.point1 = null
selection.point2 = null
selection.p1Sneaking = false
return
}
// Right Click: Set Points
if (event.action == Action.RIGHT_CLICK_BLOCK) {
event.isCancelled = true // Prevent placing
val clickedBlock = event.clickedBlock ?: return
val loc = clickedBlock.location
if (selection.point1 == null) {
// Set Point 1
selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null // Clear p2 just in case
player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
} else {
// Set Point 2
// If P2 is already set, reset to P1 new? standard standard is cyclic.
if (selection.point2 != null) {
// Resetting, treat as P1
selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null
player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
} 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.p1Sneaking
)
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN))
// 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))
}
}
}
}
}
@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
event.player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
}
}
@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

@ -0,0 +1,14 @@
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

@ -0,0 +1,15 @@
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 p1Sneaking: Boolean = false
)

View File

@ -1,7 +1,10 @@
package net.hareworks.hcu.landsector.service package net.hareworks.hcu.landsector.service
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
import net.hareworks.hcu.landsector.database.SectorRangesTable
import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.Sector
import net.hareworks.hcu.landsector.model.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.jetbrains.exposed.v1.jdbc.SchemaUtils
@ -22,7 +25,7 @@ class SectorService(private val database: Database) {
fun init() { fun init() {
transaction(database) { transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable) SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable)
SectorsTable.selectAll().forEach { SectorsTable.selectAll().forEach {
val id = it[SectorsTable.id] val id = it[SectorsTable.id]
val sector = Sector( val sector = Sector(
@ -64,6 +67,22 @@ class SectorService(private val database: Database) {
} }
} }
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]
)
}
}
}
fun getHealth(id: Int): Int? { fun getHealth(id: Int): Int? {
// Try get from cache // Try get from cache
var currentHp = hpCache[id] var currentHp = hpCache[id]
@ -119,6 +138,7 @@ class SectorService(private val database: Database) {
val sector = transaction(database) { val sector = transaction(database) {
val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null
SectorRangesTable.deleteWhere { SectorRangesTable.sectorId eq id }
SectorsTable.deleteWhere { SectorsTable.id eq id } SectorsTable.deleteWhere { SectorsTable.id eq id }
Sector( Sector(
@ -211,4 +231,42 @@ class SectorService(private val database: Database) {
} }
} }
} }
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange {
return transaction(database) {
val id = SectorRangesTable.insert {
it[SectorRangesTable.sectorId] = sectorId
it[SectorRangesTable.type] = mode.name
it[SectorRangesTable.x1] = x1
it[SectorRangesTable.y1] = y1
it[SectorRangesTable.z1] = z1
it[SectorRangesTable.x2] = x2
it[SectorRangesTable.y2] = y2
it[SectorRangesTable.z2] = z2
it[SectorRangesTable.isSneaking] = isSneaking
}[SectorRangesTable.id]
SectorRange(id, sectorId, mode, x1, y1, z1, x2, y2, z2, isSneaking)
}
}
fun getRanges(sectorId: Int): List<SectorRange> {
return transaction(database) {
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
SectorRange(
it[SectorRangesTable.id],
it[SectorRangesTable.sectorId],
SelectionMode.valueOf(it[SectorRangesTable.type]),
it[SectorRangesTable.x1],
it[SectorRangesTable.y1],
it[SectorRangesTable.z1],
it[SectorRangesTable.x2],
it[SectorRangesTable.y2],
it[SectorRangesTable.z2],
it[SectorRangesTable.isSneaking]
)
}
}
}
} }

View File

@ -0,0 +1,103 @@
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.p1Sneaking) {
// 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 {
// Sneak: 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.p1Sneaking) {
// Normal: P1 center, symmetric height
val h = abs(p1.blockY - p2.blockY)
h * 2 + 1
} else {
// Sneak: P1 base, P2 top
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

@ -0,0 +1,195 @@
package net.hareworks.hcu.landsector.task
import net.hareworks.hcu.landsector.model.SelectionMode
import net.hareworks.hcu.landsector.service.SelectionService
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
) : BukkitRunnable() {
override fun run() {
for (player in Bukkit.getOnlinePlayers()) {
visualize(player)
}
}
private fun visualize(player: Player) {
// Check if holding tool
// Check if tool is in inventory
val key = NamespacedKey(plugin, "component")
val hasTool = player.inventory.contents.any { item ->
item != null && item.type == Material.FLINT &&
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
}
if (!hasTool) return
val selection = selectionService.getSelection(player.uniqueId)
val p1 = selection.point1 ?: return
// Determine P2: Either set in selection, or dynamic based on cursor if not set (or we show review for both?)
// If p2 is set, visualize that.
// If p2 is NOT set, visualize dynamic preview using target block.
var p2 = selection.point2
var isDynamic = false
if (p2 == null) {
val target = player.getTargetBlockExact(30)
if (target != null) {
p2 = target.location
isDynamic = true
}
}
if (p2 == null) return // No p2 and no target
// If different world, ignore
if (p1.world != p2.world) return
if (selection.mode == SelectionMode.CUBOID) {
// Cuboid visualization
var minX: Double
var minY: Double
var minZ: Double
var maxX: Double
var maxY: Double
var maxZ: Double
if (!selection.p1Sneaking) {
// Normal: corner to corner
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 {
// Sneak: P1 center, P2 defines radius
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 {
// Cylinder visualization
val centerX: Double
val centerY: Double
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.p1Sneaking) {
// Normal: P1 center, symmetric height based on P2.y diff
centerX = p1.blockX + 0.5
centerY = p1.blockY + 0.5 // Logic center
centerZ = p1.blockZ + 0.5
val hDiff = abs(p1.blockY - p2.blockY)
// Bottom and Top
// From center block center, go down hDiff blocks (and include center block?)
// If P1.y = 10, P2.y = 12. Diff 2.
// Blocks: 8, 9, 10, 11, 12. Range [8, 12]. Height 5. (2*2 + 1)
// Block coords:
val baseBlockY = p1.blockY - hDiff
val topBlockY = p1.blockY + hDiff
minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0
} else {
// Sneak: P1 base center. P2 top center.
centerX = p1.blockX + 0.5
centerZ = p1.blockZ + 0.5
// Only height from p2
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
}
// Calculate blocks for cylinder outline
val cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>()
// Add 0.5 to radius to encompass the full block width of the boundary blocks
val actualRadius = radius + 0.5
val radiusSq = actualRadius * actualRadius
val rInt = actualRadius.toInt() + 1
for (dx in -rInt..rInt) {
for (dz in -rInt..rInt) {
// Check if block center is within the radius
if (dx * dx + dz * dz <= radiusSq) {
blocks.add(Pair(cX + dx, cZ + dz))
}
}
}
// Draw wireframe circle (smooth) layered with block outline
// Wireframe uses the expanded radius to match the block outline visuals
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
val color = Color.fromRGB(100, 200, 255)
// Draw bottom surface outline
GeometryVisualizer.drawBlockSurfaceOutline(
player,
minY,
blocks,
{ _, _, _ -> false },
color,
minY
)
// Draw top surface outline
GeometryVisualizer.drawBlockSurfaceOutline(
player,
maxY,
blocks,
{ _, _, _ -> false },
color,
maxY
)
// Vertical edges for the outline?
// The current generic visualizer doesn't have a specific "drawVerticalConnectors" for this map-based approach easily accessible
// or we'd have to iterate edges.
// For now, surface outlines should be sufficient as requested.
}
}
}