Minecraft Modding: Dimensions, WorldGen, Biomes, Trees

Definitions: Dimension, World, Chunk, Biome


Each of the "Overworld", the "Nether", and the "End" are called "dimensions".

Key Point: In terms of modding, each dimension is run in a separate instance of the World class. This is important as it means that you may have to think carefully if you need certain data across all the dimensions.

Due to the huge size of a world (a 256 by 256 area of the world has 17 million block positions!), the code manages data about the world in "chunks", saving them and loading them as needed. This is the reason for Minecraft's recognizable way that the world in the distance is often missing parts until you get closer. A chunk is 16 x 256 x 16 in size. "Chunk coordinates" are used in various places in the code and refer to x, y, z within the chunk.

Tip: To get chunk coordinates from world coordinates, you can use the modulo operator: chunkX = worldX%16. If you're not familiar with the % (modulo operator) see this tutorial. Alternatively you can use bit-shifting (by 4), for example chunkX = posX >> 4/

Warning: Do not get chunk coordinates by dividing by 16. This will be incorrect for negative values, due to how integers are rounded down

When a chunk is loaded for the first time it is generated. This is done with a "chunk provider" that is called from the "world provider". There is a certain degree of randomness used by the code to make things interesting, but there are still "themes" for the area of the world which are called "biomes". These are things like "taiga" and "plains". The biome is used to determine the types of blocks, types of trees, and types of entities to generate.

Biomes consist of a number of chunks, but the shape and size can vary randomly.

Understanding World "Seeds" And "Noise" Generation


In computer programming, random numbers are not truly random but instead they are "pseudo-random". This means that while they seem random enough because they are not possible for a human to predict, but they may be possible for a computer to predict as long as the computer knows the starting point and generation algorithm of the random generation. This starting point is called the "seed".

The fact that the random number can be predicted with the seed is very useful for testing computer code because it means you can repeat the logic in the case where you are investigating a bug in code that

TipMinecraft allows the user to know the seed used in the world generation, by using the /seed command. You can then enter that seed when generating a new world, and the result will be the same. Users can share seeds to let other users experience worlds that they feel are worth sharing. For example, see this article on some cool shared seed values.
 uses randomness.

The world generation in Minecraft of course is random but it is also important that the generation looks "natural" (not totally random) and therefore uses the seed value to generate "noise". The term "noise" is used for randomness that causes variation around a certain values. For example, when randomly generating the world the height of the surface of the world isn't fully random but rather varies within a range (depending on the biome type) from the height of the neighboring blocks.

Key Point: Minecraft has a NoiseGenerator class which is extended into some specific noise generator types such as Perlin and simplex. These are used to determine whether to place a block in the world while giving a "natural" randomness. They typically work by creating a value that represents the "density" of the block position and if a certain threshold is met then a block is placed. This a a complicated subject but studying how they are used in vanilla Minecraft code should give you some ideas.

Key Point: The seed used for world generation is contained in the WorldSettings class (and saved/loaded with the WorldInfo class). However, on the client side the seed value is always 0 as it is never used on that side.

Avoiding "Runaway" Cascading Generation


There are certain methods for things like setting neighboring blocks states, checking lighting, etc. that will cause neighboring chunks to need to load and generate. If these are called during world generation you can end up with chunks causing additional chunk generation which causes even more chunk generation.

To avoid this, vanilla Minecraft causes all decoration, ore and feature generation to be offset by 8 blocks in the X and Z with the neighboring chunks only then needing to be loaded in those directions. In other words, a single chunk is never generated on its own but rather always at least four. The generation then takes place around the center of where those blocks come together.

For example, in ChunkGeneratorOverworld#populate() method you'll see code like:
        if (this.settings.useDungeons)
        if (net.minecraftforge.event.terraingen.TerrainGen.populate(
            this, 
            this.world, 
            this.rand, 
            x, z, 
            flag, 
            PopulateChunkEvent.Populate.EventType.DUNGEON))
        {
            for (int j2 = 0; j2 < this.settings.dungeonChance; ++j2)
            {
                int i3 = this.rand.nextInt(16) + 8;
                int l3 = this.rand.nextInt(256);
                int l1 = this.rand.nextInt(16) + 8;
                (new WorldGenDungeons()).generate(
                    this.world, 
                    this.rand, 
                    blockpos.add(i3, l3, l1));
            }
        }

The highlighted code above is where the 8 block offset is added.

Tip: For a more detailed explanation see Reddit article on the subject.

Warning: If you don't add the 8 block offset for your custom generation you run a risk of causing runaway cascading chunk generation which will cause extreme lag.

The Classes Related To Generation For World, Chunk, And Biomes


Thanks to Matryoshika and Choonster for this information.

