Skip to content
This repository was archived by the owner on Aug 19, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/main/kotlin/org/mcediagui/DatabaseManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import java.io.File
import java.sql.Connection
import java.sql.DriverManager

/**
* 数据库连接管理器
* 使用单一持久连接,避免频繁创建/关闭连接导致文件描述符泄漏
*/
object DatabaseManager {
@Volatile
private var connection: Connection? = null
private lateinit var plugin: JavaPlugin
private val lock = Any()

fun init(plugin: JavaPlugin) {
this.plugin = plugin
Expand All @@ -18,16 +24,22 @@ object DatabaseManager {
}

fun getConnection(): Connection? {
if (connection?.isClosed == true) {
val dbFile = File(plugin.dataFolder, "mcediagui.db")
connection = DriverManager.getConnection("jdbc:sqlite:${dbFile.absolutePath}")
synchronized(lock) {
val conn = connection
if (conn == null || conn.isClosed) {
val dbFile = File(plugin.dataFolder, "mcediagui.db")
Comment on lines 26 to +30

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DatabaseManager 改为返回单一持久 Connection,但目前仅在 getConnection 内部创建/切换连接时加锁;实际 SQL 执行发生在多个 CompletableFuture 线程中,会并发共享同一个 Connection。JDBC 的 SQLite 连接通常不支持并发使用,容易出现竞态/database is locked 等问题。建议把数据库操作整体串行化(例如提供 withConnection {} 并在其中同步执行 SQL,或使用单线程 Executor 运行所有 storage future),而不是只同步获取连接。

Copilot uses AI. Check for mistakes.
connection = DriverManager.getConnection("jdbc:sqlite:${dbFile.absolutePath}")
}
return connection
}
return connection
}

fun close() {
try {
connection?.close()
} catch (_: Exception) {}
synchronized(lock) {
try {
connection?.close()
connection = null
} catch (_: Exception) {}
}
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/org/mcediagui/McediaGUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.InventoryHolder
import org.bukkit.inventory.ItemStack
Expand Down Expand Up @@ -148,6 +150,21 @@ class McediaGUI(private val plugin: JavaPlugin, private val manager: McediaManag
}
}

@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
val player = event.player as? Player ?: return
val state = playerGUIState[player.uniqueId] ?: return
// 只有在不等待聊天输入时才清理状态
if (state.tempData["awaiting_input"] == null) {
playerGUIState.remove(player.uniqueId)
}
}

@EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) {
playerGUIState.remove(event.player.uniqueId)
}

fun handleChatInput(player: Player, message: String): Boolean {
val state = playerGUIState[player.uniqueId] ?: return false
val awaiting = state.tempData["awaiting_input"] as? String ?: return false
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/org/mcediagui/McediaListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.player.PlayerArmorStandManipulateEvent
import org.bukkit.event.player.PlayerInteractAtEntityEvent
import org.bukkit.event.world.ChunkLoadEvent
import org.bukkit.event.world.EntitiesUnloadEvent
import org.bukkit.plugin.java.JavaPlugin

class McediaListener(
Expand Down Expand Up @@ -95,4 +96,24 @@ class McediaListener(
if (!manager.isEnabled()) return
manager.processPendingOperations(event.chunk.world.name, event.chunk.x, event.chunk.z)
}

/**
* 当实体被卸载时,检查是否有播放器需要从缓存中保持同步
* 注意:这里只处理被实际删除的情况(实体不再存在)
*/
@EventHandler
fun onEntitiesUnload(event: EntitiesUnloadEvent) {
if (!manager.isEnabled()) return
event.entities.filterIsInstance<ArmorStand>().forEach { armorStand ->
val customName = armorStand.customName()?.let {
net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(it)
} ?: ""
if (customName.startsWith(prefix)) {
// 如果实体被标记为无效(isDead),说明是被真正删除了,需要清理缓存
if (armorStand.isDead) {
manager.removeFromCache(armorStand.uniqueId)
}
}
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/org/mcediagui/McediaManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ class McediaManager(private val plugin: JavaPlugin) {
}

fun deleteTemplate(playerUUID: UUID, templateId: Int): Boolean = storage?.deleteTemplate(playerUUID, templateId)?.join() ?: false

/**
* 从缓存中移除播放器(用于实体被外部删除时的清理)
*/
fun removeFromCache(uuid: UUID) {
players.remove(uuid)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeFromCache 目前只移除了内存缓存,没有同步删除数据库中的 mcedia_players 记录。若 ArmorStand 被外部删除,此处清理会导致“运行中看不到但重启后又从数据库加载回来”的脏数据;也可能影响后续自动注册逻辑覆盖 createdBy/createdAt。建议在确认实体确实被永久删除时,同时调用 storage.delete(uuid)(以及必要时清理 pending ops)。

Suggested change
players.remove(uuid)
players.remove(uuid)
storage?.delete(uuid)

Copilot uses AI. Check for mistakes.
}

fun shutdown() { storage?.close(); storage = null; players.clear() }
}

Expand Down
Loading
Loading