1.15.2 Clear Chunk to Just Air

Discussion in 'Spigot Plugin Development' started by CrypticCabub, Jan 29, 2020.

  1. Fairly quick question, how would I go about resetting the contents of a Minecraft chunk to be just air (from y = 0 to build height). I am not referring to how to iterate over the blocks in the chunk and set each one to air the slow way, but how to replace the entire chunk with air in one go so that it is significantly faster than the slow 3-d iteration over the blocks within the chunk.

    Any resources that I have missed in my own searches that go into modifying worlds on a chunk-by-chunk level are also appreciated.
     
  2. I don't believe you can do that programmatically.
     
    • Funny Funny x 2
  3. You might find some use in the Anvil API. "The Anvil API is for modifying MCA files (<world>/region). If used properly, it can be the fastest method to modify the world, however it is considerably more complicated than the other approaches."

    However, I don't believe it's updated to the latest Minecraft version (considering that it's not currently supported by FAWE itself). At the very least it might point you in the right direction.
     
  4. Arent able to load a 16*16*256 backup chunk snapchot if you want to clear the whole chunk?
     
  5. Using schematics maybe? If you use worldedit, maybe you can load and place a schematic made out of air. I didn’t tried this yet, and I don’t know if WorldEdit offers an API to do that. If the API does not exist, you can always run the commands from the code.
     
  6. drives_a_ford

    Moderator

    What's wrong with the 3 loops, though? Unless you go and change the chunk in the region file (before loading the region!), that's what you will have to do.
    Sure, you can use some existing code (i.e WorldEdit), but that's really doing the exact same thing. You're just letting someone else take care of it.

    If you're worried about a performance hit from looping through all the 65536 blocks, you can get the snapshot, go async, capture all the locations that need changes (i.e are not air already) and loop through just those in sync to change them.

    In addition, you can spread out large tasks like this between multiple ticks. The way I've done this in the past is measure the time it's taken to do something and if it's been longer than (e.g) 20ms, schedule to continue on the next tick.
     
  7. Already done most of that except for going async for indexing the area to clear (interesting idea). It is still fairly heavy server load for the area that I'm trying to clear and though this particular task is a background cleanup at the end of a minigame round I am still looking for ways to make it more responsive. As for why manual looping is not desirable, a 500x500x255 area is A LOT of blocks to scan and possibly update. Even if I trim the area down to the bare minimum for the arena we are still talking at least 250x250x255 and that's only if arena size permits. At this point I write most of my minigames using a library I have built up over the years so a chunk-based mass pasting procedure was an interesting and possibly highly beneficial improvement that can be placed behind arena creation/removal APIs
     
  8. Why would you clean the arena? You can just have a main world with the arena, then copy it when the minigame starts, giving it a unique name, then let Bukkit generate it. When the minigame ends, you can unload and delete the world.
     
  9. Another viable option, that you can run partially asynchronously, is to grab the underlying NMS handle & swap out the chunk sections via reflection. Like that instead of a triple nested loop your only expenditure would be one reflective invocation & 16 array assignments of copied chunk sections (do NOT reuse chunk sections)
     
  10. The short answer: efficiency. Though it's quite possible that I have severely overestimated the overhead in allocating and deallocating entire worlds on the fly. My minigame uses a matchmaker that generates combat arenas on the fly as it gets enough players for a match. I figured that the most efficient way to do that was to take a single dedicated arena world (all void) and partition it out into sections that were just out of render range of one another so as to minimize the number of loaded chunks.
     
  11. Wouldn't it be easier to use a clean room generator and polishing your area with your generated stuff?
     
  12. I'm not sure I understand what you mean. The world generated as a void world is simple enough (I forget if I used a generator plugin or just did it manually), and then I just have some code that dynamically gives out partitions of that world and can then reclaim them once they are passed back. It was a fun little project at the time so I didn't care if it was slightly more complex, what I'm going for is runtime efficiency to try and minimize lag which seems to be increasingly important in the newer versions of Minecraft >_>

    edit:

    code for world region allocation and management:
    Code (Java):
    package com.gmail.crypticcabub.benderbattle.managers;

    import com.gmail.crypticcabub.benderbattle.BenderBattle;
    import com.gmail.crypticcabub.minigamebase.objects.Region;
    import com.gmail.crypticcabub.minigamebase.objects.exceptions.InvalidRegionException;
    import org.bukkit.*;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.block.BlockFadeEvent;
    import org.bukkit.plugin.Plugin;

    import java.util.LinkedList;

    /**
    * manager of arenas within a world to attempt to ensure that as little space on the world is used as possible.
    * <p>
    *     The used area will grow in a + grid from 0,0 as more space is needed. As soon as an arena is complete and its
    *     space released, it will be reused before any more grid space is alotted for new arenas. There is no assurance
    *     of the order in which arena spaces will be reused except that all open spaces will be reused before a new one space
    *     is allocated
    * </p>
    * <p>
    *     Once the arena grid grows throw new allocations, there is no way to shrink it again
    * </p>
    */

    public class ArenaWorldManager implements Listener {
        private static final int BUILD_HEIGHT = 255;

        private final World world;
        private final int blockSize; //how big each block of space is for a single arena (200 means a 200x200 box for each arena)
        private final int arenaHeight;
        private final int buffer;
        private final int arenaAltitude;


        private LinkedList<ArenaRegion> availableRegions = new LinkedList<>();

        private ArenaRegion nextRegion;
        private int currentGridSize;

        public class ArenaRegion extends Region {
            final int x, z;

            public ArenaRegion(int x, int z) throws InvalidRegionException {
                super(lowerBound(x, z), upperBound(x,z));
                this.x = x;
                this.z = z;
            }

            /**
             * release this ArenaRegion back to the ArenaWorldManager
             * <p>
             *     once released all references to this object should be dropped
             * </p>
             */

            public void release() {
                ArenaWorldManager.this.release(this);
            }
        }


        /*-------------------Public Interface--------------------*/

        /**
         *
         * @param worldName the name of the world to be managed by this ArenaWorldManager
         * @param arenaBlockSize the square size of each arena zone (ex. 200 will mean this world releases 200x200 arena blocks)
         * @param arenaHeight the maximum height of each arena
         * @param buffer spacing between arenas to keep them apart from one-another
         * @param arenaAltitude the base height of each arena
         */

        public ArenaWorldManager(Plugin plugin, String worldName, int arenaBlockSize, int arenaHeight, int buffer, int arenaAltitude) {
            this.blockSize = arenaBlockSize;
            this.arenaHeight = arenaHeight;
            this.buffer = buffer;
            this.arenaAltitude = arenaAltitude;

            WorldCreator worldCreator = new WorldCreator(worldName);
            worldCreator.type(WorldType.FLAT).generatorSettings("3;minecraft:air;2").generateStructures(false);
            world = Bukkit.createWorld(worldCreator);
            world.setAutoSave(false);

            nextRegion = createRegion(0, 0);
            Bukkit.getPluginManager().registerEvents(this, plugin);
        }

        public void delete() {
            Bukkit.unloadWorld(world, false);
            world.getWorldFolder().delete();
        }


        /**
         * @return the next open region usable for an arena in this world
         */

        public ArenaRegion nextRegion() {
            if(!(availableRegions.isEmpty())) {
                return availableRegions.remove();
            } else {
                return generateNextRegion();
            }
        }

        /**
         * release a region so that it can be used again
         * @param r the ArenaRegion to release
         * @throws IllegalArgumentException if the region did not originally come from this ArenaWorld
         */

        public void release(ArenaRegion r) throws IllegalArgumentException {
            BenderBattle.arenaPastingManager().cleanUpRegion(r, ()-> availableRegions.add(validate(r)));
        }

        /*--------------------End of Public Interface-----------------*/

        /**
         * verify a region as an ArenaRegion from this instance
         */

        private ArenaRegion validate(Region r) throws IllegalArgumentException {
            if(!(r instanceof ArenaRegion)) {
                throw new IllegalArgumentException("invalid region!");
            }

            ArenaRegion region = (ArenaRegion) r;

            if(!region.getMinCorner().getWorld().equals(world)) {
                throw new IllegalArgumentException("Region not from this ArenaWorld!");
            }

            return region;
        }

        /**
         * ArenaRegion creation factory method
         * @param x the x-position (row) of the region on the arena grid
         * @param z the z-position (column) of the region on the arena grid
         */

        private ArenaRegion createRegion(int x, int z) {
            try {
                return new ArenaRegion(x, z);
            } catch (InvalidRegionException e) {
                throw new RuntimeException(String.format("error creating ArenaRegion(&d,&d)", x, z));
            }
        }

        /**
         * generate the next region that should be created on the arena grid
         */

        private ArenaRegion generateNextRegion() {
            ArenaRegion region = nextRegion; //the region to return

            //prepare the new next region
            if(region.x < currentGridSize) { //descending down right side of grid
                nextRegion = createRegion(region.x + 1, region.z);
            } else if(region.z > 0) { //traversing from right-left along the bottom of the grid
                nextRegion = createRegion(region.x, region.z - 1);
            } else { //reached the lower-left corner, need to expand out
                nextRegion = expandGrid();
            }

            return nextRegion;
        }

        /**
         * expand the existing arena grid
         * @return the top-right region in the new arena grid
         */

        private ArenaRegion expandGrid() {
            currentGridSize++;
            return createRegion(0, currentGridSize); //start in the top-right corner of the newly expanded grid
        }

        /**
         * the lower bound of a region at position x,z
         */

        private Location lowerBound(int x, int z) {
            return new Location(world, (blockSize + buffer) * x, arenaAltitude, (blockSize + buffer) * z);
        }

        /**
         * the upper bound of a region at position x,z
         */

        private Location upperBound(int x, int z) {
            return new Location(world, (blockSize + buffer) * x + blockSize - 1, arenaAltitude + arenaHeight, (blockSize + buffer) * z + blockSize - 1);
        }

        @EventHandler()
        public void preventMelting(BlockFadeEvent event) {
            if(event.getBlock().getWorld().equals(world)) {
                event.setCancelled(true);
            }
        }

    }
     
    and the code for pasting/cleaning up arenas:
    Code (Java):
    package com.gmail.crypticcabub.benderbattle.managers;

    import com.gmail.crypticcabub.benderbattle.BenderBattle;
    import com.gmail.crypticcabub.minigamebase.countdowntimers.CallableResponse;
    import com.gmail.crypticcabub.minigamebase.objects.Region;
    import com.gmail.crypticcabub.minigamebase.schematics.Schematic;
    import com.gmail.crypticcabub.minigamebase.schematics.SchematicPaster;
    import com.gmail.crypticcabub.minigamebase.utils.NMSBlockChanges;
    import com.gmail.crypticcabub.minigamebase.utils.exceptions.IncompatibleServerVersionException;
    import org.bukkit.Bukkit;
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.World;
    import org.bukkit.plugin.Plugin;
    import org.bukkit.scheduler.BukkitTask;

    import java.util.Iterator;
    import java.util.NoSuchElementException;
    import java.util.PriorityQueue;
    import java.util.logging.Level;

    /**
    * static manager for ensuring that only one Arena is pasting at once
    * <p>
    *     submit an arena to be pasted and this manager will ensure it is pasted as quickly as possible without
    *     causing the server to lag before responding to the provided CallableResponse
    * </p>
    */

    public class ArenaPastingManager {

        private static final int MAX_MILLIS_PER_PASTE_TICK = 20;

        public enum Priority {
            HIGH, MED, LOW
        }

        private static class RegionCleaningTask extends PastingTask {
            private class ArenaRegionItr implements Iterator<Location> {

                private World world;
                private final int minX, maxX, minZ, maxZ;
                private int y, z, x; //current positions


                public ArenaRegionItr(Region region) {
                    world = region.getMaxCorner().getWorld();
                    minX = region.getMinCorner().getBlockX();
                    minZ = region.getMinCorner().getBlockZ();
                    maxX = region.getMaxCorner().getBlockX();
                    maxZ = region.getMaxCorner().getBlockZ();

                    y = region.getMaxCorner().getBlockY();
                    z = region.getMinCorner().getBlockZ();
                    x = region.getMinCorner().getBlockX() - 1; //so that next starts with the actual first block
                }

                /**
                 * Returns {@code true} if the iteration has more elements.
                 * (In other words, returns {@code true} if {@link #next} would
                 * return an element rather than throwing an exception.)
                 *
                 * @return {@code true} if the iteration has more elements
                 */

                @Override
                public boolean hasNext() {
                    return !(x == maxX && z == maxZ && y == 0);
                }

                /**
                 * Returns the next element in the iteration.
                 *
                 * @return the next element in the iteration
                 * @throws NoSuchElementException if the iteration has no more elements
                 */

                @Override
                public Location next() {
                    //we want to iterate from the top (y = yMax) to the bottom (y = 0) tracing x and z as usual
                    x++;

                    if(x > maxX) {
                        x = minX;
                        z++;

                        if(z > maxZ) {
                            z = minZ;
                            y--;

                            if(y < 0) throw new NoSuchElementException();
                        }
                    }

                    return new Location(world, x, y, z);
                }
            }

            private ArenaRegionItr itr;

            public RegionCleaningTask(Priority priority, Region region, CallableResponse completionResponse) {
                super(priority, completionResponse);
                itr = new ArenaRegionItr(region);
                //Bukkit.broadcastMessage("starting cleanup task");
            }

            /**
             * do a tick of pasting
             *
             * @return true if the task is complete, false if not
             */

            @Override
            boolean doPasteTick() {
                long start = System.currentTimeMillis();
                while(System.currentTimeMillis() - start < MAX_MILLIS_PER_PASTE_TICK) {
                    if(itr.hasNext()) {
                        Location loc = itr.next();
                        //System.out.println(loc);
                        if(loc.getBlock().getType() != Material.AIR) {
                            setBlock(loc, 0, (byte) 0);
                        }
                    } else {
                        //Bukkit.broadcastMessage("cleanup complete");
                        return true;
                    }
                }
                return false;
            }
        }

        private static class SchematicPastingTask extends PastingTask {

            private final Schematic schematic;
            private final Region targetRegion;
            Schematic.SchematicIterator itr;


            public SchematicPastingTask(Priority priority, Schematic schematic, Region targetRegion, CallableResponse completionResponse) {
                super(priority, completionResponse);
                this.schematic = schematic;
                this.targetRegion = targetRegion;
                this.itr = (Schematic.SchematicIterator) schematic.iterator();
            }

            @Override
            public boolean doPasteTick() {

                long start = System.currentTimeMillis();

                while (System.currentTimeMillis() - start < MAX_MILLIS_PER_PASTE_TICK) {

                    if (!itr.hasNext()) {
                        return true;
                    } else {
                        Schematic.SchematicBlock schemBlock = itr.next();
                        Location loc = targetRegion.getMinCorner().clone().add(schemBlock.getRelativeLocation());

                        setBlock(loc, schemBlock.blockID(), schemBlock.data());
                    }
                }

                return false;
            }
        }

        private static abstract class PastingTask implements Comparable<PastingTask> {
            final Priority priority;
            final CallableResponse completionResponse;

            boolean fastPasteEnabled = true;

            public PastingTask(Priority priority, CallableResponse completionResponse) {
                this.priority = priority;
                this.completionResponse = completionResponse;
            }

            public void setBlock(Location location, int blockID, byte blockData) {
                if (fastPasteEnabled) {
                    try {
                        NMSBlockChanges.setBlockSuperFast(location.getBlock(), blockID, blockData, false);
                    } catch (IncompatibleServerVersionException e) {
                        fastPasteEnabled = false;
                        Bukkit.getLogger().log(Level.SEVERE, "Incompatible server for rapid pasting! pasting slow instead");

                        //the block didn't paste so make sure it did
                        location.getBlock().setTypeIdAndData(blockID, blockData, false);
                    }
                } else {
                    location.getBlock().setTypeIdAndData(blockID, blockData, false);
                }
            }

            @Override
            public int compareTo(PastingTask o) {
                return this.priority.ordinal() - o.priority.ordinal();
            }

            /**
             * do a tick of pasting
             * @return true if the task is complete, false if not
             */

            abstract boolean doPasteTick();
        }

        private PriorityQueue<PastingTask> pastingTasks = new PriorityQueue<>();
        private BukkitTask pastingTask;

        public ArenaPastingManager(Plugin plugin) {
            pastingTask = Bukkit.getScheduler().runTaskTimer(plugin, this::pastingTick, 1, 1);
        }

        public void pasteArena(Schematic arenaSchematic, Region playRegion, CallableResponse completionResponse) {
            pastingTasks.add(new SchematicPastingTask(Priority.MED, arenaSchematic, playRegion, completionResponse));
        }

        public void cleanUpRegion(Region region, CallableResponse completionResponse) {
            pastingTasks.add(new RegionCleaningTask(Priority.LOW, region, completionResponse));
        }

        private void pastingTick() {
            if(!pastingTasks.isEmpty()) {
                PastingTask task = pastingTasks.peek();
                if(task.doPasteTick()) {
                    completePastingTask(task);
                }
            }
        }

        private void completePastingTask(PastingTask task) {
            task.completionResponse.respond();
            pastingTasks.remove(task);
        }

        public void destroy() {
            pastingTask.cancel();
            pastingTasks = null;
        }
    }
     
     
    • Friendly Friendly x 1
  13. TeamBergerhealer

    Supporter

    My guess would be World.getEmptyChunkSnapshot(x, z, true, false) and then applying it.

    I really thought it was possible to instantly set the contents of a chunk from a ChunkSnapshot, but Im unable to find any way to do it when I look at the apidocs. If anyone knows of a way to do it that I'm missing, I'd like to know too. :unsure: