diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 39bc6a45b4..307b61c1e5 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.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 8e2a0fc876..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 @@ -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; @@ -711,6 +712,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) { @@ -720,6 +725,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(); @@ -793,6 +806,248 @@ 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) { + // It's fine if we couldn't remove it + } + 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)); + @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)); + } 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", "unused"}) + 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)); + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = 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) { @@ -813,7 +1068,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 15bb3fece4..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 @@ -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; @@ -701,6 +702,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) { @@ -710,6 +715,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(); @@ -784,6 +797,247 @@ 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) { + // It's fine if we couldn't remove it + } + 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)); + @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)); + } 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", "unused"}) + 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)); + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = 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) { @@ -804,7 +1058,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 9617e3e02f..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 @@ -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(); @@ -782,6 +795,247 @@ 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) { + // It's fine if we couldn't remove it + } + 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)); + @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)); + } 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", "unused"}) + 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)); + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = 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) { @@ -802,7 +1056,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 13bc8e76fb..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 @@ -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; @@ -712,6 +713,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) { @@ -721,6 +726,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(); @@ -795,6 +808,249 @@ 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) { + // It's fine if we couldn't remove it + } + 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)); + @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)); + } 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", "unused"}) + 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)); + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = 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) { @@ -815,7 +1071,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 c621da37e4..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 @@ -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; @@ -711,6 +712,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) { @@ -720,6 +725,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(); @@ -793,6 +806,248 @@ 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) { + // It's fine if we couldn't remove it + } + 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)); + @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)); + } 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", "unused"}) + 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)); + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var ignored = 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) { @@ -813,7 +1068,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 1875de2999..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 @@ -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,17 +156,15 @@ public boolean isActive() { // we can update eagerly updateActive(); } else { - // we should update it eventually + // we should update it eventually or on the owning region thread // Suppress FutureReturnValueIgnored: We handle it in the block. - @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) - var unused = Bukkit.getScheduler().callSyncMethod(plugin, + FoliaScheduler.getRegionScheduler().execute(plugin, sender.getBlock().getLocation(), () -> { try { updateActive(); } catch (Throwable t) { WorldEdit.logger.warn("Exception while updating command block sender active state", t); } - return null; }); } return active; diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java index e719902862..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 @@ -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,108 @@ 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()) { + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var unused = 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) { + // It's fine if we couldn't remove it + } + + 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 +188,30 @@ 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) { + // It's fine if we couldn't remove it + } + }, + 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 771e0c14fa..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 @@ -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,12 @@ public void print(Component component) { @Override public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { + if (PaperLib.isPaper()) { + @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; + } return player.teleport(new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch)); } @@ -224,6 +232,12 @@ public com.sk89q.worldedit.util.Location getLocation() { @Override public boolean setLocation(com.sk89q.worldedit.util.Location location) { + if (PaperLib.isPaper()) { + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) + var unused = 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 7526989842..b7e77048df 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 3eb3f0fcbf..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 @@ -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; } } @@ -260,7 +269,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); } } } @@ -299,14 +308,26 @@ 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 + @SuppressWarnings({"FutureReturnValueIgnored", "unused"}) 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 @@ -455,46 +476,126 @@ public boolean generateStructure(StructureType type, EditSession editSession, Bl return false; } - private static volatile 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 { @@ -504,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 0c3dddecbf..e839cd1aa2 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java @@ -31,6 +31,7 @@ import com.sk89q.worldedit.bukkit.adapter.AdapterLoadException; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplLoader; +import com.sk89q.worldedit.bukkit.folia.FoliaScheduler; import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.event.platform.CommandSuggestionEvent; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; @@ -321,7 +322,7 @@ public void onDisable() { if (config != null) { config.unload(); } - this.getServer().getScheduler().cancelTasks(this); + cancelTasks(); } /** @@ -500,7 +501,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); } @@ -572,4 +573,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 4f19c3e6ec..28cce7beaf 100644 --- a/worldedit-bukkit/src/main/resources/plugin.yml +++ b/worldedit-bukkit/src/main/resources/plugin.yml @@ -3,6 +3,7 @@ main: com.sk89q.worldedit.bukkit.WorldEditPlugin version: "${internalVersion}" load: STARTUP api-version: 1.21.4 +folia-supported: true softdepend: [Vault] author: EngineHub -website: https://enginehub.org/worldedit \ No newline at end of file +website: https://enginehub.org/worldedit diff --git a/worldedit-core/src/main/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); +}