From 043ef9445d7e4861de7e0d846baa745892f3ae5f Mon Sep 17 00:00:00 2001 From: polang233 Date: Tue, 13 Oct 2026 17:30:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(data):=20=E4=BF=AE=E5=A4=8D=20del-data?= =?UTF-8?q?=20=E6=9C=AA=E5=88=A0=E9=99=A4=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - del-data / Kether data del 同步删除数据库中对应的玩家数据 - 统一 set-data、del-data 与 Kether 的数据写入入口 - 玩家数据读写改为串行执行,避免旧写入覆盖删除结果,停服时等待写入完成 --- .../menu/api/action/impl/metadaa/DelData.kt | 4 +- .../menu/api/action/impl/metadaa/SetData.kt | 5 +-- .../module/internal/data/DataStorageQueue.kt | 44 +++++++++++++++++++ .../menu/module/internal/data/Metadata.kt | 38 +++++++++++++--- .../module/internal/database/MetaDataDao.kt | 10 +++++ .../module/internal/listener/ListenerQuit.kt | 7 ++- .../module/internal/script/impl/KetherData.kt | 12 ++--- 7 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 plugin/src/main/kotlin/trplugins/menu/module/internal/data/DataStorageQueue.kt diff --git a/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/DelData.kt b/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/DelData.kt index da1e70d5..f89d88c0 100644 --- a/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/DelData.kt +++ b/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/DelData.kt @@ -22,7 +22,9 @@ class DelData(handle: ActionHandle) : ActionBase(handle) { contents.stringContent().parseContentSplited(placeholderPlayer, ";").forEach { it -> val regex = Regex(it) - data.keys.filter { it.matches(regex) }.forEach { data.remove(it) } + data.keys.filter { it.matches(regex) }.forEach { + Metadata.setData(player.cast(), Metadata.DataType.DATA, it, null) + } } } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/SetData.kt b/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/SetData.kt index 7f4fc5bd..32cc93e0 100644 --- a/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/SetData.kt +++ b/plugin/src/main/kotlin/trplugins/menu/api/action/impl/metadaa/SetData.kt @@ -24,10 +24,7 @@ class SetData(handle: ActionHandle) : ActionBase(handle) { val key = split[0] val value = split[1] - Metadata.getData(player)[key] = value - if (!Metadata.isUseLegacy) { - Metadata.saveData(player.cast(), key) - } + Metadata.setData(player.cast(), Metadata.DataType.DATA, key, value) } } } diff --git a/plugin/src/main/kotlin/trplugins/menu/module/internal/data/DataStorageQueue.kt b/plugin/src/main/kotlin/trplugins/menu/module/internal/data/DataStorageQueue.kt new file mode 100644 index 00000000..f541406d --- /dev/null +++ b/plugin/src/main/kotlin/trplugins/menu/module/internal/data/DataStorageQueue.kt @@ -0,0 +1,44 @@ +package trplugins.menu.module.internal.data + +import java.util.concurrent.Executors +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit + +/** Serializes player data writes and reads so a pending write cannot overtake a deletion. */ +internal class DataStorageQueue(private val onFailure: (Throwable) -> Unit) : AutoCloseable { + + private val executor = Executors.newSingleThreadExecutor { task -> + Thread(task, "TrMenu-PlayerData").apply { isDaemon = true } + } + + fun write(action: () -> Unit) { + val task = Runnable { + try { + action() + } catch (ex: Exception) { + onFailure(ex) + } + } + try { + executor.execute(task) + } catch (ex: RejectedExecutionException) { + // Queue already closed (plugin disabling), save directly + task.run() + } + } + + /** Called from the asynchronous player-loading task, after all earlier writes. */ + fun read(action: () -> T): T = executor.submit { action() }.get() + + override fun close() { + executor.shutdown() + try { + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + onFailure(IllegalStateException("Timed out waiting for player data to be saved")) + } + } catch (ex: InterruptedException) { + Thread.currentThread().interrupt() + onFailure(ex) + } + } +} diff --git a/plugin/src/main/kotlin/trplugins/menu/module/internal/data/Metadata.kt b/plugin/src/main/kotlin/trplugins/menu/module/internal/data/Metadata.kt index d32237dd..5b8216ea 100644 --- a/plugin/src/main/kotlin/trplugins/menu/module/internal/data/Metadata.kt +++ b/plugin/src/main/kotlin/trplugins/menu/module/internal/data/Metadata.kt @@ -47,6 +47,8 @@ object Metadata { internal val data = mutableMapOf() internal val global = ConcurrentHashMap() + private val storageQueue = DataStorageQueue { it.printStackTrace() } + @Config("data/globalData.yml") lateinit var globalData: Configuration @@ -91,11 +93,22 @@ object Metadata { } fun saveData(player: Player, key: String) { - submitAsync { - MetaDataDao.door.update(DataEntity.constructor(player, key, getData(player)[key]?.toString() ?: "")) + val uuid = player.uniqueId + val value = getData(player)[key]?.toString() + storageQueue.write { + if (value == null) { + MetaDataDao.door.delete(uuid, key) + } else { + MetaDataDao.door.update(DataEntity(uuid, key, value)) + } } } + @Awake(LifeCycle.DISABLE) + fun closeStorage() { + storageQueue.close() + } + fun pushData(player: Player, dataMap: DataMap = getData(player)) { if (isUseLegacy) { getLocalePlayer(player)?.let { @@ -108,8 +121,16 @@ object Metadata { } database?.push(player) } else { - dataMap.data.forEach { (key, value) -> - MetaDataDao.door.update(DataEntity.constructor(player, key, value?.toString() ?: "")) + val uuid = player.uniqueId + val snapshot = dataMap.data.mapValues { it.value?.toString() } + storageQueue.write { + snapshot.forEach { (key, value) -> + if (value == null) { + MetaDataDao.door.delete(uuid, key) + } else { + MetaDataDao.door.update(DataEntity(uuid, key, value)) + } + } } } } @@ -126,7 +147,8 @@ object Metadata { section.getKeys(true).forEach { key -> map[key] = section[key] } } } else { - MetaDataDao.door.get(player.uniqueId).forEach { + val uuid = player.uniqueId + storageQueue.read { MetaDataDao.door.get(uuid) }.forEach { map[it.key] = it.data } } @@ -227,7 +249,11 @@ object Metadata { fun setData(player: Player, dataType: DataType, dataName: String, value: Any?) { when (dataType) { DataType.DATA -> { - getData(player)[dataName] = value + if (value == null) { + getData(player).remove(dataName) + } else { + getData(player)[dataName] = value + } if (!isUseLegacy) { saveData(player, dataName) } diff --git a/plugin/src/main/kotlin/trplugins/menu/module/internal/database/MetaDataDao.kt b/plugin/src/main/kotlin/trplugins/menu/module/internal/database/MetaDataDao.kt index c8d1cb51..12eb6d0a 100644 --- a/plugin/src/main/kotlin/trplugins/menu/module/internal/database/MetaDataDao.kt +++ b/plugin/src/main/kotlin/trplugins/menu/module/internal/database/MetaDataDao.kt @@ -37,6 +37,16 @@ class MetaDataDao { }.filter { it?.data?.isNotEmpty() == true }.mapNotNull { it } } + fun delete(uuid: UUID, key: String) { + val containerx = container[table] + containerx.table.delete(containerx.dataSource) { + where { + "user" eq uuid.toString() + "key" eq key + } + } + } + fun get(uuid: UUID, key: String): DataEntity? { return container[table].getOne { "user" eq uuid.toString() diff --git a/plugin/src/main/kotlin/trplugins/menu/module/internal/listener/ListenerQuit.kt b/plugin/src/main/kotlin/trplugins/menu/module/internal/listener/ListenerQuit.kt index c11d2ca6..330b921e 100644 --- a/plugin/src/main/kotlin/trplugins/menu/module/internal/listener/ListenerQuit.kt +++ b/plugin/src/main/kotlin/trplugins/menu/module/internal/listener/ListenerQuit.kt @@ -19,7 +19,12 @@ object ListenerQuit { val player = e.player val session = MenuSession.getSession(player) session.shut() - submit(async = true) { + if (Metadata.isUseLegacy) { + submit(async = true) { + Metadata.pushData(player) + } + } else { + // Capture the snapshot before another session can modify this player's data. Metadata.pushData(player) } Menu.menus.forEach { diff --git a/plugin/src/main/kotlin/trplugins/menu/module/internal/script/impl/KetherData.kt b/plugin/src/main/kotlin/trplugins/menu/module/internal/script/impl/KetherData.kt index 4aae5a44..d55eac2c 100644 --- a/plugin/src/main/kotlin/trplugins/menu/module/internal/script/impl/KetherData.kt +++ b/plugin/src/main/kotlin/trplugins/menu/module/internal/script/impl/KetherData.kt @@ -25,21 +25,15 @@ class KetherData(val type: EditType, private val source: ParsedAction<*>, privat return context.newFrame(source).run().thenApply { key -> when (type) { DEL -> { - Metadata.getData(viewer).remove(key).also { - if (!Metadata.isUseLegacy) { - Metadata.saveData(viewer, key) - } + Metadata.getData(viewer)[key].also { + Metadata.setData(viewer, Metadata.DataType.DATA, key, null) } } SET -> { apply?.let { it1 -> context.newFrame(it1).run().thenApply { apply -> - Metadata.getData(viewer)[key] = apply.also { - if (!Metadata.isUseLegacy) { - Metadata.saveData(viewer, key) - } - } + Metadata.setData(viewer, Metadata.DataType.DATA, key, apply) } } } From a2416d66a4b6fd9c76234e630627da57db4b6fa0 Mon Sep 17 00:00:00 2001 From: Polang_ Date: Sat, 15 Aug 2026 23:09:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(receptacle):=20=E5=8F=91=E5=8C=85?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=89=93=E5=BC=80=E6=97=B6=E6=89=93=E6=96=AD?= =?UTF-8?q?=E6=AD=A3=E5=9C=A8=E4=BD=BF=E7=94=A8=E7=9A=84=E7=89=A9=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packet 菜单不会走 Bukkit openInventory,右键未松开时客户端会停留在举盾/拉弓状态。 打开和关闭 WindowReceptacle 时调用 clearActiveItem。 --- .../trplugins/menu/api/receptacle/vanilla/window/NMS.kt | 6 ++++++ .../menu/api/receptacle/vanilla/window/NMSImpl.kt | 9 +++++++++ .../api/receptacle/vanilla/window/WindowReceptacle.kt | 2 ++ 3 files changed, 17 insertions(+) diff --git a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMS.kt b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMS.kt index 5c0146e0..556bf361 100644 --- a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMS.kt +++ b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMS.kt @@ -81,4 +81,10 @@ abstract class NMS { abstract fun sendWindowsUpdateData(player: Player, windowId: Int = windowId(player), id: Int, value: Int) abstract fun toNMSCopy(itemStack: ItemStack?): Any? + + /** + * 发包菜单不会走 Bukkit openInventory,客户端右键没松开时会卡在使用状态。 + * 所以打断举盾/拉弓等“正在使用物品”。 + */ + abstract fun clearActiveItem(player: Player) } \ No newline at end of file diff --git a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMSImpl.kt b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMSImpl.kt index 8b3950a0..2c9be27c 100644 --- a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMSImpl.kt +++ b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/NMSImpl.kt @@ -255,6 +255,15 @@ class NMSImpl : NMS() { } } + override fun clearActiveItem(player: Player) { + runCatching { player.javaClass.getMethod("clearActiveItem").invoke(player); return } + val handle = runCatching { player.invokeMethod("getHandle") }.getOrNull() ?: return + // clearActiveItem: Spigot 1.16 及以下映射名,stopUsingItem: Mojang 映射名 + arrayOf("clearActiveItem", "stopUsingItem").firstOrNull { name -> + runCatching { handle.invokeMethod(name); true }.getOrDefault(false) + } + } + override fun toNMSCopy(itemStack: ItemStack?): Any? { if (itemStack.isAir()) return emptyItemStack return if (isUnobfuscated) { diff --git a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/WindowReceptacle.kt b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/WindowReceptacle.kt index d51d0765..4883e2e9 100644 --- a/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/WindowReceptacle.kt +++ b/api/receptacle/src/main/kotlin/trplugins/menu/api/receptacle/vanilla/window/WindowReceptacle.kt @@ -77,12 +77,14 @@ open class WindowReceptacle(var type: WindowLayout, override var title: String = override fun open(player: Player) { viewer = player player.setViewingReceptacle(this) + nmsProxy().clearActiveItem(player) initializationPackets() onOpen(player, this) } override fun close(sendPacket: Boolean) { if (viewer != null) { + nmsProxy().clearActiveItem(viewer!!) if (sendPacket) { nmsProxy().sendWindowsClose(viewer!!) } From 981405179f50183e35188d4f1c1a8e04f3a20c0b Mon Sep 17 00:00:00 2001 From: polang233 Date: Tue, 13 Oct 2026 17:30:33 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(build):=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E4=B8=BA=203.12.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 54135437..af02cfa9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=me.arasple.mc.trmenu -version=3.12.5 +version=3.12.6