feat: id主キー

This commit is contained in:
Keisuke Hirata 2025-12-09 10:13:43 +09:00
parent 576005aada
commit 32e4641e24
7 changed files with 111 additions and 46 deletions

View File

@ -14,6 +14,8 @@ class App : JavaPlugin() {
var playerIdService: PlayerIdService? = null var playerIdService: PlayerIdService? = null
var actorIdentityService: ActorIdentityService? = null
override fun onEnable() { override fun onEnable() {
// Register commands immediately to ensure they are picked up by LifecycleEventManager // Register commands immediately to ensure they are picked up by LifecycleEventManager
LandsCommand(this).register() LandsCommand(this).register()
@ -54,6 +56,7 @@ class App : JavaPlugin() {
return return
} }
this.playerIdService = pIdService this.playerIdService = pIdService
this.actorIdentityService = actorService
// Register public API as a Bukkit service // Register public API as a Bukkit service
val landsAPI = net.hareworks.hcu.lands.api.LandsAPIImpl(service) val landsAPI = net.hareworks.hcu.lands.api.LandsAPIImpl(service)

View File

@ -25,7 +25,7 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI {
} }
override fun getLand(name: String): Land? { override fun getLand(name: String): Land? {
return landService.getLand(name) return landService.findLandsByName(name).firstOrNull()
} }
override fun getLandsInWorld(world: String): List<Land> { override fun getLandsInWorld(world: String): List<Land> {
@ -45,10 +45,12 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI {
} }
override fun deleteLand(name: String): Boolean { 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 { 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)
} }
} }

View File

@ -41,7 +41,7 @@ class LandsCommand(
if (service.createLand(name, playerEntry.actorId, player.world.name)) { if (service.createLand(name, playerEntry.actorId, player.world.name)) {
sender.sendMessage(Component.text("Land '$name' created.", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Land '$name' created.", NamedTextColor.GREEN))
} else { } 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 return@executes
} }
val name: String = argument("name") val nameInput: String = argument("name")
val playerEntry = playerIdService.find(player.uniqueId)
val land: Land? = service.getLand(name) if (playerEntry == null) {
if (land == null) { sender.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
sender.sendMessage(Component.text("Land not found.", NamedTextColor.RED))
return@executes 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)) sender.sendMessage(Component.text("You do not own this land.", NamedTextColor.RED))
return@executes return@executes
} }
if (service.deleteLand(name)) { if (service.deleteLand(land.id)) {
sender.sendMessage(Component.text("Land deleted.", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Land deleted.", NamedTextColor.GREEN))
} else { } else {
sender.sendMessage(Component.text("Failed to delete.", NamedTextColor.RED)) sender.sendMessage(Component.text("Failed to delete.", NamedTextColor.RED))
@ -394,24 +395,27 @@ class LandsCommand(
} }
} }
private fun updateLandData(sender: CommandSender, name: String, action: (MutableList<Shape>) -> Unit) { private fun updateLandData(sender: CommandSender, inputName: String, action: (MutableList<Shape>) -> Unit) {
val player = sender as? Player ?: return val player = sender as? Player ?: return
val service = app.landService val service = app.landService
val playerIdService = app.playerIdService val playerIdService = app.playerIdService
if (service == null || playerIdService == null) { 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 return
} }
val land = service.getLand(name) val playerEntry = playerIdService.find(player.uniqueId)
if (land == null) { if (playerEntry == null) {
sender.sendMessage(Component.text("Land not found.", NamedTextColor.RED)) sender.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
return return
} }
val playerEntry = playerIdService.find(player.uniqueId) // Resolve land using new logic (ID or Name)
if (playerEntry == null || (land.actorId != playerEntry.actorId && !player.isOp)) { 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)) sender.sendMessage(Component.text("You do not own this land.", NamedTextColor.RED))
return return
} }
@ -421,8 +425,51 @@ class LandsCommand(
return return
} }
service.modifyLand(name) { data -> service.modifyLand(land.id) { data ->
action(data.parts) 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
}
} }

View File

@ -10,12 +10,13 @@ import org.jetbrains.exposed.v1.core.Table
import org.postgresql.util.PGobject import org.postgresql.util.PGobject
object LandsTable : Table("lands") { 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 actorId = integer("actor_id")
val world = varchar("world", 64) val world = varchar("world", 64)
val data = json("data", LandData.serializer()) val data = json("data", LandData.serializer())
override val primaryKey = PrimaryKey(name) override val primaryKey = PrimaryKey(id)
} }
fun <T : Any> Table.json(name: String, serializer: KSerializer<T>): Column<T> = fun <T : Any> Table.json(name: String, serializer: KSerializer<T>): Column<T> =

View File

