diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java index 1875de2999..b11640de54 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java @@ -133,15 +133,19 @@ public SessionKey getSessionKey() { private volatile boolean active = true; private void updateActive() { - Block block = sender.getBlock(); - if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) { - active = false; - return; + try { + Block block = sender.getBlock(); + if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) { + active = false; + return; + } + Material type = block.getType(); + active = type == Material.COMMAND_BLOCK + || type == Material.CHAIN_COMMAND_BLOCK + || type == Material.REPEATING_COMMAND_BLOCK; + } catch (Throwable t) { + WorldEdit.logger.warn("Exception while updating command block sender active state", t); } - Material type = block.getType(); - active = type == Material.COMMAND_BLOCK - || type == Material.CHAIN_COMMAND_BLOCK - || type == Material.REPEATING_COMMAND_BLOCK; } @Override @@ -151,6 +155,19 @@ public String getName() { @Override public boolean isActive() { + if (WorldEditPlugin.getInstance().isFolia()) { + // On Folia, we need to perform the update on the thread that owns the block. + if (Bukkit.isOwnedByCurrentRegion(sender.getBlock())) { + // This thread owns the block, so we can immediately update. + updateActive(); + } else { + // We need to delegate to the right thread. + Bukkit.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), this::updateActive); + } + + return active; + } + if (Bukkit.isPrimaryThread()) { // we can update eagerly updateActive(); @@ -158,15 +175,10 @@ public boolean isActive() { // we should update it eventually // Suppress FutureReturnValueIgnored: We handle it in the block. @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) - var unused = Bukkit.getScheduler().callSyncMethod(plugin, - () -> { - try { - updateActive(); - } catch (Throwable t) { - WorldEdit.logger.warn("Exception while updating command block sender active state", t); - } - return null; - }); + var unused = Bukkit.getScheduler().callSyncMethod(plugin, () -> { + updateActive(); + return null; + }); } return active; } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java index e719902862..5a529e482c 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java @@ -25,7 +25,9 @@ import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.entity.metadata.EntityProperties; import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.regions.RegionOperationException; import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; import com.sk89q.worldedit.world.NullWorld; import java.lang.ref.WeakReference; @@ -101,6 +103,10 @@ public BaseEntity getState() { @Override public boolean remove() { + if (WorldEditPlugin.getInstance().isFolia()) { + throw new RuntimeException(new RegionOperationException(TranslatableComponent.of("worldedit.bukkit.unsupported-on-folia"))); + } + org.bukkit.entity.Entity entity = entityRef.get(); if (entity != null) { try { diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index 7526989842..dde4ebce01 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java @@ -121,7 +121,14 @@ public void reload() { @Override public int schedule(long delay, long period, Runnable task) { - return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + if (plugin.isFolia()) { + Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delay, period); + // TODO Paper doesn't appear to have a concept of task IDs, so return 1 here for now. + // We may want to store these tasks and map them to our own IDs to cancel them later. + return 1; + } else { + return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + } } @Override diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java index 935e1970b4..fd3466c7a4 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java @@ -37,10 +37,12 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.regions.RegionOperationException; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; import com.sk89q.worldedit.util.TreeGenerator; +import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; import com.sk89q.worldedit.world.AbstractWorld; import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.biome.BiomeType; @@ -134,6 +136,10 @@ public List getEntities() { @Nullable @Override public com.sk89q.worldedit.entity.Entity createEntity(com.sk89q.worldedit.util.Location location, BaseEntity entity) { + if (WorldEditPlugin.getInstance().isFolia()) { + throw new RuntimeException(new RegionOperationException(TranslatableComponent.of("worldedit.bukkit.unsupported-on-folia"))); + } + BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); if (adapter != null) { try { @@ -191,6 +197,10 @@ public int getBlockLightLevel(BlockVector3 pt) { @Override public boolean regenerate(Region region, Extent extent, RegenOptions options) { + if (WorldEditPlugin.getInstance().isFolia()) { + throw new RuntimeException(new RegionOperationException(TranslatableComponent.of("worldedit.bukkit.unsupported-on-folia"))); + } + BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); try { if (adapter != null) { diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java index f90a4b7352..576bf0b32b 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java @@ -31,6 +31,7 @@ import com.sk89q.worldedit.bukkit.adapter.AdapterLoadException; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplLoader; +import com.sk89q.worldedit.bukkit.folia.FoliaExtentListener; import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.event.platform.CommandSuggestionEvent; import com.sk89q.worldedit.event.platform.ConfigurationLoadEvent; @@ -48,6 +49,7 @@ import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.registry.Registries; import com.sk89q.worldedit.registry.state.Property; +import com.sk89q.worldedit.util.concurrency.LazyReference; import com.sk89q.worldedit.util.lifecycle.Lifecycled; import com.sk89q.worldedit.util.lifecycle.SimpleLifecycled; import com.sk89q.worldedit.world.World; @@ -61,6 +63,8 @@ import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.weather.WeatherTypes; import io.papermc.lib.PaperLib; +import io.papermc.paper.ServerBuildInfo; +import net.kyori.adventure.key.Key; import org.apache.logging.log4j.Logger; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; @@ -178,6 +182,11 @@ public void onEnable() { // Enable metrics new Metrics(this, BSTATS_PLUGIN_ID); PaperLib.suggestPaper(this); + + if (isFolia()) { + // Inject Folia-anti-break extent + WorldEdit.getInstance().getEventBus().register(new FoliaExtentListener()); + } } private void setupPreWorldData() { @@ -311,7 +320,14 @@ public void onDisable() { if (config != null) { config.unload(); } - this.getServer().getScheduler().cancelTasks(this); + + if (isFolia()) { + this.getServer().getGlobalRegionScheduler().cancelTasks(this); + this.getServer().getAsyncScheduler().cancelTasks(this); + // Region schedulers do not support cancelling tasks + } else { + this.getServer().getScheduler().cancelTasks(this); + } } /** @@ -507,6 +523,27 @@ BukkitImplAdapter getBukkitImplAdapter() { return adapter.value().orElse(null); } + private final LazyReference folia = LazyReference.from(() -> { + try { + // Folia is Paper-based, so this is a good first check. + if (PaperLib.isPaper()) { + // Then we can check against the `papermc:folia` key, as per the `isBrandCompatible` javadoc. + // This API is experimental so might randomly break on us (hence the try/catch) + // TODO if we drop Spigot support in the future, remove this check and purely use Folia-compatible APIs. + return ServerBuildInfo.buildInfo().isBrandCompatible(Key.key("papermc", "folia")); + } + } catch (Throwable t) { + // Ignore, this likely means an outdated version. + LOGGER.warn("Failed to check if server is running Folia", t); + } + + return false; + }); + + protected boolean isFolia() { + return folia.getValue(); + } + private class WorldInitListener implements Listener { private boolean loaded = false; diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaExtentListener.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaExtentListener.java new file mode 100644 index 0000000000..9e93845b3a --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaExtentListener.java @@ -0,0 +1,63 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.bukkit.folia; + +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.event.extent.EditSessionEvent; +import com.sk89q.worldedit.extent.AbstractDelegateExtent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.eventbus.Subscribe; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import org.bukkit.Bukkit; + +public class FoliaExtentListener { + + @Subscribe + public void onEditSessionCreation(EditSessionEvent event) { + // This injects an extent that only allows modifying blocks & biomes that are within the current region. + // This prevents WorldEdit from inadvertently causing chunk loads or other thread-shenanigans when running on Folia. + event.setExtent(new AbstractDelegateExtent(event.getExtent()) { + + private boolean isWithinRegion(BlockVector3 location) { + return Bukkit.isOwnedByCurrentRegion(BukkitAdapter.adapt(BukkitAdapter.adapt(event.getWorld()), location)); + } + + @Override + public > boolean setBlock(BlockVector3 location, T block) throws WorldEditException { + if (isWithinRegion(location)) { + return super.setBlock(location, block); + } + + return false; + } + + @Override + public boolean setBiome(BlockVector3 position, BiomeType biome) { + if (isWithinRegion(position)) { + return super.setBiome(position, biome); + } + + return false; + } + }); + } +} diff --git a/worldedit-bukkit/src/main/resources/plugin.yml b/worldedit-bukkit/src/main/resources/plugin.yml index 4f19c3e6ec..28cce7beaf 100644 --- a/worldedit-bukkit/src/main/resources/plugin.yml +++ b/worldedit-bukkit/src/main/resources/plugin.yml @@ -3,6 +3,7 @@ main: com.sk89q.worldedit.bukkit.WorldEditPlugin version: "${internalVersion}" load: STARTUP api-version: 1.21.4 +folia-supported: true softdepend: [Vault] author: EngineHub -website: https://enginehub.org/worldedit \ No newline at end of file +website: https://enginehub.org/worldedit diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index a635a5a617..42b5935f52 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -489,5 +489,6 @@ "worldedit.cli.unknown-command": "Unknown command!", "worldedit.version.bukkit.unsupported-adapter": "This WorldEdit version does not fully support your version of Bukkit. Block entities (e.g. chests) will be empty, block properties (e.g. rotation) will be missing, and other things may not work. Update WorldEdit to restore this functionality:\n{0}", - "worldedit.bukkit.no-edit-without-adapter": "Editing on unsupported versions is disabled." + "worldedit.bukkit.no-edit-without-adapter": "Editing on unsupported versions is disabled.", + "worldedit.bukkit.unsupported-on-folia": "This feature of WorldEdit is currently not supported on Folia." }