From ad4e1c1ebbe0ceb22b3ded2487a7b3dc40536795 Mon Sep 17 00:00:00 2001 From: MineSunshineone Date: Sat, 7 Feb 2026 22:34:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=B3=84=E6=BC=8F=E5=92=8C=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=B3=84=E6=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - McediaGUI: 添加 PlayerQuitEvent 和 InventoryCloseEvent 监听器,清理 playerGUIState 防止玩家退出后状态残留 - McediaListener: 添加 EntitiesUnloadEvent 监听器,当 ArmorStand 被外部删除时同步清理播放器缓存 - McediaManager: 添加 removeFromCache() 方法供监听器调用 - DatabaseManager: 添加 synchronized 锁和 @Volatile 注解,确保多线程安全 - McediaStorage: 移除对 Connection 的 .use{} 调用,只对 Statement 使用,保持连接持久打开,避免文件描述符泄漏 --- .../kotlin/org/mcediagui/DatabaseManager.kt | 26 +++- src/main/kotlin/org/mcediagui/McediaGUI.kt | 17 ++ .../kotlin/org/mcediagui/McediaListener.kt | 21 +++ .../kotlin/org/mcediagui/McediaManager.kt | 8 + .../kotlin/org/mcediagui/McediaStorage.kt | 146 +++++++++++++----- 5 files changed, 169 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/org/mcediagui/DatabaseManager.kt b/src/main/kotlin/org/mcediagui/DatabaseManager.kt index 9b80113..06dc075 100644 --- a/src/main/kotlin/org/mcediagui/DatabaseManager.kt +++ b/src/main/kotlin/org/mcediagui/DatabaseManager.kt @@ -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 @@ -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") + 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) {} + } } } diff --git a/src/main/kotlin/org/mcediagui/McediaGUI.kt b/src/main/kotlin/org/mcediagui/McediaGUI.kt index cc499a6..b5de423 100644 --- a/src/main/kotlin/org/mcediagui/McediaGUI.kt +++ b/src/main/kotlin/org/mcediagui/McediaGUI.kt @@ -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 @@ -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 diff --git a/src/main/kotlin/org/mcediagui/McediaListener.kt b/src/main/kotlin/org/mcediagui/McediaListener.kt index 7f3d911..c0ee93f 100644 --- a/src/main/kotlin/org/mcediagui/McediaListener.kt +++ b/src/main/kotlin/org/mcediagui/McediaListener.kt @@ -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( @@ -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().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) + } + } + } + } } diff --git a/src/main/kotlin/org/mcediagui/McediaManager.kt b/src/main/kotlin/org/mcediagui/McediaManager.kt index 122a78c..94f1a99 100644 --- a/src/main/kotlin/org/mcediagui/McediaManager.kt +++ b/src/main/kotlin/org/mcediagui/McediaManager.kt @@ -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) + } + fun shutdown() { storage?.close(); storage = null; players.clear() } } diff --git a/src/main/kotlin/org/mcediagui/McediaStorage.kt b/src/main/kotlin/org/mcediagui/McediaStorage.kt index 3a0ae53..1d8677d 100644 --- a/src/main/kotlin/org/mcediagui/McediaStorage.kt +++ b/src/main/kotlin/org/mcediagui/McediaStorage.kt @@ -24,89 +24,151 @@ class SQLiteMcediaStorage(private val plugin: JavaPlugin) : McediaStorage { init { initTables() } private fun initTables() { - DatabaseManager.getConnection()?.use { conn -> conn.createStatement().use { stmt -> + val conn = DatabaseManager.getConnection() ?: return + conn.createStatement().use { stmt -> stmt.executeUpdate("CREATE TABLE IF NOT EXISTS mcedia_players (uuid TEXT PRIMARY KEY, name TEXT NOT NULL, world TEXT NOT NULL, x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL, yaw REAL NOT NULL, pitch REAL NOT NULL, video_url TEXT DEFAULT '', start_time TEXT DEFAULT '', scale REAL DEFAULT 1.0, volume INTEGER DEFAULT 100, max_volume_range REAL DEFAULT 10.0, hearing_range REAL DEFAULT 50.0, offset_x REAL DEFAULT 0.0, offset_y REAL DEFAULT 0.0, offset_z REAL DEFAULT 0.0, looping INTEGER DEFAULT 0, no_danmaku INTEGER DEFAULT 0, created_by TEXT NOT NULL, created_at INTEGER NOT NULL)") stmt.executeUpdate("CREATE TABLE IF NOT EXISTS mcedia_templates (id INTEGER NOT NULL, owner_uuid TEXT NOT NULL, name TEXT NOT NULL, scale REAL DEFAULT 1.0, volume INTEGER DEFAULT 100, max_volume_range REAL DEFAULT 10.0, hearing_range REAL DEFAULT 50.0, offset_x REAL DEFAULT 0.0, offset_y REAL DEFAULT 0.0, offset_z REAL DEFAULT 0.0, looping INTEGER DEFAULT 0, no_danmaku INTEGER DEFAULT 0, created_at INTEGER NOT NULL, PRIMARY KEY (id, owner_uuid))") stmt.executeUpdate("CREATE TABLE IF NOT EXISTS mcedia_pending_ops (uuid TEXT PRIMARY KEY, operation_type TEXT NOT NULL, world_name TEXT NOT NULL, x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL)") - }} + } } override fun loadAll(): CompletableFuture> = CompletableFuture.supplyAsync { val players = mutableListOf() - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("SELECT * FROM mcedia_players").use { stmt -> - val rs = stmt.executeQuery() - while (rs.next()) { Bukkit.getWorld(rs.getString("world"))?.let { world -> - players.add(McediaPlayer(UUID.fromString(rs.getString("uuid")), rs.getString("name"), Location(world, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch")), rs.getString("video_url") ?: "", rs.getString("start_time") ?: "", rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"), rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"), rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1, UUID.fromString(rs.getString("created_by")), rs.getLong("created_at"))) - }} - }}} catch (_: Exception) {} + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync players + conn.prepareStatement("SELECT * FROM mcedia_players").use { stmt -> + val rs = stmt.executeQuery() + while (rs.next()) { + Bukkit.getWorld(rs.getString("world"))?.let { world -> + players.add(McediaPlayer( + UUID.fromString(rs.getString("uuid")), rs.getString("name"), + Location(world, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch")), + rs.getString("video_url") ?: "", rs.getString("start_time") ?: "", + rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"), + rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"), + rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1, + UUID.fromString(rs.getString("created_by")), rs.getLong("created_at") + )) + } + } + } + } catch (_: Exception) {} players } override fun save(player: McediaPlayer): CompletableFuture = CompletableFuture.supplyAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("INSERT OR REPLACE INTO mcedia_players (uuid, name, world, x, y, z, yaw, pitch, video_url, start_time, scale, volume, max_volume_range, hearing_range, offset_x, offset_y, offset_z, looping, no_danmaku, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").use { stmt -> - stmt.setString(1, player.uuid.toString()); stmt.setString(2, player.name); stmt.setString(3, player.location.world?.name ?: "world") - stmt.setDouble(4, player.location.x); stmt.setDouble(5, player.location.y); stmt.setDouble(6, player.location.z) - stmt.setFloat(7, player.location.yaw); stmt.setFloat(8, player.location.pitch); stmt.setString(9, player.videoUrl); stmt.setString(10, player.startTime) - stmt.setDouble(11, player.scale); stmt.setInt(12, player.volume); stmt.setDouble(13, player.maxVolumeRange); stmt.setDouble(14, player.hearingRange) - stmt.setDouble(15, player.offsetX); stmt.setDouble(16, player.offsetY); stmt.setDouble(17, player.offsetZ) - stmt.setInt(18, if (player.looping) 1 else 0); stmt.setInt(19, if (player.noDanmaku) 1 else 0) - stmt.setString(20, player.createdBy.toString()); stmt.setLong(21, player.createdAt); stmt.executeUpdate() - }}; true } catch (_: Exception) { false } + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync false + conn.prepareStatement("INSERT OR REPLACE INTO mcedia_players (uuid, name, world, x, y, z, yaw, pitch, video_url, start_time, scale, volume, max_volume_range, hearing_range, offset_x, offset_y, offset_z, looping, no_danmaku, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").use { stmt -> + stmt.setString(1, player.uuid.toString()); stmt.setString(2, player.name); stmt.setString(3, player.location.world?.name ?: "world") + stmt.setDouble(4, player.location.x); stmt.setDouble(5, player.location.y); stmt.setDouble(6, player.location.z) + stmt.setFloat(7, player.location.yaw); stmt.setFloat(8, player.location.pitch); stmt.setString(9, player.videoUrl); stmt.setString(10, player.startTime) + stmt.setDouble(11, player.scale); stmt.setInt(12, player.volume); stmt.setDouble(13, player.maxVolumeRange); stmt.setDouble(14, player.hearingRange) + stmt.setDouble(15, player.offsetX); stmt.setDouble(16, player.offsetY); stmt.setDouble(17, player.offsetZ) + stmt.setInt(18, if (player.looping) 1 else 0); stmt.setInt(19, if (player.noDanmaku) 1 else 0) + stmt.setString(20, player.createdBy.toString()); stmt.setLong(21, player.createdAt); stmt.executeUpdate() + } + true + } catch (_: Exception) { false } } override fun delete(uuid: UUID): CompletableFuture = CompletableFuture.supplyAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("DELETE FROM mcedia_players WHERE uuid = ?").use { it.setString(1, uuid.toString()); it.executeUpdate() }}; true } catch (_: Exception) { false } + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync false + conn.prepareStatement("DELETE FROM mcedia_players WHERE uuid = ?").use { it.setString(1, uuid.toString()); it.executeUpdate() } + true + } catch (_: Exception) { false } } override fun getTemplates(playerUUID: UUID): CompletableFuture> = CompletableFuture.supplyAsync { val templates = mutableListOf() - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("SELECT * FROM mcedia_templates WHERE owner_uuid = ? ORDER BY id").use { stmt -> - stmt.setString(1, playerUUID.toString()); val rs = stmt.executeQuery() - while (rs.next()) { templates.add(McediaTemplate(rs.getInt("id"), UUID.fromString(rs.getString("owner_uuid")), rs.getString("name"), rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"), rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"), rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1, rs.getLong("created_at"))) } - }}} catch (_: Exception) {} + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync templates + conn.prepareStatement("SELECT * FROM mcedia_templates WHERE owner_uuid = ? ORDER BY id").use { stmt -> + stmt.setString(1, playerUUID.toString()) + val rs = stmt.executeQuery() + while (rs.next()) { + templates.add(McediaTemplate( + rs.getInt("id"), UUID.fromString(rs.getString("owner_uuid")), rs.getString("name"), + rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"), + rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"), + rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1, rs.getLong("created_at") + )) + } + } + } catch (_: Exception) {} templates } override fun getNextTemplateId(playerUUID: UUID): CompletableFuture = CompletableFuture.supplyAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("SELECT id FROM mcedia_templates WHERE owner_uuid = ?").use { stmt -> - stmt.setString(1, playerUUID.toString()); val rs = stmt.executeQuery(); val used = mutableSetOf() - while (rs.next()) { used.add(rs.getInt("id")) } - for (i in 1..7) { if (i !in used) return@supplyAsync i } - }}; null } catch (_: Exception) { null } + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync null + conn.prepareStatement("SELECT id FROM mcedia_templates WHERE owner_uuid = ?").use { stmt -> + stmt.setString(1, playerUUID.toString()) + val rs = stmt.executeQuery() + val used = mutableSetOf() + while (rs.next()) { used.add(rs.getInt("id")) } + for (i in 1..7) { if (i !in used) return@supplyAsync i } + } + null + } catch (_: Exception) { null } } override fun saveTemplate(template: McediaTemplate): CompletableFuture = CompletableFuture.supplyAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("INSERT OR REPLACE INTO mcedia_templates (id, owner_uuid, name, scale, volume, max_volume_range, hearing_range, offset_x, offset_y, offset_z, looping, no_danmaku, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").use { stmt -> - stmt.setInt(1, template.id); stmt.setString(2, template.ownerUuid.toString()); stmt.setString(3, template.name) - stmt.setDouble(4, template.scale); stmt.setInt(5, template.volume); stmt.setDouble(6, template.maxVolumeRange); stmt.setDouble(7, template.hearingRange) - stmt.setDouble(8, template.offsetX); stmt.setDouble(9, template.offsetY); stmt.setDouble(10, template.offsetZ) - stmt.setInt(11, if (template.looping) 1 else 0); stmt.setInt(12, if (template.noDanmaku) 1 else 0); stmt.setLong(13, template.createdAt); stmt.executeUpdate() - }}; true } catch (_: Exception) { false } + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync false + conn.prepareStatement("INSERT OR REPLACE INTO mcedia_templates (id, owner_uuid, name, scale, volume, max_volume_range, hearing_range, offset_x, offset_y, offset_z, looping, no_danmaku, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").use { stmt -> + stmt.setInt(1, template.id); stmt.setString(2, template.ownerUuid.toString()); stmt.setString(3, template.name) + stmt.setDouble(4, template.scale); stmt.setInt(5, template.volume); stmt.setDouble(6, template.maxVolumeRange); stmt.setDouble(7, template.hearingRange) + stmt.setDouble(8, template.offsetX); stmt.setDouble(9, template.offsetY); stmt.setDouble(10, template.offsetZ) + stmt.setInt(11, if (template.looping) 1 else 0); stmt.setInt(12, if (template.noDanmaku) 1 else 0); stmt.setLong(13, template.createdAt); stmt.executeUpdate() + } + true + } catch (_: Exception) { false } } override fun deleteTemplate(playerUUID: UUID, templateId: Int): CompletableFuture = CompletableFuture.supplyAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("DELETE FROM mcedia_templates WHERE owner_uuid = ? AND id = ?").use { it.setString(1, playerUUID.toString()); it.setInt(2, templateId); it.executeUpdate() }}; true } catch (_: Exception) { false } + try { + val conn = DatabaseManager.getConnection() ?: return@supplyAsync false + conn.prepareStatement("DELETE FROM mcedia_templates WHERE owner_uuid = ? AND id = ?").use { it.setString(1, playerUUID.toString()); it.setInt(2, templateId); it.executeUpdate() } + true + } catch (_: Exception) { false } } override fun addPendingOperation(op: PendingOperation) { CompletableFuture.runAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("INSERT OR REPLACE INTO mcedia_pending_ops (uuid, operation_type, world_name, x, y, z) VALUES (?, ?, ?, ?, ?, ?)").use { - it.setString(1, op.uuid.toString()); it.setString(2, op.operationType.name); it.setString(3, op.worldName); it.setDouble(4, op.x); it.setDouble(5, op.y); it.setDouble(6, op.z); it.executeUpdate() - }}} catch (_: Exception) {} + try { + val conn = DatabaseManager.getConnection() ?: return@runAsync + conn.prepareStatement("INSERT OR REPLACE INTO mcedia_pending_ops (uuid, operation_type, world_name, x, y, z) VALUES (?, ?, ?, ?, ?, ?)").use { + it.setString(1, op.uuid.toString()); it.setString(2, op.operationType.name); it.setString(3, op.worldName) + it.setDouble(4, op.x); it.setDouble(5, op.y); it.setDouble(6, op.z); it.executeUpdate() + } + } catch (_: Exception) {} }} override fun getPendingOperations(worldName: String, chunkX: Int, chunkZ: Int): CompletableFuture> = CompletableFuture.supplyAsync { val ops = mutableListOf() - try { val minX = chunkX * 16.0; val maxX = minX + 16; val minZ = chunkZ * 16.0; val maxZ = minZ + 16 - DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("SELECT * FROM mcedia_pending_ops WHERE world_name = ? AND x >= ? AND x < ? AND z >= ? AND z < ?").use { stmt -> + try { + val minX = chunkX * 16.0; val maxX = minX + 16; val minZ = chunkZ * 16.0; val maxZ = minZ + 16 + val conn = DatabaseManager.getConnection() ?: return@supplyAsync ops + conn.prepareStatement("SELECT * FROM mcedia_pending_ops WHERE world_name = ? AND x >= ? AND x < ? AND z >= ? AND z < ?").use { stmt -> stmt.setString(1, worldName); stmt.setDouble(2, minX); stmt.setDouble(3, maxX); stmt.setDouble(4, minZ); stmt.setDouble(5, maxZ) - val rs = stmt.executeQuery(); while (rs.next()) { ops.add(PendingOperation(UUID.fromString(rs.getString("uuid")), PendingOperationType.valueOf(rs.getString("operation_type")), rs.getString("world_name"), rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"))) } - }} + val rs = stmt.executeQuery() + while (rs.next()) { + ops.add(PendingOperation( + UUID.fromString(rs.getString("uuid")), PendingOperationType.valueOf(rs.getString("operation_type")), + rs.getString("world_name"), rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z") + )) + } + } } catch (_: Exception) {} ops } override fun removePendingOperation(uuid: UUID) { CompletableFuture.runAsync { - try { DatabaseManager.getConnection()?.use { conn -> conn.prepareStatement("DELETE FROM mcedia_pending_ops WHERE uuid = ?").use { it.setString(1, uuid.toString()); it.executeUpdate() }}} catch (_: Exception) {} + try { + val conn = DatabaseManager.getConnection() ?: return@runAsync + conn.prepareStatement("DELETE FROM mcedia_pending_ops WHERE uuid = ?").use { it.setString(1, uuid.toString()); it.executeUpdate() } + } catch (_: Exception) {} }} override fun close() {}