feat: id主キー
This commit is contained in:
parent
576005aada
commit
32e4641e24
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> =
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user