From 1352ca85af3d53b3516f2875ddd6e1253c42c0d7 Mon Sep 17 00:00:00 2001 From: R00tB33rMan <36140389+R00tB33rMan@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:51:10 -0500 Subject: [PATCH 1/5] Folia Support This is a thoroughly tested version of WorldEdit against Folia. Not only does it introduce the fundamentals that other forks have offered, but it also provides the ability to utilize `-e` tags, butchering entities, and even the `//regen` command on Spigot, Paper, and Folia alike. That said, this pull request is ready for review, so critical changes (if needed) can be made to ensure Folia is sufficient and prepared for WorldEdit upstream. This was also done for FastAsyncWorldEdit: IntellectualSites/FastAsyncWorldEdit#3363 --- .github/workflows/gradle.yml | 1 - .../impl/v1_21_3/PaperweightAdapter.java | 255 ++++++++++++++++- .../impl/v1_21_4/PaperweightAdapter.java | 255 ++++++++++++++++- .../impl/v1_21_5/PaperweightAdapter.java | 255 ++++++++++++++++- .../impl/v1_21_6/PaperweightAdapter.java | 257 +++++++++++++++++- .../impl/v1_21_9/PaperweightAdapter.java | 256 ++++++++++++++++- .../bukkit/BukkitBlockCommandSender.java | 10 +- .../sk89q/worldedit/bukkit/BukkitEntity.java | 124 +++++++-- .../sk89q/worldedit/bukkit/BukkitPlayer.java | 12 + .../bukkit/BukkitServerInterface.java | 3 +- .../sk89q/worldedit/bukkit/BukkitWorld.java | 203 ++++++++++---- .../worldedit/bukkit/WorldEditPlugin.java | 14 +- .../bukkit/folia/AsyncScheduler.java | 162 +++++++++++ .../bukkit/folia/EntityScheduler.java | 177 ++++++++++++ .../bukkit/folia/FoliaScheduler.java | 150 ++++++++++ .../bukkit/folia/GlobalRegionScheduler.java | 138 ++++++++++ .../bukkit/folia/RegionScheduler.java | 221 +++++++++++++++ .../worldedit/bukkit/folia/TaskWrapper.java | 101 +++++++ .../src/main/resources/plugin.yml | 3 +- .../worldedit/command/UtilityCommands.java | 68 +++-- .../entity/metadata/EntitySchedulerFacet.java | 37 +++ 21 files changed, 2590 insertions(+), 112 deletions(-) create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/AsyncScheduler.java create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/EntityScheduler.java create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaScheduler.java create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/GlobalRegionScheduler.java create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/RegionScheduler.java create mode 100644 worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/TaskWrapper.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/entity/metadata/EntitySchedulerFacet.java diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 66239d2e91..72bc59264b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -40,4 +40,3 @@ jobs: with: name: logs for ${{ matrix.os }} path: '**/*.log' - diff --git a/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java index 099adfcab3..31967cb45a 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -33,7 +32,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -699,6 +700,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -708,6 +713,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -781,6 +794,244 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + new NoOpWorldLoadListener(), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverWorld.registryAccess()); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -801,7 +1052,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } diff --git a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java index 7ea004366d..bfa91975e8 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -34,7 +33,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -699,6 +700,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -708,6 +713,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -781,6 +794,244 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + new NoOpWorldLoadListener(), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverWorld.registryAccess()); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -801,7 +1052,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } diff --git a/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java index 186dc34f9e..76933bff4a 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -34,7 +33,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -697,6 +698,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -706,6 +711,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -779,6 +792,244 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + new NoOpWorldLoadListener(), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverWorld.registryAccess()); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -799,7 +1050,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } diff --git a/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java index 391c4f37e9..13e69196a7 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -34,7 +33,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -710,6 +711,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -719,6 +724,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -792,6 +805,246 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + new NoOpWorldLoadListener(), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, serverWorld.registryAccess()); + blockEntity.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -812,7 +1065,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } diff --git a/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java index 98a70d13d7..2ff282bf9a 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -34,7 +33,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -709,6 +710,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -718,6 +723,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -790,6 +803,245 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, serverWorld.registryAccess()); + blockEntity.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -810,7 +1062,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } 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 12840c4dd7..60da05f811 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 @@ -20,6 +20,7 @@ package com.sk89q.worldedit.bukkit; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.extension.platform.AbstractCommandBlockActor; import com.sk89q.worldedit.session.SessionKey; import com.sk89q.worldedit.util.auth.AuthorizationException; @@ -155,12 +156,9 @@ public boolean isActive() { // we can update eagerly updateActive(); } else { - // we should update it eventually - Bukkit.getScheduler().callSyncMethod(plugin, - () -> { - updateActive(); - return null; - }); + // we should update it eventually or on the owning region thread + FoliaScheduler.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), + this::updateActive); } 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..3f0d3e3065 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 @@ -20,15 +20,20 @@ package com.sk89q.worldedit.bukkit; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.entity.metadata.EntityProperties; +import com.sk89q.worldedit.entity.metadata.EntitySchedulerFacet; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.world.NullWorld; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; import java.lang.ref.WeakReference; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -73,44 +78,106 @@ public Location getLocation() { @Override public boolean setLocation(Location location) { org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null) { - return entity.teleport(BukkitAdapter.adapt(location)); - } else { + if (entity == null) { return false; } + + if (PaperLib.isPaper()) { + FoliaScheduler.getEntityScheduler().run( + entity, + WorldEditPlugin.getInstance(), + o -> entity.teleportAsync(BukkitAdapter.adapt(location)), + null + ); + return true; + } + return entity.teleport(BukkitAdapter.adapt(location)); } @Override public BaseEntity getState() { org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null) { - if (entity instanceof Player) { - return null; - } + if (entity == null || entity instanceof Player) { + return null; + } - BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); - if (adapter != null) { - return adapter.getEntity(entity); + BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); + if (adapter == null) { + return null; + } + + try { + var loc = entity.getLocation(); + int cx = loc.getBlockX() >> 4; + int cz = loc.getBlockZ() >> 4; + if (FoliaScheduler.isFolia()) { + if (Bukkit.isOwnedByCurrentRegion(loc.getWorld(), cx, cz)) { + return adapter.getEntity(entity); + } } else { + return adapter.getEntity(entity); + } + } catch (Throwable ignored) { + } + + CompletableFuture future = new CompletableFuture<>(); + try { + FoliaScheduler.getEntityScheduler().run( + entity, + WorldEditPlugin.getInstance(), + task -> { + try { + BaseEntity result = adapter.getEntity(entity); + future.complete(result); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }, + null + ); + try { + return future.get(); + } catch (Exception e) { return null; } - } else { + } catch (Throwable ignored) { return null; } } + /** + * Remove this entity safely, using region/main-thread scheduling as appropriate. + * + * @return true if removal was scheduled or completed successfully + */ @Override public boolean remove() { org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null) { + if (entity == null) { + return true; + } + + try { + entity.remove(); + return entity.isDead(); + } catch (Throwable offThread) { try { - entity.remove(); + FoliaScheduler.getEntityScheduler().run( + entity, + WorldEditPlugin.getInstance(), + scheduledTask -> { + try { + entity.remove(); + } catch (UnsupportedOperationException ignored) { + // Some entities may refuse removal + } + }, + null + ); + return true; } catch (UnsupportedOperationException e) { return false; } - return entity.isDead(); - } else { - return true; } } @@ -119,10 +186,29 @@ public boolean remove() { @Override public T getFacet(Class cls) { org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null && EntityProperties.class.isAssignableFrom(cls)) { - return (T) new BukkitEntityProperties(entity); - } else { + if (entity == null) { return null; } + + if (EntityProperties.class.isAssignableFrom(cls)) { + return (T) new BukkitEntityProperties(entity); + } + + if (EntitySchedulerFacet.class.isAssignableFrom(cls)) { + return (T) (EntitySchedulerFacet) task -> + FoliaScheduler.getEntityScheduler().run( + entity, + WorldEditPlugin.getInstance(), + scheduledTask -> { + try { + task.run(); + } catch (Throwable ignored) { + } + }, + null + ); + } + + return null; } } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java index 38f2aae4da..1f58839a7d 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java @@ -23,6 +23,7 @@ import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.AbstractPlayerActor; import com.sk89q.worldedit.extent.inventory.BlockBag; @@ -45,6 +46,7 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.gamemode.GameMode; import com.sk89q.worldedit.world.gamemode.GameModes; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -146,6 +148,11 @@ public void print(Component component) { @Override public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { + if (PaperLib.isPaper()) { + FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), + o -> player.teleportAsync(new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch)), null); + return true; + } return player.teleport(new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch)); } @@ -224,6 +231,11 @@ public com.sk89q.worldedit.util.Location getLocation() { @Override public boolean setLocation(com.sk89q.worldedit.util.Location location) { + if (PaperLib.isPaper()) { + FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), + o -> player.teleportAsync(BukkitAdapter.adapt(location)), null); + return true; + } return player.teleport(BukkitAdapter.adapt(location)); } 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 8aed036162..9fcc550508 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 @@ -25,6 +25,7 @@ import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.command.util.PermissionCondition; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.AbstractPlatform; @@ -121,7 +122,7 @@ public void reload() { @Override public int schedule(long delay, long period, Runnable task) { - return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + return FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, o -> task.run(), delay, period).getTaskId(); } @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 39422cfc60..51e45a4e74 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 @@ -28,6 +28,7 @@ import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.UnsupportedVersionEditException; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.mask.Mask; @@ -52,7 +53,9 @@ import com.sk89q.worldedit.world.weather.WeatherTypes; import io.papermc.lib.PaperLib; import org.apache.logging.log4j.Logger; +import org.bukkit.Bukkit; import org.bukkit.Effect; +import org.bukkit.Location; import org.bukkit.TreeType; import org.bukkit.World; import org.bukkit.block.Block; @@ -71,6 +74,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nullable; @@ -84,7 +88,6 @@ public class BukkitWorld extends AbstractWorld { static { for (Effect effect : Effect.values()) { - @SuppressWarnings("deprecation") int id = effect.getId(); effects.put(id, effect); } @@ -134,23 +137,29 @@ public List getEntities() { @Nullable @Override public com.sk89q.worldedit.entity.Entity createEntity(com.sk89q.worldedit.util.Location location, BaseEntity entity) { - BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); - if (adapter != null) { - try { - Entity createdEntity = adapter.createEntity(BukkitAdapter.adapt(getWorld(), location), entity); - if (createdEntity != null) { - return new BukkitEntity(createdEntity); - } else { + final BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); + if (adapter == null) { + return null; + } + + final World bukkitWorld = getWorld(); + final Location bLoc = BukkitAdapter.adapt(bukkitWorld, location); + final BlockVector3 position = BukkitAdapter.asBlockVector(bLoc); + + try { + return executeOnRegion(position, () -> { + try { + org.bukkit.entity.Entity created = adapter.createEntity(bLoc, entity); + return created != null ? new BukkitEntity(created) : null; + } catch (Exception e) { + LOGGER.warn("Corrupt entity found when creating: {}", entity.getType().id(), e); + if (entity.getNbt() != null) { + LOGGER.warn(entity.getNbt().toString()); + } return null; } - } catch (Exception e) { - LOGGER.warn("Corrupt entity found when creating: " + entity.getType().id(), e); - if (entity.getNbt() != null) { - LOGGER.warn(entity.getNbt().toString()); - } - return null; - } - } else { + }, "Failed to create entity safely at " + bLoc); + } catch (RuntimeException e) { return null; } } @@ -259,7 +268,7 @@ public boolean clearContainerBlockContents(BlockVector3 pt) { treeTypeMapping.put(TreeGenerator.TreeType.RANDOM_MUSHROOM, TreeType.BROWN_MUSHROOM); for (TreeGenerator.TreeType type : TreeGenerator.TreeType.values()) { if (treeTypeMapping.get(type) == null) { - WorldEdit.logger.error("No TreeType mapping for TreeGenerator.TreeType." + type); + WorldEdit.logger.error("No TreeType mapping for TreeGenerator.TreeType.{}", type); } } } @@ -297,14 +306,25 @@ public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession @Override public void dropItem(Vector3 pt, BaseItemStack item) { World world = getWorld(); - world.dropItemNaturally(BukkitAdapter.adapt(world, pt), BukkitAdapter.adapt(item)); + Location loc = BukkitAdapter.adapt(world, pt); + + final int chunkX = loc.getBlockX() >> 4; + final int chunkZ = loc.getBlockZ() >> 4; + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), world, chunkX, chunkZ, + () -> world.dropItemNaturally(loc, BukkitAdapter.adapt(item)) + ); } @Override public void checkLoadedChunk(BlockVector3 pt) { World world = getWorld(); - - world.getChunkAt(pt.x() >> 4, pt.z() >> 4); + executeOnRegionVoid(pt, () -> { + int chunkX = pt.x() >> 4; + int chunkZ = pt.z() >> 4; + world.getChunkAtAsync(chunkX, chunkZ); + }, "Failed to ensure chunk [" + (pt.x() >> 4) + "," + (pt.z() >> 4) + "] is loaded safely"); } @Override @@ -454,46 +474,128 @@ public boolean generateStructure(StructureType type, EditSession editSession, Bl return false; } - private static volatile boolean hasWarnedImplError = false; + private static final boolean hasWarnedImplError = false; @Override public com.sk89q.worldedit.world.block.BlockState getBlock(BlockVector3 position) { - BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); - if (adapter != null) { - try { - return adapter.getBlock(BukkitAdapter.adapt(getWorld(), position)); - } catch (Exception e) { - if (!hasWarnedImplError) { - hasWarnedImplError = true; - LOGGER.warn("Unable to retrieve block via impl adapter", e); + return executeOnRegion(position, () -> { + World world = getWorld(); + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + return BukkitAdapter.adapt(block.getBlockData()); + }, "Failed to retrieve block state asynchronously"); + } + + @Override + public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) { + clearContainerBlockContents(position); + return executeOnRegion(position, () -> { + World world = getWorld(); + return doSetBlock(world, position, block, sideEffects); + }, "Failed to set block safely at " + position); + } + + /** + * Executes a task on the appropriate region thread for the given position. + * If already on the correct thread (Folia) or not using Folia, executes immediately. + * + * @param position the position to determine the region + * @param task the task to execute + * @param errorMessage the error message to use if execution fails + * @return the result of the task + */ + private T executeOnRegion(BlockVector3 position, java.util.function.Supplier task, String errorMessage) { + World world = getWorld(); + int chunkX = position.x() >> 4; + int chunkZ = position.z() >> 4; + + if (FoliaScheduler.isFolia()) { + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + return task.get(); + } + } else { + return task.get(); + } + + CompletableFuture future = new CompletableFuture<>(); + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), world, chunkX, chunkZ, + () -> { + try { + future.complete(task.get()); + } catch (Throwable t) { + future.completeExceptionally(t); } } + ); + + try { + return future.get(); + } catch (Exception e) { + throw new RuntimeException(errorMessage, e); } - if (WorldEditPlugin.getInstance().getLocalConfiguration().unsupportedVersionEditing) { - Block bukkitBlock = getWorld().getBlockAt(position.x(), position.y(), position.z()); - return BukkitAdapter.adapt(bukkitBlock.getBlockData()); + } + + /** + * Executes a void task on the appropriate region thread for the given position. + * If already on the correct thread (Folia) or not using Folia, executes immediately. + * + * @param position the position to determine the region + * @param task the task to execute + * @param errorMessage the error message to use if execution fails + */ + private void executeOnRegionVoid(BlockVector3 position, Runnable task, String errorMessage) { + World world = getWorld(); + int chunkX = position.x() >> 4; + int chunkZ = position.z() >> 4; + + if (FoliaScheduler.isFolia()) { + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + task.run(); + return; + } } else { - throw new RuntimeException(new UnsupportedVersionEditException()); + task.run(); + return; + } + + CompletableFuture future = new CompletableFuture<>(); + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), world, chunkX, chunkZ, + () -> { + try { + task.run(); + future.complete(null); + } catch (Throwable t) { + future.completeExceptionally(t); + } + } + ); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(errorMessage, e); } } - @Override - public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) { - clearContainerBlockContents(position); + /** + * Internal helper to perform the actual block mutation. + */ + private > boolean doSetBlock(World world, BlockVector3 position, B block, SideEffectSet sideEffects) { if (worldNativeAccess != null) { try { return worldNativeAccess.setBlock(position, block, sideEffects); } catch (Exception e) { if (block instanceof BaseBlock baseBlock && baseBlock.getNbt() != null) { - LOGGER.warn("Tried to set a corrupt tile entity at " + position.toString() - + ": " + baseBlock.getNbt(), e); + LOGGER.warn("Tried to set a corrupt tile entity at {}: {}", position.toString(), baseBlock.getNbt(), e); } else { LOGGER.warn("Failed to set block via adapter, falling back to generic", e); } } } + if (WorldEditPlugin.getInstance().getLocalConfiguration().unsupportedVersionEditing) { - Block bukkitBlock = getWorld().getBlockAt(position.x(), position.y(), position.z()); + Block bukkitBlock = world.getBlockAt(position.x(), position.y(), position.z()); bukkitBlock.setBlockData(BukkitAdapter.adapt(block), sideEffects.doesApplyAny()); return true; } else { @@ -503,17 +605,24 @@ public > boolean setBlock(BlockVector3 position, B @Override public BaseBlock getFullBlock(BlockVector3 position) { - BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); - if (adapter != null) { - return adapter.getFullBlock(BukkitAdapter.adapt(getWorld(), position)); - } else { - return getBlock(position).toBaseBlock(); - } + return executeOnRegion(position, () -> { + World world = getWorld(); + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + return BukkitAdapter.adapt(block.getBlockData()).toBaseBlock(); + }, "Failed to get full block asynchronously"); } @Override - public Set applySideEffects(BlockVector3 position, com.sk89q.worldedit.world.block.BlockState previousType, - SideEffectSet sideEffectSet) { + public Set applySideEffects(BlockVector3 position, + com.sk89q.worldedit.world.block.BlockState previousType, + SideEffectSet sideEffectSet) { + return executeOnRegion(position, () -> doApplySideEffects(position, previousType, sideEffectSet), + "Failed to apply side effects safely"); + } + + private Set doApplySideEffects(BlockVector3 position, + com.sk89q.worldedit.world.block.BlockState previousType, + SideEffectSet sideEffectSet) { if (worldNativeAccess != null) { worldNativeAccess.applySideEffects(position, previousType, sideEffectSet); return Sets.intersection( 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 4884934702..897a787e36 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 @@ -30,6 +30,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.FoliaScheduler; import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.event.platform.CommandSuggestionEvent; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; @@ -327,7 +328,7 @@ public void onDisable() { if (config != null) { config.unload(); } - this.getServer().getScheduler().cancelTasks(this); + cancelTasks(); } /** @@ -506,7 +507,7 @@ public WorldEdit getWorldEdit() { * @return an instance of the plugin * @throws NullPointerException if the plugin hasn't been enabled */ - static WorldEditPlugin getInstance() { + public static WorldEditPlugin getInstance() { return checkNotNull(INSTANCE); } @@ -575,4 +576,13 @@ public void onAsyncTabComplete(com.destroystokyo.paper.event.server.AsyncTabComp event.setHandled(true); } } + + private void cancelTasks() { + if (FoliaScheduler.isFolia()) { + FoliaScheduler.getAsyncScheduler().cancel(this); + FoliaScheduler.getGlobalRegionScheduler().cancel(this); + } else { + this.getServer().getScheduler().cancelTasks(this); + } + } } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/AsyncScheduler.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/AsyncScheduler.java new file mode 100644 index 0000000000..6e97a06a02 --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/AsyncScheduler.java @@ -0,0 +1,162 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing tasks asynchronously. + * + *

