Table of Contents

Custom Chunk Generators

ChunkGenerators are the mechanisms by which the game generates the world. They handle terrain shaping, surface building, and biome placement.

Vanilla Minecraft's NoiseChunkGenerator is extremely powerful and configurable. Using DensityFunctions and SurfaceRules, the possibilities for customization are almost endless. If you're wondering “can I do x with NoiseChunkGenerator and datapacks”, the answer is probably yes. Most situations do not call for a custom ChunkGenerator, and since creating one requires a lot of boilerplate code, it is highly recommended that you avoid making custom ChunkGenerators when possible.

As a rule of thumb, you should only implement a ChunkGenerator if you need to create a world that is not based on random noise. Examples of this might include a world generated from an external voxel engine, or a custom map like SethBling's Skygrid. If you're not sure whether a ChunkGenerator is right for you, ask someone in the Discord.

Ready? Let's get started.

Creating a ChunkGenerator

The first step in creating a chunk generator is making a new ChunkGenerator class. Extend vanilla's ChunkGenerator, and let your IDE generate all the required function stubs. Here's an explanation of everything you likely need to worry about:

public class ExampleChunkGenerator extends ChunkGenerator {
    /* this is a very important field, we will come back to the codec later */
    public static final Codec<ExampleChunkGenerator> CODEC; 
 
    /* you can add whatever fields you want to this constructor, as long as they're added to the codec as well */
    public ExampleChunkGenerator() {
    }
 
    /* the method that creates non-noise caves (i.e., all the caves we had before the caves and cliffs update) */
    @Override
    public void carve(ChunkRegion chunkRegion, long seed, NoiseConfig noiseConfig, BiomeAccess biomeAccess, StructureAccessor structureAccessor, Chunk chunk, GenerationStep.Carver carverStep) {
 
    }
 
    /* the method that places grass, dirt, and other things on top of the world, as well as handling the bedrock and deepslate layers,
    as well as a few other miscellaneous things. without this method, your world is just a blank stone (or whatever your default block is) canvas (plus any ores, etc) */
    @Override
    public void buildSurface(ChunkRegion region, StructureAccessor structures, NoiseConfig noiseConfig, Chunk chunk) {
 
    }
    /* the method that paints biomes on top of the already-generated terrain. if you leave this method alone, the entire world will be a River biome.
     note that this does not mean that the world will all be water; but drowned and salmon will spawn. */
    @Override
    public CompletableFuture<Chunk> populateBiomes(Registry<Biome> biomeRegistry, Executor executor, NoiseConfig noiseConfig, Blender blender, StructureAccessor structureAccessor, Chunk chunk) {
        return super.populateBiomes(biomeRegistry, executor, noiseConfig, blender, structureAccessor, chunk);
    }
 
    /* this method spawns entities in the world */
    @Override
    public void populateEntities(ChunkRegion region) {
    }
 
    /* the distance between the highest and lowest points in the world. in vanilla, this is 384 (64+320) */
    @Override
    public int getWorldHeight() {
        return 0;
    }
 
    /* this method builds the shape of the terrain. it places stone everywhere, which will later be overwritten with grass, terracotta, snow, sand, etc
     by the buildSurface method. it also is responsible for putting the water in oceans. it returns a CompletableFuture-- you'll likely want this to be delegated to worker threads. */
    @Override
    public CompletableFuture<Chunk> populateNoise(Executor executor, Blender blender, NoiseConfig noiseConfig, StructureAccessor structureAccessor, Chunk chunk) {
    }
 
    @Override
    public int getSeaLevel() {
        return 0;
    }
 
    /* the lowest value that blocks can be placed in the world. in a vanilla world, this is -64. */
    @Override
    public int getMinimumY() {
        return 0;
    }
 
    /* this method returns the height of the terrain at a given coordinate. it's used for structure generation */
    @Override
    public int getHeight(int x, int z, Heightmap.Type heightmap, HeightLimitView world, NoiseConfig noiseConfig) {
        return 0;
    }
 
