From 32e4641e2438a642ff04d3d07fb672bbe7ec1baa Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 9 Dec 2025 10:13:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20id=E4=B8=BB=E3=82=AD=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/net/hareworks/hcu/lands/App.kt | 3 + .../hareworks/hcu/lands/api/LandsAPIImpl.kt | 8 +- .../hcu/lands/command/LandsCommand.kt | 85 ++++++++++++++----- .../hcu/lands/database/LandsTable.kt | 5 +- .../hareworks/hcu/lands/index/LandIndex.kt | 12 +-- .../net/hareworks/hcu/lands/model/Land.kt | 1 + .../hcu/lands/service/LandService.kt | 43 ++++++---- 7 files changed, 111 insertions(+), 46 deletions(-) diff --git a/src/main/kotlin/net/hareworks/hcu/lands/App.kt b/src/main/kotlin/net/hareworks/hcu/lands/App.kt index e5509ad..511af66 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/App.kt @@ -13,6 +13,8 @@ class App : JavaPlugin() { var landService: LandService? = null var playerIdService: PlayerIdService? = null + + var actorIdentityService: ActorIdentityService? = null override fun onEnable() { // Register commands immediately to ensure they are picked up by LifecycleEventManager @@ -54,6 +56,7 @@ class App : JavaPlugin() { return } this.playerIdService = pIdService + this.actorIdentityService = actorService // Register public API as a Bukkit service val landsAPI = net.hareworks.hcu.lands.api.LandsAPIImpl(service) diff --git a/src/main/kotlin/net/hareworks/hcu/lands/api/LandsAPIImpl.kt b/src/main/kotlin/net/hareworks/hcu/lands/api/LandsAPIImpl.kt index 3510e7c..e59f795 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/api/LandsAPIImpl.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/api/LandsAPIImpl.kt @@ -25,7 +25,7 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI { } override fun getLand(name: String): Land? { - return landService.getLand(name) + return landService.findLandsByName(name).firstOrNull() } override fun getLandsInWorld(world: String): List { @@ -45,10 +45,12 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI { } override fun deleteLand(name: String): Boolean { - return landService.deleteLand(name) + val land = landService.findLandsByName(name).firstOrNull() ?: return false + return landService.deleteLand(land.id) } override fun modifyLand(name: String, modifier: (LandData) -> Unit): Boolean { - return landService.modifyLand(name, modifier) + val land = landService.findLandsByName(name).firstOrNull() ?: return false + return landService.modifyLand(land.id, modifier) } } diff --git a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt index b2f3d2f..1e734f1 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt @@ -41,7 +41,7 @@ class LandsCommand( if (service.createLand(name, playerEntry.actorId, player.world.name)) { sender.sendMessage(Component.text("Land '$name' created.", NamedTextColor.GREEN)) } else { - sender.sendMessage(Component.text("Land '$name' already exists.", NamedTextColor.RED)) + sender.sendMessage(Component.text("Failed to create land. Name '$name' might already be taken by you.", NamedTextColor.RED)) } } } @@ -58,20 +58,21 @@ class LandsCommand( return@executes } - val name: String = argument("name") - - val land: Land? = service.getLand(name) - if (land == null) { - sender.sendMessage(Component.text("Land not found.", NamedTextColor.RED)) + val nameInput: String = argument("name") + val playerEntry = playerIdService.find(player.uniqueId) + if (playerEntry == null) { + sender.sendMessage(Component.text("Identity not found.", NamedTextColor.RED)) return@executes } - val playerEntry = playerIdService.find(player.uniqueId) - if (playerEntry == null || (land.actorId != playerEntry.actorId && !player.isOp)) { + + val land = resolveLand(sender, nameInput, if (player.isOp) null else playerEntry.actorId) ?: return@executes + + if (land.actorId != playerEntry.actorId && !player.isOp) { sender.sendMessage(Component.text("You do not own this land.", NamedTextColor.RED)) return@executes } - if (service.deleteLand(name)) { + if (service.deleteLand(land.id)) { sender.sendMessage(Component.text("Land deleted.", NamedTextColor.GREEN)) } else { sender.sendMessage(Component.text("Failed to delete.", NamedTextColor.RED)) @@ -394,24 +395,27 @@ class LandsCommand( } } - private fun updateLandData(sender: CommandSender, name: String, action: (MutableList) -> Unit) { + private fun updateLandData(sender: CommandSender, inputName: String, action: (MutableList) -> Unit) { val player = sender as? Player ?: return val service = app.landService val playerIdService = app.playerIdService if (service == null || playerIdService == null) { - sender.sendMessage(Component.text("Lands service is not ready yet.", NamedTextColor.RED)) + sender.sendMessage(Component.text("Services not ready.", NamedTextColor.RED)) return } - val land = service.getLand(name) - if (land == null) { - sender.sendMessage(Component.text("Land not found.", NamedTextColor.RED)) - return - } - val playerEntry = playerIdService.find(player.uniqueId) - if (playerEntry == null || (land.actorId != playerEntry.actorId && !player.isOp)) { + if (playerEntry == null) { + sender.sendMessage(Component.text("Identity not found.", NamedTextColor.RED)) + return + } + + // Resolve land using new logic (ID or Name) + val land = resolveLand(sender, inputName, if (player.isOp) null else playerEntry.actorId) ?: return + + // Additional ownership check (resolveLand prefers owner's land but doesn't guarantee it if purely by ID) + if (land.actorId != playerEntry.actorId && !player.isOp) { sender.sendMessage(Component.text("You do not own this land.", NamedTextColor.RED)) return } @@ -421,8 +425,51 @@ class LandsCommand( return } - service.modifyLand(name) { data -> + service.modifyLand(land.id) { data -> action(data.parts) } } + + private fun resolveLand(sender: CommandSender, input: String, ownerActorId: Int? = null): Land? { + val service = app.landService!! + val playerIdService = app.playerIdService!! + + // Try ID-Name format (e.g., "123-mylasso") + val idMatch = Regex("^(\\d+)-(.+)$").find(input) + if (idMatch != null) { + val id = idMatch.groupValues[1].toInt() + val namePart = idMatch.groupValues[2] + val land = service.getLand(id) + if (land != null && land.name.equals(namePart, ignoreCase = true)) { + return land + } + } + + // Try precise Name search + val candidates = service.findLandsByName(input) + if (candidates.isEmpty()) { + sender.sendMessage(Component.text("Land '$input' not found.", NamedTextColor.RED)) + return null + } + + // If owner is specified, try to find exact match for that owner + if (ownerActorId != null) { + val owned = candidates.find { it.actorId == ownerActorId } + if (owned != null) return owned + } + + // If single match (either only one exists or filtering didn't yield unique but list size is 1) + if (candidates.size == 1) { + return candidates.first() + } + + // Multiple matches + sender.sendMessage(Component.text("There are multiple entries named \"$input\". To specify which one you mean, please enter it as id-$input.", NamedTextColor.RED)) + candidates.forEach { land -> + val ownerName = "Unknown" // app.actorIdentityService?.resolveName(land.actorId) ?: "Unknown" + sender.sendMessage(Component.text("${land.id} - ${land.name} - $ownerName", NamedTextColor.GRAY)) + } + + return null + } } diff --git a/src/main/kotlin/net/hareworks/hcu/lands/database/LandsTable.kt b/src/main/kotlin/net/hareworks/hcu/lands/database/LandsTable.kt index 30bc481..a566332 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/database/LandsTable.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/database/LandsTable.kt @@ -10,12 +10,13 @@ import org.jetbrains.exposed.v1.core.Table import org.postgresql.util.PGobject object LandsTable : Table("lands") { - val name = varchar("name", 64) + val id = integer("id").autoIncrement() + val name = varchar("name", 64).index() val actorId = integer("actor_id") val world = varchar("world", 64) val data = json("data", LandData.serializer()) - override val primaryKey = PrimaryKey(name) + override val primaryKey = PrimaryKey(id) } fun Table.json(name: String, serializer: KSerializer): Column = diff --git a/src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt b/src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt index d1d6f95..309cc1d 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt @@ -17,7 +17,7 @@ class LandIndex { private val chunkIndex = ConcurrentHashMap>() // Land -> its bounding box (cached) - private val boundingBoxCache = ConcurrentHashMap() + private val boundingBoxCache = ConcurrentHashMap() /** * Rebuilds the entire index from a collection of lands. @@ -37,7 +37,7 @@ class LandIndex { */ fun addLand(land: Land) { val bbox = calculateBoundingBox(land) - boundingBoxCache[land.name] = bbox + boundingBoxCache[land.id] = bbox val chunks = bbox.getAffectedChunks(land.world) for (chunk in chunks) { @@ -49,13 +49,13 @@ class LandIndex { * Removes a land from the index. */ fun removeLand(land: Land) { - val bbox = boundingBoxCache.remove(land.name) ?: return + val bbox = boundingBoxCache.remove(land.id) ?: return val chunks = bbox.getAffectedChunks(land.world) for (chunk in chunks) { // Use removeIf to ensure we remove the land even if its data has changed - // (since equals/hashCode would be different for modified data classes) - chunkIndex[chunk]?.removeIf { it.name == land.name } + // We use ID for identity check + chunkIndex[chunk]?.removeIf { it.id == land.id } if (chunkIndex[chunk]?.isEmpty() == true) { chunkIndex.remove(chunk) @@ -83,7 +83,7 @@ class LandIndex { // Check each candidate land in this chunk for (land in candidates) { // Early exit: check bounding box first - val bbox = boundingBoxCache[land.name] + val bbox = boundingBoxCache[land.id] if (bbox != null && !bbox.contains(x, y, z)) { continue } diff --git a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt index 3ee8d14..4557c55 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class Land( + val id: Int, val name: String, val actorId: Int, val world: String, diff --git a/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt b/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt index 52deef4..110f14d 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/service/LandService.kt @@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap import org.bukkit.Bukkit class LandService(private val database: Database) { - private val cache = ConcurrentHashMap() + private val cache = ConcurrentHashMap() // Cache key is now ID (Int) private val spatialIndex = LandIndex() fun init() { @@ -30,12 +30,13 @@ class LandService(private val database: Database) { cache.clear() LandsTable.selectAll().forEach { row -> val land = Land( + id = row[LandsTable.id], name = row[LandsTable.name], actorId = row[LandsTable.actorId], world = row[LandsTable.world], data = row[LandsTable.data] ) - cache[land.name] = land + cache[land.id] = land } // Rebuild spatial index @@ -43,60 +44,70 @@ class LandService(private val database: Database) { } fun createLand(name: String, actorId: Int, world: String): Boolean { - if (cache.containsKey(name)) return false + // Validation: A single owner cannot have multiple lands with the same name + if (cache.values.any { it.actorId == actorId && it.name.equals(name, ignoreCase = true) }) { + return false + } + var newId = -1 transaction(database) { - LandsTable.insert { + val statement = LandsTable.insert { it[LandsTable.name] = name it[LandsTable.actorId] = actorId it[LandsTable.world] = world it[LandsTable.data] = LandData() } + newId = statement[LandsTable.id] } + // Update cache and index - val land = Land(name, actorId, world, LandData()) - cache[name] = land + val land = Land(newId, name, actorId, world, LandData()) + cache[newId] = land spatialIndex.addLand(land) return true } - fun modifyLand(name: String, modifier: (LandData) -> Unit): Boolean { - val land = cache[name] ?: return false + fun modifyLand(id: Int, modifier: (LandData) -> Unit): Boolean { + val land = cache[id] ?: return false val newParts = ArrayList(land.data.parts) val newData = land.data.copy(parts = newParts) modifier(newData) transaction(database) { - LandsTable.update({ LandsTable.name eq name }) { + LandsTable.update({ LandsTable.id eq id }) { it[LandsTable.data] = newData } } val updatedLand = land.copy(data = newData) - cache[name] = updatedLand + cache[id] = updatedLand // Update spatial index spatialIndex.updateLand(updatedLand) return true } - fun deleteLand(name: String): Boolean { - if (!cache.containsKey(name)) return false - val land = cache[name]!! + fun deleteLand(id: Int): Boolean { + if (!cache.containsKey(id)) return false + val land = cache[id]!! transaction(database) { - LandsTable.deleteWhere { LandsTable.name eq name } + LandsTable.deleteWhere { LandsTable.id eq id } } - cache.remove(name) + cache.remove(id) // Remove from spatial index spatialIndex.removeLand(land) return true } - fun getLand(name: String): Land? = cache[name] + fun getLand(id: Int): Land? = cache[id] + + fun findLandsByName(name: String): List { + return cache.values.filter { it.name.equals(name, ignoreCase = true) } + } fun getAllLands(): Collection = cache.values