There are a LOT of classes involved in generating a dimension. A world is comprised of chunks which are made up of blocks. Blocks are grouped together randomly to create "terrain" (i.e. the shape of the land in terms of mountains, ravines, mesas and such), which then have ores and caves populated within it, then have the surface converted to blocks consistent with the biome, which is then decorated with features such as trees and populated with structures like villages. There are multiple classes involved in each of these steps.

Because there are so many classes involved it can be confusing, and furthermore there can be several ways to accomplish similar results. This can cause additional confusion because different tutorials may give different steps; however, the reality is many of these classes are invoked during generation and so can place blocks according to a mods needs -- but it is still best to try to do it in the intended class.

Warning: The links between the classes are somewhat convoluted. For example, the WorldProvider class has a WorldType as well as a BiomeProvider and ChunkGenerator instances, but WorldType generates those instances. So you have to make sure they all properly reference each other.

Here is a brief overview of the various classes related to generation.
  • DimensionType: An enum that defines the dimension by associating an id and name with a WorldProvider instance. You don't create a DimensionType rather you register the information using DimensionType.register() to use the Forge EnumHelper to extend the enum. You should use a unique, positive integer ID by checking first if it is registered using DimensionManager.isDimensionRegistered() method.  The "name" parameter will be converted to lowercase snake case. The "suffix" parameter seems to be related to some save file naming and should be unique as well. I believe both the name and suffix should also be unique.
  • DimensionManager: Each dimension is defined by a DimensionType registered to the DimensionManager. Warning: registering to DimensionManager is in addition to registering (which really extends the enum) to DimensionType, and it is highly recommended to use the same ID for both registrations.
  • WorldProvider: The WorldProvider associates an instance of BiomeProvider, WorldType (instance field is called terrainType) and ChunkGenerator. Also has methods to help assess aspects of the world like getBiomeForCoords().
  • BiomeProvider: (Prior to 1.9 this was called WorldChunkManager.) The BiomeProvider, in simplified terms, sets the selected chunk to a certain Biome. Yes, biomes are chunk-based, but it makes use of a byte[256] (16*16 block count) to set the blocks individually to a biome with desired size of width*depth.
  • IChunkGenerator: This is a seemingly simple interface which is used in very complex ways. It has generateChunk() method as well as populate() and generateStructures() among others. There are vanilla implementations such as ChunkGeneratorOverworldChunkGeneratorEndChunkGeneratorHell. These class implementations usually include a WorldTypeNoiseGenerator extensions (for randomness), MapGenBase extensions, etc.
  • ChunkGeneratorSettings: This is primarily meant for vanilla overworld where it collects all sorts of constants such as coordinate scaling, noise scales, ore gen counts, ore gen size, ore gen min and max heights, etc. You can use something similar or not in your own dimension, but it is interesting to review the vanilla one to get understanding of how things are controlled.
  • ChunkProvider: Don't confuse this with the generator. It is a class used for loading chunks, and although the server version does call the generator, you should not need to do anything with such a class.
  • WorldType: Each world type is listed when you start the game (in the drop down) such as the vanilla Minecraft world types: default, flat, customized, amplified, etc. This is where the BiomeProvider and ChunkGenerator instances originate from. You can also specify that your type is customizable if you @Override the isCustomizable() and onCustomizeButton() methods. The WorldType changes how the previously mentioned classes act to significantly edit the overall world generation process.
  • terrainType: The terrainType field is in the WorldProvider and references an instance of the WorldType being used for the active dimension.
  • WorldSettings: These are specifically the information selected by the user in the New World menu. Such things as whether the seed is random, if the game mode is survival or creative, whether hardcore mode is on, whether custom options were selected, and whether commands are enabled. These get transferred into the WorldInfo which is then used for saving as NBT.
  • WorldInfo: This basically defines the state of the world and game play. It contains the options that user sets when creating a world through the New World menu as well as other "stats" about the world including state of things like the weather. Things like the difficulty, aspects related to making sure that the world save matches the version, the save name, and so forth. It is either constructed from NBT when loading a saved world, or from WorldSettings when newly generated from New World menu.
  • Biome: A biome represents the top 5 or so blocks in the world to cover the surface and add "features" such as trees, as well the entities that can spawn in it. You can @Override the Biome#genTerrainBlocks() method to control how your custom biome's generated. Most vanilla biomes simply call generation of the default terrain, but BiomeMesa overrides it to generate completely custom terrain. Ore generation is done in Biome#decorate() and BiomeDecorator#decorate() methods. You can either override Biome#decorate() to add a few extra decorations or extend BiomeDecorator for completely custom decorations. 
  • BiomeProperties: An instance of this is associated with each Biome instance and contains information such as a name, temperature, height variation, whether rain is enabled, water color, etc. You don't need to directly reference the instance later, so you can instantiate it anonymously when you instantiate your biome.
  • BiomeManager: An important class. In the RegistryEvent this is the class you should register any custom biomes to using the addBiome() method. Note that there are many useful methods such as addSpawnBiome(), addStrongholdBiome(), etc that you should investigate.
  • BiomeDictionary: This helps make biomes more compatible/realistic. For example, it helps classify biomes so that cold biomes can be generated together, and so forth. You can register biome types to your custom biomes.
  • BiomeType: This is used by the BiomeManager. These are primarily used by the GenLayerBiome.getInts() method for indicating viable biomes.
  • BiomeDictionary.TYPE: This is used by the BiomeDictionary. I'm not sure really how much BiomeDictionary is used by actual mods, but the idea is that it helps mod interoperability by allowing you to understand which biomes are similar. For example, you could make a biome provider that uses biomes from other mods but checks to make sure they are compatible types. Even if you don't use types yourself, it is good practice to give your biome types so that other mods could use your biome effectively.
  • BiomeDecorator: To use a custom BiomeDecorator, override Biome#createBiomeDecorator() method to call the Biome#getModdedBiomeDecorator() method with your custom BiomeDecorator and return the value returned by it. 
    • All fields in BiomeDecorator are public so you can easily modify them.
    • A custom BiomeDecorator should override the decorate() and/or the genDecorations() methods. Look at the other classes in the Type Hierarchy of BiomeDecorator to see how vanilla decorators do it.
    • The decorate() method is primarily intended to assign WorldGenerator and IWorldGenerator implementations, such as WorldGenClay, WorldGenIron, and so forth.
  • Map generators and IWorldGenerator:
    • IWorldGenerator: This is a Forge hook (not to be confused with the vanilla Minecraft WorldGenerator class) that is intended for modded "map gen" features.
      • Warning: your IWorldGenerator classes must be registered to the GameRegistry.registerWorldGenerator() method in your mod's init loading phase. 
      • IWorldGenerator  simply contains a generate() method which is called from the GameRegistry.generateWorld() method. 
      • I'm not entirely sure they don't want you to simply call your own MapGenBase-derived classes from your chunk generator, but this works too and is called immediately after your chunk generator's populate() method.
    • MapGenBase: Map generators are responsible for adding modular, somewhat regular features. This class has vanilla extensions like MapGenCaves, MapGenRavine and MapGenStructure (which has further sub-classes for strongholds, villages, ocean monuments, etc).
    • MapGenScatteredFeature: This is a sub-class of MapGenStructure that generates swamp huts, igloos and desert pyramids based on the biome type. 