This implementation is adapted from the + * + * packetevents Folia scheduling utilities by retrooper. + * Modifications have been made for integration into WorldEdit's asynchronous task handling system + * and to align with its GPL licensing and internal code standards. + */ +public class AsyncScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.AsyncScheduler asyncScheduler; + + protected AsyncScheduler() { + if (FoliaScheduler.isFolia) { + asyncScheduler = Bukkit.getAsyncScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules the specified task to be executed asynchronously immediately. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runNow(@NotNull Plugin plugin, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskAsynchronously(plugin, () -> task.accept(null))); + } else { + return new TaskWrapper(asyncScheduler.runNow(plugin, (o) -> task.accept(null))); + } + } + + /** + * Schedules the specified task to be executed asynchronously after the specified delay. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param delay The time delay to pass before the task should be executed. + * @param timeUnit The time unit for the time delay. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, + long delay, @NotNull TimeUnit timeUnit) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLaterAsynchronously( + plugin, () -> task.accept(null), convertTimeToTicks(delay, timeUnit))); + } else { + return new TaskWrapper(asyncScheduler.runDelayed(plugin, (o) -> task.accept(null), delay, timeUnit)); + } + } + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, + * and then periodically executed with the specified period. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param delay The time delay to pass before the task should be executed. + * @param period The time period between each task execution. Any value less-than 1 is treated as 1. + * @param timeUnit The time unit for the initial delay and period. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, + long delay, long period, @NotNull TimeUnit timeUnit) { + if (period < 1) { + period = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimerAsynchronously( + plugin, () -> task.accept(null), + convertTimeToTicks(delay, timeUnit), convertTimeToTicks(period, timeUnit))); + } else { + return new TaskWrapper(asyncScheduler.runAtFixedRate( + plugin, (o) -> task.accept(null), delay, period, timeUnit)); + } + } + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, + * and then periodically executed. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param initialDelayTicks The time delay in ticks to pass before the task should be executed. + * @param periodTicks The time period in ticks between each task execution. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, + long initialDelayTicks, long periodTicks) { + if (periodTicks < 1) { + periodTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimerAsynchronously( + plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } else { + return new TaskWrapper(asyncScheduler.runAtFixedRate( + plugin, (o) -> task.accept(null), + initialDelayTicks * 50, periodTicks * 50, TimeUnit.MILLISECONDS)); + } + } + + /** + * Attempts to cancel all tasks scheduled by the specified plugin. + * + * @param plugin Specified plugin. + */ + public void cancel(@NotNull Plugin plugin) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.cancelTasks(plugin); + } else { + asyncScheduler.cancelTasks(plugin); + } + } + + /** + * Converts the specified time to ticks. + * + * @param time The time to convert. + * @param timeUnit The time unit of the time. + * @return The time converted to ticks. + */ + private long convertTimeToTicks(long time, TimeUnit timeUnit) { + return timeUnit.toMillis(time) / 50; + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/EntityScheduler.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/EntityScheduler.java new file mode 100644 index 0000000000..eaf4ec7d0d --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/EntityScheduler.java @@ -0,0 +1,177 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing entity tasks. + * + *