    /* this method returns a "core sample" of the world at a given coordinate. it's used for structure generation */
    @Override
    public VerticalBlockSample getColumnSample(int x, int z, HeightLimitView world, NoiseConfig noiseConfig) {
 
    }
 
    /* this method adds text to the f3 menu. for NoiseChunkGenerator, it's the NoiseRouter line */
    @Override
    public void getDebugHudText(List<String> text, NoiseConfig noiseConfig, BlockPos pos) {
 
    }
 
    @Override
    protected Codec<? extends ChunkGenerator> getCodec() {
        return CODEC;
    }
}

Pretty much all of these methods need to be overridden for your generator to work properly. You can copy some of them from NoiseChunkGenerator. It is especially recommended that you implement the private populateNoise method from NoiseChunkGenerator as well, and just copy over the public method. The public method from NoiseChunkGenerator just delegates the actual generation to worker threads, which each run the private method. This is an easy way to get parallelized generation– the chunk generator is not multithreaded by default!

Registering and Codecs

Like other features in the game, ChunkGenerators have to be registered. However, you don't pass the registry a static instance– instead, you give it a Codec instance. You can put whatever you want in the codec– Mojang provides serialization codecs for a lot of useful objects, including entire registries. The easiest way to do this is via RecordCodecBuilder. Here's an example codec:

    public static final Codec<ExampleChunkGenerator> CODEC = RecordCodecBuilder.create(instance ->
        instance.group(
                BiomeSource.CODEC.fieldOf("biome_source").forGetter(ExampleChunkGenerator::getBiomeSource),
                Codec.INT.fieldOf("sea_level").forGetter(ExampleChunkGenerator::getSeaLevel),
                Codec.INT.fieldOf("world_height").forGetter(ExampleChunkGenerator::getWorldHeight),
                Identifier.fieldOf("custom_block").forGetter(ExampleChunkGenerator::getCustomBlockID)
                ).apply(instance, ExampleChunkGenerator::new));

Each line in the codec corresponds to a different field in your chunk generator that should be configurable. In this example, I pass a BiomeSource and a few ints. Most objects you will want to add into your chunk generator will have a Codec defined for them. Primitives have theirs defined in the Codec class, and other classes (for example, BiomeSource or Identifier) have theirs defined in their classes.

To use the fields properly, you have to add them, along with getters, into your class:

private final BiomeSource biomeSource;
public BiomeSource getBiomeSource() {
    return this.biomeSource;
}
private final int seaLevel;
public int getSeaLevel() {
    return this.seaLevel;
}
// and the same for the other fields

and then, create a constructor for your generator that uses each of those fields in the same order that you listed them in the codec:

public ExampleChunkGenerator(BiomeSource biomeSource, int seaLevel, int worldHeight, Identifier customBlockID) {
    super(biomeSource);
    this.seaLevel = seaLevel;
    this.worldHeight = worldHeight;
    this.customBlock = Registries.BLOCK.getOrThrow(RegistryKey.of(RegistryKeys.BLOCK, defaultBlock)).getDefaultState();
    this.customBlockID = customBlockID; // this line is included because we need to have a getter for the ID specifically
}

Then, all that's left is to register your chunk generator. Put the following line into your mod initializer onInitialize method:

Registry.register(Registry.CHUNK_GENERATOR, new Identifier("wiki-example", "example"), ExampleChunkGenerator.CODEC);

Now, you can create json files to use your new chunk generator:

// resources/data/wiki-example/dimension/example.json
{
  "type": "minecraft:overworld",
  "generator": {
    "type": "wiki-example:example",
    "custom_block": "minecraft:dirt",
    "sea_level": 0,
    "world_height": 384,
    "biome_source": // etc.

And you're done! If you want to use your new generator, it might be helpful to wrap it with a WorldPreset.