feat: 設置と削除・HP

This commit is contained in:
Keisuke Hirata 2025-12-12 03:44:23 +09:00
parent 5771e1b9d1
commit ad4245d764
6 changed files with 462 additions and 22 deletions

View File

@ -59,10 +59,16 @@ class LandSectorPlugin : JavaPlugin() {
server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this) server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this)
// Schedule auto-save every 5 minutes
server.scheduler.runTaskTimerAsynchronously(this, Runnable {
service.flushChanges()
}, 6000L, 6000L)
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,18 +1,22 @@
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("give") { literal("give") {
executes { executes {
@ -22,7 +26,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,6 +36,97 @@ 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.BEDROCK) blockBase.type = Material.AIR
if (blockTop.type == Material.BEDROCK) blockTop.type = Material.AIR
// Remove Entity
val center = Location(world, x + 0.5, y.toDouble(), z + 0.5)
if (center.chunk.isLoaded) {
val entities = world.getNearbyEntities(center, 0.5, 0.5, 0.5)
entities.forEach { entity ->
if (entity is Shulker) {
val key = NamespacedKey(landSectorPlugin, "sector_id")
val sId = entity.persistentDataContainer.get(key, PersistentDataType.INTEGER)
if (sId == id || sId == null) {
entity.remove()
}
}
}
}
}
sender.sendMessage(Component.text("Sector #$id deleted.", NamedTextColor.GREEN))
}
}
}
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ 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) override val primaryKey = PrimaryKey(id)
} }

View File

@ -7,13 +7,24 @@ 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.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
class SectorListener( class SectorListener(
private val plugin: LandSectorPlugin, private val plugin: LandSectorPlugin,
@ -52,29 +63,249 @@ class SectorListener(
return return
} }
// Spawn Shulker // Record to DB first to get ID
val shulkerLoc = loc.clone().add(0.5, 1.0, 0.5) val sector = sectorService.createSector(
val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
shulker.setAI(false)
shulker.isInvulnerable = true
shulker.isInvisible = true
val param = shulker.getAttribute(Attribute.MAX_HEALTH)
param?.baseValue = 1000.0
shulker.health = 1000.0
// Place top bedrock
above2.type = Material.BEDROCK
// Record to DB (using Shulker pos as center)
sectorService.createSector(
playerEntry.actorId, playerEntry.actorId,
player.world.name, player.world.name,
above1.x, above1.x,
above1.y, above1.y,
above1.z above1.z
) )
if (sector == null) {
player.sendMessage(Component.text("Failed to create sector record.", NamedTextColor.RED))
event.isCancelled = true
return
}
// Create Visuals
val sectorKey = NamespacedKey(plugin, "sector_id")
val locCenter = loc.clone().add(0.5, 0.0, 0.5)
// 1. Command Block
val cbLoc = locCenter.clone().add(0.0, 1.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
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
val topCauldronLoc = locCenter.clone().add(0.0, 2.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
val botCauldronLoc = locCenter.clone().add(0.0, 0.6875, 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)
val shulkerLoc = loc.clone().add(0.5, 1.0, 0.5) // Y+1, sits in space 1-2.
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
block.type = Material.DEEPSLATE_TILE_SLAB
val bottomSlab = block.blockData as Slab
bottomSlab.type = Slab.Type.BOTTOM
block.blockData = bottomSlab
above2.type = Material.DEEPSLATE_TILE_SLAB
val topSlab = above2.blockData as Slab
topSlab.type = Slab.Type.TOP
above2.blockData = topSlab
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN)) player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
} }
@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
val newHp = sectorService.reduceHealth(sectorId, damage.toInt())
// 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.
// Let's reset health.
entity.health = maxHealth
entity.noDamageTicks = 0
if (newHp != null) {
val loc = entity.location
val world = entity.world
// Effects
world.playSound(loc, Sound.BLOCK_ANVIL_PLACE, 1.0f, 0.5f) // Heavy metallic sound
world.spawnParticle(Particle.BLOCK, loc.add(0.0, 0.5, 0.0), 20, 0.3, 0.3, 0.3, Material.BEDROCK.createBlockData())
// Action Bar Display
if (event is EntityDamageByEntityEvent && event.damager is Player) {
val player = event.damager as Player
val percent = newHp.toDouble() / maxHealth.toDouble()
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.
// We know Shulker is at x, y, z.
// Base is y-1, Top is y+1
val loc = entity.location
val world = entity.world
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))
// Maybe delete from DB or mark as destroyed?
// For now, valid destruction.
}
}
}
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

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

@ -7,11 +7,18 @@ 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>()
fun init() { fun init() {
transaction(database) { transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable) SchemaUtils.createMissingTablesAndColumns(SectorsTable)
@ -26,9 +33,11 @@ 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
Sector(id, ownerActorId, world, x, y, z, 1000)
} }
} }
@ -46,9 +55,106 @@ class SectorService(private val database: Database) {
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() }.singleOrNull()
} }
} }
fun reduceHealth(id: Int, amount: Int): Int? {
// Try get from cache
var currentHp = hpCache[id]
// If not in cache, load from DB
if (currentHp == null) {
val sector = transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()
} ?: return null // Not found
currentHp = sector[SectorsTable.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
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)
}
return sector
}
fun exists(id: Int): Boolean {
if (hpCache.containsKey(id)) return true
return transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.count() > 0
}
}
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
)
}
}
}
} }