@ -17,7 +17,7 @@ class LandIndex {
private val chunkIndex = ConcurrentHashMap<ChunkKey, MutableSet<Land>>() private val chunkIndex = ConcurrentHashMap<ChunkKey, MutableSet<Land>>()
// Land -> its bounding box (cached) // Land -> its bounding box (cached)
private val boundingBoxCache = ConcurrentHashMap<String, BoundingBox>() private val boundingBoxCache = ConcurrentHashMap<Int, BoundingBox>()
/** /**
* Rebuilds the entire index from a collection of lands. * Rebuilds the entire index from a collection of lands.
@ -37,7 +37,7 @@ class LandIndex {
*/ */
fun addLand(land: Land) { fun addLand(land: Land) {
val bbox = calculateBoundingBox(land) val bbox = calculateBoundingBox(land)
boundingBoxCache[land.name] = bbox boundingBoxCache[land.id] = bbox
val chunks = bbox.getAffectedChunks(land.world) val chunks = bbox.getAffectedChunks(land.world)
for (chunk in chunks) { for (chunk in chunks) {
@ -49,13 +49,13 @@ class LandIndex {
* Removes a land from the index. * Removes a land from the index.
*/ */
fun removeLand(land: Land) { fun removeLand(land: Land) {
val bbox = boundingBoxCache.remove(land.name) ?: return val bbox = boundingBoxCache.remove(land.id) ?: return
val chunks = bbox.getAffectedChunks(land.world) val chunks = bbox.getAffectedChunks(land.world)
for (chunk in chunks) { for (chunk in chunks) {
// Use removeIf to ensure we remove the land even if its data has changed // Use removeIf to ensure we remove the land even if its data has changed
// (since equals/hashCode would be different for modified data classes) // We use ID for identity check
chunkIndex[chunk]?.removeIf { it.name == land.name } chunkIndex[chunk]?.removeIf { it.id == land.id }
if (chunkIndex[chunk]?.isEmpty() == true) { if (chunkIndex[chunk]?.isEmpty() == true) {
chunkIndex.remove(chunk) chunkIndex.remove(chunk)
@ -83,7 +83,7 @@ class LandIndex {
// Check each candidate land in this chunk // Check each candidate land in this chunk
for (land in candidates) { for (land in candidates) {
// Early exit: check bounding box first // Early exit: check bounding box first
val bbox = boundingBoxCache[land.name] val bbox = boundingBoxCache[land.id]
if (bbox != null && !bbox.contains(x, y, z)) { if (bbox != null && !bbox.contains(x, y, z)) {
continue continue
} }

View File

@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Land( data class Land(
val id: Int,
val name: String, val name: String,
val actorId: Int, val actorId: Int,
val world: String, val world: String,

View File

@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap
import org.bukkit.Bukkit import org.bukkit.Bukkit
class LandService(private val database: Database) { class LandService(private val database: Database) {
private val cache = ConcurrentHashMap<String, Land>() private val cache = ConcurrentHashMap<Int, Land>() // Cache key is now ID (Int)
private val spatialIndex = LandIndex() private val spatialIndex = LandIndex()
fun init() { fun init() {
@ -30,12 +30,13 @@ class LandService(private val database: Database) {
cache.clear() cache.clear()
LandsTable.selectAll().forEach { row -> LandsTable.selectAll().forEach { row ->
val land = Land( val land = Land(
id = row[LandsTable.id],
name = row[LandsTable.name], name = row[LandsTable.name],
actorId = row[LandsTable.actorId], actorId = row[LandsTable.actorId],
world = row[LandsTable.world], world = row[LandsTable.world],
data = row[LandsTable.data] data = row[LandsTable.data]
) )
cache[land.name] = land cache[land.id] = land
} }
// Rebuild spatial index // Rebuild spatial index
@ -43,60 +44,70 @@ class LandService(private val database: Database) {
} }
fun createLand(name: String, actorId: Int, world: String): Boolean { 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) { transaction(database) {
LandsTable.insert { val statement = LandsTable.insert {
it[LandsTable.name] = name it[LandsTable.name] = name
it[LandsTable.actorId] = actorId it[LandsTable.actorId] = actorId
it[LandsTable.world] = world it[LandsTable.world] = world
it[LandsTable.data] = LandData() it[LandsTable.data] = LandData()
} }
newId = statement[LandsTable.id]
} }
// Update cache and index // Update cache and index
val land = Land(name, actorId, world, LandData()) val land = Land(newId, name, actorId, world, LandData())
cache[name] = land cache[newId] = land
spatialIndex.addLand(land) spatialIndex.addLand(land)
return true return true
} }
fun modifyLand(name: String, modifier: (LandData) -> Unit): Boolean { fun modifyLand(id: Int, modifier: (LandData) -> Unit): Boolean {
val land = cache[name] ?: return false val land = cache[id] ?: return false
val newParts = ArrayList(land.data.parts) val newParts = ArrayList(land.data.parts)
val newData = land.data.copy(parts = newParts) val newData = land.data.copy(parts = newParts)
modifier(newData) modifier(newData)
transaction(database) { transaction(database) {
LandsTable.update({ LandsTable.name eq name }) { LandsTable.update({ LandsTable.id eq id }) {
it[LandsTable.data] = newData it[LandsTable.data] = newData
} }
} }
val updatedLand = land.copy(data = newData) val updatedLand = land.copy(data = newData)
cache[name] = updatedLand cache[id] = updatedLand
// Update spatial index // Update spatial index
spatialIndex.updateLand(updatedLand) spatialIndex.updateLand(updatedLand)
return true return true
} }
fun deleteLand(name: String): Boolean { fun deleteLand(id: Int): Boolean {
if (!cache.containsKey(name)) return false if (!cache.containsKey(id)) return false
val land = cache[name]!! val land = cache[id]!!
transaction(database) { transaction(database) {
LandsTable.deleteWhere { LandsTable.name eq name } LandsTable.deleteWhere { LandsTable.id eq id }
} }
cache.remove(name) cache.remove(id)
// Remove from spatial index // Remove from spatial index
spatialIndex.removeLand(land) spatialIndex.removeLand(land)
return true return true
} }
fun getLand(name: String): Land? = cache[name] fun getLand(id: Int): Land? = cache[id]
fun findLandsByName(name: String): List<Land> {
return cache.values.filter { it.name.equals(name, ignoreCase = true) }
}
fun getAllLands(): Collection<Land> = cache.values fun getAllLands(): Collection<Land> = cache.values