diff --git a/build.gradle.kts b/build.gradle.kts index c79bb8083..bc26095a7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,7 +97,7 @@ tasks { } } runServer { - minecraftVersion("1.20.2") + minecraftVersion(supportedVersions.last()) pluginJars(*project(":worldedit-bukkit").getTasksByName("shadowJar", false).map { (it as Jar).archiveFile } .toTypedArray()) diff --git a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/PaperweightAdapter.java index 52974fb02..73e5f58bb 100644 --- a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/PaperweightAdapter.java @@ -24,7 +24,6 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.mojang.datafixers.util.Either; @@ -393,7 +392,7 @@ public final class PaperweightAdapter implements BukkitImplAdapter stateContainer, net.minecraft.world.level.block.state.BlockState newState, Map, Object> states diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java index b73e6d06e..20475bbeb 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java @@ -378,7 +378,7 @@ public final class PaperweightAdapter implements BukkitImplAdapter stateContainer, net.minecraft.world.level.block.state.BlockState newState, Map, Object> states diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java index 48bc935cb..d2073b868 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java @@ -21,7 +21,7 @@ import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.BukkitWorld; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.nbt.PaperweightLazyCompoundTag; -import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regen.PaperweightRegen; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.VersionedRegenerator; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.internal.block.BlockStateIdAccess; @@ -570,7 +570,9 @@ public final class PaperweightFaweAdapter extends CachedBukkitAdapter implements @Override public boolean regenerate(org.bukkit.World bukkitWorld, Region region, Extent target, RegenOptions options) throws Exception { - return new PaperweightRegen(bukkitWorld, region, target, options).regenerate(); + try (final VersionedRegenerator regenerator = new VersionedRegenerator(bukkitWorld, region, options, target)) { + return regenerator.regenerate().join(); + } } @Override diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regen/PaperweightRegen.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regen/PaperweightRegen.java deleted file mode 100644 index 2ec8e6e41..000000000 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regen/PaperweightRegen.java +++ /dev/null @@ -1,591 +0,0 @@ -package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regen; - -import com.fastasyncworldedit.bukkit.adapter.Regenerator; -import com.fastasyncworldedit.core.Fawe; -import com.fastasyncworldedit.core.queue.IChunkCache; -import com.fastasyncworldedit.core.queue.IChunkGet; -import com.fastasyncworldedit.core.util.ReflectionUtils; -import com.fastasyncworldedit.core.util.TaskManager; -import com.google.common.collect.ImmutableList; -import com.mojang.datafixers.util.Either; -import com.mojang.serialization.Lifecycle; -import com.sk89q.worldedit.bukkit.WorldEditPlugin; -import com.sk89q.worldedit.bukkit.adapter.Refraction; -import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.PaperweightGetBlocks; -import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.util.io.file.SafeFiles; -import com.sk89q.worldedit.world.RegenOptions; -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; -import net.minecraft.core.Holder; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.dedicated.DedicatedServer; -import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.ChunkTaskPriorityQueueSorter.Message; -import net.minecraft.server.level.ServerChunkCache; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ThreadedLevelLightEngine; -import net.minecraft.server.level.progress.ChunkProgressListener; -import net.minecraft.util.thread.ProcessorHandle; -import net.minecraft.util.thread.ProcessorMailbox; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.LevelHeightAccessor; -import net.minecraft.world.level.LevelSettings; -import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.level.biome.BiomeSource; -import net.minecraft.world.level.biome.FixedBiomeSource; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; -import net.minecraft.world.level.chunk.ChunkStatus; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.chunk.ProtoChunk; -import net.minecraft.world.level.chunk.UpgradeData; -import net.minecraft.world.level.dimension.LevelStem; -import net.minecraft.world.level.levelgen.FlatLevelSource; -import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; -import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; -import net.minecraft.world.level.levelgen.WorldOptions; -import net.minecraft.world.level.levelgen.blending.BlendingData; -import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; -import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; -import net.minecraft.world.level.storage.LevelStorageSource; -import net.minecraft.world.level.storage.PrimaryLevelData; -import org.apache.logging.log4j.Logger; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.craftbukkit.v1_20_R2.CraftServer; -import org.bukkit.craftbukkit.v1_20_R2.CraftWorld; -import org.bukkit.craftbukkit.v1_20_R2.generator.CustomChunkGenerator; -import org.bukkit.generator.BiomeProvider; -import org.bukkit.generator.BlockPopulator; - -import javax.annotation.Nullable; -import java.lang.reflect.Field; -import java.nio.file.Path; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.OptionalLong; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.function.BooleanSupplier; -import java.util.function.Supplier; - -import static net.minecraft.core.registries.Registries.BIOME; - -public class PaperweightRegen extends Regenerator { - - private static final Logger LOGGER = LogManagerCompat.getLogger(); - - private static final Field serverWorldsField; - private static final Field paperConfigField; - private static final Field flatBedrockField; - private static final Field generatorSettingFlatField; - private static final Field generatorSettingBaseSupplierField; - private static final Field delegateField; - private static final Field chunkSourceField; - private static final Field generatorStructureStateField; - private static final Field ringPositionsField; - private static final Field hasGeneratedPositionsField; - - //list of chunk stati in correct order without FULL - private static final Map chunkStati = new LinkedHashMap<>(); - - static { - chunkStati.put(ChunkStatus.EMPTY, Concurrency.FULL); // empty: radius -1, does nothing - chunkStati.put(ChunkStatus.STRUCTURE_STARTS, Concurrency.NONE); // structure starts: uses unsynchronized maps - chunkStati.put( - ChunkStatus.STRUCTURE_REFERENCES, - Concurrency.FULL - ); // structure refs: radius 8, but only writes to current chunk - chunkStati.put(ChunkStatus.BIOMES, Concurrency.FULL); // biomes: radius 0 - chunkStati.put(ChunkStatus.NOISE, Concurrency.RADIUS); // noise: radius 8 - chunkStati.put(ChunkStatus.SURFACE, Concurrency.NONE); // surface: radius 0, requires NONE - chunkStati.put(ChunkStatus.CARVERS, Concurrency.NONE); // carvers: radius 0, but RADIUS and FULL change results - /*chunkStati.put( - ChunkStatus.LIQUID_CARVERS, - Concurrency.NONE - ); // liquid carvers: radius 0, but RADIUS and FULL change results*/ - chunkStati.put(ChunkStatus.FEATURES, Concurrency.NONE); // features: uses unsynchronized maps - chunkStati.put( - ChunkStatus.LIGHT, - Concurrency.FULL - ); // light: radius 1, but no writes to other chunks, only current chunk - chunkStati.put(ChunkStatus.SPAWN, Concurrency.FULL); // spawn: radius 0 - // chunkStati.put(ChunkStatus.HEIGHTMAPS, Concurrency.FULL); // heightmaps: radius 0 - - try { - serverWorldsField = CraftServer.class.getDeclaredField("worlds"); - serverWorldsField.setAccessible(true); - - Field tmpPaperConfigField; - Field tmpFlatBedrockField; - try { //only present on paper - tmpPaperConfigField = Level.class.getDeclaredField("paperConfig"); - tmpPaperConfigField.setAccessible(true); - - tmpFlatBedrockField = tmpPaperConfigField.getType().getDeclaredField("generateFlatBedrock"); - tmpFlatBedrockField.setAccessible(true); - } catch (Exception e) { - tmpPaperConfigField = null; - tmpFlatBedrockField = null; - } - paperConfigField = tmpPaperConfigField; - flatBedrockField = tmpFlatBedrockField; - - generatorSettingBaseSupplierField = NoiseBasedChunkGenerator.class.getDeclaredField(Refraction.pickName( - "settings", "e")); - generatorSettingBaseSupplierField.setAccessible(true); - - generatorSettingFlatField = FlatLevelSource.class.getDeclaredField(Refraction.pickName("settings", "d")); - generatorSettingFlatField.setAccessible(true); - - delegateField = CustomChunkGenerator.class.getDeclaredField("delegate"); - delegateField.setAccessible(true); - - chunkSourceField = ServerLevel.class.getDeclaredField(Refraction.pickName("chunkSource", "I")); - chunkSourceField.setAccessible(true); - - generatorStructureStateField = ChunkMap.class.getDeclaredField(Refraction.pickName("chunkGeneratorState", "v")); - generatorStructureStateField.setAccessible(true); - - ringPositionsField = ChunkGeneratorStructureState.class.getDeclaredField(Refraction.pickName("ringPositions", "g")); - ringPositionsField.setAccessible(true); - - hasGeneratedPositionsField = ChunkGeneratorStructureState.class.getDeclaredField( - Refraction.pickName("hasGeneratedPositions", "h") - ); - hasGeneratedPositionsField.setAccessible(true); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - //runtime - private ServerLevel originalServerWorld; - private ServerChunkCache originalChunkProvider; - private ServerLevel freshWorld; - private ServerChunkCache freshChunkProvider; - private LevelStorageSource.LevelStorageAccess session; - private StructureTemplateManager structureTemplateManager; - private ThreadedLevelLightEngine threadedLevelLightEngine; - private ChunkGenerator chunkGenerator; - - private Path tempDir; - - private boolean generateFlatBedrock = false; - - public PaperweightRegen(org.bukkit.World originalBukkitWorld, Region region, Extent target, RegenOptions options) { - super(originalBukkitWorld, region, target, options); - } - - @Override - protected boolean prepare() { - this.originalServerWorld = ((CraftWorld) originalBukkitWorld).getHandle(); - originalChunkProvider = originalServerWorld.getChunkSource(); - - //flat bedrock? (only on paper) - if (paperConfigField != null) { - try { - generateFlatBedrock = flatBedrockField.getBoolean(paperConfigField.get(originalServerWorld)); - } catch (Exception ignored) { - } - } - - seed = options.getSeed().orElse(originalServerWorld.getSeed()); - chunkStati.forEach((s, c) -> super.chunkStatuses.put(new ChunkStatusWrap(s), c)); - - return true; - } - - @Override - @SuppressWarnings("unchecked") - protected boolean initNewWorld() throws Exception { - //world folder - tempDir = java.nio.file.Files.createTempDirectory("FastAsyncWorldEditWorldGen"); - - //prepare for world init (see upstream implementation for reference) - org.bukkit.World.Environment environment = originalBukkitWorld.getEnvironment(); - org.bukkit.generator.ChunkGenerator generator = originalBukkitWorld.getGenerator(); - LevelStorageSource levelStorageSource = LevelStorageSource.createDefault(tempDir); - ResourceKey levelStemResourceKey = getWorldDimKey(environment); - session = levelStorageSource.createAccess("faweregentempworld", levelStemResourceKey); - PrimaryLevelData originalWorldData = originalServerWorld.serverLevelData; - - MinecraftServer server = originalServerWorld.getCraftServer().getServer(); - WorldOptions originalOpts = originalWorldData.worldGenOptions(); - WorldOptions newOpts = options.getSeed().isPresent() - ? originalOpts.withSeed(OptionalLong.of(seed)) - : originalOpts; - LevelSettings newWorldSettings = new LevelSettings( - "faweregentempworld", - originalWorldData.settings.gameType(), - originalWorldData.settings.hardcore(), - originalWorldData.settings.difficulty(), - originalWorldData.settings.allowCommands(), - originalWorldData.settings.gameRules(), - originalWorldData.settings.getDataConfiguration() - ); - - PrimaryLevelData.SpecialWorldProperty specialWorldProperty = - originalWorldData.isFlatWorld() - ? PrimaryLevelData.SpecialWorldProperty.FLAT - : originalWorldData.isDebugWorld() - ? PrimaryLevelData.SpecialWorldProperty.DEBUG - : PrimaryLevelData.SpecialWorldProperty.NONE; - PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable()); - - BiomeProvider biomeProvider = getBiomeProvider(); - - - //init world - freshWorld = Fawe.instance().getQueueHandler().sync((Supplier) () -> new ServerLevel( - server, - server.executor, - session, - newWorldData, - originalServerWorld.dimension(), - DedicatedServer.getServer().registryAccess().registry(Registries.LEVEL_STEM).orElseThrow() - .getOrThrow(levelStemResourceKey), - new RegenNoOpWorldLoadListener(), - originalServerWorld.isDebug(), - seed, - ImmutableList.of(), - false, - originalServerWorld.getRandomSequences(), - environment, - generator, - biomeProvider - ) { - - private final Holder singleBiome = options.hasBiomeType() ? DedicatedServer.getServer().registryAccess() - .registryOrThrow(BIOME).asHolderIdMap().byIdOrThrow( - WorldEditPlugin.getInstance().getBukkitImplAdapter().getInternalBiomeId(options.getBiomeType()) - ) : null; - - @Override - public void tick(BooleanSupplier shouldKeepTicking) { //no ticking - } - - @Override - public Holder getUncachedNoiseBiome(int biomeX, int biomeY, int biomeZ) { - if (options.hasBiomeType()) { - return singleBiome; - } - return PaperweightRegen.this.chunkGenerator.getBiomeSource().getNoiseBiome( - biomeX, biomeY, biomeZ, getChunkSource().randomState().sampler() - ); - } - }).get(); - freshWorld.noSave = true; - removeWorldFromWorldsMap(); - newWorldData.checkName(originalServerWorld.serverLevelData.getLevelName()); //rename to original world name - if (paperConfigField != null) { - paperConfigField.set(freshWorld, originalServerWorld.paperConfig()); - } - - ChunkGenerator originalGenerator = originalChunkProvider.getGenerator(); - if (originalGenerator instanceof FlatLevelSource flatLevelSource) { - FlatLevelGeneratorSettings generatorSettingFlat = flatLevelSource.settings(); - chunkGenerator = new FlatLevelSource(generatorSettingFlat); - } else if (originalGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) { - Holder generatorSettingBaseSupplier = (Holder) generatorSettingBaseSupplierField.get( - originalGenerator); - BiomeSource biomeSource; - if (options.hasBiomeType()) { - - biomeSource = new FixedBiomeSource( - DedicatedServer.getServer().registryAccess() - .registryOrThrow(BIOME).asHolderIdMap().byIdOrThrow( - WorldEditPlugin.getInstance().getBukkitImplAdapter().getInternalBiomeId(options.getBiomeType()) - ) - ); - } else { - biomeSource = originalGenerator.getBiomeSource(); - } - chunkGenerator = new NoiseBasedChunkGenerator( - biomeSource, - generatorSettingBaseSupplier - ); - } else if (originalGenerator instanceof CustomChunkGenerator customChunkGenerator) { - chunkGenerator = customChunkGenerator.getDelegate(); - } else { - LOGGER.error("Unsupported generator type {}", originalGenerator.getClass().getName()); - return false; - } - if (generator != null) { - chunkGenerator = new CustomChunkGenerator(freshWorld, chunkGenerator, generator); - generateConcurrent = generator.isParallelCapable(); - } -// chunkGenerator.conf = freshWorld.spigotConfig; - Does not exist anymore, may need to be re-addressed - - freshChunkProvider = new ServerChunkCache( - freshWorld, - session, - server.getFixerUpper(), - server.getStructureManager(), - server.executor, - chunkGenerator, - freshWorld.spigotConfig.viewDistance, - freshWorld.spigotConfig.simulationDistance, - server.forceSynchronousWrites(), - new RegenNoOpWorldLoadListener(), - (chunkCoordIntPair, state) -> { - }, - () -> server.overworld().getDataStorage() - ) { - // redirect to LevelChunks created in #createChunks - @Override - public ChunkAccess getChunk(int x, int z, ChunkStatus chunkstatus, boolean create) { - ChunkAccess chunkAccess = getChunkAt(x, z); - if (chunkAccess == null && create) { - chunkAccess = createChunk(getProtoChunkAt(x, z)); - } - return chunkAccess; - } - }; - - if (seed == originalOpts.seed() && !options.hasBiomeType()) { - // Optimisation for needless ring position calculation when the seed and biome is the same. - ChunkGeneratorStructureState state = (ChunkGeneratorStructureState) generatorStructureStateField.get(originalChunkProvider.chunkMap); - boolean hasGeneratedPositions = hasGeneratedPositionsField.getBoolean(state); - if (hasGeneratedPositions) { - Map>> origPositions = - (Map>>) ringPositionsField.get(state); - Map>> copy = new Object2ObjectArrayMap<>( - origPositions); - ChunkGeneratorStructureState newState = (ChunkGeneratorStructureState) generatorStructureStateField.get(freshChunkProvider.chunkMap); - ringPositionsField.set(newState, copy); - hasGeneratedPositionsField.setBoolean(newState, true); - } - } - - - chunkSourceField.set(freshWorld, freshChunkProvider); - //let's start then - structureTemplateManager = server.getStructureManager(); - threadedLevelLightEngine = new NoOpLightEngine(freshChunkProvider); - - return true; - } - - @Override - protected void cleanup() { - try { - session.close(); - } catch (Exception ignored) { - } - - //shutdown chunk provider - try { - Fawe.instance().getQueueHandler().sync(() -> { - try { - freshChunkProvider.close(false); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } catch (Exception ignored) { - } - - //remove world from server - try { - Fawe.instance().getQueueHandler().sync(this::removeWorldFromWorldsMap); - } catch (Exception ignored) { - } - - //delete directory - try { - SafeFiles.tryHardToDeleteDir(tempDir); - } catch (Exception ignored) { - } - } - - @Override - protected ProtoChunk createProtoChunk(int x, int z) { - return new FastProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, freshWorld, - this.freshWorld.registryAccess().registryOrThrow(BIOME), null - ); - } - - @Override - protected LevelChunk createChunk(ProtoChunk protoChunk) { - return new LevelChunk( - freshWorld, - protoChunk, - null // we don't want to add entities - ); - } - - @Override - protected ChunkStatusWrap getFullChunkStatus() { - return new ChunkStatusWrap(ChunkStatus.FULL); - } - - @Override - protected List getBlockPopulators() { - return originalServerWorld.getWorld().getPopulators(); - } - - @Override - protected void populate(LevelChunk levelChunk, Random random, BlockPopulator blockPopulator) { - // BlockPopulator#populate has to be called synchronously for TileEntity access - TaskManager.taskManager().task(() -> { - final CraftWorld world = freshWorld.getWorld(); - final Chunk chunk = world.getChunkAt(levelChunk.locX, levelChunk.locZ); - blockPopulator.populate(world, random, chunk); - }); - } - - @Override - protected IChunkCache initSourceQueueCache() { - return (chunkX, chunkZ) -> new PaperweightGetBlocks(freshWorld, chunkX, chunkZ) { - @Override - public LevelChunk ensureLoaded(ServerLevel nmsWorld, int x, int z) { - return getChunkAt(x, z); - } - }; - } - - //util - @SuppressWarnings("unchecked") - private void removeWorldFromWorldsMap() { - Fawe.instance().getQueueHandler().sync(() -> { - try { - Map map = (Map) serverWorldsField.get(Bukkit.getServer()); - map.remove("faweregentempworld"); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - }); - } - - private ResourceKey getWorldDimKey(org.bukkit.World.Environment env) { - return switch (env) { - case NETHER -> LevelStem.NETHER; - case THE_END -> LevelStem.END; - default -> LevelStem.OVERWORLD; - }; - } - - private static class RegenNoOpWorldLoadListener implements ChunkProgressListener { - - private RegenNoOpWorldLoadListener() { - } - - @Override - public void updateSpawnPos(ChunkPos spawnPos) { - } - - @Override - public void onStatusChange(ChunkPos pos, @Nullable ChunkStatus status) { - } - - @Override - public void start() { - - } - - @Override - public void stop() { - } - - // TODO Paper only(?) @Override - public void setChunkRadius(int radius) { - } - - } - - private class FastProtoChunk extends ProtoChunk { - - public FastProtoChunk( - final ChunkPos pos, - final UpgradeData upgradeData, - final LevelHeightAccessor world, - final Registry biomeRegistry, - @Nullable final BlendingData blendingData - ) { - super(pos, upgradeData, world, biomeRegistry, blendingData); - } - - // avoid warning on paper - - // compatibility with spigot - - public boolean generateFlatBedrock() { - return generateFlatBedrock; - } - - // no one will ever see the entities! - @Override - public List getEntities() { - return Collections.emptyList(); - } - - } - - protected class ChunkStatusWrap extends ChunkStatusWrapper { - - private final ChunkStatus chunkStatus; - - public ChunkStatusWrap(ChunkStatus chunkStatus) { - this.chunkStatus = chunkStatus; - } - - @Override - public int requiredNeighborChunkRadius() { - return chunkStatus.getRange(); - } - - @Override - public String name() { - return chunkStatus.toString(); - } - - @Override - public CompletableFuture processChunk(List accessibleChunks) { - return chunkStatus.generate( - Runnable::run, // TODO revisit, we might profit from this somehow? - freshWorld, - chunkGenerator, - structureTemplateManager, - threadedLevelLightEngine, - c -> CompletableFuture.completedFuture(Either.left(c)), - accessibleChunks - ); - } - - } - - /** - * A light engine that does nothing. As light is calculated after pasting anyway, we can avoid - * work this way. - */ - static class NoOpLightEngine extends ThreadedLevelLightEngine { - - private static final ProcessorMailbox MAILBOX = ProcessorMailbox.create(task -> { - }, "fawe-no-op"); - private static final ProcessorHandle> HANDLE = ProcessorHandle.of("fawe-no-op", m -> { - }); - - public NoOpLightEngine(final ServerChunkCache chunkProvider) { - super(chunkProvider, chunkProvider.chunkMap, false, MAILBOX, HANDLE); - } - - @Override - public CompletableFuture lightChunk(final ChunkAccess chunk, final boolean excludeBlocks) { - return CompletableFuture.completedFuture(chunk); - } - - } - -} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/DelegatingWorldGenRegion.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/DelegatingWorldGenRegion.java new file mode 100644 index 000000000..3b73c7d11 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/DelegatingWorldGenRegion.java @@ -0,0 +1,101 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator; + +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level.FaweProtoChunkAccess; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class DelegatingWorldGenRegion extends WorldGenRegion { + + private final RegenOptions regenOptions; + + public DelegatingWorldGenRegion( + final ServerLevel world, + final List chunkAccesses, + final ChunkStatus status, + final int placementRadius, + final RegenOptions regenOptions + ) { + super(world, chunkAccesses, status, placementRadius); + this.regenOptions = regenOptions; + } + + + @Override + public boolean ensureCanWrite(final BlockPos pos) { + return this.hasChunk(pos.getX() >> 4, pos.getZ() >> 4) && + pos.getY() >= getMinBuildHeight() && pos.getY() <= getMaxBuildHeight(); + } + + @Override + public boolean isOldChunkAround(final @NotNull ChunkPos chunkPos, final int checkRadius) { + return false; // We don't migrate old worlds, won't be properly blended either way + } + + /** + * Don't notify ServerLevel on place and don't set chunk for postprocessing + */ + @Override + public boolean setBlock( + final @NotNull BlockPos pos, + final @NotNull BlockState state, + final int flags, + final int maxUpdateDepth + ) { + if (!this.ensureCanWrite(pos)) { + return false; + } + ChunkAccess chunk = this.getChunk(pos); + BlockState oldState = chunk.setBlockState(pos, state, false); + if (state.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) state.getBlock()).newBlockEntity(pos, state); + if (tileEntity != null) { + chunk.setBlockEntity(tileEntity); + } else { + chunk.removeBlockEntity(pos); + } + } else if (oldState != null && oldState.hasBlockEntity()) { + chunk.removeBlockEntity(pos); + } + return true; + } + + @Override + public @NotNull Holder getNoiseBiome(final int biomeX, final int biomeY, final int biomeZ) { + FaweProtoChunkAccess chunkAccess = (FaweProtoChunkAccess) this.getChunk( + QuartPos.toSection(biomeX), QuartPos.toSection(biomeZ), ChunkStatus.EMPTY, false + ); + if (chunkAccess == null) { + return DedicatedServer.getServer().registryAccess().registryOrThrow(Registries.BIOME) + .getHolderOrThrow(Biomes.PLAINS); + } + int l = QuartPos.fromBlock(this.getMinBuildHeight()); + int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1; + int j1 = Mth.clamp(biomeY, l, i1); + int k1 = this.getSectionIndex(QuartPos.toBlock(j1)); + return chunkAccess.getSection(k1).getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3); + } + + @Override + public long getSeed() { + return this.regenOptions.getSeed().orElse(super.getSeed()); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/VersionedRegenerator.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/VersionedRegenerator.java new file mode 100644 index 000000000..4c9ff1009 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/VersionedRegenerator.java @@ -0,0 +1,185 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator; + +import com.fastasyncworldedit.bukkit.adapter.regeneration.ChunkWorker; +import com.fastasyncworldedit.bukkit.adapter.regeneration.Regenerator; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.PaperweightFaweAdapter; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level.FaweChunkSource; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker.BiomesWorker; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker.CarversWorker; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker.FeaturesWorker; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker.NoiseWorker; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker.SurfaceWorker; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; +import com.sk89q.worldedit.world.RegenOptions; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.levelgen.Heightmap; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R2.CraftWorld; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class VersionedRegenerator extends Regenerator { + + /** + * Each chunk has their own ChunkSource as chunks are worked on in parallel and therefor neighbour chunks may differ in status + */ + private final Long2ObjectLinkedOpenHashMap chunkSources; + + public VersionedRegenerator( + final World world, final Region region, final RegenOptions options, + final Extent extent + ) { + super(world, region, options, extent); + this.chunkSources = new Long2ObjectLinkedOpenHashMap<>(region.getChunks().size()); + + for (final BlockVector2 chunk : region.getChunks()) { + FaweChunkSource source = new FaweChunkSource( + region, + serverLevel().registryAccess().registryOrThrow(Registries.BIOME), + options, + serverLevel() + ); + this.chunkSources.put(ChunkPos.asLong(chunk.getX(), chunk.getZ()), source); + } + } + + @Override + public CompletableFuture flushPalettesIntoWorld(final ChunkAccess chunkAccess, final WorldGenRegion worldGenRegion) { + return CompletableFuture.supplyAsync(() -> { + boolean result = false; + final int minX = Math.max(chunkAccess.getPos().getMinBlockX(), region.getMinimumPoint().getBlockX()); + final int minY = Math.max(chunkAccess.getMinBuildHeight(), region.getMinimumY()); + final int minZ = Math.max(chunkAccess.getPos().getMinBlockZ(), region.getMinimumPoint().getBlockZ()); + final int maxX = Math.min(chunkAccess.getPos().getMaxBlockX(), region.getMaximumPoint().getBlockX()); + final int maxY = Math.min(chunkAccess.getMaxBuildHeight(), region.getMaximumY()); + final int maxZ = Math.min(chunkAccess.getPos().getMaxBlockZ(), region.getMaximumPoint().getBlockZ()); + for (final BlockPos blockPos : BlockPos.MutableBlockPos.betweenClosed( + minX, minY, minZ, + maxX, maxY, maxZ + )) { + // We still have to validate boundaries, as region may not be a cuboid + if (!(region.contains(blockPos.getX(), blockPos.getY(), blockPos.getZ()))) { + continue; + } + if ((options.shouldRegenBiomes() || options.hasBiomeType()) && + (blockPos.getX() % 4 == 0 && blockPos.getY() % 4 == 0 && blockPos.getZ() % 4 == 0)) { + result |= this.extent.setBiome( + blockPos.getX(), blockPos.getY(), blockPos.getZ(), + BiomeTypes.get(worldGenRegion.getBiome(blockPos).unwrapKey() + .orElseThrow().location().toString()) + ); + } + final BlockEntity blockEntity = chunkAccess.getBlockEntity(blockPos); + final com.sk89q.worldedit.world.block.BlockState blockState = ((PaperweightFaweAdapter) WorldEditPlugin + .getInstance() + .getBukkitImplAdapter()) + .adapt(blockEntity != null ? blockEntity.getBlockState() : chunkAccess.getBlockState(blockPos)); + if (blockEntity != null) { + result |= this.extent.setBlock( + blockPos.getX(), blockPos.getY(), blockPos.getZ(), + blockState.toBaseBlock(LazyReference.from(() -> { + //noinspection unchecked + if (!(WorldEditPlugin.getInstance().getBukkitImplAdapter() + .toNativeBinary(blockEntity.saveWithId()) instanceof CompoundBinaryTag tag)) { + throw new IllegalStateException("Entity Binary-Tag is no CompoundBinaryTag"); + } + return tag; + })) + ); + continue; + } + result |= this.extent.setBlock(blockPos.getX(), blockPos.getY(), blockPos.getZ(), blockState); + } + this.chunkSources.remove(chunkAccess.getPos().toLong()); + return result; + }, provideExecutor().get()); + } + + @Override + public @Nullable ChunkAccess toNativeChunkAccess( + final ServerLevel level, + final BlockVector2 chunk, + final int x, + final int z, + final ChunkStatus leastStatus + ) { + return this.chunkSources.get(ChunkPos.asLong(chunk.getX(), chunk.getZ())) + .getChunk(x, z, leastStatus, true); + } + + @Override + protected @NonNull CompletableFuture<@Nullable Void> primeHeightmaps(final ChunkAccess chunk) { + return CompletableFuture.runAsync( + () -> Heightmap.primeHeightmaps(chunk, ChunkStatus.POST_FEATURES), + provideExecutor().get() + ); + } + + @Override + public WorldGenRegion worldGenRegionForRegion( + final ServerLevel level, + final List chunkAccesses, + final Region region, + final ChunkStatus status + ) { + return new DelegatingWorldGenRegion( + level, chunkAccesses, + status, + PLACEMENT_RADII[status.getIndex() - ChunkStatus.BIOMES.getIndex()], + options + ); + } + + @Override + public ChunkGenerator generatorFromLevel(final ServerLevel serverLevel) { + return serverLevel.chunkSource.getGenerator(); + } + + @Override + protected void setChunkStatus(final List chunkAccesses, final ChunkStatus status) { + chunkAccesses.forEach(chunkAccess -> ((ProtoChunk) chunkAccess).setStatus(status)); + } + + @Override + public @NonNull List<@NonNull ChunkWorker> workers() { + return List.of( + BiomesWorker.INSTANCE, + NoiseWorker.INSTANCE, + SurfaceWorker.INSTANCE, + CarversWorker.INSTANCE, + FeaturesWorker.INSTANCE + ); + } + + @Override + public @NonNull ServerLevel serverLevel() { + return ((CraftWorld) this.world).getHandle(); + } + + @Override + public void close() throws IOException { + this.chunkSources.clear(); + super.close(); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweBlockGetter.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweBlockGetter.java new file mode 100644 index 000000000..a1df33376 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweBlockGetter.java @@ -0,0 +1,65 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class FaweBlockGetter implements BlockGetter { + + private final FaweChunkSource chunkSource; + + public FaweBlockGetter(final FaweChunkSource chunkSource) { + this.chunkSource = chunkSource; + } + + @Nullable + @Override + public BlockEntity getBlockEntity(final BlockPos pos) { + final ChunkAccess chunkAccess = this.chunkSource.getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunkAccess == null ? null : chunkAccess.getBlockEntity(pos); + } + + @Override + public @NotNull BlockState getBlockState(final BlockPos pos) { + final ChunkAccess chunkAccess = this.chunkSource.getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunkAccess == null ? Blocks.VOID_AIR.defaultBlockState() : chunkAccess.getBlockState(pos); + } + + @Nullable + @Override + public BlockState getBlockStateIfLoaded(final @NotNull BlockPos pos) { + final ChunkAccess chunkAccess = this.chunkSource.getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunkAccess == null ? Blocks.VOID_AIR.defaultBlockState() : chunkAccess.getBlockStateIfLoaded(pos); + } + + @Nullable + @Override + public FluidState getFluidIfLoaded(final @NotNull BlockPos pos) { + final ChunkAccess chunkAccess = this.chunkSource.getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunkAccess == null ? null : chunkAccess.getFluidIfLoaded(pos); + } + + @Override + public @NotNull FluidState getFluidState(final @NotNull BlockPos pos) { + final ChunkAccess chunkAccess = this.chunkSource.getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunkAccess == null ? Fluids.EMPTY.defaultFluidState() : chunkAccess.getFluidState(pos); + } + + @Override + public int getHeight() { + return this.chunkSource.levelHeightAccessor().getHeight(); + } + + @Override + public int getMinBuildHeight() { + return this.chunkSource.levelHeightAccessor().getMinBuildHeight(); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweChunkSource.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweChunkSource.java new file mode 100644 index 000000000..9bc1027d7 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweChunkSource.java @@ -0,0 +1,105 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level; + +import com.fastasyncworldedit.bukkit.adapter.regeneration.Regenerator; +import com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.noop.NoopLightEngine; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.core.Registry; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.function.BooleanSupplier; + +public class FaweChunkSource extends ChunkSource { + + private final Registry biomeRegistry; + private final RegenOptions options; + private final LevelHeightAccessor levelHeightAccessor; + private final BlockGetter blockGetter; + private final Long2ObjectLinkedOpenHashMap cachedAccesses; + private final Region region; + + public FaweChunkSource( + final Region region, final Registry biomeRegistry, final RegenOptions options, + final LevelHeightAccessor levelHeightAccessor + ) { + this.region = region; + this.biomeRegistry = biomeRegistry; + this.options = options; + this.levelHeightAccessor = levelHeightAccessor; + this.cachedAccesses = new Long2ObjectLinkedOpenHashMap<>(); + this.blockGetter = new FaweBlockGetter(this); + } + + @Nullable + @Override + public ChunkAccess getChunk(final int x, final int z, final @NotNull ChunkStatus leastStatus, final boolean create) { + if (x < (this.region.getMinimumPoint().getBlockX() >> 4) - Regenerator.TASK_MARGIN) { + return null; + } + if (x > (this.region.getMaximumPoint().getBlockX() >> 4) + Regenerator.TASK_MARGIN) { + return null; + } + if (z < (this.region.getMinimumPoint().getBlockZ() >> 4) - Regenerator.TASK_MARGIN) { + return null; + } + if (z > (this.region.getMaximumPoint().getBlockZ() >> 4) + Regenerator.TASK_MARGIN) { + return null; + } + FaweProtoChunkAccess chunkAccess = cachedAccesses.get(ChunkPos.asLong(x, z)); + if (chunkAccess == null && create) { + cachedAccesses.put(ChunkPos.asLong(x, z), chunkAccess = new FaweProtoChunkAccess(this, new ChunkPos(x, z), + biomeRegistry, leastStatus, options + )); + } + if (chunkAccess != null && !chunkAccess.getStatus().isOrAfter(leastStatus)) { + return null; + } + return chunkAccess; + } + + @Override + public void tick(final @NotNull BooleanSupplier shouldKeepTicking, final boolean tickChunks) { + } + + @Override + public @NotNull String gatherStats() { + return toString(); + } + + @Override + public int getLoadedChunksCount() { + return this.cachedAccesses.size(); + } + + @Override + public @NotNull LevelLightEngine getLightEngine() { + return new NoopLightEngine(this); + } + + @Override + public @NotNull BlockGetter getLevel() { + return this.blockGetter; + } + + @Override + public void close() throws IOException { + super.close(); + this.cachedAccesses.clear(); + } + + public LevelHeightAccessor levelHeightAccessor() { + return levelHeightAccessor; + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkAccess.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkAccess.java new file mode 100644 index 000000000..5b564c274 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkAccess.java @@ -0,0 +1,108 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level; + +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * ChunkAccess which just delegates everything to an extent + */ +public class FaweProtoChunkAccess extends ProtoChunk { + + private final int hashedCoords; + + public FaweProtoChunkAccess( + final FaweChunkSource source, + final ChunkPos pos, + final Registry biomeRegistry, + final ChunkStatus status, + final RegenOptions options + ) { + super( + pos, UpgradeData.EMPTY, null, + NoopProtoChunkTick.BLOCKS, NoopProtoChunkTick.FLUIDS, + source.getLevel(), biomeRegistry, null + ); + for (int i = 0; i < getSections().length; i++) { + getSections()[i] = new FaweProtoChunkSection(biomeRegistry, options); + } + setStatus(status); + + this.hashedCoords = Objects.hash(getPos().x, getPos().z); + } + + @Override + public @NotNull BlockState getBlockState(final int x, final int y, final int z) { + if (this.isOutsideBuildHeight(y)) { + return Blocks.VOID_AIR.defaultBlockState(); + } + return getSection(getSectionIndex(y)).getBlockState(x & 15, y & 15, z & 15); + } + + @Nullable + @Override + public BlockState setBlockState(final @NotNull BlockPos pos, final @NotNull BlockState state, final boolean moved) { + if (this.isOutsideBuildHeight(pos)) { + return null; + } + return getSection(getSectionIndex(pos.getY())).setBlockState(pos.getX() & 15, pos.getY() & 15, pos.getZ() & 15, state); + } + + @Override + public void setBlockEntity(final @NotNull BlockEntity blockEntity) { + if (this.isOutsideBuildHeight(blockEntity.getBlockPos())) { + return; + } + super.setBlockEntity(blockEntity); + } + + @Override + public void addEntity(final @NotNull Entity entity) { + if (this.isOutsideBuildHeight(entity.getBlockY())) { + return; + } + super.addEntity(entity); + } + + @Override + public void removeBlockEntity(final @NotNull BlockPos pos) { + if (this.isOutsideBuildHeight(pos.getY())) { + return; + } + super.removeBlockEntity(pos); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof FaweProtoChunkAccess otherChunk)) { + return false; + } + return otherChunk.getPos().equals(this.getPos()); + } + + @Override + public int hashCode() { + return this.hashedCoords; + } + + @Override + public String toString() { + return "FaweProtoChunkAccess{" + + "chunkPos=" + chunkPos + + '}'; + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkSection.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkSection.java new file mode 100644 index 000000000..a567404ba --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/FaweProtoChunkSection.java @@ -0,0 +1,134 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level; + +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeResolver; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import org.jetbrains.annotations.NotNull; + +public class FaweProtoChunkSection extends LevelChunkSection { + + private final RegenOptions options; + + public FaweProtoChunkSection(final Registry biomeRegistry, final RegenOptions options) { + super( + new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), + PalettedContainer.Strategy.SECTION_STATES, null + ), + new PalettedContainer<>( + biomeRegistry.asHolderIdMap(), + options.hasBiomeType() && options.getBiomeType() != null ? + biomeRegistry.getHolderOrThrow(ResourceKey.create( + Registries.BIOME, ResourceLocation.of(options.getBiomeType().getId(), ':') + )) : biomeRegistry.getHolderOrThrow(Biomes.PLAINS), + PalettedContainer.Strategy.SECTION_BIOMES, + null + ) + ); + this.options = options; + } + + @Override + public @NotNull BlockState setBlockState(final int x, final int y, final int z, final @NotNull BlockState state) { + return this.states.getAndSetUnchecked(x, y, z, state); + } + + @Override + public @NotNull BlockState setBlockState( + final int x, + final int y, + final int z, + final @NotNull BlockState state, + final boolean lock + ) { + return setBlockState(x, y, z, state); + } + + /** + * We don't recalculate blocks after modification, just access every chunk + * + * @return always {@code false} + */ + @Override + public boolean hasOnlyAir() { + return false; + } + + @Override + public boolean isRandomlyTicking() { + return false; + } + + @Override + public boolean isRandomlyTickingBlocks() { + return false; + } + + @Override + public boolean isRandomlyTickingFluids() { + return false; + } + + /** + * We have no working TickingList (and don't need one) and don't recalculate block counts (see {@link #hasOnlyAir()}) + */ + @Override + public void recalcBlockCounts() { + } + + /** + * If a custom BiomeType has been supplied by the user, don't allow any biome writes into the palette + */ + @Override + public void setBiome(final int x, final int y, final int z, final @NotNull Holder biome) { + if (options.hasBiomeType()) { + return; + } + super.setBiome(x, y, z, biome); + } + + /** + * If a custom BiomeType has been supplied by the user, we don't write into the data palette as the default value is + * already the user defined biome type. + */ + @Override + public void fillBiomesFromNoise( + final @NotNull BiomeResolver biomeSupplier, + final Climate.@NotNull Sampler sampler, + final int x, + final int y, + final int z + ) { + if (options.hasBiomeType()) { + // Don't populate anything - is handled by default palette value set in constructor + return; + } + super.fillBiomesFromNoise(biomeSupplier, sampler, x, y, z); + } + + /** + * No need for locking + */ + @Override + public void acquire() { + } + + /** + * No need for locking + */ + @Override + public void release() { + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/NoopProtoChunkTick.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/NoopProtoChunkTick.java new file mode 100644 index 000000000..27041061b --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/level/NoopProtoChunkTick.java @@ -0,0 +1,45 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.level; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.ProtoChunkTicks; +import net.minecraft.world.ticks.SavedTick; +import net.minecraft.world.ticks.ScheduledTick; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Function; + +public class NoopProtoChunkTick extends ProtoChunkTicks { + + public static ProtoChunkTicks BLOCKS = new NoopProtoChunkTick<>(); + public static ProtoChunkTicks FLUIDS = new NoopProtoChunkTick<>(); + + @Override + public @NotNull List> scheduledTicks() { + return List.of(); + } + + @Override + public void schedule(final @NotNull ScheduledTick orderedTick) { + } + + @Override + public boolean hasScheduledTick(final @NotNull BlockPos pos, final @NotNull T type) { + return false; + } + + @Override + public int count() { + return 0; + } + + @Override + public @NotNull Tag save(final long time, final @NotNull Function typeToNameFunction) { + return new ListTag(); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/noop/NoopLightEngine.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/noop/NoopLightEngine.java new file mode 100644 index 000000000..61d7e4bf9 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/noop/NoopLightEngine.java @@ -0,0 +1,33 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.noop; + +import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.util.thread.ProcessorMailbox; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LightChunkGetter; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class NoopLightEngine extends ThreadedLevelLightEngine { + + private static final ProcessorMailbox MAILBOX = ProcessorMailbox.create(task -> { + }, "fawe-no-op"); + private static final ProcessorHandle> HANDLE = ProcessorHandle.of( + "fawe-no-op", + m -> { + } + ); + + public NoopLightEngine(final LightChunkGetter lightChunkGetter) { + //noinspection DataFlowIssue - nobody cares if the ChunkMap is null + super(lightChunkGetter, null, false, MAILBOX, HANDLE); + } + + @Override + public @NotNull CompletableFuture lightChunk(final @NotNull ChunkAccess chunk, final boolean excludeBlocks) { + return CompletableFuture.completedFuture(chunk); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/BiomesWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/BiomesWorker.java new file mode 100644 index 000000000..ce6261b58 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/BiomesWorker.java @@ -0,0 +1,50 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +public class BiomesWorker extends VersionedChunkWorker { + + public static final VersionedChunkWorker INSTANCE = new BiomesWorker(); + + public BiomesWorker() { + super(ChunkStatus.BIOMES, false); + } + + @Override + public CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ) { + /* + * Clone the original RandomState into a new wrapper with the new seed provided by the options (if applicable) + */ + final RandomState seededRandomState = seededRandomState( + level.chunkSource.randomState(), + level, + chunkGenerator, + regenOptions + ); + return chunkGenerator.createBiomes( + executor.get(), seededRandomState, Blender.empty(), level.structureManager(), chunks.get(chunks.size() / 2) + ).thenApply(chunkAccess -> true); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/CarversWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/CarversWorker.java new file mode 100644 index 000000000..1c8a9ac6b --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/CarversWorker.java @@ -0,0 +1,42 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.GenerationStep; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +public class CarversWorker extends VersionedChunkWorker { + + public static final VersionedChunkWorker INSTANCE = new CarversWorker(); + + public CarversWorker() { + super(ChunkStatus.CARVERS, true); + } + + @Override + public CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ) { + return CompletableFuture.runAsync(() -> chunkGenerator.applyCarvers( + worldGenRegion, regenOptions.getSeed().orElse(level.getSeed()), + seededRandomState(level.chunkSource.randomState(), level, chunkGenerator, regenOptions), + level.getBiomeManager(), level.structureManager(), chunks.get(chunks.size() / 2), GenerationStep.Carving.AIR + ), executor.get()).thenApply(unused -> true); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/FeaturesWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/FeaturesWorker.java new file mode 100644 index 000000000..b2370cabc --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/FeaturesWorker.java @@ -0,0 +1,39 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +public class FeaturesWorker extends VersionedChunkWorker { + + public static final VersionedChunkWorker INSTANCE = new FeaturesWorker(); + + public FeaturesWorker() { + super(ChunkStatus.FEATURES, false); + } + + @Override + public CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ) { + return CompletableFuture.runAsync(() -> chunkGenerator.applyBiomeDecoration( + worldGenRegion, chunks.get(chunks.size() / 2), level.structureManager() + ), executor.get()).thenApply(unused -> true); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/NoiseWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/NoiseWorker.java new file mode 100644 index 000000000..e542d3c65 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/NoiseWorker.java @@ -0,0 +1,44 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.blending.Blender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +public class NoiseWorker extends VersionedChunkWorker { + + public static final VersionedChunkWorker INSTANCE = new NoiseWorker(); + + public NoiseWorker() { + super(ChunkStatus.NOISE, true); + } + + @Override + public CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ) { + return chunkGenerator.fillFromNoise( + executor.get(), + Blender.empty(), + seededRandomState(level.chunkSource.randomState(), level, chunkGenerator, regenOptions), + level.structureManager(), + chunks.get(chunks.size() / 2) + ).thenApply(chunkAccess -> true); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/SurfaceWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/SurfaceWorker.java new file mode 100644 index 000000000..9d127c96c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/SurfaceWorker.java @@ -0,0 +1,41 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +public class SurfaceWorker extends VersionedChunkWorker { + + public static final VersionedChunkWorker INSTANCE = new SurfaceWorker(); + + public SurfaceWorker() { + super(ChunkStatus.SURFACE, false); + } + + @Override + public CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ) { + return CompletableFuture.runAsync(() -> chunkGenerator.buildSurface( + worldGenRegion, level.structureManager(), + seededRandomState(level.chunkSource.randomState(), level, chunkGenerator, regenOptions), + chunks.get(chunks.size() / 2) + ), executor.get()).thenApply(unused -> true); + } + +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/VersionedChunkWorker.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/VersionedChunkWorker.java new file mode 100644 index 000000000..6a7cec2f9 --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/regenerator/worker/VersionedChunkWorker.java @@ -0,0 +1,68 @@ +package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_20_R2.regenerator.worker; + +import com.fastasyncworldedit.bukkit.adapter.regeneration.ChunkWorker; +import com.sk89q.worldedit.world.RegenOptions; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.RandomState; + +public abstract class VersionedChunkWorker extends ChunkWorker { + + private final ChunkStatus status; + private final boolean primeHeightmapsAfter; + + public VersionedChunkWorker(final ChunkStatus status, final boolean primeHeightmapsAfter) { + this.status = status; + this.primeHeightmapsAfter = primeHeightmapsAfter; + } + + protected RandomState seededRandomState( + final RandomState originalState, + final ServerLevel level, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions + ) { + return regenOptions.getSeed().stream().mapToObj(value -> { + if (chunkGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) { + return RandomState.create( + noiseBasedChunkGenerator.generatorSettings().value(), + level.registryAccess().lookupOrThrow(Registries.NOISE), + value + ); + } + return RandomState.create( + NoiseGeneratorSettings.dummy(), + level.registryAccess().lookupOrThrow(Registries.NOISE), + value + ); + }).findFirst().orElse(originalState); + } + + @Override + public ChunkStatus status() { + return this.status; + } + + @Override + public ChunkStatus previousStatus() { + return status.getParent(); + } + + @Override + public ChunkStatus nextStatus() { + return status.getNextStatus(); + } + + @Override + public boolean shouldPrimeHeightmapsAfter() { + return this.primeHeightmapsAfter; + } + +} diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java index 607bc75bf..7a4732a8e 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java @@ -68,7 +68,7 @@ public abstract class Regenerator The native (versioned) ServerLevel + * @param The native (versioned) ServerLevel + * @param The native (versioned) ChunkAccess + * @param The native (versioned) ChunkGenerator (NMS, not CB) + * @param The native (versioned) ChunkStatus + */ +public abstract class ChunkWorker { + + /** + * Do the actual work by calling the native code for generation this specific step. + * + * @param level The level being assigned to the {@link ChunkAccess} for accessing original data like seeds. + * @param chunks The {@link ChunkAccess ChunkAccesses} to be worked on. + * @param worldGenRegion The assigned {@link WorldGenRegion} for the {@code chunks} and this specific step. + * @param chunkGenerator The original {@link ChunkGenerator} assigned to the {@link ServerLevel level}. + * @param regenOptions The possible options as passed by the api or command parameters. + * @param region The selected region by the user to be regenerated. + * @param executor The executor used to schedule asynchronous tasks. + * @return A future resolving when this step has finished containing its result (success / failure) + */ + public abstract CompletableFuture work( + final ServerLevel level, + final List chunks, + final WorldGenRegion worldGenRegion, + final ChunkGenerator chunkGenerator, + final RegenOptions regenOptions, + final Region region, + final Supplier executor + ); + + /** + * @return The native {@link ChunkStatus} assigned to this generation step. + */ + public abstract ChunkStatus status(); + + /** + * @return The previous {@link ChunkStatus} as defined by {@link #status()}. + */ + public abstract ChunkStatus previousStatus(); + + /** + * @return The following {@link ChunkStatus} as defined by {@link #status()}. + */ + public abstract ChunkStatus nextStatus(); + + /** + * @return {@code true} if heightmaps shall be primed after this step has finished. + */ + public abstract boolean shouldPrimeHeightmapsAfter(); + +} diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/Regenerator.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/Regenerator.java new file mode 100644 index 000000000..444fa1d87 --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/Regenerator.java @@ -0,0 +1,314 @@ +package com.fastasyncworldedit.bukkit.adapter.regeneration; + +import com.fastasyncworldedit.bukkit.adapter.regeneration.queue.QueuedRegenerationSection; +import com.fastasyncworldedit.core.util.TaskManager; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; +import org.bukkit.World; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * Abstraction of World-Regeneration logic. Contains boilerplate code for faster versioned implementation. + *

+ * Regeneration happens in batches of 4 chunks at a time to reduce memory overhead: When regenerating chunks some generation + * steps require adjacent chunks to be loaded and populated as well - whereas the maximum radius is 8. Therefor for each chunk + * generating, {@code 289} chunks have to be kept in memory each. Adjacent chunks may not be shared due to parallelism and + * potential different states and differing {@link ChunkStatus} which will lead to generation failure. + * + * @param The native (versioned) ServerLevel + * @param The native (versioned) ServerLevel + * @param The native (versioned) ChunkAccess + * @param The native (versioned) ChunkGenerator (NMS, not CB) + * @param The native (versioned) ChunkStatus + */ +public abstract class Regenerator + implements Closeable { + + public static final int TASK_MARGIN = 8; + protected static final int[] PLACEMENT_RADII = new int[]{-1, 0, 0, 0, 1}; + private static final short QUEUE_LIMIT = 4; + private static final int PER_CHUNK_CACHE_SIZE = (TASK_MARGIN + TASK_MARGIN + 1) * (TASK_MARGIN + TASK_MARGIN + 1); + + protected final World world; + protected final Region region; + protected final RegenOptions options; + protected final Extent extent; + + private final Queue queue; + + /** + * Instantiates a new Regeneration-Context for single-use only. + * + * @param world The world containing the regeneration selection. + * @param region The selected region by the user to be regenerated. + * @param options The possible options as passed by the api or command parameters. + * @param extent The outgoing extent to where the regenerated blocks shall be flushed. + */ + protected Regenerator(final World world, final Region region, final RegenOptions options, final Extent extent) { + this.world = world; + this.region = region; + this.options = options; + this.extent = extent; + this.queue = new ArrayDeque<>((int) Math.ceil((double) region.getChunks().size() / QUEUE_LIMIT)); + this.populateQueue(); + } + + /** + * Simply triggers the initial {@link #poll()} which will also recursively trigger all other chunks. + * + * @return {@code true} if all chunks fully regenerated inside the selection. + */ + public CompletableFuture regenerate() { + return this.poll(); + } + + /** + * Regenerates a single chunk by populating the squared list containing the target chunk and their required adjacent chunks. + * Triggers {@link #runWorker(int, List)} with the index {@code 0} to start the worker chain. + * + * @param chunk The chunk position (absolute) to be regenerated. + * @return The Future completing when the chunk is regenerated. + * @see #runWorker(int, List) + */ + public CompletableFuture regenerateChunk(BlockVector2 chunk) { + final ServerLevel level = serverLevel(); + // Has to be ordered, as generating chunks are often get by the index: size(list) / 2 + final List accesses = new ArrayList<>(PER_CHUNK_CACHE_SIZE); + for (int x = chunk.getX() - TASK_MARGIN; x <= chunk.getX() + TASK_MARGIN; x++) { + for (int z = chunk.getZ() - TASK_MARGIN; z <= chunk.getZ() + TASK_MARGIN; z++) { + accesses.add(toNativeChunkAccess(level, chunk, x, z, workers().get(0).previousStatus())); + } + } + return runWorker(0, accesses); + } + + + /** + * Polls the next {@link QueuedRegenerationSection} to be regenerated. If the queue is empty after + * {@link Queue#poll() polling} the next {@link QueuedRegenerationSection}, no further calls to this method are + * allowed. + *

+ * Automatically calls this method again after regeneration the polled {@link QueuedRegenerationSection} if the queue + * contains further items. Their results are combined. + * + * @return A Future resolving when the polled {@link QueuedRegenerationSection} has finished regenerating. + * Contains {@code true} if every section has regenerated successfully. Otherwise {@code false}. + */ + private @NonNull CompletableFuture<@NonNull Boolean> poll() { + final QueuedRegenerationSection section = this.queue.poll(); + if (section == null) { + throw new IllegalStateException("Attempted to call #poll() while Queue is empty"); + } + CompletableFuture future = section.regenerate(); + if (this.queue.peek() != null) { + // Basically, regenerate the polled section, then: + // If successful, call this method again after regeneration and return its result (as that would be the logical + // combination either way, as we know this step was successful) + // If failed, return false and don't continue regenerating. Nobody want's a half-cooked chicken. + future = future.thenCompose(thisResult -> thisResult ? poll() : CompletableFuture.completedFuture(false)); + } + return future; + } + + /** + * Executes a {@link ChunkWorker} based on their index by accessing {@link #workers()}. + * A {@link ChunkWorker} represents a generation step as defined in {@link ChunkStatus} as well. + * + * @param workerIndex The index of the {@link ChunkWorker} in the list to execute. + * @param chunkAccesses The list of {@link ChunkAccess ChunkAccesses} for regeneration. + * @return A future resolving when the generation step finished containing its result (success / failed). + */ + private CompletableFuture runWorker( + final int workerIndex, + final List chunkAccesses + ) { + final ServerLevel level = serverLevel(); + final ChunkAccess center = chunkAccesses.get(chunkAccesses.size() / 2); + final ChunkWorker worker = + workers().get(workerIndex); + final WorldGenRegion worldGenRegion = worldGenRegionForRegion(level, chunkAccesses, region, worker.status()); + + CompletableFuture result = worker.work( + level, chunkAccesses, worldGenRegion, generatorFromLevel(level), this.options, this.region, provideExecutor() + ).thenCompose(success -> { + // The ChunkStatus must always be set to the previously (finished) generation step after finished. + setChunkStatus(chunkAccesses, worker.status()); + // If this worker step was successful, do post-processing (like priming heightmaps) + if (success) { + return postProcessChunkGeneration(worker, center).thenApply(unused -> true); + } + return CompletableFuture.completedFuture(false); + }); + // If we finished the last worker step, flush all changes into the world (EditSession) and complete the future + if (workerIndex >= workers().size() - 1) { + return result.thenCompose(success -> success ? flushPalettesIntoWorld(center, worldGenRegion) : + CompletableFuture.completedFuture(false)); + } + // If there are still more worker steps, enqueue the next one (if the current step finished successfully) + return result.thenCompose(success -> { + if (success) { + return runWorker(workerIndex + 1, chunkAccesses); + } + throw new CompletionException( + "Cant enqueue next worker", + new RuntimeException("Worker " + worker.status() + " returned non-true result for " + center) + ); + }); + } + + /** + * Optional post-processing of a chunk after finishing each {@link ChunkWorker}. + * Currently only re-calculates heightmaps for the next {@link ChunkWorker} if + * {@link ChunkWorker#shouldPrimeHeightmapsAfter()} is {@code true} + * + * @param worker The recently finished {@link ChunkWorker}. + * @param chunkAccess The target {@link ChunkAccess} which was populated using the worker. + * @return A future resolving {@code null} when the heightmap calculation finished. + */ + private CompletableFuture postProcessChunkGeneration( + ChunkWorker worker, + ChunkAccess chunkAccess + ) { + if (worker.shouldPrimeHeightmapsAfter()) { + return primeHeightmaps(chunkAccess); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Divides the Chunks inside the provided {@link Region} into smaller batches as limited by {@link #QUEUE_LIMIT}. + */ + private void populateQueue() { + final BlockVector2[] chunks = this.region.getChunks().toArray(BlockVector2[]::new); + // Sort, so that adjacent chunks are generated in batches + Arrays.sort(chunks, Comparator.comparingInt(BlockVector2::getX).thenComparing(BlockVector2::getZ)); + final int queued = (int) Math.ceil((double) chunks.length / QUEUE_LIMIT); + final int divisor = chunks.length % QUEUE_LIMIT; + for (int i = 0; i < queued; i++) { + final boolean last = i == queued - 1; + final int start = i * QUEUE_LIMIT; + final int end = start + (last && divisor != 0 ? divisor : QUEUE_LIMIT); + this.queue.add(new QueuedRegenerationSection(this, Arrays.copyOfRange(chunks, start, end))); + } + } + + @Override + public void close() throws IOException { + } + + /** + * May be overridden in version specific code, if required. + * + * @return A supplier providing an executor for asynchronous tasks. + */ + public @NonNull Supplier<@NonNull Executor> provideExecutor() { + return () -> task -> TaskManager.taskManager().async(task); + } + + /** + * Places all blocks, block entities and biomes into the actual work by accessing the block- and biome-palettes of the + * chunk and setting their native contents into the {@link #extent passed Extent}. + * + * @param chunkAccess The fully-regenerated chunk populated with blocks and biomes. + * @param worldGenRegion The associated {@link WorldGenRegion} to provide access to biomes. + * @return A future resolving when all changes are flushed containing its result (success / failure). + */ + public abstract CompletableFuture flushPalettesIntoWorld( + final ChunkAccess chunkAccess, final WorldGenRegion worldGenRegion + ); + + /** + * Attempts to either create a new native {@link ChunkAccess} or get a cached version of an already created one. + * If the cached {@link ChunkAccess} current {@link ChunkStatus} is less than {@code leastStatus} (as defined in the + * ordinal order in native code) it's expected to return {@code null}. + *
+ * This method must return or create a chunk for out-of-bounds chunks (as defined by the selected {@link #region}) + * as those are created for the required placement radii of certain generation stages. These {@link ChunkAccess accesses + * } shall not flush or set their changes into the {@link #extent outgoing Extent}. + * + * @param level The level containing the chunk (based on {@link #world} and {@link #serverLevel()}). + * @param contextChunk The working context for which the chunk should be converted (= working chunk coordinate). + * @param x The x coordinate of the native {@link ChunkAccess}. + * @param z The z coordinate of the native {@link ChunkAccess}. + * @param leastStatus The current status of the chunk to be set. + * @return The cached {@link ChunkAccess} if existing and applicable status, otherwise created or {@code null}. + */ + protected abstract @Nullable ChunkAccess toNativeChunkAccess( + final ServerLevel level, final BlockVector2 contextChunk, final int x, final int z, final ChunkStatus leastStatus + ); + + /** + * Prime (recalculate) heightmaps of noised {@link ChunkAccess ChunkAccesses}. + * This method must process all required heightmaps required by following steps. + * Results must be written back into the native {@link ChunkAccess} for access by following generation stages. (Extents + * don't have any functionality to store those results). + * + * @param chunk The {@link ChunkAccess} to prime the heightmaps for. + * @return A Future resolving when the priming / calculation finished. + */ + protected abstract @NonNull CompletableFuture<@Nullable Void> primeHeightmaps(ChunkAccess chunk); + + /** + * Create a new native {@link WorldGenRegion} for the passed {@link ChunkStatus} (as those differ in {@code placement + * radii} spanning the passed {@link Region}. + * + * @param level The {@link ServerLevel} to be wrapped. + * @param chunkAccesses The containing {@link ChunkAccess ChunkAccesses} for the {@link WorldGenRegion}. + * @param region The {@link Region} to be contained by the {@link WorldGenRegion}. + * @param status The {@link ChunkStatus} for the {@link WorldGenRegion} to be created. + * @return The new {@link WorldGenRegion}. + */ + protected abstract WorldGenRegion worldGenRegionForRegion( + final ServerLevel level, + final List chunkAccesses, + final Region region, + final ChunkStatus status + ); + + /** + * Getter-Abstraction to retrieve the native {@link ChunkGenerator} from a {@link ServerLevel}. + * + * @param level The {@link ServerLevel} to get the {@link ChunkGenerator} from. + * @return The original {@link ChunkGenerator} of the {@link ServerLevel}. + */ + protected abstract ChunkGenerator generatorFromLevel(ServerLevel level); + + /** + * Set's the {@link ChunkStatus} for all {@link ChunkAccess ChunkAccesses} after finishing a generation step. + * + * @param chunkAccesses The {@link ChunkAccess ChunkAccesses} to be set the new {@link ChunkStatus} + * @param status The {@link ChunkStatus} by the recently finished {@link ChunkWorker} ({@link ChunkWorker#status()}) + */ + protected abstract void setChunkStatus(List chunkAccesses, ChunkStatus status); + + /** + * A list of all versioned {@link ChunkWorker ChunkWorkers} to fully regenerate the selection. Must be in order. + * + * @return All {@link ChunkWorker ChunkWorkers}. + */ + protected abstract @NonNull List<@NonNull ChunkWorker> workers(); + + /** + * Getter-Abstraction to retrieve the native {@link ServerLevel} from the passed {@link #world World}. + * + * @return The native assigned {@link ServerLevel} for the {@link World Bukkit World} + */ + protected abstract @NonNull ServerLevel serverLevel(); + +} diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/queue/QueuedRegenerationSection.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/queue/QueuedRegenerationSection.java new file mode 100644 index 000000000..42743960f --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/regeneration/queue/QueuedRegenerationSection.java @@ -0,0 +1,37 @@ +package com.fastasyncworldedit.bukkit.adapter.regeneration.queue; + +import com.fastasyncworldedit.bukkit.adapter.regeneration.Regenerator; +import com.sk89q.worldedit.math.BlockVector2; + +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * A section of the to-regenerated selection. + */ +public class QueuedRegenerationSection { + + private final Regenerator regenerator; + private final BlockVector2[] chunks; + + public QueuedRegenerationSection(final Regenerator regenerator, final BlockVector2[] chunks) { + this.regenerator = regenerator; + this.chunks = chunks; + } + + /** + * Start the regeneration process for this part of the selection. + * @return A future completing when all {@link #chunks} are regenerated including their logically combined results + * (successful / failed) + */ + public CompletableFuture regenerate() { + final Set> futures = Arrays.stream(chunks) + .map(this.regenerator::regenerateChunk) + .collect(Collectors.toUnmodifiableSet()); + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) + .thenApply(unused -> futures.stream().allMatch(CompletableFuture::join)); + } + +} diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateRegionManager.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateRegionManager.java index 4ef1a3d05..0e97a501b 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateRegionManager.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateRegionManager.java @@ -27,6 +27,7 @@ import com.sk89q.worldedit.function.visitor.FlatRegionVisitor; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockType; @@ -57,25 +58,26 @@ public class FaweDelegateRegionManager { TaskManager.taskManager().async(() -> { synchronized (FaweDelegateRegionManager.class) { World world = BukkitAdapter.adapt(getWorld(area.getWorldName())); - EditSession session = WorldEdit.getInstance().newEditSessionBuilder().world(world).checkMemory(false). - fastMode(true).limitUnlimited().changeSetNull().build(); - for (CuboidRegion region : regions) { - region.setPos1(region.getPos1().withY(minY)); - region.setPos2(region.getPos2().withY(maxY)); - session.setBlocks((Region) region, blocks); - } - try { - session.flushQueue(); + try(final EditSession session = WorldEdit.getInstance().newEditSessionBuilder().world(world).checkMemory(false). + fastMode(true).limitUnlimited().changeSetNull().build()) { for (CuboidRegion region : regions) { - FaweAPI.fixLighting(world, region, null, - RelightMode.valueOf(com.fastasyncworldedit.core.configuration.Settings.settings().LIGHTING.MODE) - ); + region.setPos1(region.getPos1().withY(minY)); + region.setPos2(region.getPos2().withY(maxY)); + session.setBlocks((Region) region, blocks); } - } catch (MaxChangedBlocksException e) { - e.printStackTrace(); - } finally { - if (whenDone != null) { - TaskManager.taskManager().task(whenDone); + try { + session.flushQueue(); + for (CuboidRegion region : regions) { + FaweAPI.fixLighting(world, region, null, + RelightMode.valueOf(com.fastasyncworldedit.core.configuration.Settings.settings().LIGHTING.MODE) + ); + } + } catch (MaxChangedBlocksException e) { + e.printStackTrace(); + } finally { + if (whenDone != null) { + TaskManager.taskManager().task(whenDone); + } } } } @@ -358,19 +360,18 @@ public class FaweDelegateRegionManager { public boolean regenerateRegion(final Location pos1, final Location pos2, boolean ignore, final Runnable whenDone) { TaskManager.taskManager().async(() -> { synchronized (FaweDelegateRegionManager.class) { - World pos1World = BukkitAdapter.adapt(getWorld(pos1.getWorldName())); - try (EditSession editSession = WorldEdit.getInstance().newEditSessionBuilder().world(pos1World) + World world = BukkitAdapter.adapt(getWorld(pos1.getWorldName())); + try (final EditSession editSession = WorldEdit.getInstance().newEditSessionBuilder().world(world) .checkMemory(false) .fastMode(true) .limitUnlimited() .changeSetNull() .build()) { - CuboidRegion region = new CuboidRegion( - BlockVector3.at(pos1.getX(), pos1.getY(), pos1.getZ()), - BlockVector3.at(pos2.getX(), pos2.getY(), pos2.getZ()) + world.regenerate( + new CuboidRegion(pos1.getBlockVector3(), pos2.getBlockVector3()), + editSession, RegenOptions.builder().regenBiomes(true).build() ); - editSession.regenerate(region); - editSession.flushQueue(); + Operations.completeBlindly(editSession.commit()); } if (whenDone != null) { TaskManager.taskManager().task(whenDone); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/PositionTransformExtent.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/PositionTransformExtent.java index 6d74acc16..50ce8aeec 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/PositionTransformExtent.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/PositionTransformExtent.java @@ -98,6 +98,11 @@ public class PositionTransformExtent extends ResettableExtent { return super.setBiome(getPos(mutable.getX(), mutable.getY(), mutable.getZ()), biome); } + @Override + public boolean setBiome(final int x, final int y, final int z, final BiomeType biome) { + return super.setBiome(getPos(x, y, z), biome); + } + public void setTransform(Transform transform) { this.transform = transform; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/Either.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/Either.java new file mode 100644 index 000000000..54e36b041 --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/Either.java @@ -0,0 +1,37 @@ +package com.fastasyncworldedit.core.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.function.Function; + +public record Either(@Nullable L left, @Nullable R right) { + + public Either map(@NonNull Function leftMapper, @NonNull Function rightMapper) { + if (left() != null) { + return new Either<>(leftMapper.apply(left()), null); + } + if (right() != null) { + return new Either<>(null, rightMapper.apply(right())); + } + return new Either<>(null, null); + } + + public Result accept( + @NonNull Function lConsumer, @NonNull Function rConsumer, + @NonNull Result nullFallback + ) { + if (left() != null) { + return lConsumer.apply(left()); + } + if (right() != null) { + return rConsumer.apply(right()); + } + return nullFallback; + } + + public static Either empty() { + return new Either<>(null, null); + } + +} diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java index 7abb51284..6cb43e714 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java @@ -8,9 +8,11 @@ import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; import com.sk89q.worldedit.util.nbt.IntBinaryTag; import com.sk89q.worldedit.util.nbt.ShortBinaryTag; import com.sk89q.worldedit.world.storage.InvalidFormatException; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashMap; import java.util.Map; +import java.util.UUID; public class NbtUtils { @@ -80,4 +82,38 @@ public class NbtUtils { return value; } + /** + * Gets the uuid out of an {@link CompoundBinaryTag} + * + * @param tag The tag containing the potential UUID + * @return the UUID or {@code null} if no format was found + */ + public static @Nullable UUID getUuid(CompoundBinaryTag tag) { + final int[] uuidTag = tag.getIntArray("UUID"); + + if (uuidTag.length > 0) { + return new UUID( + (long) uuidTag[0] << 32 | (uuidTag[1] & 0xFFFFFFFFL), + (long) uuidTag[2] << 32 | (uuidTag[3] & 0xFFFFFFFFL) + ); + } + + final long uuidMost = tag.getLong("UUIDMost"); + if (uuidMost != 0) { + return new UUID(uuidMost, tag.getLong("UUIDLeast")); + } + + final long worldUuidMost = tag.getLong("WorldUUIDMost"); + if (worldUuidMost != 0) { + return new UUID(uuidMost, tag.getLong("WorldUUIDLeast")); + } + + final long persistentIdmsb = tag.getLong("PersistentIDMSB"); + if (persistentIdmsb != 0) { + return new UUID(persistentIdmsb, tag.getLong("PersistentIDLSB")); + } + + return null; + } + } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/ReflectionUtils.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/ReflectionUtils.java index 7b8ffcd2c..f3fd3d4ec 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/ReflectionUtils.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/ReflectionUtils.java @@ -1,5 +1,6 @@ package com.fastasyncworldedit.core.util; +import org.checkerframework.checker.nullness.qual.NonNull; import sun.misc.Unsafe; import javax.annotation.Nonnull; @@ -10,6 +11,8 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.function.Function; +import java.util.function.Supplier; /** * This is an internal class not meant to be used outside the FAWE internals. @@ -121,6 +124,32 @@ public class ReflectionUtils { } } + @SuppressWarnings("unchecked") + public static T getFieldThrowing(@Nonnull Field field, Object instance) throws IllegalAccessException { + field.setAccessible(true); + return (T) field.get(instance); + } + + @SuppressWarnings("unchecked") + public static T getFieldOr(@Nonnull Field field, Object instance, @NonNull Supplier fallback) { + field.setAccessible(true); + try { + return (T) field.get(instance); + } catch (IllegalAccessException e) { + return fallback.get(); + } + } + + @SuppressWarnings("unchecked") + public static T getFieldOr(@Nonnull Field field, Object instance, @NonNull Function fallback) { + field.setAccessible(true); + try { + return (T) field.get(instance); + } catch (IllegalAccessException e) { + return fallback.apply(e); + } + } + public static Class getClass(String name) { try { return Class.forName(name); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java index 7b6be41d2..422a21194 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java @@ -201,6 +201,9 @@ public class BlockArrayClipboard implements Clipboard { //FAWE start @Override public boolean setTile(int x, int y, int z, CompoundTag tag) { + if (!region.contains(x, y, z)) { + return false; + } x -= offset.getX(); y -= offset.getY(); z -= offset.getZ(); @@ -214,6 +217,9 @@ public class BlockArrayClipboard implements Clipboard { @Override public > boolean setBlock(int x, int y, int z, B block) throws WorldEditException { + if (!region.contains(x, y, z)) { + return false; + } x -= offset.getX(); y -= offset.getY(); z -= offset.getZ(); @@ -238,6 +244,9 @@ public class BlockArrayClipboard implements Clipboard { @Override public boolean setBiome(BlockVector3 position, BiomeType biome) { + if (!region.contains(position)) { + return false; + } int x = position.getBlockX() - offset.getX(); int y = position.getBlockY() - offset.getY(); int z = position.getBlockZ() - offset.getZ(); @@ -246,6 +255,9 @@ public class BlockArrayClipboard implements Clipboard { @Override public boolean setBiome(int x, int y, int z, BiomeType biome) { + if (!region.contains(x, y, z)) { + return false; + } x -= offset.getX(); y -= offset.getY(); z -= offset.getZ();