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 a1e0d13cb7..7ed4346505 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -152,6 +153,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -180,6 +182,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -198,6 +201,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -775,13 +779,40 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, 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)) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() .getWorldData().overworldData(); @@ -830,20 +861,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio gen, bukkitWorld.getBiomeProvider() ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -873,6 +898,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -902,7 +932,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1309,4 +1339,38 @@ public void tick() { } } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + 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); + } + } + } } 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 a152c8c924..9aae70a86f 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -150,6 +151,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -178,6 +180,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -196,6 +199,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -755,13 +759,40 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, 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)) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() .getWorldData().overworldData(); @@ -811,20 +842,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio gen, bukkitWorld.getBiomeProvider() ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -854,6 +879,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -876,7 +906,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1275,4 +1305,38 @@ public void stop() { } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + 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); + } + } + } } 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 4012e7addf..4cdf770679 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -150,6 +151,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -178,6 +180,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -196,6 +199,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -753,13 +757,40 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, 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)) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() .getWorldData().overworldData(); @@ -809,20 +840,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio gen, bukkitWorld.getBiomeProvider() ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -852,6 +877,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -874,7 +904,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1300,4 +1330,38 @@ public void stop() { } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + 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); + } + } + } } 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 7d6c0d2052..2b117bb77f 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -153,6 +154,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -181,6 +183,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -199,6 +202,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -775,13 +779,40 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, 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)) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() .getWorldData().overworldData(); @@ -831,20 +862,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio gen, bukkitWorld.getBiomeProvider() ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -874,6 +899,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -903,7 +933,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1329,4 +1359,38 @@ public void stop() { } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + 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); + } + } + } } 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 4bc0e6f733..b70b0970fa 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -152,6 +153,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -180,6 +182,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -198,6 +201,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -774,13 +778,40 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, 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)) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() .getWorldData().overworldData(); @@ -829,20 +860,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio gen, bukkitWorld.getBiomeProvider() ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -872,6 +897,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -901,7 +931,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1308,4 +1338,38 @@ public void tick() { } } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + 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); + } + } + } } diff --git a/worldedit-bukkit/adapters/adapter-26.1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_1/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-26.1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_1/PaperweightAdapter.java index 1c5a24e403..8af605e168 100644 --- a/worldedit-bukkit/adapters/adapter-26.1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_1/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-26.1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_1/PaperweightAdapter.java @@ -33,6 +33,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -152,6 +153,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -181,6 +183,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -199,6 +202,7 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -783,10 +787,6 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { - if (options.getSeed().isPresent()) { - throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); - } - try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -797,13 +797,44 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, RegenOptions options) throws Exception { + if (options.getSeed().isPresent()) { + throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); + } + 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")) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld"); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PaperWorldLoader.LoadedWorldData loadedWorldData = new PaperWorldLoader.LoadedWorldData( @@ -834,20 +865,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio new SavedDataStorage(session.getDimensionPath(originalWorld.dimension()), originalWorld.getServer().getFixerUpper(), originalWorld.registryAccess()), loadedWorldData ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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 _) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -877,6 +902,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -906,7 +936,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1313,4 +1343,38 @@ public void tick() { } } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException _) { + // It's fine if we couldn't remove it + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + } + } } diff --git a/worldedit-bukkit/adapters/adapter-26.2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_2/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-26.2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_2/PaperweightAdapter.java index 42a9d0f3ff..57da8a28eb 100644 --- a/worldedit-bukkit/adapters/adapter-26.2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_2/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-26.2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_2/PaperweightAdapter.java @@ -33,6 +33,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -152,6 +153,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -181,6 +183,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -200,6 +203,7 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -788,10 +792,6 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { - if (options.getSeed().isPresent()) { - throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); - } - try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -802,13 +802,44 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, RegenOptions options) throws Exception { + if (options.getSeed().isPresent()) { + throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); + } + 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")) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld"); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PaperWorldLoader.LoadedWorldData loadedWorldData = new PaperWorldLoader.LoadedWorldData( @@ -839,20 +870,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio new SavedDataStorage(session.getDimensionPath(originalWorld.dimension()), originalWorld.getServer().getFixerUpper(), originalWorld.registryAccess()), loadedWorldData ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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 _) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -882,6 +907,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -911,7 +941,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1318,4 +1348,38 @@ public void tick() { } } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException _) { + // It's fine if we couldn't remove it + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + } + } } diff --git a/worldedit-bukkit/adapters/adapter-26.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_3/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-26.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_3/PaperweightAdapter.java index 0c3c26ff62..534710ff3a 100644 --- a/worldedit-bukkit/adapters/adapter-26.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_3/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-26.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v26_3/PaperweightAdapter.java @@ -33,6 +33,7 @@ import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.adapter.AsyncRegeneration; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Watchdog; @@ -154,6 +155,7 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -183,6 +185,7 @@ import org.spigotmc.SpigotConfig; import org.spigotmc.WatchdogThread; +import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -202,6 +205,7 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -808,10 +812,6 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt @Override public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) { - if (options.getSeed().isPresent()) { - throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); - } - try { doRegen(bukkitWorld, region, extent, options); } catch (Exception e) { @@ -822,13 +822,44 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen } private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception { + try (TemporaryWorld temporaryWorld = createTemporaryWorld(bukkitWorld, options)) { + regenForWorld(region, extent, temporaryWorld.level, options); + } + } + + @Override + public CompletionStage regenerateAsync(World bukkitWorld, Region region, Extent extent, RegenOptions options) { + TemporaryWorld temporaryWorld; + try { + temporaryWorld = createTemporaryWorld(bukkitWorld, options); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + ServerLevel freshWorld = temporaryWorld.level; + // Folia disables the vanilla chunk futures, so the temporary world is loaded through the Bukkit API. + World freshBukkitWorld = freshWorld.getWorld(); + return AsyncRegeneration.run( + region, + chunk -> freshBukkitWorld.getChunkAtAsync(chunk.x(), chunk.z(), true) + .thenApply(loaded -> loaded == null ? null : ((CraftChunk) loaded).getHandle(ChunkStatus.FULL)), + chunks -> copyChunks(region, extent, freshWorld, options, chunks), + temporaryWorld + ); + } + + private TemporaryWorld createTemporaryWorld(World bukkitWorld, RegenOptions options) throws Exception { + if (options.getSeed().isPresent()) { + throw new UnsupportedOperationException("26.1+ worldgen does not support overriding the seed for regen"); + } + 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")) { + LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld"); + try { ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle(); PaperWorldLoader.LoadedWorldData loadedWorldData = new PaperWorldLoader.LoadedWorldData( @@ -859,20 +890,14 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio new SavedDataStorage(session.getDimensionPath(originalWorld.dimension()), originalWorld.getServer().getFixerUpper(), originalWorld.registryAccess()), loadedWorldData ); + return new TemporaryWorld(freshWorld, session, tempDir); + } catch (Exception e) { try { - regenForWorld(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 _) { - // It's fine if we couldn't remove it + new TemporaryWorld(null, session, tempDir).close(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } - SafeFiles.tryHardToDeleteDir(tempDir); + throw e; } } @@ -902,6 +927,11 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } return chunkLoadings.stream().allMatch(CompletableFuture::isDone); }); + copyChunks(region, extent, serverWorld, options, chunkLoadings); + } + + private void copyChunks(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options, + List> chunkLoadings) throws WorldEditException { Map chunks = new HashMap<>(); for (CompletableFuture future : chunkLoadings) { @Nullable @@ -931,7 +961,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld } extent.setBlock(vec, state.toBaseBlock()); if (options.shouldRegenBiomes()) { - Biome origBiome = chunk.getNoiseBiome(vec.x(), vec.y(), vec.z()).value(); + Biome origBiome = chunk.getNoiseBiome(vec.x() >> 2, vec.y() >> 2, vec.z() >> 2).value(); BiomeType adaptedBiome = adapt(serverWorld, origBiome); if (adaptedBiome != null) { extent.setBiome(vec, adaptedBiome); @@ -1341,4 +1371,38 @@ public void tick() { } } } + + /** + * A world that exists only to generate chunks in; closing it releases everything it uses. + */ + private final class TemporaryWorld implements AutoCloseable { + @Nullable + private final ServerLevel level; + private final LevelStorageSource.LevelStorageAccess session; + private final Path tempDir; + + private TemporaryWorld(@Nullable ServerLevel level, LevelStorageSource.LevelStorageAccess session, Path tempDir) { + this.level = level; + this.session = session; + this.tempDir = tempDir; + } + + @Override + public void close() throws IOException { + try (session) { + if (level != null) { + level.getChunkSource().close(false); + } + } finally { + try { + @SuppressWarnings("unchecked") + Map map = (Map) serverWorldsField.get(Bukkit.getServer()); + map.remove("worldeditregentempworld"); + } catch (IllegalAccessException _) { + // It's fine if we couldn't remove it + } + SafeFiles.tryHardToDeleteDir(tempDir); + } + } + } } 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 3facb13992..b10ad6450b 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,7 +28,9 @@ 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.FoliaRegeneration; import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.internal.util.LogManagerCompat; @@ -74,6 +76,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nullable; @@ -215,6 +218,17 @@ public boolean regenerate(Region region, Extent extent, RegenOptions options) { } } + @Override + public CompletionStage regenerateAsync(Region region, Extent extent, RegenOptions options, + @Nullable Actor actor) { + WorldEditPlugin plugin = WorldEditPlugin.getInstance(); + BukkitImplAdapter adapter = plugin.getBukkitImplAdapter(); + if (!plugin.isFolia() || adapter == null) { + return super.regenerateAsync(region, extent, options, actor); + } + return FoliaRegeneration.regenerate(plugin, adapter, this, region, extent, options, actor); + } + @Override public boolean clearContainerBlockContents(BlockVector3 pt) { checkNotNull(pt); diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/AsyncRegeneration.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/AsyncRegeneration.java new file mode 100644 index 0000000000..1f05ff0389 --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/AsyncRegeneration.java @@ -0,0 +1,125 @@ +/* + * 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.adapter; + +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.regions.Region; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * Drives {@link BukkitImplAdapter#regenerateAsync} for adapters that generate in a temporary world. + * + *

Chunk futures complete on the temporary world's own region threads. Once all of them are done + * the result is copied, then the temporary world is released on the global region thread: a tick + * thread, as closing a world requires, that is never inside one of that world's own chunk tasks.

+ */ +public final class AsyncRegeneration { + + private AsyncRegeneration() { + } + + /** + * Copies generated chunks into the output extent. + * + * @param the adapter's chunk type + */ + @FunctionalInterface + public interface ChunkCopier { + + /** + * Copy the generated chunks into the output extent. + * + * @param chunks the chunk futures, all of which are complete + * @throws WorldEditException if writing to the extent fails + */ + void copy(List> chunks) throws WorldEditException; + } + + /** + * Generate every chunk of the region, copy the result and release the temporary world. + * + * @param region the region to generate + * @param loadChunk schedules generation of one chunk; the future completes with null on failure + * @param copier copies the generated chunks into the output extent + * @param temporaryWorld released on the global region thread once copying is done + * @param the adapter's chunk type + * @return a future that completes after the temporary world has been released + */ + public static CompletableFuture run(Region region, + Function> loadChunk, + ChunkCopier copier, + AutoCloseable temporaryWorld) { + List> chunks = new ArrayList<>(); + for (BlockVector2 chunk : region.getChunks()) { + try { + chunks.add(loadChunk.apply(chunk)); + } catch (RuntimeException e) { + // Still wait for the chunks already scheduled before releasing the world. + chunks.add(CompletableFuture.failedFuture(e)); + break; + } + } + return CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new)) + .handle((_, error) -> { + if (error != null) { + return error; + } + try { + copier.copy(chunks); + return null; + } catch (Throwable t) { + return t; + } + }) + .thenCompose(failure -> release(temporaryWorld, failure)); + } + + private static CompletableFuture release(AutoCloseable temporaryWorld, @Nullable Throwable failure) { + CompletableFuture result = new CompletableFuture<>(); + // Closing a world requires a tick thread; the global one is also never inside this world's chunk tasks. + Bukkit.getGlobalRegionScheduler().execute(JavaPlugin.getPlugin(WorldEditPlugin.class), () -> { + Throwable finalFailure = failure; + try { + temporaryWorld.close(); + } catch (Exception e) { + if (finalFailure == null) { + finalFailure = e; + } else { + finalFailure.addSuppressed(e); + } + } + if (finalFailure == null) { + result.complete(null); + } else { + result.completeExceptionally(finalFailure); + } + }); + return result; + } +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java index caa8ad3a26..2af5b84cf0 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java @@ -58,6 +58,7 @@ import java.util.Map; import java.util.OptionalInt; import java.util.Set; +import java.util.concurrent.CompletionStage; import javax.annotation.Nullable; /** @@ -281,6 +282,24 @@ default boolean regenerate(World world, Region region, Extent extent, RegenOptio throw new UnsupportedOperationException("This adapter does not support regeneration."); } + /** + * Regenerate a region into the given extent without blocking the calling thread. + * + *

Used on Folia, where chunk generation cannot be awaited on a region thread. The extent is + * written on an arbitrary tick thread once every chunk has generated, so it must not depend on + * a particular thread. Temporary generation resources are released before the returned future + * completes.

+ * + * @param world the world to regen in + * @param region the region to regen + * @param extent the extent to use for setting blocks + * @param options the regeneration options + * @return a future that completes once the extent has been written + */ + default CompletionStage regenerateAsync(World world, Region region, Extent extent, RegenOptions options) { + throw new UnsupportedOperationException("This adapter does not support asynchronous regeneration."); + } + /** * Clears the contents of a Clearable block. * diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaRegeneration.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaRegeneration.java new file mode 100644 index 0000000000..64c52d0d9a --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/folia/FoliaRegeneration.java @@ -0,0 +1,258 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.bukkit.folia; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.bukkit.BukkitPlayer; +import com.sk89q.worldedit.bukkit.BukkitWorld; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.function.operation.Operations; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.regions.RegionOperationException; +import com.sk89q.worldedit.session.request.Request; +import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; +import com.sk89q.worldedit.world.RegenOptions; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; + +/** + * Runs {@code //regen} on Folia, where terrain generation cannot be awaited on a region thread. + * + *

The adapter generates the terrain into a detached snapshot. The chunks of the selection are then + * loaded and held with plugin tickets, the snapshot is written to the extent on the thread owning them, + * and the returned future completes on the actor's own scheduler. A clipboard is written directly by + * the adapter, since it never touches the world.

+ * + *

Temporary worlds are created and released on the global region thread, which also serialises + * the changes they make to the server's world map, and only one of them exists at a time. Applying a + * finished result may overlap with the next generation.

+ */ +public final class FoliaRegeneration { + + /** + * Freshly loaded chunks can take a few ticks to merge into the region applying the result. + */ + private static final int OWNERSHIP_ATTEMPTS = 40; + + /** + * Whether a temporary world currently exists. Only changed on the global region thread. + */ + private static volatile boolean generating; + + private FoliaRegeneration() { + } + + /** + * Regenerate the region without blocking the calling region thread. + * + * @param plugin the plugin to schedule with + * @param adapter the adapter generating the terrain + * @param world the world to regenerate in + * @param region the region to regenerate + * @param extent the extent to write the result to + * @param options the regeneration options + * @param actor the actor that requested the regeneration, or null for an API call without one + * @return a future completing with {@code true} on the actor's scheduler, or exceptionally on failure + */ + public static CompletionStage regenerate(WorldEditPlugin plugin, BukkitImplAdapter adapter, + BukkitWorld world, Region region, Extent extent, + RegenOptions options, @Nullable Actor actor) { + CompletableFuture result = new CompletableFuture<>(); + if (extent instanceof EditSession editSession && editSession.getBlockBag() != null) { + // The block bag reads the player's inventory, which may be in another region by then. + result.completeExceptionally( + new RegionOperationException(TranslatableComponent.of("worldedit.regen.inventory-unsupported"))); + return result; + } + boolean direct = extent instanceof Clipboard; + BlockArrayClipboard snapshot = direct ? null : new BlockArrayClipboard(region); + Extent generated = direct ? extent : snapshot; + Bukkit.getGlobalRegionScheduler().execute(plugin, () -> { + if (generating) { + complete(plugin, actor, result, + new RegionOperationException(TranslatableComponent.of("worldedit.regen.busy"))); + return; + } + generating = true; + CompletionStage generation; + try { + generation = adapter.regenerateAsync(world.getWorld(), region, generated, options); + } catch (Exception e) { + generation = CompletableFuture.failedFuture(e); + } + generation.whenComplete((_, error) -> { + // The adapter completes this on the global region thread, after releasing its temporary world. + generating = false; + if (error != null || direct) { + complete(plugin, actor, result, error); + return; + } + Job job = new Job(plugin, world, region, snapshot, extent, options, actor, result, + ConcurrentHashMap.newKeySet()); + loadChunks(job).whenComplete((_, loadError) -> { + if (loadError != null) { + finish(job, loadError); + } else { + applyWhenOwned(job, 0); + } + }); + }); + }); + return result; + } + + private static CompletionStage loadChunks(Job job) { + World world = job.world().getWorld(); + List> loads = new ArrayList<>(); + for (BlockVector2 chunk : job.region().getChunks()) { + loads.add(world.getChunkAtAsync(chunk.x(), chunk.z()).thenCompose(_ -> holdChunk(job, chunk))); + } + return CompletableFuture.allOf(loads.toArray(CompletableFuture[]::new)); + } + + private static CompletableFuture holdChunk(Job job, BlockVector2 chunk) { + World world = job.world().getWorld(); + CompletableFuture held = new CompletableFuture<>(); + // A plugin ticket loads the chunk synchronously if needed, so it is added on the thread owning the chunk. + Bukkit.getRegionScheduler().execute(job.plugin(), world, chunk.x(), chunk.z(), () -> { + try { + if (world.addPluginChunkTicket(chunk.x(), chunk.z(), job.plugin())) { + job.tickets().add(chunk); + } + held.complete(null); + } catch (Throwable t) { + held.completeExceptionally(t); + } + }); + return held; + } + + private static void applyWhenOwned(Job job, int attempt) { + World world = job.world().getWorld(); + BlockVector3 anchor = job.region().getMinimumPoint(); + Bukkit.getRegionScheduler().runDelayed(job.plugin(), world, anchor.x() >> 4, anchor.z() >> 4, _ -> { + if (!isOwned(world, job.region())) { + if (attempt < OWNERSHIP_ATTEMPTS) { + applyWhenOwned(job, attempt + 1); + } else { + finish(job, new RegionOperationException(TranslatableComponent.of("worldedit.regen.folia-region"))); + } + return; + } + Throwable failure = null; + try { + apply(job); + } catch (Exception e) { + failure = e; + } + finish(job, failure); + }, 1); + } + + private static boolean isOwned(World world, Region region) { + for (BlockVector2 chunk : region.getChunks()) { + if (!Bukkit.isOwnedByCurrentRegion(world, chunk.x(), chunk.z())) { + return false; + } + } + return true; + } + + private static void apply(Job job) throws WorldEditException { + Extent extent = job.extent(); + try { + // Masks parsed without an extent, such as the global mask, read it from the current request. + Request.runWithRequest(() -> { + Request.request().setWorld(job.world()); + if (extent instanceof EditSession editSession) { + Request.request().setEditSession(editSession); + } + try { + for (BlockVector3 position : job.region()) { + extent.setBlock(position, job.snapshot().getFullBlock(position)); + if (job.options().shouldRegenBiomes()) { + extent.setBiome(position, job.snapshot().getBiome(position)); + } + } + } catch (WorldEditException e) { + throw new CompletionException(e); + } finally { + // Flush here, as the thread the command returns to may not write to this region. + Operations.completeBlindly(extent.commit()); + } + }); + } catch (CompletionException e) { + if (e.getCause() instanceof WorldEditException cause) { + throw cause; + } + throw e; + } + } + + private static void finish(Job job, @Nullable Throwable failure) { + World world = job.world().getWorld(); + for (BlockVector2 chunk : job.tickets()) { + world.removePluginChunkTicket(chunk.x(), chunk.z(), job.plugin()); + } + complete(job.plugin(), job.actor(), job.result(), failure); + } + + private static void complete(WorldEditPlugin plugin, @Nullable Actor actor, CompletableFuture result, + @Nullable Throwable failure) { + Runnable completion = () -> { + if (failure == null) { + result.complete(true); + } else { + result.completeExceptionally(failure); + } + }; + if (actor instanceof BukkitPlayer player) { + // If the player is already gone, complete right here so that history is still recorded. + if (!player.getPlayer().getScheduler().execute(plugin, completion, completion, 1)) { + completion.run(); + } + } else { + // Other actors and API callers continue on the thread that finished the job. + completion.run(); + } + } + + private record Job(WorldEditPlugin plugin, BukkitWorld world, Region region, BlockArrayClipboard snapshot, + Extent extent, RegenOptions options, @Nullable Actor actor, + CompletableFuture result, Set tickets) { + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java index e258169d62..2690bb2c69 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java @@ -52,6 +52,7 @@ import com.sk89q.worldedit.internal.annotation.Offset; import com.sk89q.worldedit.internal.annotation.Selection; import com.sk89q.worldedit.internal.expression.ExpressionException; +import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.internal.util.TransformUtil; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; @@ -74,6 +75,7 @@ import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.generation.TreeType; +import org.apache.logging.log4j.Logger; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; @@ -82,6 +84,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; import static com.sk89q.worldedit.command.util.Logging.LogMode.ALL; import static com.sk89q.worldedit.command.util.Logging.LogMode.ORIENTATION_REGION; @@ -97,6 +102,8 @@ @CommandContainer(superTypes = CommandPermissionsConditionGenerator.Registration.class) public class RegionCommands { + private static final Logger LOGGER = LogManagerCompat.getLogger(); + /** * Create a new instance. */ @@ -450,39 +457,69 @@ public int stack(Actor actor, World world, EditSession editSession, LocalSession ) @CommandPermissions("worldedit.regen") @Logging(REGION) - void regenerate(Actor actor, World world, LocalSession session, EditSession editSession, - @Selection Region region, + void regenerate(Actor actor, World world, LocalSession session, + @Selection Region selection, @Arg(desc = "The seed to regenerate with, otherwise uses world seed", def = "") Long seed, @Switch(name = 'b', desc = "Regenerate biomes as well") boolean regenBiomes, @Switch(name = 'c', desc = "Regenerate to the clipboard") boolean toClipboard) throws WorldEditException { + // Regeneration may complete after this command has returned, so it gets its own edit session. + Region region = selection.clone(); + RegenOptions options = RegenOptions.builder() + .seed(seed) + .regenBiomes(regenBiomes) + .build(); + BlockArrayClipboard clipboard = toClipboard ? new BlockArrayClipboard(region) : null; + EditSession editSession = toClipboard ? null : session.createEditSession(actor); + if (clipboard != null) { + clipboard.setOrigin(session.getPlacementPosition(actor)); + } else { + editSession.enableStandardMode(); + } + Extent outputExtent = clipboard != null ? clipboard : editSession; Mask mask = session.getMask(); - boolean success; + CompletionStage result; try { session.setMask(null); - RegenOptions options = RegenOptions.builder() - .seed(seed) - .regenBiomes(regenBiomes) - .build(); - Extent outputExtent = editSession; - BlockArrayClipboard clipboard = null; - if (toClipboard) { - clipboard = new BlockArrayClipboard(region); - clipboard.setOrigin(session.getPlacementPosition(actor)); - outputExtent = clipboard; - } - success = world.regenerate(region, outputExtent, options); - if (success && toClipboard) { - session.setClipboard(new ClipboardHolder(clipboard)); - } + result = world.regenerateAsync(region, outputExtent, options, actor); + } catch (RuntimeException e) { + result = CompletableFuture.failedFuture(e); } finally { session.setMask(mask); } - if (success) { - actor.printInfo(TranslatableComponent.of("worldedit.regen.regenerated")); + if (!result.toCompletableFuture().isDone()) { + actor.printInfo(TranslatableComponent.of("worldedit.regen.generating")); + } + result.whenComplete((success, failure) -> { + if (editSession != null) { + session.remember(editSession); + editSession.close(); + WorldEdit.getInstance().flushBlockBag(actor, editSession); + } + if (failure != null) { + reportRegenerationFailure(actor, failure); + } else if (success) { + if (clipboard != null) { + session.setClipboard(new ClipboardHolder(clipboard)); + } + actor.printInfo(TranslatableComponent.of("worldedit.regen.regenerated")); + } else { + actor.printError(TranslatableComponent.of("worldedit.regen.failed")); + } + }); + } + + private static void reportRegenerationFailure(Actor actor, Throwable failure) { + Throwable cause = failure; + while (cause instanceof CompletionException && cause.getCause() != null) { + cause = cause.getCause(); + } + if (cause instanceof WorldEditException exception && exception.getRichMessage() != null) { + actor.printError(exception.getRichMessage()); } else { + LOGGER.warn("Regeneration failed.", cause); actor.printError(TranslatableComponent.of("worldedit.regen.failed")); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java index 7a1c3a742d..8de50d36c9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java @@ -24,6 +24,7 @@ import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.mask.Mask; @@ -48,6 +49,8 @@ import java.nio.file.Path; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import javax.annotation.Nullable; /** @@ -267,6 +270,26 @@ default boolean regenerate(Region region, Extent extent, RegenOptions options) { + ") does not implement the general Extent variant of this method"); } + /** + * Regenerate the given region into the given extent, possibly without blocking the calling thread. + * + *

The default implementation simply calls {@link #regenerate(Region, Extent, RegenOptions)}. + * Platforms that cannot generate terrain on the calling thread override it instead. An override + * must write and flush the extent on a thread that is allowed to edit the region, loading the + * region's chunks if necessary, and must complete the returned future on the thread that the + * actor's commands run on, or on the writing thread when the actor has no thread of its own.

+ * + * @param region the region + * @param extent the {@link Extent} to write the regenerated blocks and biomes to + * @param options the regeneration options + * @param actor the actor that requested the regeneration, or null for an API call without one + * @return a future completing with whether regeneration was successful + */ + default CompletionStage regenerateAsync(Region region, Extent extent, RegenOptions options, + @Nullable Actor actor) { + return CompletableFuture.completedFuture(regenerate(region, extent, options)); + } + /** * Generate a tree at the given position. * diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index 8f4240cf79..ded259bacd 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -227,6 +227,10 @@ "worldedit.replace.replaced": "{0} blocks have been replaced.", "worldedit.stack.changed": "{0} blocks changed. Undo with //undo", "worldedit.stack.intersecting-region": "Stack offset must not collide with the region when using block units", + "worldedit.regen.folia-region": "On Folia, the whole selection must end up in a single region. Try again, or use a smaller selection.", + "worldedit.regen.inventory-unsupported": "Regeneration on Folia does not support inventory usage. Disable inventory usage or regenerate to the clipboard with -c.", + "worldedit.regen.busy": "Another regeneration is still generating terrain. Try again in a moment.", + "worldedit.regen.generating": "Generating terrain...", "worldedit.regen.regenerated": "Region regenerated.", "worldedit.regen.failed": "Unable to regenerate chunks. Check console for details.", "worldedit.walls.changed": "{0} blocks have been changed.",