Dimensions and Chunks
This guide covers the part of a world that everyone thinks of first: the actual terrain. It's here to help to understand how Minecraft organizes that terrain on disk, since the API mirrors it closely.
1. How terrain is organized
A world is split into up to three dimensions: the overworld, the nether and the end. Each one lives in its own folder under dimensions/minecraft/, and each is completely independent, they have their own terrain, their own force loaded chunks, and so on.
Inside a dimension, terrain is split into chunks. A chunk is a 16 by 16 column of blocks, going from the bottom of the world to the top. Chunks are identified by chunk coordinates, which are just block coordinates divided by 16, rounded down. So block (35, 70, -12) sits inside chunk (2, -1).
A chunk itself is further split vertically into sections, each one a 16x16x16 cube of blocks. A tall chunk might have dozens of sections stacked on top of each other, each with its own Y value (section 0 covers world Y 0 to 15, section -1 covers Y -16 to -1, and so on).
Finally, chunks aren't stored one file per chunk, that would be a lot of tiny files. Instead, groups of 32x32 chunks are bundled together into a single region file, named like r.0.0.mca. There are actually three kinds of region files per dimension:
region/
Stores the chunk's blocks and biomes.
entities/
Stores everything living or moving in that chunk: mobs, dropped items, minecarts, and so on.
poi/
Stores points of interest, like job sites, beds and bells, mostly used by villagers.
minified-worlds handles picking the right region file and the right offset inside it for you, you just ask for a chunk by its chunk coordinates.
2. Opening a dimension
Each dimension is represented by a small subclass of Dimension: Overworld, Nether and End. They all take the world folder in their constructor.
Overworld overworld = new Overworld(worldDirectory.toFile());val overworld = Overworld(worldDirectory.toFile())If you don't know or don't care which dimension you're dealing with, you can also construct a plain Dimension from a dimension folder directly, and it figures out the type from the folder's name (overworld, the_nether or the_end):
File dimensionFolder = new File(worldDirectory.toFile(), "dimensions/minecraft/the_nether");
Dimension nether = new Dimension(dimensionFolder);val dimensionFolder = File(worldDirectory.toFile(), "dimensions/minecraft/the_nether")
val nether = Dimension(dimensionFolder)End also loads ender_dragon_fight.dat for you, available through getEnderDragonFight(), which tells you things like how many times the dragon has been defeated in that world.
3. Reading a chunk
Call readChunk(chunkX, chunkZ) with chunk coordinates, not block coordinates. It reads the right region file, finds the chunk inside it, and wraps it in a Chunk object. If that chunk hasn't been generated yet, you get null back instead of an error, so always check for that.
Chunk chunk = overworld.readChunk(2, -1);
if (chunk == null) {
System.out.println("That chunk hasn't been generated yet.");
return;
}val chunk = overworld.readChunk(2, -1)
if (chunk == null) {
println("That chunk hasn't been generated yet.")
return
}4. Reading and writing blocks
Once you have a Chunk, getBlock and setBlock take plain block coordinates (not local, not chunk relative, the same coordinates you'd see in game with F3 open).
BlockState block = chunk.getBlock(35, 70, -12);
System.out.println(block.name()); // e.g. "minecraft:stone"
chunk.setBlock(35, 70, -12, BlockState.of("minecraft:diamond_block"));val block = chunk.getBlock(35, 70, -12)
println(block.name) // e.g. "minecraft:stone"
chunk.setBlock(35, 70, -12, BlockState.of("minecraft:diamond_block"))A BlockState is just a block name plus a map of properties, for blocks that have them, like facing, waterlogged or half.
Map<String, String> properties = Map.of("facing", "north", "waterlogged", "false");
BlockState state = new BlockState("minecraft:oak_stairs", properties);
chunk.setBlock(10, 64, 10, state);val properties = mapOf("facing" to "north", "waterlogged" to "false")
val state = BlockState("minecraft:oak_stairs", properties)
chunk.setBlock(10, 64, 10, state)Since DataVersion 5006, Minecraft leaves out the properties of a block's default state and only stores its name. So in such a world, a block in its default state (an upright oak log, still water, ...) comes back without any properties, and BlockState.of("minecraft:oak_log") means exactly that when you set one. You don't have to care which format a chunk uses, minified-worlds reads both and writes new blocks the way the chunk's DataVersion expects.
If you set a block in a section that doesn't exist yet (say, you're building far above the existing terrain), the chunk quietly creates that section for you as air first, so you never have to create sections manually just to place a block.
setBlock also takes care of whatever depends on the block. The chunk's light and heightmaps are thrown away, Minecraft recalculates both the next time it loads the chunk. And if a block is replaced by a different one (not just turned around), its block entity goes with it, so turning a chest into stone takes the chest's items along, like it would in game. What setBlock can't do is create block entities: a chest or sign you place gets no block entity data, Minecraft only makes an empty one later, once something in the game asks for it.
Why blocks aren't stored as one id per block
It's worth knowing this, even if the library hides it from you. Minecraft doesn't store a block id for every single one of the 4096 blocks in a section. Most sections only use a handful of different block types (stone, dirt, air, and maybe one or two ores), so storing a full id for every block would waste a lot of space.
Instead, each section keeps a palette: a small list of the block states actually used in that section. Every block position then just stores an index into that palette, packed as tightly as possible (using only as many bits as needed to cover the palette's size, at least 4 for blocks). An index never spans two longs though, the bits left over at the top of each long just stay unused. If a whole section is just one block, like a section of pure stone, the palette has a single entry and no index data is stored at all, since there's nothing to choose between.
This is also why adding a brand new block type to a section can be a bit of work behind the scenes: the palette grows, and if it crosses a size that needs one more bit per entry, the whole packed array has to be rebuilt with the wider bit width. minified-worlds does all of that for you inside setBlock.
5. Biomes
Biomes work almost the same way as blocks, palette and all, except they're stored per 4x4x4 block area instead of per block, since biomes rarely need to change at a finer resolution than that.
Chunk doesn't expose biome reading and writing directly right now, that lives one level down on ChunkSection. What Chunk does handle for you is picking a sensible biome whenever a brand new section gets created: it looks at the sections directly above and below it and reuses whichever biome shows up most between the two, falling back to minecraft:plains if the chunk has no sections at all yet. So as long as you're just placing blocks with setBlock, new sections still end up with a reasonable biome without any extra work.
6. Saving chunk changes
Changes you make with setBlock happen in memory. To write them back to disk, pass the chunk to saveChunkData.
overworld.saveChunkData(chunk);overworld.saveChunkData(chunk)This writes into the chunk's region file, creating that file if it didn't exist yet (useful if you're generating brand new chunks rather than editing existing ones).
saveChunkData holds the world's session lock while it writes, so it throws an IOException if Minecraft has the world open. It also marks the points of interest (beds, job sites, ...) of every section you changed blocks in, so Minecraft rebuilds them from the blocks the next time it loads the chunk.
7. Closing the dimension
Dimension keeps region files open while you're reading and writing, to avoid reopening the same file over and over. Once you're done, call close() to release those file handles.
overworld.close();overworld.close()Next steps
Chunks also carry entities and points of interest, stored separately from the terrain itself.