All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fastasyncworldedit.bukkit.adapter.Regenerator Maven / Gradle / Ivy

package com.fastasyncworldedit.bukkit.adapter;

import com.fastasyncworldedit.core.configuration.Settings;
import com.fastasyncworldedit.core.queue.IChunkCache;
import com.fastasyncworldedit.core.queue.IChunkGet;
import com.fastasyncworldedit.core.queue.implementation.SingleThreadQueueExtent;
import com.fastasyncworldedit.core.util.MathMan;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.bukkit.BukkitWorld;
import com.sk89q.worldedit.extent.Extent;
import com.sk89q.worldedit.function.pattern.Pattern;
import com.sk89q.worldedit.internal.util.LogManagerCompat;
import com.sk89q.worldedit.math.BlockVector2;
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.biome.BiomeType;
import com.sk89q.worldedit.world.block.BaseBlock;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.apache.logging.log4j.Logger;
import org.bukkit.generator.BlockPopulator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Represents an abstract regeneration handler.
 *
 * @param  the type of the {@code IChunkAccess} of the current Minecraft implementation
 * @param    the type of the {@code ProtoChunk} of the current Minecraft implementation
 * @param         the type of the {@code Chunk} of the current Minecraft implementation
 * @param   the type of the {@code ChunkStatusWrapper} wrapping the {@code ChunkStatus} enum
 */
public abstract class Regenerator> {

    private static final Logger LOGGER = LogManagerCompat.getLogger();

    protected final org.bukkit.World originalBukkitWorld;
    protected final Region region;
    protected final Extent target;
    protected final RegenOptions options;

    //runtime
    protected final Map chunkStati = new LinkedHashMap<>();
    private final Long2ObjectLinkedOpenHashMap protoChunks = new Long2ObjectLinkedOpenHashMap<>();
    private final Long2ObjectOpenHashMap chunks = new Long2ObjectOpenHashMap<>();
    protected boolean generateConcurrent = true;
    protected long seed;
    private ExecutorService executor;
    private SingleThreadQueueExtent source;

    /**
     * Initializes an abstract regeneration handler.
     *
     * @param originalBukkitWorld the Bukkit world containing all the information on how to regenerate the {code Region}
     * @param region              the selection to regenerate
     * @param target              the target {@code Extent} to paste the regenerated blocks into
     * @param options             the options to used while regenerating and pasting into the target {@code Extent}
     */
    public Regenerator(org.bukkit.World originalBukkitWorld, Region region, Extent target, RegenOptions options) {
        this.originalBukkitWorld = originalBukkitWorld;
        this.region = region;
        this.target = target;
        this.options = options;
    }

    private static Random getChunkRandom(long worldseed, int x, int z) {
        Random random = new Random();
        random.setSeed(worldseed);
        long xRand = random.nextLong() / 2L * 2L + 1L;
        long zRand = random.nextLong() / 2L * 2L + 1L;
        random.setSeed((long) x * xRand + (long) z * zRand ^ worldseed);
        return random;
    }

    /**
     * Regenerates the selected {@code Region}.
     *
     * @return whether or not the regeneration process was successful
     * @throws Exception when something goes terribly wrong
     */
    public boolean regenerate() throws Exception {
        if (!prepare()) {
            return false;
        }

        try {
            if (!initNewWorld()) {
                cleanup0();
                return false;
            }
        } catch (Exception e) {
            cleanup0();
            throw e;
        }

        try {
            if (!generate()) {
                cleanup0();
                return false;
            }
        } catch (Exception e) {
            cleanup0();
            throw e;
        }

        try {
            copyToWorld();
        } catch (Exception e) {
            cleanup0();
            throw e;
        }

        cleanup0();
        return true;
    }

    /**
     * Returns the {@code ProtoChunk} at the given chunk coordinates.
     *
     * @param x the chunk x coordinate
     * @param z the chunk z coordinate
     * @return the {@code ProtoChunk} at the given chunk coordinates or null if it is not part of the regeneration process or has not been initialized yet.
     */
    protected ProtoChunk getProtoChunkAt(int x, int z) {
        return protoChunks.get(MathMan.pairInt(x, z));
    }

