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.
LocalCachedChunkAccessor
Section titled “LocalCachedChunkAccessor”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, radiusInBlocksLocalCachedChunkAccessor 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 lookupsfor (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
BlockSelection
Section titled “BlockSelection”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 internallyselection.placeNoReturn("Pasting", player, world, componentAccessor);Dirty Chunk Tracking
Section titled “Dirty Chunk Tracking”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 modifyfor (...) { chunk.setBlock(x, y, z, blockId, blockType, rotation, filler, bulkFlags); dirtyChunks.add(chunk.getIndex());}
// after all edits - recalculate lighting and send chunk updates to clientsdirtyChunks.forEach(chunkIndex -> { WorldChunk chunk = world.getChunkIfInMemory(chunkIndex); if (chunk != null) { world.getChunkLighting().invalidateLightInChunk(chunk); } world.getNotificationHandler().updateChunk(chunkIndex);});Complete Example
Section titled “Complete Example”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); }); });}