Skip to content

Bulk Operations

For large-scale block edits (fill, replace, copy/paste), use these patterns from the built-in BuilderTools. See Blocks for single-block operations and SetBlockSettings flags.

When editing many blocks, repeatedly calling world.getChunk() is expensive - each call involves hash map lookups and thread synchronization. LocalCachedChunkAccessor solves this by storing chunk references in a flat array for O(1) access.

How it works:

  • Pre-allocates a square region of chunk slots centered on a point
  • First access to a chunk fetches it from the world and caches the reference
  • Subsequent accesses return the cached reference directly via array index
  • Requests outside the cached region pass through to the world
// create accessor covering the edit area
// parameters: world, centerX, centerZ, radiusInBlocks
LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(
world,
(minX + maxX) / 2, // center of edit region
(minZ + maxZ) / 2,
Math.max(maxX - minX, maxZ - minZ) // radius covers entire region
);
// chunk lookups are now O(1) array access instead of hash map lookups
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
long chunkIndex = ChunkUtil.indexChunkFromBlock(x, z);
WorldChunk chunk = accessor.getChunk(chunkIndex);
// ... edit blocks
}
}

Limitations:

  • Not thread-safe - create one per operation, don’t share between threads
  • Fixed size at construction - estimate your maximum radius upfront
  • Holds chunk references until accessor is discarded

Thread-safe container for storing block data, used by BuilderTools for copy/paste and undo/redo. Stores blocks in a Long2ObjectMap keyed by packed coordinates, with a read-write lock for concurrent access.

BlockSelection selection = new BlockSelection();
// store blocks using relative coordinates (selection-local)
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
int blockId = world.getBlock(x, y, z);
int rotation = world.getBlockRotation(x, y, z);
int filler = world.getBlockFiller(x, y, z);
selection.addBlockAtLocalPos(x - minX, y - minY, z - minZ, blockId, rotation, filler, 0);
}
}
}
// apply all stored blocks to world at target position
// handles chunk batching, lighting, and client updates internally
selection.placeNoReturn("Pasting", player, world, componentAccessor);

When using SetBlockSettings flags to skip per-block updates (for speed), you must manually batch those updates after all edits complete:

LongSet dirtyChunks = new LongOpenHashSet();
// during edits - track which chunks we modify
for (...) {
chunk.setBlock(x, y, z, blockId, blockType, rotation, filler, bulkFlags);
dirtyChunks.add(chunk.getIndex());
}
// after all edits - recalculate lighting and send chunk updates to clients
dirtyChunks.forEach(chunkIndex -> {
WorldChunk chunk = world.getChunkIfInMemory(chunkIndex);
if (chunk != null) {
world.getChunkLighting().invalidateLightInChunk(chunk);
}
world.getNotificationHandler().updateChunk(chunkIndex);
});
public void bulkFill(World world, int x1, int y1, int z1, int x2, int y2, int z2, String blockKey) {
world.execute(() -> {
int blockId = BlockType.getAssetMap().getIndex(blockKey);
BlockType blockType = BlockType.getAssetMap().getAsset(blockId);
// skip per-block notifications/particles - we batch at the end
int bulkFlags = SetBlockSettings.NO_NOTIFY
| SetBlockSettings.NO_SEND_PARTICLES
| SetBlockSettings.NO_SEND_AUDIO;
// fast chunk lookups for the edit region
LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(
world, (x1 + x2) / 2, (z1 + z2) / 2, Math.max(x2 - x1, z2 - z1)
);
LongSet dirtyChunks = new LongOpenHashSet();
for (int y = y1; y <= y2; y++) {
for (int z = z1; z <= z2; z++) {
for (int x = x1; x <= x2; x++) {
long chunkIndex = ChunkUtil.indexChunkFromBlock(x, z);
WorldChunk chunk = accessor.getChunk(chunkIndex);
chunk.setBlock(x, y, z, blockId, blockType, 0, 0, bulkFlags);
dirtyChunks.add(chunkIndex);
}
}
}
// batch: recalculate lighting and notify clients once per chunk
dirtyChunks.forEach(chunkIndex -> {
WorldChunk chunk = world.getChunkIfInMemory(chunkIndex);
if (chunk != null) {
world.getChunkLighting().invalidateLightInChunk(chunk);
}
world.getNotificationHandler().updateChunk(chunkIndex);
});
});
}