    /**
     * Returns the {@code Chunk} at the given chunk coordinates.
     *
     * @param x the chunk x coordinate
     * @param z the chunk z coordinate
     * @return the {@code Chunk} at the given chunk coordinates or null if it is not part of the regeneration process or has not been converted yet.
     */
    protected Chunk getChunkAt(int x, int z) {
        return chunks.get(MathMan.pairInt(x, z));
    }

    private boolean generate() throws Exception {
        if (generateConcurrent) {
            //Using concurrent chunk generation
            executor = Executors.newFixedThreadPool(Settings.settings().QUEUE.PARALLEL_THREADS, new ThreadFactoryBuilder()
                    .setNameFormat("fawe-regen-%d")
                    .build()
            );
        } // else using sequential chunk generation, concurrent not supported

        //TODO: can we get that required radius down without affecting chunk generation (e.g. strucures, features, ...)?
        //for now it is working well and fast, if we are bored in the future we could do the research (a lot of it) to reduce the border radius

        //generate chunk coords lists with a certain radius
        Int2ObjectOpenHashMap> chunkCoordsForRadius = new Int2ObjectOpenHashMap<>();
        chunkStati.keySet().stream().map(ChunkStatusWrapper::requiredNeighborChunkRadius0).distinct().forEach(radius -> {
            if (radius == -1) { //ignore ChunkStatus.EMPTY
                return;
            }
            int border = 10 - radius; //9 = 8 + 1, 8: max border radius used in chunk stages, 1: need 1 extra chunk for chunk
            // features to generate at the border of the region
            chunkCoordsForRadius.put(radius, getChunkCoordsRegen(region, border));
        });

        //create chunks
        for (Long xz : chunkCoordsForRadius.get(0)) {
            ProtoChunk chunk = createProtoChunk(MathMan.unpairIntX(xz), MathMan.unpairIntY(xz));
            protoChunks.put(xz, chunk);
        }

        //generate lists for RegionLimitedWorldAccess, need to be square with odd length (e.g. 17x17), 17 = 1 middle chunk + 8 border chunks * 2
        Int2ObjectOpenHashMap>> worldlimits = new Int2ObjectOpenHashMap<>();
        chunkStati.keySet().stream().map(ChunkStatusWrapper::requiredNeighborChunkRadius0).distinct().forEach(radius -> {
            if (radius == -1) { //ignore ChunkStatus.EMPTY
                return;
            }
            Long2ObjectOpenHashMap> map = new Long2ObjectOpenHashMap<>();
            for (Long xz : chunkCoordsForRadius.get(radius)) {
                int x = MathMan.unpairIntX(xz);
                int z = MathMan.unpairIntY(xz);
                List l = new ArrayList<>((radius + 1 + radius) * (radius + 1 + radius));
                for (int zz = z - radius; zz <= z + radius; zz++) { //order is important, first z then x
                    for (int xx = x - radius; xx <= x + radius; xx++) {
                        l.add(protoChunks.get(MathMan.pairInt(xx, zz)));
                    }
                }
                map.put(xz, l);
            }
            worldlimits.put(radius, map);
        });

        //run generation tasks excluding FULL chunk status
        for (Map.Entry entry : chunkStati.entrySet()) {
            ChunkStatus chunkStatus = entry.getKey();
            int radius = chunkStatus.requiredNeighborChunkRadius0();

            List coords = chunkCoordsForRadius.get(radius);
            if (this.generateConcurrent && entry.getValue() == Concurrency.RADIUS) {
                SequentialTasks>> tasks = getChunkStatusTaskRows(coords, radius);
                for (ConcurrentTasks> para : tasks) {
                    List scheduled = new ArrayList<>(tasks.size());
                    for (SequentialTasks row : para) {
                        scheduled.add(() -> {
                            for (Long xz : row) {
                                chunkStatus.processChunkSave(xz, worldlimits.get(radius).get(xz));
                            }
                        });
                    }
                    try {
                        List> futures = new ArrayList<>();
                        scheduled.forEach(task -> futures.add(executor.submit(task)));
                        for (Future future : futures) {
                            future.get();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } else if (this.generateConcurrent && entry.getValue() == Concurrency.FULL) {
                // every chunk can be processed individually
                List scheduled = new ArrayList<>(coords.size());
                for (long xz : coords) {
                    scheduled.add(() -> {
                        chunkStatus.processChunkSave(xz, worldlimits.get(radius).get(xz));
                    });
                }
                try {
                    List> futures = new ArrayList<>();
                    scheduled.forEach(task -> futures.add(executor.submit(task)));
                    for (Future future : futures) {
                        future.get();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else { // Concurrency.NONE or generateConcurrent == false
                // run sequential
                for (long xz : coords) {
                    chunkStatus.processChunkSave(xz, worldlimits.get(radius).get(xz));
                }
            }
        }

        //convert to proper chunks
        for (Long xz : chunkCoordsForRadius.get(0)) {
            ProtoChunk proto = protoChunks.get(xz);
            chunks.put(xz, createChunk(proto));
        }

        //final chunkstatus
        ChunkStatus FULL = getFullChunkStatus();
        for (Long xz : chunkCoordsForRadius.get(0)) { //FULL.requiredNeighbourChunkRadius() == 0!
            Chunk chunk = chunks.get(xz);
            FULL.processChunkSave(xz, Arrays.asList(chunk));
        }

        //populate
        List populators = getBlockPopulators();
        for (Long xz : chunkCoordsForRadius.get(0)) {
            int x = MathMan.unpairIntX(xz);
            int z = MathMan.unpairIntY(xz);

            //prepare chunk seed
            Random random = getChunkRandom(seed, x, z);

            //actually populate
            Chunk c = chunks.get(xz);
            populators.forEach(pop -> {
                populate(c, random, pop);
            });
        }

        source = new SingleThreadQueueExtent(BukkitWorld.HAS_MIN_Y ? originalBukkitWorld.getMinHeight() : 0,
                BukkitWorld.HAS_MIN_Y ? originalBukkitWorld.getMaxHeight() : 256);
        source.init(target, initSourceQueueCache(), null);
        return true;
    }

    private void copyToWorld() {
        //Setting Blocks
        boolean genbiomes = options.shouldRegenBiomes();
        boolean hasBiome = options.hasBiomeType();
        BiomeType biome = options.getBiomeType();
        if (!genbiomes && !hasBiome) {
            target.setBlocks(region, new PlacementPattern());
        }
        if (hasBiome) {
            target.setBlocks(region, new WithBiomePlacementPattern(ignored -> biome));
        } else if (genbiomes) {
            target.setBlocks(region, new WithBiomePlacementPattern(vec -> source.getBiome(vec)));
        }
    }

    private class PlacementPattern implements Pattern {

        @Override
        public BaseBlock applyBlock(final BlockVector3 position) {
            return source.getFullBlock(position);
        }

        @Override
        public boolean apply(final Extent extent, final BlockVector3 get, final BlockVector3 set) throws WorldEditException {
            return extent.setBlock(set.getX(), set.getY(), set.getZ(), source.getFullBlock(get.getX(), get.getY(), get.getZ()));
        }

    }

    private class WithBiomePlacementPattern implements Pattern {

        private final Function biomeGetter;

        private WithBiomePlacementPattern(final Function biomeGetter) {
            this.biomeGetter = biomeGetter;
        }

        @Override
        public BaseBlock applyBlock(final BlockVector3 position) {
            return source.getFullBlock(position);
        }

        @Override
        public boolean apply(final Extent extent, final BlockVector3 get, final BlockVector3 set) throws WorldEditException {
            return extent.setBlock(set.getX(), set.getY(), set.getZ(), source.getFullBlock(get.getX(), get.getY(), get.getZ()))
                    && extent.setBiome(set.getX(), set.getY(), set.getZ(), biomeGetter.apply(get));
        }

    }

    //functions to be implemented by sub class
    private void cleanup0() {
        if (executor != null) {
            executor.shutdownNow();
        }
        cleanup();
    }

    /**
     * 

Implement the preparation process in here. DO NOT instanciate any variable here that require the cleanup function. This function is for gathering further information before initializing a new * world.

* *

Fields required to be initialized: chunkStati, seed

*

For chunkStati also see {code ChunkStatusWrapper}.

* * @return whether or not the preparation process was successful */ protected abstract boolean prepare(); /** * Implement the creation of the seperate world in here. *

* Fields required to be initialized: generateConcurrent * * @return true if everything went fine, otherwise false. When false is returned the Regenerator halts the regeneration process and calls the cleanup function. * @throws java.lang.Exception When the implementation of this method throws and exception the Regenerator halts the regeneration process and calls the cleanup function. */ protected abstract boolean initNewWorld() throws Exception; //functions to implement by sub class - regenate related /** * Implement the cleanup of all the mess that is created during the regeneration process (initNewWorld() and generate()).This function must not throw any exceptions. */ protected abstract void cleanup(); /** * Implement the initialization of a {@code ProtoChunk} here. * * @param x the x coorinate of the {@code ProtoChunk} to create * @param z the z coorinate of the {@code ProtoChunk} to create * @return an initialized {@code ProtoChunk} */ protected abstract ProtoChunk createProtoChunk(int x, int z); /** * Implement the convertion of a {@code ProtoChunk} to a {@code Chunk} here. * * @param protoChunk the {@code ProtoChunk} to be converted to a {@code Chunk} * @return the converted {@code Chunk} */ protected abstract Chunk createChunk(ProtoChunk protoChunk); /** * Return the {@code ChunkStatus.FULL} here. * ChunkStatus.FULL is the last step of vanilla chunk generation. * * @return {@code ChunkStatus.FULL} */ protected abstract ChunkStatus getFullChunkStatus(); /** * Return a list of {@code BlockPopulator} used to populate the original world here. * * @return {@code ChunkStatus.FULL} */ protected abstract List getBlockPopulators(); /** * Implement the population of the {@code Chunk} with the given chunk random and {@code BlockPopulator} here. * * @param chunk the {@code Chunk} to populate * @param random the chunk random to use for population * @param pop the {@code BlockPopulator} to use */ protected abstract void populate(Chunk chunk, Random random, BlockPopulator pop); /** * Implement the initialization an {@code IChunkCache} here. Use will need the {@code getChunkAt} function * * @return an initialized {@code IChunkCache} */ protected abstract IChunkCache initSourceQueueCache(); //algorithms private List getChunkCoordsRegen(Region region, int border) { //needs to be square num of chunks BlockVector3 oldMin = region.getMinimumPoint(); BlockVector3 newMin = BlockVector3.at( (oldMin.getX() >> 4 << 4) - border * 16, oldMin.getY(), (oldMin.getZ() >> 4 << 4) - border * 16 ); BlockVector3 oldMax = region.getMaximumPoint(); BlockVector3 newMax = BlockVector3.at( (oldMax.getX() >> 4 << 4) + (border + 1) * 16 - 1, oldMax.getY(), (oldMax.getZ() >> 4 << 4) + (border + 1) * 16 - 1 ); Region adjustedRegion = new CuboidRegion(newMin, newMax); return adjustedRegion.getChunks().stream() .map(c -> BlockVector2.at(c.getX(), c.getZ())) .sorted(Comparator .comparingInt(BlockVector2::getZ) .thenComparingInt(BlockVector2::getX)) //needed for RegionLimitedWorldAccess .map(c -> MathMan.pairInt(c.getX(), c.getZ())) .collect(Collectors.toList()); } /** * Creates a list of chunkcoord rows that may be executed concurrently * * @param allcoords the coords that should be sorted into rows, must be sorted by z and x * @param requiredNeighborChunkRadius the radius of neighbor chunks that may not be written to concurrently (ChunkStatus * .requiredNeighborRadius) * @return a list of chunkcoords rows that may be executed concurrently */ private SequentialTasks>> getChunkStatusTaskRows( List allcoords, int requiredNeighborChunkRadius ) { int requiredneighbors = Math.max(0, requiredNeighborChunkRadius); int minx = allcoords.isEmpty() ? 0 : MathMan.unpairIntX(allcoords.get(0)); int maxx = allcoords.isEmpty() ? 0 : MathMan.unpairIntX(allcoords.get(allcoords.size() - 1)); int minz = allcoords.isEmpty() ? 0 : MathMan.unpairIntY(allcoords.get(0)); int maxz = allcoords.isEmpty() ? 0 : MathMan.unpairIntY(allcoords.get(allcoords.size() - 1)); SequentialTasks>> tasks; if (maxz - minz > maxx - minx) { int numlists = Math.min(requiredneighbors * 2 + 1, maxx - minx + 1); Int2ObjectOpenHashMap> byx = new Int2ObjectOpenHashMap(); int expectedListLength = (allcoords.size() + 1) / (maxx - minx); //init lists for (int i = minx; i <= maxx; i++) { byx.put(i, new SequentialTasks(expectedListLength)); } //sort into lists by x coord for (Long xz : allcoords) { byx.get(MathMan.unpairIntX(xz)).add(xz); } //create parallel tasks tasks = new SequentialTasks(numlists); for (int offset = 0; offset < numlists; offset++) { ConcurrentTasks> para = new ConcurrentTasks((maxz - minz + 1) / numlists + 1); for (int i = 0; minx + i * numlists + offset <= maxx; i++) { para.add(byx.get(minx + i * numlists + offset)); } tasks.add(para); } } else { int numlists = Math.min(requiredneighbors * 2 + 1, maxz - minz + 1); Int2ObjectOpenHashMap> byz = new Int2ObjectOpenHashMap(); int expectedListLength = (allcoords.size() + 1) / (maxz - minz); //init lists for (int i = minz; i <= maxz; i++) { byz.put(i, new SequentialTasks(expectedListLength)); } //sort into lists by x coord for (Long xz : allcoords) { byz.get(MathMan.unpairIntY(xz)).add(xz); } //create parallel tasks tasks = new SequentialTasks(numlists); for (int offset = 0; offset < numlists; offset++) { ConcurrentTasks> para = new ConcurrentTasks((maxx - minx + 1) / numlists + 1); for (int i = 0; minz + i * numlists + offset <= maxz; i++) { para.add(byz.get(minz + i * numlists + offset)); } tasks.add(para); } } return tasks; } //classes public enum Concurrency { FULL, RADIUS, NONE } /** * This class is used to wrap the ChunkStatus of the current Minecraft implementation and as the implementation to execute a chunk generation step. * * @param the IChunkAccess class of the current Minecraft implementation */ public static abstract class ChunkStatusWrapper { /** * Return the required neighbor chunk radius the wrapped {@code ChunkStatus} requires. * * @return the radius of required neighbor chunks */ public abstract int requiredNeighborChunkRadius(); int requiredNeighborChunkRadius0() { return Math.max(0, requiredNeighborChunkRadius()); } /** * Return the name of the wrapped {@code ChunkStatus}. * * @return the radius of required neighbor chunks */ public abstract String name(); /** * Return the name of the wrapped {@code ChunkStatus}. * * @param xz represents the chunk coordinates of the chunk to process as denoted by {@code MathMan} * @param accessibleChunks a list of chunks that will be used during the execution of the wrapped {@code ChunkStatus}. * This list is order in the correct order required by the {@code ChunkStatus}, unless Mojang suddenly decides to do things differently. */ public abstract CompletableFuture processChunk(Long xz, List accessibleChunks); void processChunkSave(Long xz, List accessibleChunks) { try { processChunk(xz, accessibleChunks).get(); } catch (Exception e) { LOGGER.error( "Error while running " + name() + " on chunk " + MathMan.unpairIntX(xz) + "/" + MathMan.unpairIntY(xz), e ); } } } public static class SequentialTasks extends Tasks { public SequentialTasks(int expectedsize) { super(expectedsize); } } public static class ConcurrentTasks extends Tasks { public ConcurrentTasks(int expectedsize) { super(expectedsize); } } public static class Tasks implements Iterable { private final List tasks; public Tasks(int expectedsize) { tasks = new ArrayList(expectedsize); } public void add(T task) { tasks.add(task); } public List list() { return tasks; } public int size() { return tasks.size(); } @Override public Iterator iterator() { return tasks.iterator(); } @Override public String toString() { return tasks.toString(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy