feat: ファクション所有とドラフト・アクティベート

This commit is contained in:
Keisuke Hirata 2025-12-20 02:02:28 +09:00
parent 2621d28e67
commit 767f974228
15 changed files with 733 additions and 121 deletions

2
Lands

@ -1 +1 @@
Subproject commit d27057dc89e228ce120c5b5b4534b3c130e8480a Subproject commit 0f432b0e89614bc07bd710423284da5e0228e25a

View File

@ -46,7 +46,14 @@ class LandSectorPlugin : JavaPlugin() {
return return
} }
val service = SectorService(db) val landsPlugin = server.pluginManager.getPlugin("lands") as? net.hareworks.hcu.lands.App
val landService = landsPlugin?.landService
if (landService == null) {
logger.severe("Lands plugin or service not ready.")
return
}
val service = SectorService(db, landService)
try { try {
service.init() service.init()
} catch (e: Exception) { } catch (e: Exception) {
@ -63,7 +70,8 @@ class LandSectorPlugin : JavaPlugin() {
val selService = selectionService val selService = selectionService
if (selService != null) { if (selService != null) {
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), this) val factionService = net.hareworks.hcu.faction.service.FactionService()
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService, factionService), this)
server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this) server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
// Schedule visualization task // Schedule visualization task

View File