Creating Your Own Dimension


Although you can make selective edits to modify the existing dimensions, I find that ultimately people want to do a full-custom dimension.

See Jabelar's Custom Dimension Tutorial which implements a custom "cloud" dimension with custom single-biome chunk generator together with custom filler blocks and decorations like custom trees, flowers and grass.

Finding A Free Dimension ID


Here is some example code for a method that will find a free dimension ID. I personally use this for the DimensionType then use the DimensionType.getID() to ensure same is used when registering to the DimensionManager. Anyway, here is the code:


    @Nullable
    private static Integer findFreeDimensionID()
    {
        for (int i=2; i
        {
            if (!DimensionManager.isDimensionRegistered(i))
            {
                // DEBUG
                System.out.println("Found free dimension ID = "+i);
                return i;
            }
        }
        
        // DEBUG
        System.out.println("ERROR: Could not find free dimension ID");
        return null;
    }


Getting A List Of All Biomes


Since Forge has starting using events for biome registration, there is now the IForgeRegistry registry which is passed into the event as event.getRegistry(). From that event you can use the getValues() to get a List containing all the biomes.

But outside of the event, the Forge registries are stored in the ForgeRegistries class so you can access ForgeRegistries.BIOMES directly and call the getValues() method.

How To Delete A Biome


In earlier versions of Forge there was a GameRegistry.removeBiome() method which no longer exists.  Now you can work directly with the List in the BiomeManager, using normal List methods to remove (or add).

Detecting What Biome Is At A Given Location


If you ever want your code to do something special based on being in a biome, like maybe if you want it to play different background sounds in your custom biome, you can check using the following: worldObj.getChunkFromBlockCoords(blockpos).getBiome(blockpos, worldObj.getBiomeProvider()).getBiomeName(). Of course you might have to replace the worldObj with whatever instance of a World you have available at that point in the code.

Changing The Biome At A Location


Thanks to Choonster for this tip.

Biome ids  are stored in the Chunk#blockBiomeArray field which you can modify directly. This array stores the biome id of at each X, Z position within the chunk, using (Z & 15) << 4 | (X & 15) as the index. So, if you wanted to modify it you could use the World#getChunkFromBlockCoords() method to get the Chunk at the specified position, then use Chunk#getBiomeArray() to get the biome array. Use Biome#getIdForBiome() to get the id of a biome.

Making Sure Random Generation Is Tied To The World Seed


In Minecraft worlds are generated randomly for each new game and each new area explored. However, in computer programming, it is possible to repeat the "random" sequence by starting with the same "seed" value. This is very useful in debugging code but is also fun in Minecraft because if you find a cool world gets generated you can share the seed with your friends and they can generate the exact same world.

Therefore, if you're going to add randomness to any of your custom generation code, you should tie it to the world's seed so they your custom stuff will also be able to be generated the same way each time someone uses the same seed.

To do this, when you need randomness in your custom generation code you should use the public World.rand field and get your random numbers using the appropriate methods for Random class.

Creating An Empty Dimension


You can do this by simply following the steps to make a custom dimension, but in your custom ChunkProvider class @Override the ChunkProvider#provideChunk() method to return an empty chunk.

For example:
public Chunk provideChunk(int parChunkXIndex, int parChunkYIndex)
{
Chunk chunk = new Chunk(this.worldObj, parChunkXIndex, parChunkYIndex);
chunk.generateSkylightMap();
return chunk;
}

Changing How Fast The Day Progresses


You cannot (easily) change the pace of the day in the vanilla because the duration of a day is a fixed value in the WorldProvider#calculateCelestialAngle() method, but you can easily do this in your own dimension. In other words make a custom WorldProvider.

Custom Tree Generation


See Jabelar's Custom Tree tutorial. It includes custom log, leaves and sapling.

Custom Flower Generation


See Jabelar's Custom Flower tutorial.

Preventing And Changing Generation Of Decorations (Trees, Cactus, Etc.)


You can handle the DecorateBiomeEvent.decorate event and check for the EventType. If you don't understand how to handle an event, check out Jabelar's Event Handling Tutorial.

Based on the EventType you can do your own thing and use the event.setResult(Result.DENY) to prevent the standard decoration.

Possible EventTypes you can intercept are:
  • BIG_SHROOM
  • CACTUS
  • CLAY
  • DEAD_BUSH
  • LILYPAD
  • FLOWERS
  • GRASS
  • LAKE
  • PUMPKIN
  • REED
  • SAND
  • SAND_PASS2
  • SHROOM
  • TREE
  • CUSTOM -- it seems this is to allow you to post custom event that can then be handled.

Changing The Contents Of Vanilla Generated Chests


The ChestGenHooks class has categories BONUS_CHEST and VILLAGE_BLACKSMITH. Register your loot there.

Changing The Spawners In Vanilla Generated Structures


For generated dungeons, there is a Forge hook called DungeonHooks that allows you to manipulate the list of mobs that might spawn:

  • It is pretty simple, it contains a list that is initialized to the vanilla selection: skeleton, zombie and spider. 
  • There is a method to remove mobs (removeDungeonMob() method) and one to add mobs (addDungeonMob() method). So during the init loading phase of your mod you can call these methods to change the list as you see fit. Technically you could modify this later in the game as well, maybe in response to some sort of condition.
  • Note that currently there isn't a way to get the full list from DungeonHooks so mods might conflict and it is difficult to tell if another mod has manipulated the list (I have submitted an issue to the MinecraftForge project to fix this.)

However, other structures with spawners like mineshafts and strongholds do not use the DungeonHooks class. (I have also submitted an issue to the MinecraftForge project to consider extending this functionality.) So if you want to change these you have to do a more "heavy" modification where you substitute your own class for the generator. This isn't so difficult though since you can copy all the code from the vanilla class except for changing one line where the spawner is instantiated. To do this:

  • Create A Modified Structure Generator: Basically create a copy of the vanilla generator class  you're trying to modify and change the line related to the spawner to spawn the mobs you desire. 
  • Use Events To Substitute Your Generator: The generator instances are invoked by the chunk generators, such as ChunkGeneratorOverworld, ChunkGeneratorHell, etc. There are events that allow you to replace them. For example:
    • The genNetherBridge field in ChunkGeneratorHell is private however there is an InitMapGenEvent with EventType.NETHER_BRIDGE that allows you to replace it with your own generator. Note: To use this event you need to use the event.setNewGen() method.
    • The WorldGenDungeons is invoked directly (no instance field) in ChunkGeneratorOverworld however there is an PopulateChunkEvent.Populate event with EventType.DUNGEON. Note: To use this event you need to directly call your own generation then cancel the vanilla generation by setting the event result to Result.DENY.

2 comments:

  1. I've found you can easily change the duration of days using a different method. First, you subscribe to the WorldEvent.load event and set the DoDaylightCycle gamerule to false. This effectively cancels normal day progression. Then, you subscribe to the WorldTick event and progress the WorldTime however often you want.

    ReplyDelete