From b2ba2745ab505009b4269654fbeec584d8deb8ff Mon Sep 17 00:00:00 2001 From: Maddy Miller Date: Mon, 16 Feb 2026 21:46:44 +1000 Subject: [PATCH 1/2] Add very basic Folia support --- .../bukkit/BukkitBlockCommandSender.java | 35 ++++++++--- .../sk89q/worldedit/bukkit/BukkitEntity.java | 6 ++ .../bukkit/BukkitServerInterface.java | 9 ++- .../sk89q/worldedit/bukkit/BukkitWorld.java | 10 +++ .../worldedit/bukkit/WorldEditPlugin.java | 38 ++++++++++- .../bukkit/folia/FoliaExtentListener.java | 63 +++++++++++++++++++ .../src/main/resources/plugin.yml | 3 +- .../src/main/resources/lang/strings.json | 3 +- 8 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaExtentListener.java 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..22bc7fa012 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 @@ -151,6 +151,25 @@ 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(), () -> { + try { + updateActive(); + } catch (Throwable t) { + WorldEdit.logger.warn("Exception while updating command block sender active state", t); + } + }); + } + + return active; + } + if (Bukkit.isPrimaryThread()) { // we can update eagerly updateActive(); @@ -159,14 +178,14 @@ public boolean isActive() { // 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; - }); + () -> { + try { + updateActive(); + } catch (Throwable t) { + WorldEdit.logger.warn("Exception while updating command block sender active state", t); + } + 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..872e83cb3a 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..f06b206983 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,26 @@ 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. + } + + 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." } From 9296490e634481260f68b9a819484f88458bcddc Mon Sep 17 00:00:00 2001 From: Maddy Miller Date: Sun, 22 Feb 2026 13:36:40 +1000 Subject: [PATCH 2/2] PR feedback --- .../bukkit/BukkitBlockCommandSender.java | 41 ++++++++----------- .../bukkit/BukkitServerInterface.java | 4 +- .../worldedit/bukkit/WorldEditPlugin.java | 1 + 3 files changed, 20 insertions(+), 26 deletions(-) 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 22bc7fa012..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 @@ -158,13 +162,7 @@ public boolean isActive() { updateActive(); } else { // We need to delegate to the right thread. - Bukkit.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), () -> { - try { - updateActive(); - } catch (Throwable t) { - WorldEdit.logger.warn("Exception while updating command block sender active state", t); - } - }); + Bukkit.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), this::updateActive); } return active; @@ -177,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/BukkitServerInterface.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index 872e83cb3a..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 @@ -123,9 +123,9 @@ public void reload() { public int schedule(long delay, long period, Runnable task) { 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. + // 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; + return 1; } else { return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); } 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 f06b206983..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 @@ -534,6 +534,7 @@ BukkitImplAdapter getBukkitImplAdapter() { } } catch (Throwable t) { // Ignore, this likely means an outdated version. + LOGGER.warn("Failed to check if server is running Folia", t); } return false;