@ -36,8 +36,13 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
integer("sectorId") { integer("sectorId") {
executes { executes {
val id = argument<Int>("sectorId") val id = argument<Int>("sectorId")
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) val service = landSectorPlugin.sectorService ?: return@executes
// TODO: Validate selection and lock in
if (service.activateSector(id)) {
sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED))
}
} }
} }
} }
@ -118,6 +123,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
} }
} }
literal("transfer") {
integer("sectorId") {
integer("targetActorId") {
executes {
val sectorId = argument<Int>("sectorId")
val targetActorId = argument<Int>("targetActorId")
val player = sender as? Player ?: return@executes
val service = landSectorPlugin.sectorService ?: return@executes
if (service.transferSector(sectorId, targetActorId)) {
sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED))
}
}
}
}
}
literal("range") {
literal("delete") {
integer("sectorId") {
integer("rangeIndex") {
executes {
val sectorId = argument<Int>("sectorId")
val rangeIndex = argument<Int>("rangeIndex")
val service = landSectorPlugin.sectorService ?: return@executes
if (service.deleteRange(sectorId, rangeIndex)) {
sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED))
}
}
}
}
}
}
literal("delete") { literal("delete") {
integer("id") { integer("id") {
executes { executes {

View File

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

View File

@ -11,20 +11,7 @@ object SectorsTable : Table("land_sectors") {
val z = integer("z") val z = integer("z")
val hp = integer("hp").default(1000) val hp = integer("hp").default(1000)
override val primaryKey = PrimaryKey(id) val landId = integer("land_id").nullable()
}
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

@ -31,11 +31,15 @@ import net.hareworks.hcu.landsector.service.SelectionService
import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.ClickEvent
import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerInteractEntityEvent
import net.hareworks.hcu.faction.service.FactionService
import net.hareworks.hcu.faction.database.schema.FactionRole
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 private val selectionService: SelectionService,
private val factionService: FactionService
) : Listener { ) : Listener {
@EventHandler @EventHandler
@ -216,7 +220,33 @@ class SectorListener(
if (pEntry == null) return if (pEntry == null) return
val sector = sectorService.getSector(sectorId) ?: return val sector = sectorService.getSector(sectorId) ?: return
if (sector.ownerActorId != pEntry.actorId) { // Resolve Owner Name
var ownerName = "Unknown"
// Try getting faction name
val factionName = factionService.getFactionName(sector.ownerActorId)
if (factionName != null) {
ownerName = "Faction: $factionName"
} else {
// Assume player. Ideally resolve UUID from ActorID, but for now ID or fallback
// We don't have direct Player Actor ID -> Name cache here easily without querying DB or UUID service
// Just show ID for now to be safe, or "Player #ID"
ownerName = "Player #${sector.ownerActorId}"
}
val canManage = if (sector.ownerActorId == pEntry.actorId) {
true
} else {
// Or if owner is my faction and I am EXEC/OWNER
val myFaction = factionService.getFactionOfPlayer(player.uniqueId)
if (myFaction == sector.ownerActorId) {
val myRole = factionService.getRole(myFaction, player.uniqueId)
myRole == FactionRole.OWNER || myRole == FactionRole.EXEC
} else {
false
}
}
if (!canManage) {
player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED)) player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED))
return return
} }
@ -230,7 +260,21 @@ class SectorListener(
// Build Page Content // Build Page Content
val content = Component.text() val content = Component.text()
.append(Component.text("Sector Core @ ${sector.x},${sector.y},${sector.z}\n\n", NamedTextColor.BLACK)) .append(Component.text("Sector Core @ ${sector.x},${sector.y},${sector.z}\n", NamedTextColor.BLACK))
.append(Component.text("Owner: $ownerName\n\n", NamedTextColor.DARK_GRAY))
// Transfer Check
val myFaction = factionService.getFactionOfPlayer(player.uniqueId)
if (myFaction != null && sector.ownerActorId != myFaction) {
val myRole = factionService.getRole(myFaction, player.uniqueId)
if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) {
content.append(
Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD)
.clickEvent(ClickEvent.runCommand("/landsector transfer $sectorId $myFaction"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction")))
)
}
}
// Actions Row 1 // Actions Row 1
content.append( content.append(
@ -260,6 +304,12 @@ class SectorListener(
content.append(Component.text("Parts:", NamedTextColor.BLACK)) content.append(Component.text("Parts:", NamedTextColor.BLACK))
ranges.forEach { range -> ranges.forEach { range ->
content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY))
content.append(Component.text(" "))
content.append(
Component.text("[x]", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range")))
)
} }
} }

View File

@ -7,5 +7,6 @@ data class Sector(
val x: Int, val x: Int,
val y: Int, val y: Int,
val z: Int, val z: Int,
val hp: Int val hp: Int,
val landId: Int? = null
) )

View File

@ -1,7 +1,8 @@
package net.hareworks.hcu.landsector.service package net.hareworks.hcu.landsector.service
import net.hareworks.hcu.landsector.database.SectorDraftsTable
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
import net.hareworks.hcu.landsector.database.SectorRangesTable // SectorRangesTable removed
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.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode import net.hareworks.hcu.landsector.model.SelectionMode
@ -17,15 +18,21 @@ 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 import java.util.concurrent.ConcurrentHashMap
class SectorService(private val database: Database) { // ... class definition ...
class SectorService(private val database: Database, private val landService: net.hareworks.hcu.lands.service.LandService) {
private val hpCache = ConcurrentHashMap<Int, Int>() private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>() private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
private val sectorsCache = ConcurrentHashMap<Int, Sector>() private val sectorsCache = ConcurrentHashMap<Int, Sector>()
// Draft storage: SectorId -> Land (Draft)
private val draftLands = ConcurrentHashMap<Int, net.hareworks.hcu.lands.model.Land>()
fun init() { fun init() {
transaction(database) { transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable) SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorDraftsTable)
// Load Sectors
SectorsTable.selectAll().forEach { SectorsTable.selectAll().forEach {
val id = it[SectorsTable.id] val id = it[SectorsTable.id]
val sector = Sector( val sector = Sector(
@ -35,10 +42,28 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
it[SectorsTable.hp] it[SectorsTable.hp],
it[SectorsTable.landId]
) )
sectorsCache[id] = sector sectorsCache[id] = sector
} }
// Load Drafts
SectorDraftsTable.selectAll().forEach {
val sId = it[SectorDraftsTable.sectorId]
val data = it[SectorDraftsTable.data]
val sector = sectorsCache[sId]
if (sector != null && sector.landId == null) {
draftLands[sId] = net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sId",
actorId = sector.ownerActorId,
world = sector.world,
data = data
)
}
}
} }
} }
@ -51,11 +76,29 @@ class SectorService(private val database: Database) {
it[SectorsTable.y] = y it[SectorsTable.y] = y
it[SectorsTable.z] = z it[SectorsTable.z] = z
it[SectorsTable.hp] = 1000 it[SectorsTable.hp] = 1000
it[SectorsTable.landId] = null
}[SectorsTable.id] }[SectorsTable.id]
hpCache[id] = 1000 hpCache[id] = 1000
val sector = Sector(id, ownerActorId, world, x, y, z, 1000) val sector = Sector(id, ownerActorId, world, x, y, z, 1000, null)
sectorsCache[id] = sector sectorsCache[id] = sector
// Initialize draft land
val draftLand = net.hareworks.hcu.lands.model.Land(
id = -1, // Dummy ID
name = "Draft Sector $id",
actorId = ownerActorId,
world = world,
data = net.hareworks.hcu.lands.model.LandData()
)
draftLands[id] = draftLand
// Persist draft
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = id
it[SectorDraftsTable.data] = draftLand.data
}
sector sector
} }
} }
@ -77,7 +120,8 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
it[SectorsTable.hp] it[SectorsTable.hp],
it[SectorsTable.landId]
) )
} }
} }
@ -138,7 +182,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 } SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq id }
SectorsTable.deleteWhere { SectorsTable.id eq id } SectorsTable.deleteWhere { SectorsTable.id eq id }
Sector( Sector(
@ -148,7 +192,8 @@ class SectorService(private val database: Database) {
record[SectorsTable.x], record[SectorsTable.x],
record[SectorsTable.y], record[SectorsTable.y],
record[SectorsTable.z], record[SectorsTable.z],
record[SectorsTable.hp] record[SectorsTable.hp],
record[SectorsTable.landId]
) )
} }
@ -156,6 +201,11 @@ class SectorService(private val database: Database) {
hpCache.remove(id) hpCache.remove(id)
dirtySet.remove(id) dirtySet.remove(id)
sectorsCache.remove(id) sectorsCache.remove(id)
draftLands.remove(id)
if (sector.landId != null) {
landService.deleteLand(sector.landId)
}
} }
return sector return sector
@ -203,7 +253,8 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
hp hp,
it[SectorsTable.landId]
) )
} }
} }
@ -226,47 +277,196 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
hp hp,
it[SectorsTable.landId]
) )
} }
} }
} }
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange { fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): Any? {
return transaction(database) { val sector = getSector(sectorId) ?: return null
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) // Cannot edit if already confirmed
if (sector.landId != null) return null
val draft = draftLands.computeIfAbsent(sectorId) {
net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sectorId",
actorId = sector.ownerActorId,
world = sector.world,
data = net.hareworks.hcu.lands.model.LandData()
)
} }
val shape = when (mode) {
SelectionMode.CUBOID -> {
net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2)
}
SelectionMode.CYLINDER -> {
// Convert bounds to cylinder params
// Approximating from bounding box
val minX = minOf(x1, x2)
val maxX = maxOf(x1, x2)
val minZ = minOf(z1, z2)
val maxZ = maxOf(z1, z2)
val minY = minOf(y1, y2)
val maxY = maxOf(y1, y2)
val cx = (minX + maxX) / 2
val cz = (minZ + maxZ) / 2
val cy = (minY + maxY) / 2
// Radius is max dist from center to edge of bounding box in X or Z
val radiusX = (maxX - minX) / 2.0
val radiusZ = (maxZ - minZ) / 2.0
val radius = maxOf(radiusX, radiusZ)
val height = maxY - minY + 1
val bottom = (cy - minY)
val top = (maxY - cy)
net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top)
}
else -> return null
}
// Use LandService modify pattern or direct replacement since stored in local map
val newParts = draft.data.parts.toMutableList()
newParts.add(shape)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
// Check if exists first? Or upsert if supported easily.
// We know it exists from creation, but safety check.
val count = SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
if (count == 0) {
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = sectorId
it[SectorDraftsTable.data] = newDraft.data
}
}
}
return shape
} }
fun getRanges(sectorId: Int): List<SectorRange> { fun getRanges(sectorId: Int): List<SectorRange> {
return transaction(database) { val sector = getSector(sectorId) ?: return emptyList()
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
val land = if (sector.landId != null) {
landService.getLand(sector.landId)
} else {
draftLands[sectorId]
} ?: return emptyList()
// Convert Shapes back to SectorRanges for UI compatibility
return land.data.parts.mapIndexed { index, shape ->
when (shape) {
is net.hareworks.hcu.lands.model.Shape.Cuboid -> {
SectorRange( SectorRange(
it[SectorRangesTable.id], index, // Use index as ID for UI
it[SectorRangesTable.sectorId], sectorId,
SelectionMode.valueOf(it[SectorRangesTable.type]), SelectionMode.CUBOID,
it[SectorRangesTable.x1], shape.x1, shape.y1, shape.z1,
it[SectorRangesTable.y1], shape.x2, shape.y2, shape.z2,
it[SectorRangesTable.z1], false
it[SectorRangesTable.x2],
it[SectorRangesTable.y2],
it[SectorRangesTable.z2],
it[SectorRangesTable.isSneaking]
) )
} }
is net.hareworks.hcu.lands.model.Shape.Cylinder -> {
SectorRange(
index,
sectorId,
SelectionMode.CYLINDER,
shape.x, shape.y, shape.z,
shape.x, shape.y, shape.z,
false
)
}
// Default fallback for unknown shapes if any (shouldn't happen with current limited types)
else -> SectorRange(index, sectorId, SelectionMode.CUBOID, 0,0,0,0,0,0, false)
}
}
}
fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Cannot edit confirmed
val draft = draftLands[sectorId] ?: return false
if (rangeIndex < 0 || rangeIndex >= draft.data.parts.size) return false
val newParts = draft.data.parts.toMutableList()
newParts.removeAt(rangeIndex)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
}
return true
}
fun activateSector(sectorId: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Already active
val draft = draftLands[sectorId] ?: return false
if (draft.data.parts.isEmpty()) return false
val landName = "Sector_${sectorId}_${System.currentTimeMillis()}"
if (landService.createLand(landName, draft.actorId, draft.world)) {
// Find the created land to get ID
val lands = landService.findLandsByName(landName)
val createdLand = lands.firstOrNull() ?: return false
// Update parts
landService.modifyLand(createdLand.id) { data ->
data.copy(parts = draft.data.parts)
}
// Update Sector DB & Delete Draft
transaction(database) {
SectorsTable.update({ SectorsTable.id eq sectorId }) {
it[SectorsTable.landId] = createdLand.id
}
SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq sectorId }
}
// Update cache & Clear draft
sectorsCache[sectorId] = sector.copy(landId = createdLand.id)
draftLands.remove(sectorId)
return true
}
return false
}
fun transferSector(id: Int, newOwnerActorId: Int): Boolean {
return transaction(database) {
val updated = SectorsTable.update({ SectorsTable.id eq id }) {
it[ownerActorId] = newOwnerActorId
}
if (updated > 0) {
// Update cache
val current = sectorsCache[id]
if (current != null) {
sectorsCache[id] = current.copy(ownerActorId = newOwnerActorId)
}
true
} else {
false
}
} }
} }
} }