This implementation provides compatibility with both Folia and standard Bukkit scheduling, + * allowing tasks to be safely executed on entities within their owning regions. + * + *

Origin: This class is adapted from the + * + * packetevents Folia EntityScheduler implementation by retrooper. + * Modifications were made for WorldEdit's internal use and code consistency. + */ +public class EntityScheduler { + + private BukkitScheduler bukkitScheduler; + + protected EntityScheduler() { + if (!FoliaScheduler.isFolia) { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task with the given delay. + * + *

If the task failed to schedule because the scheduler is retired (entity removed), then + * returns false. Otherwise, either the run callback will be invoked after the specified delay, + * or the retired callback will be invoked if the scheduler is retired. Note that the retired + * callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + * + *

It is guaranteed that the run and retired callback are invoked on the region which owns + * the entity. + * + * @param plugin Plugin which owns the specified task. + * @param run The callback to run after the specified delay, may not be null. + * @param retired Retire callback to run if the entity is retired before the run callback can + * be invoked, may be null. + * @param delay The delay in ticks before the run callback is invoked. + */ + public void execute(@NotNull Entity entity, @NotNull Plugin plugin, @NotNull Runnable run, + @Nullable Runnable retired, long delay) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTaskLater(plugin, run, delay); + } else { + entity.getScheduler().execute(plugin, run, retired, delay); + } + } + + /** + * Schedules a task to execute on the next tick. + * + *

If the task failed to schedule because the scheduler is retired (entity removed), then + * returns null. Otherwise, either the task callback will be invoked after the specified delay, + * or the retired callback will be invoked if the scheduler is retired. Note that the retired + * callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + * + *

It is guaranteed that the task and retired callback are invoked on the region which owns + * the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run callback can + * be invoked, may be null. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Entity entity, @NotNull Plugin plugin, + @NotNull Consumer task, @Nullable Runnable retired) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTask(plugin, () -> task.accept(null))); + } else { + return new TaskWrapper(entity.getScheduler().run(plugin, (o) -> task.accept(null), retired)); + } + } + + /** + * Schedules a task with the given delay. + * + *

If the task failed to schedule because the scheduler is retired (entity removed), + * then returns null. Otherwise, either the task callback will be invoked after the specified + * delay, or the retired callback will be invoked if the scheduler is retired. + * + *

It is guaranteed that the task and retired callback are invoked on the region which owns + * the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run callback + * can be invoked, may be null. + * @param delayTicks The delay in ticks before the run callback is invoked. Any value less + * than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Entity entity, @NotNull Plugin plugin, + @NotNull Consumer task, @Nullable Runnable retired, + long delayTicks) { + if (delayTicks < 1) { + delayTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } else { + return new TaskWrapper(entity.getScheduler() + .runDelayed(plugin, (o) -> task.accept(null), retired, delayTicks)); + } + } + + /** + * Schedules a repeating task with the given delay and period. + * + *

If the task failed to schedule because the scheduler is retired (entity removed), + * then returns null. Otherwise, either the task callback will be invoked after the specified + * delay, or the retired callback will be invoked if the scheduler is retired. + * + *

It is guaranteed that the task and retired callback are invoked on the region which owns + * the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run + * callback can be invoked, may be null. + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any + * value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Entity entity, @NotNull Plugin plugin, + @NotNull Consumer task, @Nullable Runnable retired, + long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) { + initialDelayTicks = 1; + } + if (periodTicks < 1) { + periodTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimer(plugin, () -> task.accept(null), + initialDelayTicks, periodTicks)); + } else { + return new TaskWrapper(entity.getScheduler() + .runAtFixedRate(plugin, (o) -> task.accept(null), retired, + initialDelayTicks, periodTicks)); + } + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaScheduler.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaScheduler.java new file mode 100644 index 0000000000..d3153d160e --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaScheduler.java @@ -0,0 +1,150 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +/** + * Utility class to handle scheduling tasks. + * + *

This class uses Paper's threaded-region schedulers when running on Folia, + * otherwise it falls back to the standard Bukkit scheduler. + * + *

Note: This implementation is adapted from the + * + * packetevents Folia scheduling utilities by retrooper. + * Adjustments were made to fit WorldEdit’s initialization flow and scheduling abstractions. + */ +public class FoliaScheduler { + + static final boolean isFolia; + private static Class regionizedServerInitEventClass; + + private static AsyncScheduler asyncScheduler; + private static EntityScheduler entityScheduler; + private static GlobalRegionScheduler globalRegionScheduler; + private static RegionScheduler regionScheduler; + + static { + boolean folia; + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + folia = true; + + Class raw = Class.forName("io.papermc.paper.threadedregions.RegionizedServerInitEvent"); + if (!Event.class.isAssignableFrom(raw)) { + throw new ClassNotFoundException("RegionizedServerInitEvent does not extend Event"); + } + + @SuppressWarnings("unchecked") + Class eventClass = (Class) raw; + regionizedServerInitEventClass = eventClass; + } catch (ClassNotFoundException e) { + folia = false; + } + + isFolia = folia; + } + + /** + * Checks whether the server is running Folia. + * + * @return Whether the server is running Folia + */ + public static boolean isFolia() { + return isFolia; + } + + /** + * Returns the async scheduler. + * + * @return async scheduler instance of {@link AsyncScheduler} + */ + public static AsyncScheduler getAsyncScheduler() { + if (asyncScheduler == null) { + asyncScheduler = new AsyncScheduler(); + } + return asyncScheduler; + } + + /** + * Returns the entity scheduler. + * + * @return entity scheduler instance of {@link EntityScheduler} + */ + public static EntityScheduler getEntityScheduler() { + if (entityScheduler == null) { + entityScheduler = new EntityScheduler(); + } + return entityScheduler; + } + + /** + * Returns the global region scheduler. + * + * @return global region scheduler instance of {@link GlobalRegionScheduler} + */ + public static GlobalRegionScheduler getGlobalRegionScheduler() { + if (globalRegionScheduler == null) { + globalRegionScheduler = new GlobalRegionScheduler(); + } + return globalRegionScheduler; + } + + /** + * Returns the region scheduler. + * + * @return region scheduler instance of {@link RegionScheduler} + */ + public static RegionScheduler getRegionScheduler() { + if (regionScheduler == null) { + regionScheduler = new RegionScheduler(); + } + return regionScheduler; + } + + /** + * Run a task after the server has finished initializing. + * + *

Undefined behavior if called after the server has finished initializing. + * We still need to use reflection to get the server init event class, + * as this is only part of the Folia API. + * + * @param plugin The plugin owning this task + * @param run The task to run + */ + public static void runTaskOnInit(Plugin plugin, Runnable run) { + if (!isFolia) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, run); + } else { + Bukkit.getServer().getPluginManager().registerEvent( + regionizedServerInitEventClass, + new Listener() { }, + EventPriority.HIGHEST, + (listener, event) -> run.run(), + plugin + ); + } + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/GlobalRegionScheduler.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/GlobalRegionScheduler.java new file mode 100644 index 0000000000..8ed7d5fcb5 --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/GlobalRegionScheduler.java @@ -0,0 +1,138 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing global region tasks. + * + *

