diff --git a/Lands b/Lands index d27057d..0f432b0 160000 --- a/Lands +++ b/Lands @@ -1 +1 @@ -Subproject commit d27057dc89e228ce120c5b5b4534b3c130e8480a +Subproject commit 0f432b0e89614bc07bd710423284da5e0228e25a diff --git a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt index acc9d30..e0e7640 100644 --- a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -46,7 +46,14 @@ class LandSectorPlugin : JavaPlugin() { 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 { service.init() } catch (e: Exception) { @@ -63,7 +70,8 @@ class LandSectorPlugin : JavaPlugin() { val selService = selectionService 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) // Schedule visualization task diff --git a/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt b/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt index aba7ee9..35e26d5 100644 --- a/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt +++ b/bin/main/net/hareworks/hcu/landsector/command/LandSectorCommand.kt @@ -36,8 +36,13 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { integer("sectorId") { executes { val id = argument("sectorId") - sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) - // TODO: Validate selection and lock in + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.activateSector(id)) { + sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) + } } } } @@ -118,6 +123,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } + literal("transfer") { + integer("sectorId") { + integer("targetActorId") { + executes { + val sectorId = argument("sectorId") + val targetActorId = argument("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("sectorId") + val rangeIndex = argument("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") { integer("id") { executes { diff --git a/bin/main/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt b/bin/main/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt new file mode 100644 index 0000000..25ba3ed --- /dev/null +++ b/bin/main/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt @@ -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) +} diff --git a/bin/main/net/hareworks/hcu/landsector/database/SectorsTable.kt b/bin/main/net/hareworks/hcu/landsector/database/SectorsTable.kt index fc94862..d304da9 100644 --- a/bin/main/net/hareworks/hcu/landsector/database/SectorsTable.kt +++ b/bin/main/net/hareworks/hcu/landsector/database/SectorsTable.kt @@ -11,20 +11,7 @@ object SectorsTable : Table("land_sectors") { 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") + val landId = integer("land_id").nullable() override val primaryKey = PrimaryKey(id) } diff --git a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt index 859ee70..459fb19 100644 --- a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -31,11 +31,15 @@ import net.hareworks.hcu.landsector.service.SelectionService import net.kyori.adventure.text.event.ClickEvent import org.bukkit.event.player.PlayerInteractEntityEvent +import net.hareworks.hcu.faction.service.FactionService +import net.hareworks.hcu.faction.database.schema.FactionRole + class SectorListener( private val plugin: LandSectorPlugin, private val sectorService: SectorService, private val playerIdService: PlayerIdService, - private val selectionService: SelectionService + private val selectionService: SelectionService, + private val factionService: FactionService ) : Listener { @EventHandler @@ -216,7 +220,33 @@ class SectorListener( if (pEntry == null) 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)) return } @@ -230,7 +260,21 @@ class SectorListener( // Build Page Content 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 content.append( @@ -260,6 +304,12 @@ class SectorListener( content.append(Component.text("Parts:", NamedTextColor.BLACK)) ranges.forEach { range -> 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"))) + ) } } diff --git a/bin/main/net/hareworks/hcu/landsector/model/Sector.kt b/bin/main/net/hareworks/hcu/landsector/model/Sector.kt index 053efad..83499b9 100644 --- a/bin/main/net/hareworks/hcu/landsector/model/Sector.kt +++ b/bin/main/net/hareworks/hcu/landsector/model/Sector.kt @@ -7,5 +7,6 @@ data class Sector( val x: Int, val y: Int, val z: Int, - val hp: Int + val hp: Int, + val landId: Int? = null ) diff --git a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt index 88f49e0..d842281 100644 --- a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt @@ -1,7 +1,8 @@ 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.SectorRangesTable +// SectorRangesTable removed import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.SectorRange 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 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() private val dirtySet = ConcurrentHashMap.newKeySet() private val sectorsCache = ConcurrentHashMap() + + // Draft storage: SectorId -> Land (Draft) + private val draftLands = ConcurrentHashMap() fun init() { transaction(database) { - SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable) + SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorDraftsTable) + + // Load Sectors SectorsTable.selectAll().forEach { val id = it[SectorsTable.id] val sector = Sector( @@ -35,10 +42,28 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], it[SectorsTable.z], - it[SectorsTable.hp] + it[SectorsTable.hp], + it[SectorsTable.landId] ) sectorsCache[id] = sector } + + // Load Drafts + SectorDraftsTable.selectAll().forEach { + val sId = it[SectorDraftsTable.sectorId] + val data = it[SectorDraftsTable.data] + val sector = sectorsCache[sId] + + if (sector != null && sector.landId == null) { + draftLands[sId] = net.hareworks.hcu.lands.model.Land( + id = -1, + name = "Draft Sector $sId", + actorId = sector.ownerActorId, + world = sector.world, + data = data + ) + } + } } } @@ -51,11 +76,29 @@ class SectorService(private val database: Database) { it[SectorsTable.y] = y it[SectorsTable.z] = z it[SectorsTable.hp] = 1000 + it[SectorsTable.landId] = null }[SectorsTable.id] hpCache[id] = 1000 - val sector = Sector(id, ownerActorId, world, x, y, z, 1000) + val sector = Sector(id, ownerActorId, world, x, y, z, 1000, null) sectorsCache[id] = sector + + // Initialize draft land + val draftLand = net.hareworks.hcu.lands.model.Land( + id = -1, // Dummy ID + name = "Draft Sector $id", + actorId = ownerActorId, + world = world, + data = net.hareworks.hcu.lands.model.LandData() + ) + draftLands[id] = draftLand + + // Persist draft + SectorDraftsTable.insert { + it[SectorDraftsTable.sectorId] = id + it[SectorDraftsTable.data] = draftLand.data + } + sector } } @@ -77,7 +120,8 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], 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 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 } Sector( @@ -148,7 +192,8 @@ class SectorService(private val database: Database) { record[SectorsTable.x], record[SectorsTable.y], 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) dirtySet.remove(id) sectorsCache.remove(id) + draftLands.remove(id) + + if (sector.landId != null) { + landService.deleteLand(sector.landId) + } } return sector @@ -203,7 +253,8 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], it[SectorsTable.z], - hp + hp, + it[SectorsTable.landId] ) } } @@ -226,46 +277,195 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], 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 { - 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 addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): Any? { + val sector = getSector(sectorId) ?: return null + + // Cannot edit if already confirmed + if (sector.landId != null) return null + + val draft = draftLands.computeIfAbsent(sectorId) { + net.hareworks.hcu.lands.model.Land( + id = -1, + name = "Draft Sector $sectorId", + actorId = sector.ownerActorId, + world = sector.world, + data = net.hareworks.hcu.lands.model.LandData() + ) } + + val shape = when (mode) { + SelectionMode.CUBOID -> { + net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2) + } + SelectionMode.CYLINDER -> { + // Convert bounds to cylinder params + // Approximating from bounding box + val minX = minOf(x1, x2) + val maxX = maxOf(x1, x2) + val minZ = minOf(z1, z2) + val maxZ = maxOf(z1, z2) + val minY = minOf(y1, y2) + val maxY = maxOf(y1, y2) + + val cx = (minX + maxX) / 2 + val cz = (minZ + maxZ) / 2 + val cy = (minY + maxY) / 2 + + // Radius is max dist from center to edge of bounding box in X or Z + val radiusX = (maxX - minX) / 2.0 + val radiusZ = (maxZ - minZ) / 2.0 + val radius = maxOf(radiusX, radiusZ) + + val height = maxY - minY + 1 + val bottom = (cy - minY) + val top = (maxY - cy) + + net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top) + } + else -> return null + } + + // Use LandService modify pattern or direct replacement since stored in local map + val newParts = draft.data.parts.toMutableList() + newParts.add(shape) + val newDraft = draft.copy(data = draft.data.copy(parts = newParts)) + draftLands[sectorId] = newDraft + + // Persist change + transaction(database) { + // Check if exists first? Or upsert if supported easily. + // We know it exists from creation, but safety check. + val count = SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) { + it[SectorDraftsTable.data] = newDraft.data + } + if (count == 0) { + SectorDraftsTable.insert { + it[SectorDraftsTable.sectorId] = sectorId + it[SectorDraftsTable.data] = newDraft.data + } + } + } + + return shape } fun getRanges(sectorId: Int): List { + val sector = getSector(sectorId) ?: return emptyList() + + val land = if (sector.landId != null) { + landService.getLand(sector.landId) + } else { + draftLands[sectorId] + } ?: return emptyList() + + // Convert Shapes back to SectorRanges for UI compatibility + return land.data.parts.mapIndexed { index, shape -> + when (shape) { + is net.hareworks.hcu.lands.model.Shape.Cuboid -> { + SectorRange( + index, // Use index as ID for UI + sectorId, + SelectionMode.CUBOID, + shape.x1, shape.y1, shape.z1, + shape.x2, shape.y2, shape.z2, + false + ) + } + is net.hareworks.hcu.lands.model.Shape.Cylinder -> { + SectorRange( + index, + sectorId, + SelectionMode.CYLINDER, + shape.x, shape.y, shape.z, + shape.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) { - 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] - ) + 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 } } } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt index acc9d30..e0e7640 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -46,7 +46,14 @@ class LandSectorPlugin : JavaPlugin() { 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 { service.init() } catch (e: Exception) { @@ -63,7 +70,8 @@ class LandSectorPlugin : JavaPlugin() { val selService = selectionService 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) // Schedule visualization task diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt b/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt index aba7ee9..35e26d5 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/command/LandSectorCommand.kt @@ -36,8 +36,13 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { integer("sectorId") { executes { val id = argument("sectorId") - sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW)) - // TODO: Validate selection and lock in + val service = landSectorPlugin.sectorService ?: return@executes + + if (service.activateSector(id)) { + sender.sendMessage(Component.text("Sector #$id activated and land secured!", NamedTextColor.GREEN)) + } else { + sender.sendMessage(Component.text("Failed to activate sector #$id (Already active or empty parts?).", NamedTextColor.RED)) + } } } } @@ -118,6 +123,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) { } } + literal("transfer") { + integer("sectorId") { + integer("targetActorId") { + executes { + val sectorId = argument("sectorId") + val targetActorId = argument("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("sectorId") + val rangeIndex = argument("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") { integer("id") { executes { diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt b/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt new file mode 100644 index 0000000..25ba3ed --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorDraftsTable.kt @@ -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) +} diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorsTable.kt b/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorsTable.kt index fc94862..d304da9 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorsTable.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/database/SectorsTable.kt @@ -11,20 +11,7 @@ object SectorsTable : Table("land_sectors") { 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") + val landId = integer("land_id").nullable() override val primaryKey = PrimaryKey(id) } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt index 859ee70..459fb19 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -31,11 +31,15 @@ import net.hareworks.hcu.landsector.service.SelectionService import net.kyori.adventure.text.event.ClickEvent import org.bukkit.event.player.PlayerInteractEntityEvent +import net.hareworks.hcu.faction.service.FactionService +import net.hareworks.hcu.faction.database.schema.FactionRole + class SectorListener( private val plugin: LandSectorPlugin, private val sectorService: SectorService, private val playerIdService: PlayerIdService, - private val selectionService: SelectionService + private val selectionService: SelectionService, + private val factionService: FactionService ) : Listener { @EventHandler @@ -216,7 +220,33 @@ class SectorListener( if (pEntry == null) 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)) return } @@ -230,7 +260,21 @@ class SectorListener( // Build Page Content 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 content.append( @@ -260,6 +304,12 @@ class SectorListener( content.append(Component.text("Parts:", NamedTextColor.BLACK)) ranges.forEach { range -> 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"))) + ) } } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/model/Sector.kt b/src/main/kotlin/net/hareworks/hcu/landsector/model/Sector.kt index 053efad..83499b9 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/model/Sector.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/model/Sector.kt @@ -7,5 +7,6 @@ data class Sector( val x: Int, val y: Int, val z: Int, - val hp: Int + val hp: Int, + val landId: Int? = null ) diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt index 88f49e0..d842281 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt @@ -1,7 +1,8 @@ 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.SectorRangesTable +// SectorRangesTable removed import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.SectorRange 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 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() private val dirtySet = ConcurrentHashMap.newKeySet() private val sectorsCache = ConcurrentHashMap() + + // Draft storage: SectorId -> Land (Draft) + private val draftLands = ConcurrentHashMap() fun init() { transaction(database) { - SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable) + SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorDraftsTable) + + // Load Sectors SectorsTable.selectAll().forEach { val id = it[SectorsTable.id] val sector = Sector( @@ -35,10 +42,28 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], it[SectorsTable.z], - it[SectorsTable.hp] + it[SectorsTable.hp], + it[SectorsTable.landId] ) sectorsCache[id] = sector } + + // Load Drafts + SectorDraftsTable.selectAll().forEach { + val sId = it[SectorDraftsTable.sectorId] + val data = it[SectorDraftsTable.data] + val sector = sectorsCache[sId] + + if (sector != null && sector.landId == null) { + draftLands[sId] = net.hareworks.hcu.lands.model.Land( + id = -1, + name = "Draft Sector $sId", + actorId = sector.ownerActorId, + world = sector.world, + data = data + ) + } + } } } @@ -51,11 +76,29 @@ class SectorService(private val database: Database) { it[SectorsTable.y] = y it[SectorsTable.z] = z it[SectorsTable.hp] = 1000 + it[SectorsTable.landId] = null }[SectorsTable.id] hpCache[id] = 1000 - val sector = Sector(id, ownerActorId, world, x, y, z, 1000) + val sector = Sector(id, ownerActorId, world, x, y, z, 1000, null) sectorsCache[id] = sector + + // Initialize draft land + val draftLand = net.hareworks.hcu.lands.model.Land( + id = -1, // Dummy ID + name = "Draft Sector $id", + actorId = ownerActorId, + world = world, + data = net.hareworks.hcu.lands.model.LandData() + ) + draftLands[id] = draftLand + + // Persist draft + SectorDraftsTable.insert { + it[SectorDraftsTable.sectorId] = id + it[SectorDraftsTable.data] = draftLand.data + } + sector } } @@ -77,7 +120,8 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], 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 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 } Sector( @@ -148,7 +192,8 @@ class SectorService(private val database: Database) { record[SectorsTable.x], record[SectorsTable.y], 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) dirtySet.remove(id) sectorsCache.remove(id) + draftLands.remove(id) + + if (sector.landId != null) { + landService.deleteLand(sector.landId) + } } return sector @@ -203,7 +253,8 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], it[SectorsTable.z], - hp + hp, + it[SectorsTable.landId] ) } } @@ -226,46 +277,195 @@ class SectorService(private val database: Database) { it[SectorsTable.x], it[SectorsTable.y], 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 { - 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 addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): Any? { + val sector = getSector(sectorId) ?: return null + + // Cannot edit if already confirmed + if (sector.landId != null) return null + + val draft = draftLands.computeIfAbsent(sectorId) { + net.hareworks.hcu.lands.model.Land( + id = -1, + name = "Draft Sector $sectorId", + actorId = sector.ownerActorId, + world = sector.world, + data = net.hareworks.hcu.lands.model.LandData() + ) } + + val shape = when (mode) { + SelectionMode.CUBOID -> { + net.hareworks.hcu.lands.model.Shape.Cuboid(x1, y1, z1, x2, y2, z2) + } + SelectionMode.CYLINDER -> { + // Convert bounds to cylinder params + // Approximating from bounding box + val minX = minOf(x1, x2) + val maxX = maxOf(x1, x2) + val minZ = minOf(z1, z2) + val maxZ = maxOf(z1, z2) + val minY = minOf(y1, y2) + val maxY = maxOf(y1, y2) + + val cx = (minX + maxX) / 2 + val cz = (minZ + maxZ) / 2 + val cy = (minY + maxY) / 2 + + // Radius is max dist from center to edge of bounding box in X or Z + val radiusX = (maxX - minX) / 2.0 + val radiusZ = (maxZ - minZ) / 2.0 + val radius = maxOf(radiusX, radiusZ) + + val height = maxY - minY + 1 + val bottom = (cy - minY) + val top = (maxY - cy) + + net.hareworks.hcu.lands.model.Shape.Cylinder(cx, cy, cz, radius, bottom, top) + } + else -> return null + } + + // Use LandService modify pattern or direct replacement since stored in local map + val newParts = draft.data.parts.toMutableList() + newParts.add(shape) + val newDraft = draft.copy(data = draft.data.copy(parts = newParts)) + draftLands[sectorId] = newDraft + + // Persist change + transaction(database) { + // Check if exists first? Or upsert if supported easily. + // We know it exists from creation, but safety check. + val count = SectorDraftsTable.update({ SectorDraftsTable.sectorId eq sectorId }) { + it[SectorDraftsTable.data] = newDraft.data + } + if (count == 0) { + SectorDraftsTable.insert { + it[SectorDraftsTable.sectorId] = sectorId + it[SectorDraftsTable.data] = newDraft.data + } + } + } + + return shape } fun getRanges(sectorId: Int): List { + val sector = getSector(sectorId) ?: return emptyList() + + val land = if (sector.landId != null) { + landService.getLand(sector.landId) + } else { + draftLands[sectorId] + } ?: return emptyList() + + // Convert Shapes back to SectorRanges for UI compatibility + return land.data.parts.mapIndexed { index, shape -> + when (shape) { + is net.hareworks.hcu.lands.model.Shape.Cuboid -> { + SectorRange( + index, // Use index as ID for UI + sectorId, + SelectionMode.CUBOID, + shape.x1, shape.y1, shape.z1, + shape.x2, shape.y2, shape.z2, + false + ) + } + is net.hareworks.hcu.lands.model.Shape.Cylinder -> { + SectorRange( + index, + sectorId, + SelectionMode.CYLINDER, + shape.x, shape.y, shape.z, + shape.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) { - 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] - ) + 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 } } }