diff --git a/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSMap.kt b/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSMap.kt index a18f8730e..b1819c8b0 100644 --- a/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSMap.kt +++ b/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSMap.kt @@ -13,7 +13,7 @@ import org.tabooproject.reflex.Reflex.Companion.invokeConstructor import org.tabooproject.reflex.Reflex.Companion.invokeMethod import org.tabooproject.reflex.Reflex.Companion.setProperty import org.tabooproject.reflex.Reflex.Companion.unsafeInstance -import taboolib.common.platform.function.submit +import taboolib.platform.util.submit import taboolib.common.util.unsafeLazy import taboolib.library.xseries.XMaterial import taboolib.platform.util.ItemBuilder @@ -255,7 +255,7 @@ class NMSMap(val image: BufferedImage, var hand: Hand = Hand.MAIN, val builder: } fun sendTo(player: Player) { - submit(delay = 3) { + player.submit(delay = 3) { val container = if (MinecraftVersion.isUniversal) { player.getProperty("entity/inventoryMenu") } else { diff --git a/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSToast.kt b/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSToast.kt index a10585b50..1e416cb79 100644 --- a/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSToast.kt +++ b/module/bukkit-nms/bukkit-nms-legacy/src/main/kotlin/taboolib/module/nms/NMSToast.kt @@ -6,12 +6,12 @@ import com.google.gson.JsonObject import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.NamespacedKey +import org.bukkit.advancement.Advancement import org.bukkit.entity.Player import org.tabooproject.reflex.Reflex.Companion.getProperty import org.tabooproject.reflex.Reflex.Companion.invokeMethod import org.tabooproject.reflex.Reflex.Companion.setProperty import taboolib.common.UnsupportedVersionException -import taboolib.common.platform.function.submit import taboolib.common.platform.function.warning import taboolib.common.util.t import taboolib.common.util.unsafeLazy @@ -21,6 +21,8 @@ import taboolib.module.nms.type.Toast import taboolib.module.nms.type.ToastBackground import taboolib.module.nms.type.ToastFrame import taboolib.platform.BukkitPlugin +import taboolib.platform.util.submit +import taboolib.platform.util.submitGlobal import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -100,18 +102,28 @@ fun Player.sendToast(icon: Material, message: String, frame: ToastFrame = ToastF } val cache = Toast(icon, message, frame) val jsonToast = toJsonToast(icon.invokeMethod("getKey").toString(), message, frame, background) - // 在主线程操作 - submit { - // 向服务器注册成就 - val namespaceKey = toastMap.getOrPut(cache) { - injectAdvancement(NamespacedKey(BukkitPlugin.getInstance(), "toast_${UUID.randomUUID()}"), jsonToast) + // 服务端成就注册必须在全局线程执行 + submitGlobal { + val namespaceKey = toastMap.compute(cache) { _, cachedKey -> + if (cachedKey == null || Bukkit.getAdvancement(cachedKey) == null) { + injectAdvancement(NamespacedKey(BukkitPlugin.getInstance(), "toast_${UUID.randomUUID()}"), jsonToast) + } else { + cachedKey + } + } ?: return@submitGlobal + val advancement = Bukkit.getAdvancement(namespaceKey) + if (advancement == null) { + warning("Advancement $namespaceKey not found.") + return@submitGlobal } - // 向玩家注册成就 - awardAdvancement(this@sendToast, namespaceKey) - // 延迟注销,否则会出问题 - submit(delay = 20) { - revokeAdvancement(this@sendToast, namespaceKey) - ejectAdvancement(namespaceKey) + // 玩家进度必须在玩家所属线程修改 + this@sendToast.submit(now = true) { + awardAdvancement(this@sendToast, advancement) + // 延迟注销,否则会出问题 + this@sendToast.submit(delay = 20) { + revokeAdvancement(this@sendToast, advancement) + submitGlobal { ejectAdvancement(namespaceKey) } + } } } } @@ -119,28 +131,20 @@ fun Player.sendToast(icon: Material, message: String, frame: ToastFrame = ToastF /** * 赋予玩家成就 */ -private fun awardAdvancement(player: Player, key: NamespacedKey) { - val advancement = Bukkit.getAdvancement(key) - if (advancement == null) { - warning("Advancement $key not found.") - return - } - if (!player.getAdvancementProgress(advancement).isDone) { - player.getAdvancementProgress(advancement).remainingCriteria.forEach { - player.getAdvancementProgress(advancement).awardCriteria(it) - } +private fun awardAdvancement(player: Player, advancement: Advancement) { + val progress = player.getAdvancementProgress(advancement) + if (!progress.isDone) { + progress.remainingCriteria.forEach { progress.awardCriteria(it) } } } /** * 注销玩家成就 */ -private fun revokeAdvancement(player: Player, key: NamespacedKey) { - val advancement = Bukkit.getAdvancement(key) - if (advancement != null && player.getAdvancementProgress(advancement).isDone) { - player.getAdvancementProgress(advancement).awardedCriteria.forEach { - player.getAdvancementProgress(advancement).revokeCriteria(it) - } +private fun revokeAdvancement(player: Player, advancement: Advancement) { + val progress = player.getAdvancementProgress(advancement) + if (progress.isDone) { + progress.awardedCriteria.forEach { progress.revokeCriteria(it) } } } diff --git a/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSign.kt b/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSign.kt index 22eef30e8..ef4a66de8 100644 --- a/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSign.kt +++ b/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSign.kt @@ -10,11 +10,8 @@ import taboolib.common.Inject import taboolib.common.platform.Platform import taboolib.common.platform.PlatformSide import taboolib.common.platform.event.SubscribeEvent -import taboolib.common.platform.function.submit import taboolib.common.util.unsafeLazy -import taboolib.platform.BukkitPlugin -import taboolib.platform.Folia -import taboolib.platform.FoliaExecutor +import taboolib.platform.util.runTask import java.lang.reflect.Constructor import java.util.concurrent.ConcurrentHashMap @@ -145,13 +142,7 @@ private object NMSSignListener { MinecraftVersion.isHigherOrEqual(MinecraftVersion.V1_9) -> e.packet.read>("b")!! else -> e.packet.read>("b")!!.map { nmsProxy().deserialize(it) }.toTypedArray() } - if (Folia.isFolia) { - FoliaExecutor.REGION_SCHEDULER.run(BukkitPlugin.getInstance(), e.player.location) { - function.invoke(lines) - } - } else { - submit { function.invoke(lines) } - } + e.player.runTask(Runnable { function.invoke(lines) }) } } } diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/PacketSender.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/PacketSender.kt index 9b1213190..8cab9d63c 100644 --- a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/PacketSender.kt +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/PacketSender.kt @@ -10,7 +10,7 @@ import taboolib.common.Inject import taboolib.common.platform.Platform import taboolib.common.platform.PlatformSide import taboolib.common.platform.event.SubscribeEvent -import taboolib.common.platform.function.submit +import taboolib.common.platform.function.submitAsync import taboolib.common.reflect.ClassHelper import java.lang.reflect.Constructor import java.util.concurrent.ConcurrentHashMap @@ -247,6 +247,6 @@ object PacketSender { @SubscribeEvent private fun onQuit(e: PlayerQuitEvent) { - submit(delay = 20) { playerConnectionMap.remove(e.player.name) } + submitAsync(delay = 20) { playerConnectionMap.remove(e.player.name) } } } \ No newline at end of file diff --git a/module/bukkit/bukkit-hook/src/main/kotlin/taboolib/platform/compat/PlaceholderExpansion.kt b/module/bukkit/bukkit-hook/src/main/kotlin/taboolib/platform/compat/PlaceholderExpansion.kt index bb61940ac..6a1e78975 100644 --- a/module/bukkit/bukkit-hook/src/main/kotlin/taboolib/platform/compat/PlaceholderExpansion.kt +++ b/module/bukkit/bukkit-hook/src/main/kotlin/taboolib/platform/compat/PlaceholderExpansion.kt @@ -11,6 +11,8 @@ import taboolib.common.inject.ClassVisitor import taboolib.common.platform.Awake import taboolib.common.platform.function.registerBukkitListener import taboolib.common.platform.function.submit +import taboolib.platform.Folia +import taboolib.platform.FoliaExecutor import taboolib.common.util.unsafeLazy import taboolib.platform.BukkitPlugin import java.util.function.Supplier @@ -138,7 +140,11 @@ interface PlaceholderExpansion { if (expansion.autoReload) { registerBukkitListener(ExpansionUnregisterEvent::class.java) { if (it.expansion == papiExpansion) { - submit { papiExpansion.register() } + if (Folia.isFolia) { + FoliaExecutor.runGlobal { papiExpansion.register() } + } else { + submit { papiExpansion.register() } + } } } } diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/ClickListener.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/ClickListener.kt index 957760ca8..09519dafb 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/ClickListener.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/ClickListener.kt @@ -18,10 +18,10 @@ import taboolib.common.platform.Ghost import taboolib.common.platform.Platform import taboolib.common.platform.PlatformSide import taboolib.common.platform.event.SubscribeEvent -import taboolib.common.platform.function.submit import taboolib.common.platform.function.submitAsync import taboolib.module.ui.type.impl.ChestImpl import taboolib.platform.util.isNotAir +import taboolib.platform.util.runTask import taboolib.platform.util.setMeta @Inject @@ -30,10 +30,12 @@ internal object ClickListener { @Awake(LifeCycle.DISABLE) fun onDisable() { - Bukkit.getOnlinePlayers().forEach { - if (MenuHolder.fromInventory(InventoryViewProxy.getTopInventory(it.openInventory)) != null) { - it.closeInventory() - } + Bukkit.getOnlinePlayers().forEach { player -> + player.runTask(Runnable { + if (MenuHolder.fromInventory(InventoryViewProxy.getTopInventory(player.openInventory)) != null) { + player.closeInventory() + } + }) } } @@ -41,11 +43,11 @@ internal object ClickListener { fun onOpen(e: InventoryOpenEvent) { val builder = MenuHolder.fromInventory(e.inventory) as? ChestImpl ?: return val player = e.player as Player - // 构建回调 - submit { + // 构建回调必须在玩家所属线程执行 + player.runTask(Runnable { builder.buildCallback(player, e.inventory) builder.finalBuildCallback(player, e.inventory) - } + }) // 异步构建回调 submitAsync { builder.asyncBuildCallback(player, e.inventory) diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilder.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilder.kt index f9d21a71c..826481b96 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilder.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilder.kt @@ -21,6 +21,8 @@ import taboolib.module.ui.virtual.VirtualInventory import taboolib.module.ui.virtual.inject import taboolib.module.ui.virtual.openVirtualInventory import taboolib.platform.util.isNotAir +import taboolib.platform.util.isOwnedByCurrentRegion +import taboolib.platform.util.runTask /** * 允许在 Vanilla Inventory 中使用 Raw Title @@ -84,11 +86,18 @@ inline fun buildMenu(title: String = "chest", builder: T.() - /** * 构建一个菜单并为玩家打开 */ -inline fun HumanEntity.openMenu(title: String = "chest", builder: T.() -> Unit) { - try { - openMenu(buildMenu(title, builder)) - } catch (ex: Throwable) { - ex.printStackTrace() +inline fun HumanEntity.openMenu(title: String = "chest", crossinline builder: T.() -> Unit) { + val openAction = Runnable { + try { + openMenu(buildMenu(title, builder)) + } catch (ex: Throwable) { + ex.printStackTrace() + } + } + if (isOwnedByCurrentRegion()) { + openAction.run() + } else { + runTask(openAction) } } @@ -96,6 +105,10 @@ inline fun HumanEntity.openMenu(title: String = "chest", buil * 打开一个构建后的菜单 */ fun HumanEntity.openMenu(buildMenu: Inventory, changeId: Boolean = true) { + if (!isOwnedByCurrentRegion()) { + runTask(Runnable { openMenu(buildMenu, changeId) }) + return + } try { if (buildMenu is VirtualInventory) { val remoteInventory = openVirtualInventory(buildMenu, changeId) diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilderRaw.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilderRaw.kt index e59495eb0..3ab00cfbb 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilderRaw.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/MenuBuilderRaw.kt @@ -8,6 +8,6 @@ inline fun buildMenu(title: Source, builder: T.() -> Unit): I return buildMenu(title.toRawMessage(), builder) } -inline fun HumanEntity.openMenu(title: Source, builder: T.() -> Unit) { +inline fun HumanEntity.openMenu(title: Source, crossinline builder: T.() -> Unit) { openMenu(title.toRawMessage(), builder) } \ No newline at end of file diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/impl/PageableChestImpl.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/impl/PageableChestImpl.kt index 6d986f414..5a41bb0d3 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/impl/PageableChestImpl.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/impl/PageableChestImpl.kt @@ -6,11 +6,10 @@ import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack import taboolib.common.util.subList import taboolib.module.ui.ClickEvent +import taboolib.module.ui.openMenu import taboolib.module.ui.type.PageableChest -import taboolib.module.ui.virtual.VirtualInventory -import taboolib.module.ui.virtual.inject -import taboolib.module.ui.virtual.openVirtualInventory import taboolib.platform.util.isNotAir +import taboolib.platform.util.runTask import java.util.concurrent.CopyOnWriteArrayList open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest { @@ -124,11 +123,7 @@ open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest ) { // 刷新页面 fun refresh() { - if (virtualized) { - viewer.openVirtualInventory(build() as VirtualInventory).inject(this) - } else { - viewer.openInventory(build()) - } + viewer.openMenu(build()) pageChangeCallback(viewer) } // 设置物品 @@ -160,11 +155,7 @@ open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest ) { // 刷新页面 fun refresh() { - if (virtualized) { - viewer.openVirtualInventory(build() as VirtualInventory).inject(this) - } else { - viewer.openInventory(build()) - } + viewer.openMenu(build()) pageChangeCallback(viewer) } // 设置物品 @@ -176,7 +167,7 @@ open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest refresh() } else if (roll) { // 若循环翻页, 则跳转到最后一页 - page = maxPage - 1 + page = (maxPage - 1).coerceAtLeast(0) refresh() } } @@ -219,33 +210,41 @@ open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest elementsCache = elementsCallback() // 本次页面所使用的元素缓存 - val elementMap = hashMapOf() - val elementItems = subList(elementsCache, page * menuSlots.size, (page + 1) * menuSlots.size) + val elementItems = if (menuSlots.isEmpty()) { + emptyList() + } else { + subList(elementsCache, page * menuSlots.size, (page + 1) * menuSlots.size) + } + val pageElements = elementItems.mapIndexedNotNull { index, element -> + menuSlots.getOrNull(index)?.let { slot -> Triple(index, slot, element) } + } + val elementMap = pageElements.associate { (_, slot, element) -> slot to element } // 计算最大页数 - maxPage = elementsCache.size / menuSlots.size + maxPage = if (menuSlots.isEmpty()) 0 else (elementsCache.size + menuSlots.size - 1) / menuSlots.size - /** - * 构建事件处理函数 - */ - fun processBuild(p: Player, inventory: Inventory, async: Boolean) { + // 同步生成回调 + onFinalBuild { p, inventory -> viewer = p - elementItems.forEachIndexed { index, item -> - val slot = menuSlots.getOrNull(index) ?: 0 - elementMap[slot] = item - // 生成元素对应物品 - val callback = if (async) asyncGenerateCallback else generateCallback - val itemStack = callback(viewer, item, index, slot) + pageElements.forEach { (index, slot, element) -> + val itemStack = generateCallback(p, element, index, slot) if (itemStack.isNotAir()) { inventory.setItem(slot, itemStack) } } } - - // 生成回调 - onFinalBuild { p, it -> processBuild(p, it, false) } - // 生成异步回调 - onFinalBuild(async = true) { p, it -> processBuild(p, it, true) } + // 异步阶段只生成物品,实际 Inventory 修改切回玩家所属线程 + onFinalBuild(async = true) { p, inventory -> + val generatedItems = pageElements.mapNotNull { (index, slot, element) -> + asyncGenerateCallback(p, element, index, slot).takeIf { it.isNotAir() }?.let { slot to it } + } + p.runTask(Runnable { + if (lastInventory !== inventory) { + return@Runnable + } + generatedItems.forEach { (slot, itemStack) -> inventory.setItem(slot, itemStack) } + }) + } // 生成点击回调 selfClick { if (menuLocked) { @@ -261,6 +260,6 @@ open class PageableChestImpl(title: String) : ChestImpl(title), PageableChest * 是否存在下一页 */ private fun isNext(page: Int, size: Int, entry: Int): Boolean { - return size / entry.toDouble() > page + 1 + return entry > 0 && size / entry.toDouble() > page + 1 } } \ No newline at end of file diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/storable/DragActionContext.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/storable/DragActionContext.kt index 867297811..60a32946e 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/storable/DragActionContext.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/type/storable/DragActionContext.kt @@ -4,7 +4,6 @@ import org.bukkit.entity.Player import org.bukkit.event.inventory.DragType import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack -import taboolib.common.platform.function.submit import taboolib.module.ui.ClickEvent import taboolib.module.ui.type.impl.StorableChestImpl.RuleImpl diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandler.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandler.kt index e7b7c1c19..70b4f736e 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandler.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandler.kt @@ -18,6 +18,7 @@ import taboolib.module.nms.nmsProxy import taboolib.module.ui.InventoryViewProxy import taboolib.module.ui.MenuHolder import taboolib.module.ui.type.AnvilCallback +import taboolib.platform.util.runTask import java.util.concurrent.ConcurrentHashMap /** @@ -84,12 +85,15 @@ abstract class InventoryHandler { val player = e.player val remoteInventory = playerRemoteInventoryMap[player.name] if (remoteInventory != null && (remoteInventory.id == id || id == 0)) { - playerRemoteInventoryMap.remove(player.name)?.close(sendPacket = false) - try { - player.updateInventory() - } catch (ex: NoSuchMethodError) { - ex.printStackTrace() - } + val removedInventory = playerRemoteInventoryMap.remove(player.name) ?: return + player.runTask(Runnable { + removedInventory.close(sendPacket = false) + try { + player.updateInventory() + } catch (ex: NoSuchMethodError) { + ex.printStackTrace() + } + }) } } // 点击 @@ -100,31 +104,36 @@ abstract class InventoryHandler { } val id = e.packet.read(if (MinecraftVersion.isUniversal) "containerId" else "a")!! val player = e.player - val remoteInventory = playerRemoteInventoryMap[player.name] - if (remoteInventory != null && remoteInventory.id == id) { - remoteInventory.handleClick(e.packet) - } + val packet = e.packet + player.runTask(Runnable { + val remoteInventory = playerRemoteInventoryMap[player.name] + if (remoteInventory != null && remoteInventory.id == id) { + remoteInventory.handleClick(packet) + } + }) } // 重命名 "PacketPlayInItemName", "ServerboundRenameItemPacket" -> { val text = e.packet.read(if (MinecraftVersion.isUniversal) "name" else "a") ?: return val player = e.player - // 虚拟容器处理 - val virtualInventory = playerRemoteInventoryMap[player.name]?.inventory - if (virtualInventory != null) { - val builder = MenuHolder.fromInventory(virtualInventory) - if (builder is AnvilCallback) { - builder.invoke(player, text, virtualInventory) + player.runTask(Runnable { + // 虚拟容器处理 + val virtualInventory = playerRemoteInventoryMap[player.name]?.inventory + if (virtualInventory != null) { + val builder = MenuHolder.fromInventory(virtualInventory) + if (builder is AnvilCallback) { + builder.invoke(player, text, virtualInventory) + } } - } - // 普通容器处理 - else { - val openInventory = InventoryViewProxy.getTopInventory(player.openInventory) - val builder = MenuHolder.fromInventory(openInventory) - if (builder is AnvilCallback) { - builder.invoke(player, text, openInventory) + // 普通容器处理 + else { + val openInventory = InventoryViewProxy.getTopInventory(player.openInventory) + val builder = MenuHolder.fromInventory(openInventory) + if (builder is AnvilCallback) { + builder.invoke(player, text, openInventory) + } } - } + }) } } } diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandlerImpl.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandlerImpl.kt index 661c4dacd..78a6fef1a 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandlerImpl.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/InventoryHandlerImpl.kt @@ -16,8 +16,6 @@ import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.inventory.ItemStack import taboolib.common.UnsupportedVersionException -import taboolib.common.platform.function.isPrimaryThread -import taboolib.common.platform.function.submit import taboolib.module.nms.MinecraftVersion import taboolib.module.nms.Packet import taboolib.module.nms.sendBundlePacket @@ -25,6 +23,8 @@ import taboolib.module.nms.sendPacket import taboolib.module.ui.InventoryViewProxy import taboolib.platform.util.isAir import taboolib.platform.util.isNotAir +import taboolib.platform.util.isOwnedByCurrentRegion +import taboolib.platform.util.runTask /** * TabooLib @@ -287,6 +287,10 @@ class InventoryHandlerImpl : InventoryHandler() { } override fun close(sendPacket: Boolean) { + if (!viewer.isOwnedByCurrentRegion()) { + viewer.runTask(Runnable { close(sendPacket) }) + return + } if (isClosed) { return } @@ -300,17 +304,9 @@ class InventoryHandlerImpl : InventoryHandler() { } } // 处理回调 - if (isPrimaryThread) { - onCloseCallback?.invoke() - } else { - submit { onCloseCallback?.invoke() } - } + onCloseCallback?.invoke() // 唤起事件 - if (isPrimaryThread) { - Bukkit.getPluginManager().callEvent(InventoryCloseEvent(createInventoryView())) - } else { - submit { Bukkit.getPluginManager().callEvent(InventoryCloseEvent(createInventoryView())) } - } + Bukkit.getPluginManager().callEvent(InventoryCloseEvent(createInventoryView())) } override fun onClick(callback: RemoteInventory.ClickEvent.() -> Unit) { @@ -365,6 +361,10 @@ class InventoryHandlerImpl : InventoryHandler() { } fun handle(slotNum: Int, buttonNum: Int, clickType: String) { + if (!viewer.isOwnedByCurrentRegion()) { + viewer.runTask(Runnable { handle(slotNum, buttonNum, clickType) }) + return + } val vClickType = when (clickType) { // 左右键 "PICKUP" -> { @@ -402,7 +402,7 @@ class InventoryHandlerImpl : InventoryHandler() { else -> inventory.getStorageItem(slotNum - inventory.size) } // 处理回调 - submit { onClickCallback?.invoke(RemoteInventory.ClickEvent(vClickType.toBukkit(), slotNum, buttonNum, clickItem ?: air)) } + onClickCallback?.invoke(RemoteInventory.ClickEvent(vClickType.toBukkit(), slotNum, buttonNum, clickItem ?: air)) // 处理页面 if (clickItem.isNotAir()) { // 一般点击方式 diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventory.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventory.kt index f5f23cbc4..07ed49d69 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventory.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventory.kt @@ -8,6 +8,8 @@ import org.bukkit.inventory.Inventory import org.bukkit.inventory.InventoryHolder import org.bukkit.inventory.ItemStack import taboolib.common.util.t +import taboolib.platform.util.isOwnedByCurrentRegion +import taboolib.platform.util.runTask /** * TabooLib @@ -77,7 +79,7 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List if (storageContents == null) { initStorageItems() } @@ -88,7 +90,7 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List) { + fun setStorageItems(items: List) = mutate { remoteInventory -> storageContents = items remoteInventory?.refresh(bukkitInventory.contents.map { it ?: ItemStack(Material.AIR) }, storageContents) } @@ -109,7 +111,7 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List bukkitInventory.maxStackSize = p0 } @@ -117,7 +119,7 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List bukkitInventory.setItem(slot, item) remoteInventory?.sendSlotChange(slot, item ?: ItemStack(Material.AIR)) } @@ -134,7 +136,7 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List) { + override fun setContents(p0: Array) = mutate { remoteInventory -> bukkitInventory.contents = p0 remoteInventory?.refresh(bukkitInventory.contents.map { it ?: ItemStack(Material.AIR) }, storageContents) } @@ -223,6 +225,16 @@ class VirtualInventory(val bukkitInventory: Inventory, storageContents: List Unit) { + val remoteInventory = remoteInventory + val viewer = remoteInventory?.viewer + if (viewer != null && !viewer.isOwnedByCurrentRegion()) { + viewer.runTask(Runnable { action(remoteInventory) }) + } else { + action(remoteInventory) + } + } + /** * 对于老版本, Inventory 下有 getTitle 函数 * 部分插件监听 InventoryCloseEvent 时调用, 所以得给个标题给他们玩 diff --git a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventoryFactory.kt b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventoryFactory.kt index 32736ca99..4347566e9 100644 --- a/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventoryFactory.kt +++ b/module/bukkit/bukkit-ui/src/main/kotlin/taboolib/module/ui/virtual/VirtualInventoryFactory.kt @@ -9,14 +9,15 @@ import org.bukkit.event.inventory.InventoryOpenEvent import org.bukkit.inventory.Inventory import org.bukkit.inventory.InventoryView import org.bukkit.inventory.ItemStack -import taboolib.common.platform.function.isPrimaryThread -import taboolib.common.platform.function.submit import taboolib.module.nms.MinecraftVersion import taboolib.module.ui.ClickEvent import taboolib.module.ui.ClickType import taboolib.module.ui.type.Basic import taboolib.module.ui.type.Chest import taboolib.module.ui.type.impl.ChestImpl +import taboolib.platform.util.callRegionAsync +import taboolib.platform.util.isOwnedByCurrentRegion +import java.util.concurrent.CompletableFuture /** * 将背包转换为 VirtualInventory 实例 @@ -29,18 +30,23 @@ fun Inventory.virtualize(storageContents: List? = null): VirtualInven * 使玩家打开虚拟页面 */ fun HumanEntity.openVirtualInventory(inventory: VirtualInventory, updateId: Boolean = true): RemoteInventory { + check(isOwnedByCurrentRegion()) { + "Virtual inventory must be opened on the thread that owns the viewer. Use openVirtualInventoryAsync(), HumanEntity.openMenu(), or Entity.runTask() instead." + } val remoteInventory = InventoryHandler.instance.openInventory(this as Player, inventory, ItemStack(Material.AIR), updateId) inventory.remoteInventory = remoteInventory InventoryHandler.playerRemoteInventoryMap[name] = remoteInventory - // 唤起事件 - if (isPrimaryThread) { - Bukkit.getPluginManager().callEvent(InventoryOpenEvent(remoteInventory.createInventoryView())) - } else { - submit { Bukkit.getPluginManager().callEvent(InventoryOpenEvent(remoteInventory.createInventoryView())) } - } + Bukkit.getPluginManager().callEvent(InventoryOpenEvent(remoteInventory.createInventoryView())) return remoteInventory } +/** + * 在玩家所属线程打开虚拟页面,并通过 Future 非阻塞返回远程页面。 + */ +fun HumanEntity.openVirtualInventoryAsync(inventory: VirtualInventory, updateId: Boolean = true): CompletableFuture { + return callRegionAsync { openVirtualInventory(inventory, updateId) } +} + fun RemoteInventory.inject(menu: Basic) = inject(menu as ChestImpl) fun RemoteInventory.inject(menu: Chest) = inject(menu as ChestImpl) diff --git a/module/bukkit/bukkit-util/build.gradle.kts b/module/bukkit/bukkit-util/build.gradle.kts index dc72204c0..1a85dba49 100644 --- a/module/bukkit/bukkit-util/build.gradle.kts +++ b/module/bukkit/bukkit-util/build.gradle.kts @@ -18,4 +18,5 @@ dependencies { compileOnly("ink.ptms.core:v12111:12111-minimize:universal") compileOnly("ink.ptms.core:v12101:12101-minimize:universal") compileOnly("ink.ptms.core:v11200:11200-minimize") + testImplementation("ink.ptms.core:v11200:11200-minimize") } \ No newline at end of file diff --git a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/lang/TypeBossBar.kt b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/lang/TypeBossBar.kt index a62108a65..9a19d72de 100644 --- a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/lang/TypeBossBar.kt +++ b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/lang/TypeBossBar.kt @@ -3,10 +3,10 @@ package taboolib.platform.lang import org.bukkit.Bukkit import org.bukkit.boss.BarColor import org.bukkit.boss.BarStyle +import org.bukkit.entity.Player import taboolib.common.Inject import taboolib.common.LifeCycle import taboolib.common.platform.* -import taboolib.common.platform.function.submit import taboolib.common.platform.function.warning import taboolib.common.util.replaceWithOrder import taboolib.common.util.t @@ -14,6 +14,7 @@ import taboolib.common5.cdouble import taboolib.common5.clong import taboolib.module.lang.Language import taboolib.module.lang.Type +import taboolib.platform.util.submit /** * TabooLib @@ -62,16 +63,19 @@ class TypeBossBar : Type { return } if (sender is ProxyPlayer) { - val bossBar = Bukkit.createBossBar(text!!.translate(sender, *args).replaceWithOrder(*args), color, style) - bossBar.progress = if (method == "INCREASE") 0.0 else 1.0 - bossBar.addPlayer(sender.cast()) - submit(period = period) { - val progress = bossBar.progress + if (method == "INCREASE") step else -step - if (progress in 0.0..1.0) { - bossBar.progress = progress - } else { - bossBar.removeAll() - cancel() + val player = sender.cast() + player.submit(now = true) { + val bossBar = Bukkit.createBossBar(text!!.translate(sender, *args).replaceWithOrder(*args), color, style) + bossBar.progress = if (method == "INCREASE") 0.0 else 1.0 + bossBar.addPlayer(player) + player.submit(period = period) { + val progress = bossBar.progress + if (method == "INCREASE") step else -step + if (progress in 0.0..1.0) { + bossBar.progress = progress + } else { + bossBar.removeAll() + cancel() + } } } } else { diff --git a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitBook.kt b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitBook.kt index 4a19d0275..b5bd8e85c 100644 --- a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitBook.kt +++ b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitBook.kt @@ -7,7 +7,6 @@ import taboolib.common.Inject import taboolib.common.platform.Platform import taboolib.common.platform.PlatformSide import taboolib.common.platform.event.SubscribeEvent -import taboolib.common.platform.function.submit import taboolib.library.xseries.XMaterial import java.util.concurrent.ConcurrentHashMap @@ -57,7 +56,7 @@ internal object BookListener { consumer(pages) if (lore.getOrNull(1) == regex[1]) { inputs.remove(event.player.name) - submit(delay = 1) { + event.player.submit(delay = 1) { event.player.inventory.takeItem(99) { i -> i.hasLore(regex[0]) } } } diff --git a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitChat.kt b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitChat.kt index 79e930d64..0634da236 100644 --- a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitChat.kt +++ b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/BukkitChat.kt @@ -7,7 +7,6 @@ import taboolib.common.Inject import taboolib.common.platform.Platform import taboolib.common.platform.PlatformSide import taboolib.common.platform.event.SubscribeEvent -import taboolib.common.platform.function.submit import java.util.concurrent.ConcurrentHashMap /** @@ -36,7 +35,7 @@ fun Player.nextChatInTick(tick: Long, func: (message: String) -> Unit, timeout: reuse(this) } else { ChatListener.inputs[name] = func - submit(delay = tick) { + this@nextChatInTick.submit(delay = tick) { if (ChatListener.inputs.containsKey(name)) { timeout(this@nextChatInTick) ChatListener.inputs.remove(name) diff --git a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/ItemMatcher.kt b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/ItemMatcher.kt index b9f57c8e7..65105f574 100644 --- a/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/ItemMatcher.kt +++ b/module/bukkit/bukkit-util/src/main/kotlin/taboolib/platform/util/ItemMatcher.kt @@ -4,6 +4,29 @@ import org.bukkit.entity.Player import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack +internal data class RemovalPlan(val entry: T, val amount: Int) + +internal fun planRemoval(amount: Int, entries: Sequence, amountOf: (T) -> Int): List>? { + if (amount <= 0) { + return emptyList() + } + val plan = ArrayList>() + var remainingAmount = amount + for (entry in entries) { + val availableAmount = amountOf(entry) + if (availableAmount <= 0) { + continue + } + val takenAmount = minOf(availableAmount, remainingAmount) + plan += RemovalPlan(entry, takenAmount) + remainingAmount -= takenAmount + if (remainingAmount == 0) { + return plan + } + } + return null +} + /** * 检查玩家背包中的特定物品是否达到特定数量 * @@ -31,7 +54,11 @@ fun Inventory.checkItem(item: ItemStack, amount: Int = 1, remove: Boolean = fals if (item.isAir()) { error("air") } - return hasItem(amount) { it.isSimilar(item) } && (!remove || takeItem(amount) { it.isSimilar(item) }) + return if (remove) { + takeItem(amount) { it.isSimilar(item) } + } else { + hasItem(amount) { it.isSimilar(item) } + } } /** @@ -42,6 +69,9 @@ fun Inventory.checkItem(item: ItemStack, amount: Int = 1, remove: Boolean = fals * @return boolean */ fun Inventory.hasItem(amount: Int = 1, matcher: (itemStack: ItemStack) -> Boolean): Boolean { + if (amount <= 0) { + return true + } var checkAmount = amount contents.forEach { itemStack -> if (itemStack.isNotAir() && matcher(itemStack)) { @@ -63,24 +93,22 @@ fun Inventory.hasItem(amount: Int = 1, matcher: (itemStack: ItemStack) -> Boolea * @return boolean */ fun Inventory.takeItem(amount: Int = 1, takeList: MutableList = mutableListOf(), matcher: (itemStack: ItemStack) -> Boolean): Boolean { - var takeAmount = amount - contents.forEachIndexed { index, itemStack -> - if (itemStack.isNotAir() && matcher(itemStack)) { - takeAmount -= itemStack.amount - if (takeAmount < 0) { - takeList.add(itemStack.clone().apply { this.amount = takeAmount + itemStack.amount }) - itemStack.amount -= takeAmount + itemStack.amount - return takeList.isNotEmpty() - } else { - takeList.add(itemStack.clone()) - setItem(index, null) - if (takeAmount == 0) { - return takeList.isNotEmpty() - } - } + val matchedItems = contents.asSequence().mapIndexedNotNull { index, itemStack -> + if (itemStack.isNotAir() && matcher(itemStack)) index to itemStack else null + } + val removalPlan = planRemoval(amount, matchedItems) { (_, itemStack) -> itemStack.amount } ?: return false + val takenItems = ArrayList(removalPlan.size) + removalPlan.forEach { (entry, takenAmount) -> + val (index, itemStack) = entry + takenItems += itemStack.clone().apply { this.amount = takenAmount } + if (takenAmount == itemStack.amount) { + setItem(index, null) + } else { + setItem(index, itemStack.clone().apply { this.amount = itemStack.amount - takenAmount }) } } - return takeList.isNotEmpty() + takeList += takenItems + return true } diff --git a/module/bukkit/bukkit-util/src/test/kotlin/taboolib/platform/util/ItemMatcherTest.kt b/module/bukkit/bukkit-util/src/test/kotlin/taboolib/platform/util/ItemMatcherTest.kt new file mode 100644 index 000000000..cfb0c5f15 --- /dev/null +++ b/module/bukkit/bukkit-util/src/test/kotlin/taboolib/platform/util/ItemMatcherTest.kt @@ -0,0 +1,47 @@ +package taboolib.platform.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class ItemMatcherTest { + + @Test + fun `insufficient amount produces no removal plan`() { + val entries = listOf("first" to 2, "second" to 3) + + val plan = planRemoval(6, entries.asSequence()) { it.second } + + assertNull(plan) + assertEquals(listOf("first" to 2, "second" to 3), entries) + } + + @Test + fun `removal plan takes exact amount across stacks`() { + val entries = sequenceOf("first" to 3, "second" to 5) + + val plan = planRemoval(6, entries) { it.second } + + assertEquals( + listOf(RemovalPlan("first" to 3, 3), RemovalPlan("second" to 5, 3)), + plan + ) + } + + @Test + fun `non-positive amount produces an empty plan`() { + assertEquals(emptyList>(), planRemoval(0, sequenceOf(2, 3)) { it }) + } + + @Test + fun `planning stops after enough items are found`() { + var evaluations = 0 + val plan = planRemoval(4, sequenceOf(2, 3, 4)) { + evaluations++ + it + } + + assertEquals(2, evaluations) + assertEquals(listOf(2, 2), plan?.map { it.amount }) + } +} diff --git a/platform/platform-bukkit-impl/build.gradle.kts b/platform/platform-bukkit-impl/build.gradle.kts index 2044a3cab..ba010f351 100644 --- a/platform/platform-bukkit-impl/build.gradle.kts +++ b/platform/platform-bukkit-impl/build.gradle.kts @@ -19,6 +19,10 @@ dependencies { compileOnly("ink.ptms.core:v12110:12110:mapped") compileOnly("io.paper:folia-api:1.21.4") compileOnly("net.md-5:bungeecord-chat:1.20") + testImplementation(project(":common")) + testImplementation(project(":common-platform-api")) + testImplementation(project(":platform:platform-bukkit")) + testImplementation("io.paper:folia-api:1.21.4") // 用于处理命令 // ClassCastException: Cannot cast java.lang.String to net.kyori.adventure.text.Component diff --git a/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/BukkitExecutor.kt b/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/BukkitExecutor.kt index b6f81d5fd..9c3692a65 100644 --- a/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/BukkitExecutor.kt +++ b/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/BukkitExecutor.kt @@ -54,6 +54,12 @@ class BukkitExecutor : PlatformExecutor { } override fun submit(runnable: PlatformExecutor.PlatformRunnable): PlatformExecutor.PlatformTask { + if (Folia.isFolia && !runnable.now && !runnable.async) { + error( + "Context-free synchronous tasks are unsupported on Folia. " + + "Use Location.submit(), Entity.submit(), or an explicit global scheduler." + ) + } // 服务器已启动 val task = createRunningTask(runnable) return if (started) { @@ -133,39 +139,24 @@ class BukkitExecutor : PlatformExecutor { } override fun execute(async: Boolean, delay: Long, period: Long) { - scheduledTask = if (async) { - if (period < 1) { - if (delay < 1) { - FoliaExecutor.ASYNC_SCHEDULER.runNow(BukkitPlugin.getInstance()) { task -> - runnable.executor(BukkitPlatformTask { task.cancel() }) - } - } else { - FoliaExecutor.ASYNC_SCHEDULER.runDelayed(BukkitPlugin.getInstance(), { task -> - runnable.executor(BukkitPlatformTask { task.cancel() }) - }, delay.coerceAtLeast(1) * 50, TimeUnit.MILLISECONDS) - } - } else { - FoliaExecutor.ASYNC_SCHEDULER.runAtFixedRate(BukkitPlugin.getInstance(), { task -> + check(async) { + "Context-free synchronous tasks are unsupported on Folia. " + + "Use Location.submit(), Entity.submit(), or an explicit global scheduler." + } + scheduledTask = if (period < 1) { + if (delay < 1) { + FoliaExecutor.ASYNC_SCHEDULER.runNow(BukkitPlugin.getInstance()) { task -> runnable.executor(BukkitPlatformTask { task.cancel() }) - }, delay.coerceAtLeast(1) * 50, period * 50, TimeUnit.MILLISECONDS) - } - } else { - if (period < 1) { - // Delay ticks may not be <= 0, 蠢 - if (delay < 1) { - FoliaExecutor.GLOBAL_REGION_SCHEDULER.run(BukkitPlugin.getInstance()) { task -> - runnable.executor(BukkitPlatformTask { task.cancel() }) - } - } else { - FoliaExecutor.GLOBAL_REGION_SCHEDULER.runDelayed(BukkitPlugin.getInstance(), { task -> - runnable.executor(BukkitPlatformTask { task.cancel() }) - }, delay.coerceAtLeast(1)) } } else { - FoliaExecutor.GLOBAL_REGION_SCHEDULER.runAtFixedRate(BukkitPlugin.getInstance(), { task -> + FoliaExecutor.ASYNC_SCHEDULER.runDelayed(BukkitPlugin.getInstance(), { task -> runnable.executor(BukkitPlatformTask { task.cancel() }) - }, delay.coerceAtLeast(1), period) + }, delay.coerceAtLeast(1) * 50, TimeUnit.MILLISECONDS) } + } else { + FoliaExecutor.ASYNC_SCHEDULER.runAtFixedRate(BukkitPlugin.getInstance(), { task -> + runnable.executor(BukkitPlatformTask { task.cancel() }) + }, delay.coerceAtLeast(1) * 50, period * 50, TimeUnit.MILLISECONDS) } } diff --git a/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/util/FoliaExecutor.kt b/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/util/FoliaExecutor.kt index 5f04a690d..1dc38e7d5 100644 --- a/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/util/FoliaExecutor.kt +++ b/platform/platform-bukkit-impl/src/main/kotlin/taboolib/platform/util/FoliaExecutor.kt @@ -15,7 +15,38 @@ import taboolib.platform.BukkitPlugin import taboolib.platform.Folia import taboolib.platform.FoliaExecutor import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException + +/** + * 在 Bukkit 主线程或 Folia 全局区域线程执行不属于具体实体、区块或位置的任务。 + */ +@JvmOverloads +fun submitGlobal( + now: Boolean = false, + delay: Long = 0, + period: Long = 0, + executor: PlatformExecutor.PlatformTask.() -> Unit, +): PlatformExecutor.PlatformTask { + if (!Folia.isFolia) { + val runNow = now && Bukkit.isPrimaryThread() + return submitPlatform(runNow, false, if (now) 0 else delay, if (now) 0 else period, executor) + } + val scheduledTask = if (now || period < 1) { + if (now || delay < 1) { + FoliaExecutor.GLOBAL_REGION_SCHEDULER.run(BukkitPlugin.getInstance()) { task -> + executor(BukkitExecutor.BukkitPlatformTask { task.cancel() }) + } + } else { + FoliaExecutor.GLOBAL_REGION_SCHEDULER.runDelayed(BukkitPlugin.getInstance(), { task -> + executor(BukkitExecutor.BukkitPlatformTask { task.cancel() }) + }, delay.coerceAtLeast(1)) + } + } else { + FoliaExecutor.GLOBAL_REGION_SCHEDULER.runAtFixedRate(BukkitPlugin.getInstance(), { task -> + executor(BukkitExecutor.BukkitPlatformTask { task.cancel() }) + }, delay.coerceAtLeast(1), period) + } + return BukkitExecutor.BukkitPlatformTask { scheduledTask.cancel() } +} // ============================================ // Location 扩展函数 @@ -25,14 +56,27 @@ import java.util.concurrent.ExecutionException * 在指定位置所属的 Folia 区域线程中执行回调并返回结果。 */ fun Location.callRegion(executor: () -> T): T { - if (isOwnedByCurrentRegion()) { - return callDirect(executor) + check(isOwnedByCurrentRegion()) { + "The current thread does not own this location. Use Location.callRegionAsync(), runTask(), or submit() instead." } + return executor() +} + +/** + * 在指定位置所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun Location.callRegionAsync(executor: () -> T): CompletableFuture { val future = CompletableFuture() - FoliaExecutor.REGION_SCHEDULER.run(BukkitPlugin.getInstance(), this) { + if (isOwnedByCurrentRegion()) { future.completeWith(executor) + } else if (Folia.isFolia) { + FoliaExecutor.REGION_SCHEDULER.run(BukkitPlugin.getInstance(), this) { + future.completeWith(executor) + } + } else { + submitPlatform { future.completeWith(executor) } } - return future.awaitResult() + return future } /** @@ -80,10 +124,11 @@ fun Location.submit( useScheduler: Boolean = true, executor: PlatformExecutor.PlatformTask.() -> Unit, ): PlatformExecutor.PlatformTask { - // 如果是异步执行、或不是 Folia 环境 + // 如果不是 Folia 环境 if (!Folia.isFolia) { return if (useScheduler || async) { - submitPlatform(now, async, delay, period, executor) + val runNow = now && (async || Bukkit.isPrimaryThread()) + submitPlatform(runNow, async, if (now) 0 else delay, if (now) 0 else period, executor) } else { val task = BukkitExecutor.BukkitPlatformTask { } if (now) { @@ -101,17 +146,17 @@ fun Location.submit( // Folia 环境下,使用 RegionScheduler 在指定位置执行 var scheduledTask: ScheduledTask? = null - if (now) { - // 立即执行 + if (now && isOwnedByCurrentRegion()) { + // 当前线程拥有该区域时立即执行 val task = BukkitExecutor.BukkitPlatformTask { scheduledTask?.cancel() } executor(task) return task } // 延迟或定时执行 - scheduledTask = if (period < 1) { + scheduledTask = if (now || period < 1) { // 单次执行 - if (delay < 1) { + if (now || delay < 1) { FoliaExecutor.REGION_SCHEDULER.run(BukkitPlugin.getInstance(), this) { task -> val platformTask = BukkitExecutor.BukkitPlatformTask { task.cancel() } executor(platformTask) @@ -141,19 +186,32 @@ fun Location.submit( * 在实体所属的 Folia 实体线程中执行回调并返回结果。 */ fun Entity.callRegion(executor: () -> T): T { - if (isOwnedByCurrentRegion()) { - return callDirect(executor) + check(isOwnedByCurrentRegion()) { + "The current thread does not own this entity. Use Entity.callRegionAsync(), runTask(), or submit() instead." } + return executor() +} + +/** + * 在实体所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun Entity.callRegionAsync(executor: () -> T): CompletableFuture { val future = CompletableFuture() - val scheduledTask = FoliaExecutor.getEntityScheduler(this).run(BukkitPlugin.getInstance(), { + if (isOwnedByCurrentRegion()) { future.completeWith(executor) - }, { - future.completeExceptionally(IllegalStateException("Entity scheduler retired.")) - }) - if (scheduledTask == null && !future.isDone) { - future.completeExceptionally(IllegalStateException("Entity scheduler rejected task.")) + } else if (Folia.isFolia) { + val scheduledTask = FoliaExecutor.getEntityScheduler(this).run(BukkitPlugin.getInstance(), { + future.completeWith(executor) + }, { + future.completeExceptionally(IllegalStateException("Entity scheduler retired.")) + }) + if (scheduledTask == null && !future.isDone) { + future.completeExceptionally(IllegalStateException("Entity scheduler rejected task.")) + } + } else { + submitPlatform { future.completeWith(executor) } } - return future.awaitResult() + return future } /** @@ -202,10 +260,11 @@ fun Entity.submit( useScheduler: Boolean = true, executor: PlatformExecutor.PlatformTask.() -> Unit, ): PlatformExecutor.PlatformTask { - // 如果是异步执行、或不是 Folia 环境 + // 如果不是 Folia 环境 if (!Folia.isFolia) { return if (useScheduler || async) { - submitPlatform(now, async, delay, period, executor) + val runNow = now && (async || Bukkit.isPrimaryThread()) + submitPlatform(runNow, async, if (now) 0 else delay, if (now) 0 else period, executor) } else { val task = BukkitExecutor.BukkitPlatformTask { } if (now) { @@ -223,8 +282,8 @@ fun Entity.submit( // Folia 环境下,使用 Entity Scheduler var scheduledTask: ScheduledTask? = null - if (now) { - // 立即执行 + if (now && isOwnedByCurrentRegion()) { + // 当前线程拥有该实体时立即执行 val task = BukkitExecutor.BukkitPlatformTask { scheduledTask?.cancel() } executor(task) return task @@ -234,9 +293,9 @@ fun Entity.submit( val entityScheduler = FoliaExecutor.getEntityScheduler(this) // 延迟或定时执行 - scheduledTask = if (period < 1) { + scheduledTask = if (now || period < 1) { // 单次执行 - if (delay < 1) { + if (now || delay < 1) { entityScheduler.run(BukkitPlugin.getInstance(), { task -> val platformTask = BukkitExecutor.BukkitPlatformTask { task.cancel() } executor(platformTask) @@ -269,6 +328,13 @@ fun Block.callRegion(executor: () -> T): T { return location.callRegion(executor) } +/** + * 在方块所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun Block.callRegionAsync(executor: () -> T): CompletableFuture { + return location.callRegionAsync(executor) +} + /** * 在方块所在位置执行一个任务(Folia 安全) * @@ -313,6 +379,13 @@ fun Chunk.callRegion(executor: () -> T): T { return Location(world, (x shl 4) + 8.0, 64.0, (z shl 4) + 8.0).callRegion(executor) } +/** + * 在区块中心所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun Chunk.callRegionAsync(executor: () -> T): CompletableFuture { + return Location(world, (x shl 4) + 8.0, 64.0, (z shl 4) + 8.0).callRegionAsync(executor) +} + /** * 在区块中心位置执行一个任务(Folia 安全) * @@ -359,6 +432,13 @@ fun World.callRegion(x: Double, z: Double, executor: () -> T): T { return Location(this, x, 64.0, z).callRegion(executor) } +/** + * 在指定世界坐标所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun World.callRegionAsync(x: Double, z: Double, executor: () -> T): CompletableFuture { + return Location(this, x, 64.0, z).callRegionAsync(executor) +} + /** * 在指定世界方块坐标所属的 Folia 区域线程中执行回调并返回结果。 */ @@ -366,6 +446,13 @@ fun World.callRegion(x: Int, y: Int, z: Int, executor: () -> T): T { return Location(this, x.toDouble(), y.toDouble(), z.toDouble()).callRegion(executor) } +/** + * 在指定世界方块坐标所属线程中执行回调,并通过 Future 非阻塞返回结果。 + */ +fun World.callRegionAsync(x: Int, y: Int, z: Int, executor: () -> T): CompletableFuture { + return Location(this, x.toDouble(), y.toDouble(), z.toDouble()).callRegionAsync(executor) +} + /** * 在指定世界坐标执行一个任务(Folia 安全) * @@ -407,26 +494,22 @@ fun World.submit( return location.submit(now, async, delay, period, useScheduler, executor) } -private fun callDirect(executor: () -> T): T { - return executor() -} - -private fun Location.isOwnedByCurrentRegion(): Boolean { +fun Location.isOwnedByCurrentRegion(): Boolean { if (!Folia.isFolia) { - return true + return Bukkit.isPrimaryThread() } return kotlin.runCatching { - Bukkit::class.java.invokeMethod("isOwnedByCurrentRegion", this, isStatic = true, remap = false) ?: true - }.getOrDefault(true) + Bukkit::class.java.invokeMethod("isOwnedByCurrentRegion", this, isStatic = true, remap = false) == true + }.getOrDefault(false) } -private fun Entity.isOwnedByCurrentRegion(): Boolean { +fun Entity.isOwnedByCurrentRegion(): Boolean { if (!Folia.isFolia) { - return true + return Bukkit.isPrimaryThread() } return kotlin.runCatching { - Bukkit::class.java.invokeMethod("isOwnedByCurrentRegion", this, isStatic = true, remap = false) ?: true - }.getOrDefault(true) + Bukkit::class.java.invokeMethod("isOwnedByCurrentRegion", this, isStatic = true, remap = false) == true + }.getOrDefault(false) } private fun CompletableFuture.completeWith(executor: () -> T) { @@ -436,21 +519,3 @@ private fun CompletableFuture.completeWith(executor: () -> T) { completeExceptionally(throwable) } } - -private fun CompletableFuture.awaitResult(): T { - try { - return get() - } catch (exception: InterruptedException) { - Thread.currentThread().interrupt() - throw RuntimeException(exception) - } catch (exception: ExecutionException) { - val cause = exception.cause - if (cause is RuntimeException) { - throw cause - } - if (cause is Error) { - throw cause - } - throw RuntimeException(cause) - } -} diff --git a/platform/platform-bukkit-impl/src/test/kotlin/taboolib/platform/BukkitExecutorTest.kt b/platform/platform-bukkit-impl/src/test/kotlin/taboolib/platform/BukkitExecutorTest.kt new file mode 100644 index 000000000..2dc0e14ff --- /dev/null +++ b/platform/platform-bukkit-impl/src/test/kotlin/taboolib/platform/BukkitExecutorTest.kt @@ -0,0 +1,52 @@ +package taboolib.platform + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import taboolib.common.platform.service.PlatformExecutor + +class BukkitExecutorTest { + + @Test + fun `context-free synchronous task is rejected before enqueue on Folia`() { + withFolia { + val executor = BukkitExecutor() + assertThrows(IllegalStateException::class.java) { + executor.submit(runnable(now = false, async = false)) + } + } + } + + @Test + fun `asynchronous and immediate tasks keep their existing entry points on Folia`() { + withFolia { + val executor = BukkitExecutor() + assertDoesNotThrow { executor.submit(runnable(now = false, async = true)) } + assertDoesNotThrow { executor.submit(runnable(now = true, async = false)) } + } + } + + @Test + fun `Folia running task cannot bypass synchronous context check`() { + withFolia { + val task = BukkitExecutor.FoliaRunningTask(runnable(now = false, async = false)) + assertThrows(IllegalStateException::class.java) { + task.execute(async = false, delay = 0, period = 0) + } + } + } + + private fun runnable(now: Boolean, async: Boolean): PlatformExecutor.PlatformRunnable { + return PlatformExecutor.PlatformRunnable(now, async, 0, 0) {} + } + + private fun withFolia(block: () -> Unit) { + val previous = Folia.isFolia + Folia.isFolia = true + try { + block() + } finally { + Folia.isFolia = previous + } + } +} diff --git a/platform/platform-bukkit/src/main/java/taboolib/platform/BukkitPlugin.java b/platform/platform-bukkit/src/main/java/taboolib/platform/BukkitPlugin.java index caeea9d3c..2e1fbfb17 100644 --- a/platform/platform-bukkit/src/main/java/taboolib/platform/BukkitPlugin.java +++ b/platform/platform-bukkit/src/main/java/taboolib/platform/BukkitPlugin.java @@ -110,7 +110,7 @@ public void onEnable() { if (!TabooLib.isStopped()) { // 创建调度器,执行 onActive() 方法 if (Folia.isFolia) { - FoliaExecutor.ASYNC_SCHEDULER.runNow(this, task -> invokeActive()); + FoliaExecutor.GLOBAL_REGION_SCHEDULER.run(this, task -> invokeActive()); } else { Bukkit.getScheduler().runTask(this, this::invokeActive); } diff --git a/platform/platform-bukkit/src/main/java/taboolib/platform/FoliaExecutor.java b/platform/platform-bukkit/src/main/java/taboolib/platform/FoliaExecutor.java index e5727f54e..e855c15a1 100644 --- a/platform/platform-bukkit/src/main/java/taboolib/platform/FoliaExecutor.java +++ b/platform/platform-bukkit/src/main/java/taboolib/platform/FoliaExecutor.java @@ -53,4 +53,11 @@ public class FoliaExecutor { public static EntityScheduler getEntityScheduler(final Entity entity) throws InvocationTargetException, IllegalAccessException { return (EntityScheduler) GET_ENTITY_SCHEDULER.invoke(entity); } + + /** + * 在 Folia 全局区域线程执行不属于具体实体或位置的任务。 + */ + public static void runGlobal(final Runnable runnable) { + GLOBAL_REGION_SCHEDULER.run(BukkitPlugin.getInstance(), task -> runnable.run()); + } }