Note: This implementation is adapted from the + * + * packetevents Folia scheduling utilities by retrooper. + * Modifications were made for WorldEdit integration and improved task wrapping compatibility. + */ +public class GlobalRegionScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler globalRegionScheduler; + + protected GlobalRegionScheduler() { + if (FoliaScheduler.isFolia) { + globalRegionScheduler = Bukkit.getGlobalRegionScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task to be executed on the global region. + * + * @param plugin The plugin that owns the task + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTask(plugin, run); + } else { + globalRegionScheduler.execute(plugin, run); + } + } + + /** + * Schedules a task to be executed on the global region. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTask(plugin, () -> task.accept(null))); + } else { + return new TaskWrapper(globalRegionScheduler.run(plugin, (o) -> task.accept(null))); + } + } + + /** + * Schedules a task to be executed on the global region after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param delay The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, long delay) { + if (delay < 1) { + delay = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLater(plugin, () -> task.accept(null), delay)); + } else { + return new TaskWrapper(globalRegionScheduler.runDelayed(plugin, (o) -> task.accept(null), delay)); + } + } + + /** + * Schedules a repeating task to be executed on the global region after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, + long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) { + initialDelayTicks = 1; + } + if (periodTicks < 1) { + periodTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimer(plugin, () -> task.accept(null), + initialDelayTicks, periodTicks)); + } else { + return new TaskWrapper(globalRegionScheduler.runAtFixedRate( + plugin, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } + } + + /** + * Attempts to cancel all tasks scheduled by the specified plugin. + * + * @param plugin Specified plugin. + */ + public void cancel(@NotNull Plugin plugin) { + if (!FoliaScheduler.isFolia) { + Bukkit.getScheduler().cancelTasks(plugin); + } else { + globalRegionScheduler.cancelTasks(plugin); + } + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/RegionScheduler.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/RegionScheduler.java new file mode 100644 index 0000000000..d9e7eb07fe --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/RegionScheduler.java @@ -0,0 +1,221 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing region tasks. + * + *

Note: This implementation is adapted from the + * + * packetevents Folia scheduling utilities by retrooper. + * Adjustments were made for WorldEdit's region execution model and unified task wrapping. + */ +public class RegionScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.RegionScheduler regionScheduler; + + protected RegionScheduler() { + if (FoliaScheduler.isFolia) { + regionScheduler = Bukkit.getRegionScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task to be executed on the region which owns the location. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTask(plugin, run); + } else { + regionScheduler.execute(plugin, world, chunkX, chunkZ, run); + } + } + + /** + * Schedules a task to be executed on the region which owns the location. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull Location location, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + Bukkit.getScheduler().runTask(plugin, run); + } else { + regionScheduler.execute(plugin, location, run); + } + } + + /** + * Schedules a task to be executed on the region which owns the location on the next tick. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTask(plugin, () -> task.accept(null))); + } else { + return new TaskWrapper(regionScheduler.run(plugin, world, chunkX, chunkZ, (o) -> task.accept(null))); + } + } + + /** + * Schedules a task to be executed on the region which owns the location on the next tick. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTask(plugin, () -> task.accept(null))); + } else { + return new TaskWrapper(regionScheduler.run(plugin, location, (o) -> task.accept(null))); + } + } + + /** + * Schedules a task to be executed on the region which owns the location after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @param delayTicks The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, + @NotNull Consumer task, long delayTicks) { + if (delayTicks < 1) { + delayTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } else { + return new TaskWrapper(regionScheduler.runDelayed(plugin, world, chunkX, chunkZ, (o) -> task.accept(null), delayTicks)); + } + } + + /** + * Schedules a task to be executed on the region which owns the location after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @param delayTicks The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Location location, + @NotNull Consumer task, long delayTicks) { + if (delayTicks < 1) { + delayTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } else { + return new TaskWrapper(regionScheduler.runDelayed(plugin, location, (o) -> task.accept(null), delayTicks)); + } + } + + /** + * Schedules a repeating task to be executed on the region which owns the location after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, + @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) { + initialDelayTicks = 1; + } + if (periodTicks < 1) { + periodTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskTimer(plugin, () -> task.accept(null), + initialDelayTicks, periodTicks)); + } else { + return new TaskWrapper(regionScheduler.runAtFixedRate( + plugin, world, chunkX, chunkZ, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } + } + + /** + * Schedules a repeating task to be executed on the region which owns the location after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Location location, + @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) { + initialDelayTicks = 1; + } + if (periodTicks < 1) { + periodTicks = 1; + } + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskTimer(plugin, () -> task.accept(null), + initialDelayTicks, periodTicks)); + } else { + return new TaskWrapper(regionScheduler.runAtFixedRate( + plugin, location, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/TaskWrapper.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/TaskWrapper.java new file mode 100644 index 0000000000..4b159ad843 --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/TaskWrapper.java @@ -0,0 +1,101 @@ +/* + * 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 io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a wrapper around {@code BukkitTask} and Paper's {@code ScheduledTask}. + * This class provides a unified interface for interacting with both Bukkit's task scheduler + * and Paper's threaded-region task scheduler. + * + *

Note: This implementation is adapted from the + * + * packetevents Folia scheduling utilities by retrooper. + * It was refactored for WorldEdit’s API consistency and internal task abstraction layer. + */ +public class TaskWrapper { + + private BukkitTask bukkitTask; + private ScheduledTask scheduledTask; + + /** + * Constructs a new TaskWrapper around a BukkitTask. + * + * @param bukkitTask the BukkitTask to wrap + */ + public TaskWrapper(@NotNull BukkitTask bukkitTask) { + this.bukkitTask = bukkitTask; + } + + /** + * Constructs a new TaskWrapper around Paper's ScheduledTask. + * + * @param scheduledTask the ScheduledTask to wrap + */ + public TaskWrapper(@NotNull ScheduledTask scheduledTask) { + this.scheduledTask = scheduledTask; + } + + /** + * Retrieves the Plugin that owns this task. + * + * @return the owning {@link Plugin} + */ + public Plugin getOwner() { + return bukkitTask != null ? bukkitTask.getOwner() : scheduledTask.getOwningPlugin(); + } + + /** + * Checks if the task is canceled. + * + * @return true if the task is canceled, false otherwise + */ + public boolean isCancelled() { + return bukkitTask != null ? bukkitTask.isCancelled() : scheduledTask.isCancelled(); + } + + /** + * Cancels the task. If the task is running, it will be canceled. + */ + public void cancel() { + if (bukkitTask != null) { + bukkitTask.cancel(); + } else { + scheduledTask.cancel(); + } + } + + /** + * Gets the task ID for this task. + * + * @return the task ID + */ + public int getTaskId() { + if (bukkitTask != null) { + return bukkitTask.getTaskId(); + } else { + return scheduledTask.hashCode(); + } + } +} diff --git a/worldedit-bukkit/src/main/resources/plugin.yml b/worldedit-bukkit/src/main/resources/plugin.yml index 3f91920e37..c960f454ac 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.3 +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/java/com/sk89q/worldedit/command/UtilityCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java index 818770a769..7d3065e7a9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java @@ -23,7 +23,6 @@ import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.LocalSession; -import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.command.argument.HeightConverter; @@ -35,14 +34,13 @@ import com.sk89q.worldedit.command.util.PrintCommandHelp; import com.sk89q.worldedit.command.util.WorldEditAsyncCommandBuilder; import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.entity.metadata.EntitySchedulerFacet; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.function.EntityFunction; import com.sk89q.worldedit.function.mask.BlockTypeMask; import com.sk89q.worldedit.function.mask.ExistingBlockMask; import com.sk89q.worldedit.function.mask.Mask; -import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.pattern.Pattern; -import com.sk89q.worldedit.function.visitor.EntityVisitor; import com.sk89q.worldedit.internal.annotation.VertHeight; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; @@ -66,9 +64,10 @@ import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; import java.util.function.Supplier; import static com.sk89q.worldedit.command.util.Logging.LogMode.PLACEMENT; @@ -451,15 +450,12 @@ public int butcher(Actor actor, flags.or(CreatureButcher.Flags.ARMOR_STAND, killArmorStands, "worldedit.butcher.armorstands"); flags.or(CreatureButcher.Flags.WATER, killWater, "worldedit.butcher.water"); - int killed = killMatchingEntities(radius, actor, flags::createFunction); + Integer finalRadius = radius; - actor.printInfo(TranslatableComponent.of( - "worldedit.butcher.killed", - TextComponent.of(killed), - TextComponent.of(radius) - )); + killMatchingEntities(radius, actor, flags::createFunction, total -> + actor.printInfo(TranslatableComponent.of("worldedit.butcher.killed", TextComponent.of(total), TextComponent.of(finalRadius)))); - return killed; + return 0; } @Command( @@ -479,36 +475,58 @@ public int remove(Actor actor, return 0; } - int removed = killMatchingEntities(radius, actor, remover::createFunction); - actor.printInfo(TranslatableComponent.of("worldedit.remove.removed", TextComponent.of(removed))); - return removed; + killMatchingEntities(radius, actor, remover::createFunction, total -> + actor.printInfo(TranslatableComponent.of("worldedit.remove.removed", TextComponent.of(total)))); + return 0; } - private int killMatchingEntities(Integer radius, Actor actor, Supplier func) throws IncompleteRegionException, - MaxChangedBlocksException { - List visitors = new ArrayList<>(); - + private int killMatchingEntities(Integer radius, Actor actor, Supplier function, IntConsumer onDone) throws IncompleteRegionException { LocalSession session = we.getSessionManager().get(actor); BlockVector3 center = session.getPlacementPosition(actor); EditSession editSession = session.createEditSession(actor); - List entities; + final List entities; if (radius >= 0) { CylinderRegion region = CylinderRegion.createRadius(editSession, center, radius); entities = editSession.getEntities(region); } else { entities = editSession.getEntities(); } - visitors.add(new EntityVisitor(entities.iterator(), func.get())); - int killed = 0; - for (EntityVisitor visitor : visitors) { - Operations.completeLegacy(visitor); - killed += visitor.getAffected(); + final EntityFunction predicate = function.get(); + + final AtomicInteger remaining = new AtomicInteger(entities.size()); + final AtomicInteger killed = new AtomicInteger(0); + + for (Entity entity : entities) { + final EntitySchedulerFacet facet = entity.getFacet(EntitySchedulerFacet.class); + + Runnable work = () -> { + boolean affected = false; + try { + affected = predicate.apply(entity); + } catch (Throwable ignored) { + // This instance is not relevant + } + + if (affected) { + killed.incrementAndGet(); + } + + if (remaining.decrementAndGet() == 0) { + onDone.accept(killed.get()); + } + }; + + if (facet != null) { + facet.runOnEntityThread(work); + } else { + work.run(); + } } session.remember(editSession); editSession.close(); - return killed; + return 0; } private DecimalFormat formatForLocale(Locale locale) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/entity/metadata/EntitySchedulerFacet.java b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/metadata/EntitySchedulerFacet.java new file mode 100644 index 0000000000..7bcabf3a66 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/metadata/EntitySchedulerFacet.java @@ -0,0 +1,37 @@ +/* + * 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.entity.metadata; + +/** + * Facet that allows scheduling tasks on the owning region/thread of an entity. + * Implemented on platform adapters (e.g., Bukkit). + * + *

NOTE: The task will run asynchronously relative to the caller. Do not block + * the caller waiting for completion on region-threaded servers.

+ */ +public interface EntitySchedulerFacet { + + /** + * Schedule the given task to run on the entity's owning region/thread. + * The task SHOULD perform any thread-affine state reads and writes + * (e.g., getCustomName(), remove(), etc.). + */ + void runOnEntityThread(Runnable task); +} From c107fa849c9d152db050f25bf63e6a74dd084a7a Mon Sep 17 00:00:00 2001 From: RootBeer Date: Thu, 18 Dec 2025 22:41:42 -0500 Subject: [PATCH 2/5] Proper 1.21.11 Support --- .../impl/v1_21_11/PaperweightAdapter.java | 256 +++++++++++++++++- 1 file changed, 254 insertions(+), 2 deletions(-) diff --git a/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java index a36052ebbb..b61d4eb4bd 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Futures; import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; @@ -34,7 +33,9 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; import com.sk89q.worldedit.extent.Extent; @@ -709,6 +710,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + if (FoliaScheduler.isFolia()) { + return regenerateFolia(bukkitWorld, region, extent, options); + } + try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -718,6 +723,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen return true; } + private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + try { + return doRegenFolia(bukkitWorld, region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed on Folia.", e); + } + } + private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { Environment env = bukkitWorld.getEnvironment(); ChunkGenerator gen = bukkitWorld.getGenerator(); @@ -790,6 +803,245 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio } } + private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + Environment env = bukkitWorld.getEnvironment(); + ChunkGenerator gen = bukkitWorld.getGenerator(); + + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + ResourceKey worldDimKey = getWorldDimKey(env); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) { + ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + WorldOptions newOpts = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + LevelSettings newWorldSettings = new LevelSettings( + "worldeditregentempworld", + levelProperties.settings.gameType(), + levelProperties.settings.hardcore(), + levelProperties.settings.difficulty(), + levelProperties.settings.allowCommands(), + levelProperties.settings.gameRules(), + levelProperties.settings.getDataConfiguration() + ); + + @SuppressWarnings("deprecation") + PrimaryLevelData.SpecialWorldProperty specialWorldProperty = + levelProperties.isFlatWorld() + ? PrimaryLevelData.SpecialWorldProperty.FLAT + : levelProperties.isDebugWorld() + ? PrimaryLevelData.SpecialWorldProperty.DEBUG + : PrimaryLevelData.SpecialWorldProperty.NONE; + + PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); + + ServerLevel freshWorld = new ServerLevel( + originalWorld.getServer(), + originalWorld.getServer().executor, + session, newWorldData, + originalWorld.dimension(), + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + originalWorld.isDebug(), + seed, + ImmutableList.of(), + false, + originalWorld.getRandomSequences(), + env, + gen, + bukkitWorld.getBiomeProvider() + ); + + try { + ChunkPos spawnChunk = new ChunkPos( + freshWorld.getChunkSource().randomState().sampler().findSpawnPosition() + ); + + try { + Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection"); + randomSpawnField.setAccessible(true); + randomSpawnField.set(freshWorld, spawnChunk); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to set spawn chunk for Folia", e); + } + + MinecraftServer console = originalWorld.getServer(); + CompletableFuture initFuture = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().run( + WorldEditPlugin.getInstance(), + freshWorld.getWorld(), + spawnChunk.x, spawnChunk.z, + o -> { + try { + console.initWorld(freshWorld, newWorldData, newWorldData.worldGenOptions()); + initFuture.complete(null); + } catch (Exception e) { + initFuture.completeExceptionally(e); + } + } + ); + + initFuture.get(); + + regenForWorldFolia(region, extent, freshWorld, options); + } finally { + freshWorld.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException ignored) { + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + + return true; + } + + @SuppressWarnings("unchecked") + private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException { + Map> blockStates = new HashMap<>(); + Map biomes = new HashMap<>(); + Map> blocksByChunk = new HashMap<>(); + + for (BlockVector3 vec : region) { + ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4); + blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec); + } + + World bukkitWorld = serverWorld.getWorld(); + List> extractionFutures = new ArrayList<>(); + + for (Map.Entry> entry : blocksByChunk.entrySet()) { + ChunkPos chunkPos = entry.getKey(); + List blocks = entry.getValue(); + CompletableFuture future = new CompletableFuture<>(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkPos.x, + chunkPos.z, + () -> { + try { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); + } else if (chunkAccess != null) { + try { + for (BlockVector3 vec : blocks) { + BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z()); + final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos); + int internalId = Block.getId(blockData); + BlockStateHolder state = BlockStateIdAccess.getBlockStateById(internalId); + Objects.requireNonNull(state); + BlockEntity blockEntity = chunkAccess.getBlockEntity(pos); + if (blockEntity != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, serverWorld.registryAccess()); + blockEntity.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag))); + } + synchronized (blockStates) { + blockStates.put(vec, state); + } + if (options.shouldRegenBiomes()) { + Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + BiomeType adaptedBiome = adapt(serverWorld, origBiome); + if (adaptedBiome != null) { + synchronized (biomes) { + biomes.put(vec, adaptedBiome); + } + } + } + } + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + } else { + future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed.")); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + extractionFutures.add(future); + } + + CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture[0])).join(); + + for (BlockVector3 vec : region) { + BlockStateHolder state = blockStates.get(vec); + if (state != null) { + extent.setBlock(vec, state.toBaseBlock()); + if (options.shouldRegenBiomes()) { + BiomeType biome = biomes.get(vec); + if (biome != null) { + extent.setBiome(vec, biome); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { + ServerChunkCache chunkManager = serverWorld.getChunkSource(); + List> chunkLoadings = new ArrayList<>(); + World bukkitWorld = serverWorld.getWorld(); + + for (BlockVector2 chunk : region.getChunks()) { + CompletableFuture future = new CompletableFuture<>(); + final int chunkX = chunk.x(); + final int chunkZ = chunk.z(); + + FoliaScheduler.getRegionScheduler().execute( + WorldEditPlugin.getInstance(), + bukkitWorld, + chunkX, + chunkZ, + () -> { + try { + CompletableFuture> chunkFuture = + ((CompletableFuture>) + getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); + chunkFuture.thenApply(either -> either.orElse(null)) + .whenComplete((chunkAccess, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } else { + future.complete(chunkAccess); + } + }); + } catch (IllegalAccessException | InvocationTargetException e) { + future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e)); + } + } + ); + chunkLoadings.add(future); + } + return chunkLoadings; + } + private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) { Identifier key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome); if (key == null) { @@ -810,7 +1062,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld executor.managedBlock(() -> { // bail out early if a future fails if (chunkLoadings.stream().anyMatch(ftr -> - ftr.isDone() && Futures.getUnchecked(ftr) == null + ftr.isDone() && ftr.getNow(null) == null )) { return false; } From 98d08c72537edcd89d60fccbc58665dd2bd38d4f Mon Sep 17 00:00:00 2001 From: RootBeer Date: Sun, 4 Jan 2026 02:12:08 -0500 Subject: [PATCH 3/5] Rebase --- .../bukkit/BukkitBlockCommandSender.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 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 60da05f811..05701e5ecc 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 @@ -62,7 +62,7 @@ public String getName() { @Override @Deprecated public void printRaw(String msg) { - for (String part : msg.split("\n")) { + for (String part : msg.split("\n", 0)) { sender.sendMessage(part); } } @@ -70,7 +70,7 @@ public void printRaw(String msg) { @Override @Deprecated public void print(String msg) { - for (String part : msg.split("\n")) { + for (String part : msg.split("\n", 0)) { print(TextComponent.of(part, TextColor.LIGHT_PURPLE)); } } @@ -78,7 +78,7 @@ public void print(String msg) { @Override @Deprecated public void printDebug(String msg) { - for (String part : msg.split("\n")) { + for (String part : msg.split("\n", 0)) { print(TextComponent.of(part, TextColor.GRAY)); } } @@ -86,7 +86,7 @@ public void printDebug(String msg) { @Override @Deprecated public void printError(String msg) { - for (String part : msg.split("\n")) { + for (String part : msg.split("\n", 0)) { print(TextComponent.of(part, TextColor.RED)); } } @@ -157,8 +157,15 @@ public boolean isActive() { updateActive(); } else { // we should update it eventually or on the owning region thread + // Suppress FutureReturnValueIgnored: We handle it in the block. FoliaScheduler.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), - this::updateActive); + () -> { + try { + updateActive(); + } catch (Throwable t) { + WorldEdit.logger.warn("Exception while updating command block sender active state", t); + } + }); } return active; } From 2a9e0af7a4ff6c5a4770174ee5e7eca1cec38b08 Mon Sep 17 00:00:00 2001 From: RootBeer Date: Sun, 4 Jan 2026 02:38:01 -0500 Subject: [PATCH 4/5] Resolve compilation issues --- .../bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java | 9 ++++++--- .../bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java | 9 ++++++--- .../bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java | 9 ++++++--- .../bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java | 9 ++++++--- .../bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java | 9 ++++++--- .../bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java | 9 ++++++--- .../java/com/sk89q/worldedit/bukkit/BukkitEntity.java | 5 ++++- .../java/com/sk89q/worldedit/bukkit/BukkitPlayer.java | 6 ++++-- .../java/com/sk89q/worldedit/bukkit/BukkitWorld.java | 3 +-- 9 files changed, 45 insertions(+), 23 deletions(-) diff --git a/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java index 738c7884b2..673b8d5253 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_11/PaperweightAdapter.java @@ -905,6 +905,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -942,7 +943,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -1006,7 +1008,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1027,7 +1029,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); diff --git a/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java index 971d1fd2e9..8d8d943ff4 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java @@ -897,6 +897,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -934,7 +935,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -996,7 +998,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1017,7 +1019,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); diff --git a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java index ccb519b5ff..7f41796a0b 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java @@ -897,6 +897,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -934,7 +935,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -996,7 +998,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1017,7 +1019,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); diff --git a/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java index 3bad2e618e..6ce7230ed8 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_5/PaperweightAdapter.java @@ -895,6 +895,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -932,7 +933,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -994,7 +996,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1015,7 +1017,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); diff --git a/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java index 292e5dfc93..345b25b084 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_6/PaperweightAdapter.java @@ -908,6 +908,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -945,7 +946,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -1009,7 +1011,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1030,7 +1032,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); diff --git a/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java index dcd9f51ad8..af25cb6e2c 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_9/PaperweightAdapter.java @@ -905,6 +905,7 @@ private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, Re Map map = (Map) serverWorldsField.get(Bukkit.getServer()); map.remove("worldeditregentempworld"); } catch (IllegalAccessException ignored) { + // It's fine if we couldn't remove it } SafeFiles.tryHardToDeleteDir(tempDir); } @@ -942,7 +943,8 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable)); @@ -1006,7 +1008,7 @@ private void regenForWorldFolia(Region region, Extent extent, ServerLevel server } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) private List> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) { ServerChunkCache chunkManager = serverWorld.getChunkSource(); List> chunkLoadings = new ArrayList<>(); @@ -1027,7 +1029,8 @@ private List> submitChunkLoadTasksFolia(Region re CompletableFuture> chunkFuture = ((CompletableFuture>) getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true)); - chunkFuture.thenApply(either -> either.orElse(null)) + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = chunkFuture.thenApply(either -> either.orElse(null)) .whenComplete((chunkAccess, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); 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 3f0d3e3065..78122850cb 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 @@ -83,7 +83,8 @@ public boolean setLocation(Location location) { } if (PaperLib.isPaper()) { - FoliaScheduler.getEntityScheduler().run( + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var unused = FoliaScheduler.getEntityScheduler().run( entity, WorldEditPlugin.getInstance(), o -> entity.teleportAsync(BukkitAdapter.adapt(location)), @@ -118,6 +119,7 @@ public BaseEntity getState() { return adapter.getEntity(entity); } } catch (Throwable ignored) { + // It's fine if we couldn't remove it } CompletableFuture future = new CompletableFuture<>(); @@ -203,6 +205,7 @@ public T getFacet(Class cls) { try { task.run(); } catch (Throwable ignored) { + // It's fine if we couldn't remove it } }, null diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java index b9aca5e4e4..9bd5edd2cc 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java @@ -149,7 +149,8 @@ public void print(Component component) { @Override public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { if (PaperLib.isPaper()) { - FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var unused = FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), o -> player.teleportAsync(new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch)), null); return true; } @@ -232,7 +233,8 @@ public com.sk89q.worldedit.util.Location getLocation() { @Override public boolean setLocation(com.sk89q.worldedit.util.Location location) { if (PaperLib.isPaper()) { - FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var unused = FoliaScheduler.getEntityScheduler().run(player, WorldEditPlugin.getInstance(), o -> player.teleportAsync(BukkitAdapter.adapt(location)), null); return true; } 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 53ef569914..f07fa3be76 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 @@ -320,6 +320,7 @@ public void dropItem(Vector3 pt, BaseItemStack item) { } @Override + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) public void checkLoadedChunk(BlockVector3 pt) { World world = getWorld(); executeOnRegionVoid(pt, () -> { @@ -475,8 +476,6 @@ public boolean generateStructure(StructureType type, EditSession editSession, Bl return false; } - private static final boolean hasWarnedImplError = false; - @Override public com.sk89q.worldedit.world.block.BlockState getBlock(BlockVector3 position) { return executeOnRegion(position, () -> { From 616be3c59ebae26ceb0e09be0e22684fae986a7f Mon Sep 17 00:00:00 2001 From: RootBeer Date: Wed, 7 Jan 2026 14:14:06 -0500 Subject: [PATCH 5/5] Rebase --- worldedit-bukkit/src/main/resources/plugin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worldedit-bukkit/src/main/resources/plugin.yml b/worldedit-bukkit/src/main/resources/plugin.yml index c960f454ac..28cce7beaf 100644 --- a/worldedit-bukkit/src/main/resources/plugin.yml +++ b/worldedit-bukkit/src/main/resources/plugin.yml @@ -2,7 +2,7 @@ name: WorldEdit main: com.sk89q.worldedit.bukkit.WorldEditPlugin version: "${internalVersion}" load: STARTUP -api-version: 1.21.3 +api-version: 1.21.4 folia-supported: true softdepend: [Vault] author: EngineHub