feat: id主キー
This commit is contained in:
parent
576005aada
commit
32e4641e24
|
|
@ -14,6 +14,8 @@ class App : JavaPlugin() {
|
|||
|
||||
var playerIdService: PlayerIdService? = null
|
||||
|
||||
var actorIdentityService: ActorIdentityService? = null
|
||||
|
||||
override fun onEnable() {
|
||||
// Register commands immediately to ensure they are picked up by LifecycleEventManager
|
||||
LandsCommand(this).register()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Land> {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Shape>) -> Unit) {
|
||||
private fun updateLandData(sender: CommandSender, inputName: String, action: (MutableList<Shape>) -> 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) {
|
||||
sender.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
|
||||
return
|
||||
}
|
||||
|
||||
val playerEntry = playerIdService.find(player.uniqueId)
|
||||
if (playerEntry == null || (land.actorId != playerEntry.actorId && !player.isOp)) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <T : Any> Table.json(name: String, serializer: KSerializer<T>): Column<T> =
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class LandIndex {
|
|||
private val chunkIndex = ConcurrentHashMap<ChunkKey, MutableSet<Land>>()
|
||||
|
||||
// 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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Serializable
|
||||
data class Land(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val actorId: Int,
|
||||
val world: String,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap
|
|||
import org.bukkit.Bukkit
|
||||
|
||||
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()
|
||||
|
||||
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<Land> {
|
||||
return cache.values.filter { it.name.equals(name, ignoreCase = true) }
|
||||
}
|
||||
|
||||
fun getAllLands(): Collection<Land> = cache.values
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user