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!!) } 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 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) } } }