10 Anvil API
Andrew Gazelka edited this page 2018-06-27 21:11:59 -05:00

Overview

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.

I would only recommend using this if the world is unloaded, or you are checking at least 1024 chunks.

Using the MCAQueue (extends FaweQueue)

Creating an MCAQueue with a loaded world

If a world is loaded, the MCAQUeue must wrap the implementation FaweQueue. The default queue will assist the MCAQUeue in modifying world files that are in use.

FaweQueue defaultQueue = SetQueue.IMP.getNewQueue(folder, true, false);
MCAQueue mcaQueue = new MCAQueue(defaultQueue);

Create an MCAQueue from an unloaded world

If the world is unloaded, we just need to provide the world name, directory and tell it if it has sky light.

String world = "world"; // The name of the world
File folder = new File(world + File.separater + "region");
boolean hasSky = true; // If the world has sky
MCAQueue mcaQueue = new MCAQueue(world, folder, hasSky);

Using it like a normal FaweQueue

// Set a block at a position
mcaQueue.setBlock(x, y, z, FaweCache.getBlock(BlockID.Stone, 0));
mcaQueue.flushQueue();

Using it in an EditSession or AsyncWorld

Change the queue used by an EditSession when constructing it with the EditSessionBuilder#queue(mcaQueue)

Change the queue used by an AsyncWorld in the constructor new AsyncWorld(world, queue)

MCAQueue filtering

A MCAFilter lets you modify large areas very quickly by making use of multiple cores and using a structure that reduces lookup costs. The world is filtered from the least expensive to modify, to the most (Path, File, MCAFile, MCAChunk, BaseBlock). I.e. it is much quicker to replace an entire file or chunk than it is to modify individual blocks.

To use an existing filter, see here. You can see them being used in the AnvilCommands class

// If the world is loaded (use RegionWrapper.Global() for the entire world)
MCAFilter result = mcaQueue.filterCopy(filter, region);
// OR If the world is unloaded
// MCAFilter result = mcaQueue.filterRegion(filter, region);
// OR If the world is unloaded AND you want to filter the entire world
// MCAFilter result = mcaQueue.filterWorld(filter, region);

Copying from another MCAQueue

Blocks can be copied/pasted from another MCAFile. Note: If you just want to replace the entire world, it would be faster to use a filter to replace the entire .mca files.

mcaQueue.pasteRegion(mcaQueueFrom, regionFrom, Vector offset);

Generating MCA files

See HeightMapMCAGenerator
Or for more control, the MCAWriter

File dir = new File("TestWorld/region");
// Create a new generator from a heightmap
// Note: If you don't want to use an image, use the other constructor
BufferedImage heightMap = ImageIO.read(new URL("https://i.imgur.com/plFXYiI.png"));
// Our new generator
HeightMapMCAGenerator gen = new HeightMapMCAGenerator(heightMap, dir);
// Add some cliffs
BufferedImage cliffHeightMap = ImageIO.read(new URL("https://i.imgur.com/wx5oiA7.png"));
boolean onlyWhite = false; // Only use the white parts of the heightmap
gen.setColumn(cliffHeightMap, new BaseBlock(BlockID.STONE), onlyWhite);
// Add some tallgrass
gen.setOverlay(new BlockMask(gen, new BaseBlock(BlockID.GRASS)), new BaseBlock(BlockID.LONG_GRASS));
// Add some caves
gen.addCaves();
// Add some ores
gen.addDefaultOres(new BlockMask(gen, new BaseBlock(BlockID.STONE)));
// Add some trees
World world = WorldEdit.getInstance().getServer().getWorlds().get(0);
WorldData worldData = world.getWorldData();
File treeFolder = new File("trees");
ClipboardHolder[] trees = ClipboardFormat.SCHEMATIC.loadAllFromDirectory(treeFolder, worldData);
Mask flat = new AngleMask(gen, 0, 0); // Only flat terrain
boolean randomRotate = true;
gen.addSchems(flat, worldData, trees, 50, randomRotate);
// You can also get/set specific blocks
// Note: Individual block changes are slower than the methods above
gen.setBlock(0, 255, 0, new BaseBlock(BlockID.SPONGE)); // Set a specific block
System.out.println(gen.getLazyBlock(0, 255, 0)); // Get a specific block
// Done, let's generate the world!
gen.generate();

Replacing blocks

String worldName = "world";
File root = new File(worldName + File.separator + "region");
MCAQueue queue = new MCAQueue(worldName, root, true);
MCAFilterCounter counter = queue.filterWorld(new MCAFilterCounter() {
    @Override
    public void applyBlock(int x, int y, int z, BaseBlock block, MutableLong ignore) {
        if (block.getId() == 5) { // Change the id if it's 5
            block.setId(7);
        }
    }
});
// See: FaweBlockMatcher.fromBlocks(...) which returns true if a block matches
// See: FaweBlockMatcher.setBlocks(...) which will modify a provided block

Deleting chunks outside a radius

MCAQueue queue = new MCAQueue(worldName, root, true);

final int radius = 4000;
final int radiusSquared = radius * radius;

queue.filterWorld(new MCAFilter() {
    @Override
    public MCAFile applyFile(MCAFile mca) {
        int distanceX = Math.abs((mca.getX() << 9) + 256) - 256;
        int distanceZ = Math.abs((mca.getZ() << 9) + 256) - 256;
        int distanceSquared = distanceX * distanceX + distanceZ * distanceZ;
        if (distanceSquared > radiusSquared) {
            // Delete this file since it's outside the radius
            mca.close(ForkJoinPool.commonPool());
            mca.getFile().delete();
            return null;
        } else if (distanceSquared + 512 < radiusSquared) {
            // Don't filter this MCAFile since it's well within the radius
            return null;
        } else {
            return mca;
        }
    }

    @Override
    public boolean appliesChunk(int cx, int cz) {
        int distanceX = Math.abs((cx << 4) + 8) - 8;
        int distanceZ = Math.abs((cz << 4) + 8) - 8;
        int distanceSquared = distanceX * distanceX + distanceZ * distanceZ;
        if (distanceSquared > radiusSquared) {
            // Chunk is outside the radius
            return true;
        } else {
            // We don't care about chunks inside the radius
            return false;
        }
    }

    @Override
    public MCAChunk applyChunk(MCAChunk chunk) {
        chunk.setDeleted(true);
        // We don't want to filter blocks
        return null;
    }
});

Using an MCAWorld

String worldName = "world";
boolean hasSky = true;
File root = new File(worldName + File.separator + "region");
MCAWorld world = new MCAWorld(worldName, root, hasSky);
EditSession editSession = new EditSessionBuilder(world)
    .checkMemory(false)
    .allowedRegionsEverywhere()
    .fastmode(true)
    .changeSetNull()
    .limitUnlimited()
    .build();
// Do stuff here with the editSession (non generated sections of the world cannot be modified)