View File

@ -46,7 +46,14 @@ class LandSectorPlugin : JavaPlugin() {
return return
} }
val service = SectorService(db) val landsPlugin = server.pluginManager.getPlugin("lands") as? net.hareworks.hcu.lands.App
val landService = landsPlugin?.landService
if (landService == null) {
logger.severe("Lands plugin or service not ready.")
return
}
val service = SectorService(db, landService)
try { try {
service.init() service.init()
} catch (e: Exception) { } catch (e: Exception) {
@ -63,7 +70,8 @@ class LandSectorPlugin : JavaPlugin() {
val selService = selectionService val selService = selectionService
if (selService != null) { if (selService != null) {
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), this) val factionService = net.hareworks.hcu.faction.service.FactionService()
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService, factionService), this)
server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this) server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
// Schedule visualization task // Schedule visualization task

View File

@ -36,8 +36,13 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
integer("sectorId") { integer("sectorId") {
executes { executes {
val id = argument<Int>("sectorId") val id = argument<Int>("sectorId")
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) val service = landSectorPlugin.sectorService ?: return@executes
// TODO: Validate selection and lock in
if (service.activateSector(id)) {
sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED))
}
} }
} }
} }
@ -118,6 +123,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
} }
} }
literal("transfer") {
integer("sectorId") {
integer("targetActorId") {
executes {
val sectorId = argument<Int>("sectorId")
val targetActorId = argument<Int>("targetActorId")
val player = sender as? Player ?: return@executes
val service = landSectorPlugin.sectorService ?: return@executes
if (service.transferSector(sectorId, targetActorId)) {
sender.sendMessage(Component.text("Transfer successful!", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Transfer failed.", NamedTextColor.RED))
}
}
}
}
}
literal("range") {
literal("delete") {
integer("sectorId") {
integer("rangeIndex") {
executes {
val sectorId = argument<Int>("sectorId")
val rangeIndex = argument<Int>("rangeIndex")
val service = landSectorPlugin.sectorService ?: return@executes
if (service.deleteRange(sectorId, rangeIndex)) {
sender.sendMessage(Component.text("Range #$rangeIndex in Sector #$sectorId deleted.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Failed to delete range. (Invalid index or sector already confirmed?)", NamedTextColor.RED))
}
}
}
}
}
}
literal("delete") { literal("delete") {
integer("id") { integer("id") {
executes { executes {

View File

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

View File

@ -11,20 +11,7 @@ object SectorsTable : Table("land_sectors") {
val z = integer("z") val z = integer("z")
val hp = integer("hp").default(1000) val hp = integer("hp").default(1000)
override val primaryKey = PrimaryKey(id) val landId = integer("land_id").nullable()
}
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

@ -31,11 +31,15 @@ import net.hareworks.hcu.landsector.service.SelectionService
import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.ClickEvent
import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerInteractEntityEvent
import net.hareworks.hcu.faction.service.FactionService
import net.hareworks.hcu.faction.database.schema.FactionRole
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 private val selectionService: SelectionService,
private val factionService: FactionService
) : Listener { ) : Listener {
@EventHandler @EventHandler
@ -216,7 +220,33 @@ class SectorListener(
if (pEntry == null) return if (pEntry == null) return
val sector = sectorService.getSector(sectorId) ?: return val sector = sectorService.getSector(sectorId) ?: return
if (sector.ownerActorId != pEntry.actorId) { // Resolve Owner Name
var ownerName = "Unknown"
// Try getting faction name
val factionName = factionService.getFactionName(sector.ownerActorId)
if (factionName != null) {
ownerName = "Faction: $factionName"
} else {
// Assume player. Ideally resolve UUID from ActorID, but for now ID or fallback
// We don't have direct Player Actor ID -> Name cache here easily without querying DB or UUID service
// Just show ID for now to be safe, or "Player #ID"
ownerName = "Player #${sector.ownerActorId}"
}
val canManage = if (sector.ownerActorId == pEntry.actorId) {
true
} else {
// Or if owner is my faction and I am EXEC/OWNER
val myFaction = factionService.getFactionOfPlayer(player.uniqueId)
if (myFaction == sector.ownerActorId) {
val myRole = factionService.getRole(myFaction, player.uniqueId)
myRole == FactionRole.OWNER || myRole == FactionRole.EXEC
} else {
false
}
}
if (!canManage) {
player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED)) player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED))
return return
} }
@ -230,7 +260,21 @@ class SectorListener(
// Build Page Content // Build Page Content
val content = Component.text() val content = Component.text()
.append(Component.text("Sector Core @ ${sector.x},${sector.y},${sector.z}\n\n", NamedTextColor.BLACK)) .append(Component.text("Sector Core @ ${sector.x},${sector.y},${sector.z}\n", NamedTextColor.BLACK))
.append(Component.text("Owner: $ownerName\n\n", NamedTextColor.DARK_GRAY))
// Transfer Check
val myFaction = factionService.getFactionOfPlayer(player.uniqueId)
if (myFaction != null && sector.ownerActorId != myFaction) {
val myRole = factionService.getRole(myFaction, player.uniqueId)
if (myRole == FactionRole.OWNER || myRole == FactionRole.EXEC) {
content.append(
Component.text("[Transfer to Faction]\n\n", NamedTextColor.GOLD)
.clickEvent(ClickEvent.runCommand("/landsector transfer $sectorId $myFaction"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to transfer ownership to your faction")))
)
}
}
// Actions Row 1 // Actions Row 1
content.append( content.append(
@ -260,6 +304,12 @@ class SectorListener(
content.append(Component.text("Parts:", NamedTextColor.BLACK)) content.append(Component.text("Parts:", NamedTextColor.BLACK))
ranges.forEach { range -> ranges.forEach { range ->
content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY))
content.append(Component.text(" "))
content.append(
Component.text("[x]", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range")))
)
} }
} }

View File

@ -7,5 +7,6 @@ data class Sector(
val x: Int, val x: Int,
val y: Int, val y: Int,
val z: Int, val z: Int,
val hp: Int val hp: Int,
val landId: Int? = null
) )

View File

@ -1,7 +1,8 @@
package net.hareworks.hcu.landsector.service package net.hareworks.hcu.landsector.service
import net.hareworks.hcu.landsector.database.SectorDraftsTable
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
import net.hareworks.hcu.landsector.database.SectorRangesTable // SectorRangesTable removed
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.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode import net.hareworks.hcu.landsector.model.SelectionMode
@ -17,15 +18,21 @@ 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 import java.util.concurrent.ConcurrentHashMap
class SectorService(private val database: Database) { // ... class definition ...
class SectorService(private val database: Database, private val landService: net.hareworks.hcu.lands.service.LandService) {
private val hpCache = ConcurrentHashMap<Int, Int>() private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>() private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
private val sectorsCache = ConcurrentHashMap<Int, Sector>() private val sectorsCache = ConcurrentHashMap<Int, Sector>()
// Draft storage: SectorId -> Land (Draft)
private val draftLands = ConcurrentHashMap<Int, net.hareworks.hcu.lands.model.Land>()
fun init() { fun init() {
transaction(database) { transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable) SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorDraftsTable)
// Load Sectors
SectorsTable.selectAll().forEach { SectorsTable.selectAll().forEach {
val id = it[SectorsTable.id] val id = it[SectorsTable.id]
val sector = Sector( val sector = Sector(
@ -35,10 +42,28 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
it[SectorsTable.hp] it[SectorsTable.hp],
it[SectorsTable.landId]
) )
sectorsCache[id] = sector sectorsCache[id] = sector
} }
// Load Drafts
SectorDraftsTable.selectAll().forEach {
val sId = it[SectorDraftsTable.sectorId]
val data = it[SectorDraftsTable.data]
val sector = sectorsCache[sId]
if (sector != null && sector.landId == null) {
draftLands[sId] = net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sId",
actorId = sector.ownerActorId,
world = sector.world,
data = data
)
}
}
} }
} }
@ -51,11 +76,29 @@ class SectorService(private val database: Database) {
it[SectorsTable.y] = y it[SectorsTable.y] = y
it[SectorsTable.z] = z it[SectorsTable.z] = z
it[SectorsTable.hp] = 1000 it[SectorsTable.hp] = 1000
it[SectorsTable.landId] = null
}[SectorsTable.id] }[SectorsTable.id]
hpCache[id] = 1000 hpCache[id] = 1000
val sector = Sector(id, ownerActorId, world, x, y, z, 1000) val sector = Sector(id, ownerActorId, world, x, y, z, 1000, null)
sectorsCache[id] = sector sectorsCache[id] = sector
// Initialize draft land
val draftLand = net.hareworks.hcu.lands.model.Land(
id = -1, // Dummy ID
name = "Draft Sector $id",
actorId = ownerActorId,
world = world,
data = net.hareworks.hcu.lands.model.LandData()
)
draftLands[id] = draftLand
// Persist draft
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = id
it[SectorDraftsTable.data] = draftLand.data
}
sector sector
} }
} }
@ -77,7 +120,8 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
it[SectorsTable.hp] it[SectorsTable.hp],
it[SectorsTable.landId]
) )
} }
} }
@ -138,7 +182,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 } SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq id }
SectorsTable.deleteWhere { SectorsTable.id eq id } SectorsTable.deleteWhere { SectorsTable.id eq id }
Sector( Sector(
@ -148,7 +192,8 @@ class SectorService(private val database: Database) {
record[SectorsTable.x], record[SectorsTable.x],
record[SectorsTable.y], record[SectorsTable.y],
record[SectorsTable.z], record[SectorsTable.z],
record[SectorsTable.hp] record[SectorsTable.hp],
record[SectorsTable.landId]
) )
} }
@ -156,6 +201,11 @@ class SectorService(private val database: Database) {
hpCache.remove(id) hpCache.remove(id)
dirtySet.remove(id) dirtySet.remove(id)
sectorsCache.remove(id) sectorsCache.remove(id)
draftLands.remove(id)
if (sector.landId != null) {
landService.deleteLand(sector.landId)
}
} }
return sector return sector
@ -203,7 +253,8 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
hp hp,
it[SectorsTable.landId]
) )
} }
} }
@ -226,47 +277,196 @@ class SectorService(private val database: Database) {
it[SectorsTable.x], it[SectorsTable.x],
it[SectorsTable.y], it[SectorsTable.y],
it[SectorsTable.z], it[SectorsTable.z],
hp hp,
it[SectorsTable.landId]
) )
} }
} }
} }
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange { fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): Any? {
return transaction(database) { val sector = getSector(sectorId) ?: return null
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) // Cannot edit if already confirmed
if (sector.landId != null) return null
val draft = draftLands.computeIfAbsent(sectorId) {
net.hareworks.hcu.lands.model.Land(
id = -1,
name = "Draft Sector $sectorId",
actorId = sector.ownerActorId,
world = sector.world,
data = net.hareworks.hcu.lands.model.LandData()
)
} }
val shape = when (mode) {
SelectionMode.CUBOID -> {
net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2)
}
SelectionMode.CYLINDER -> {
// Convert bounds to cylinder params
// Approximating from bounding box
val minX = minOf(x1, x2)
val maxX = maxOf(x1, x2)
val minZ = minOf(z1, z2)
val maxZ = maxOf(z1, z2)
val minY = minOf(y1, y2)
val maxY = maxOf(y1, y2)
val cx = (minX + maxX) / 2
val cz = (minZ + maxZ) / 2
val cy = (minY + maxY) / 2
// Radius is max dist from center to edge of bounding box in X or Z
val radiusX = (maxX - minX) / 2.0
val radiusZ = (maxZ - minZ) / 2.0
val radius = maxOf(radiusX, radiusZ)
val height = maxY - minY + 1
val bottom = (cy - minY)
val top = (maxY - cy)
net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top)
}
else -> return null
}
// Use LandService modify pattern or direct replacement since stored in local map
val newParts = draft.data.parts.toMutableList()
newParts.add(shape)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
// Check if exists first? Or upsert if supported easily.
// We know it exists from creation, but safety check.
val count = SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
if (count == 0) {
SectorDraftsTable.insert {
it[SectorDraftsTable.sectorId] = sectorId
it[SectorDraftsTable.data] = newDraft.data
}
}
}
return shape
} }
fun getRanges(sectorId: Int): List<SectorRange> { fun getRanges(sectorId: Int): List<SectorRange> {
return transaction(database) { val sector = getSector(sectorId) ?: return emptyList()
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
val land = if (sector.landId != null) {
landService.getLand(sector.landId)
} else {
draftLands[sectorId]
} ?: return emptyList()
// Convert Shapes back to SectorRanges for UI compatibility
return land.data.parts.mapIndexed { index, shape ->
when (shape) {
is net.hareworks.hcu.lands.model.Shape.Cuboid -> {
SectorRange( SectorRange(
it[SectorRangesTable.id], index, // Use index as ID for UI
it[SectorRangesTable.sectorId], sectorId,
SelectionMode.valueOf(it[SectorRangesTable.type]), SelectionMode.CUBOID,
it[SectorRangesTable.x1], shape.x1, shape.y1, shape.z1,
it[SectorRangesTable.y1], shape.x2, shape.y2, shape.z2,
it[SectorRangesTable.z1], false
it[SectorRangesTable.x2],
it[SectorRangesTable.y2],
it[SectorRangesTable.z2],
it[SectorRangesTable.isSneaking]
) )
} }
is net.hareworks.hcu.lands.model.Shape.Cylinder -> {
SectorRange(
index,
sectorId,
SelectionMode.CYLINDER,
shape.x, shape.y, shape.z,
shape.x, shape.y, shape.z,
false
)
}
// Default fallback for unknown shapes if any (shouldn't happen with current limited types)
else -> SectorRange(index, sectorId, SelectionMode.CUBOID, 0,0,0,0,0,0, false)
}
}
}
fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Cannot edit confirmed
val draft = draftLands[sectorId] ?: return false
if (rangeIndex < 0 || rangeIndex >= draft.data.parts.size) return false
val newParts = draft.data.parts.toMutableList()
newParts.removeAt(rangeIndex)
val newDraft = draft.copy(data = draft.data.copy(parts = newParts))
draftLands[sectorId] = newDraft
// Persist change
transaction(database) {
SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) {
it[SectorDraftsTable.data] = newDraft.data
}
}
return true
}
fun activateSector(sectorId: Int): Boolean {
val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Already active
val draft = draftLands[sectorId] ?: return false
if (draft.data.parts.isEmpty()) return false
val landName = "Sector_${sectorId}_${System.currentTimeMillis()}"
if (landService.createLand(landName, draft.actorId, draft.world)) {
// Find the created land to get ID
val lands = landService.findLandsByName(landName)
val createdLand = lands.firstOrNull() ?: return false
// Update parts
landService.modifyLand(createdLand.id) { data ->
data.copy(parts = draft.data.parts)
}
// Update Sector DB & Delete Draft
transaction(database) {
SectorsTable.update({ SectorsTable.id eq sectorId }) {
it[SectorsTable.landId] = createdLand.id
}
SectorDraftsTable.deleteWhere { SectorDraftsTable.sectorId eq sectorId }
}
// Update cache & Clear draft
sectorsCache[sectorId] = sector.copy(landId = createdLand.id)
draftLands.remove(sectorId)
return true
}
return false
}
fun transferSector(id: Int, newOwnerActorId: Int): Boolean {
return transaction(database) {
val updated = SectorsTable.update({ SectorsTable.id eq id }) {
it[ownerActorId] = newOwnerActorId
}
if (updated > 0) {
// Update cache
val current = sectorsCache[id]
if (current != null) {
sectorsCache[id] = current.copy(ownerActorId = newOwnerActorId)
}
true
} else {
false
}
} }
} }
} }