Skip to content

These docs were made completely by AI, so they might be right, or wrong, you'll need to test them yourself. This was made for a easier understanding of everything. So use at your own risk. If anything is wrong, please don't hurt to make a PR on the page you have a problem with. ON GITHUB

Adding Biomes

This guide covers creating new biomes in LCE, including how to subclass Biome, configure properties, set up mob spawns, customize decorators, and hook into the layer-based world generation pipeline.

The biome system lives entirely in Minecraft.World/. Here are the core classes:

FilePurpose
Biome.h / Biome.cppBase class, static biome registry, mob spawn lists
BiomeDecorator.hFeature placement (ores, trees, grass, flowers)
BiomeSource.hProvides biome data to the chunk generator
BiomeInitLayer.hPicks which biomes show up during world gen
Layer.h / Layer.cppLayer pipeline that turns noise into biome IDs

LCE supports up to 256 biomes (the Biome::biomes[256] static array). The 23 vanilla biomes use IDs 0-22.

Create a new class that extends Biome. Most biome subclasses are pretty small. They just configure properties in the constructor and maybe override getTreeFeature() or decorate().

#pragma once
#include "Biome.h"
class MyBiome : public Biome
{
public:
MyBiome(int id);
// Override for custom tree generation
virtual Feature *getTreeFeature(Random *random);
// Override for custom decoration (optional)
virtual void decorate(Level *level, Random *random, int xo, int zo);
};

Implementation (Minecraft.World/MyBiome.cpp)

Section titled “Implementation (Minecraft.World/MyBiome.cpp)”
#include "MyBiome.h"
MyBiome::MyBiome(int id) : Biome(id)
{
// Customize mob spawns -- base Biome constructor
// already adds default friendlies and enemies.
// Remove defaults if needed:
// friendlies.clear();
// enemies.clear();
// Add custom spawns:
enemies.push_back(new MobSpawnerData(eTYPE_MY_MOB, 8, 1, 3));
friendlies_wolf.push_back(
new MobSpawnerData(eTYPE_WOLF, 5, 4, 4));
// Configure the decorator
decorator->treeCount = 10;
decorator->grassCount = 2;
decorator->flowerCount = 4;
}

Properties are set through a builder-pattern chain in Biome::staticCtor(). Here’s how the vanilla forest biome is registered:

Biome::forest = (new ForestBiome(4))
->setColor(0x056621)
->setName(L"Forest")
->setLeafColor(0x4EBA31)
->setTemperatureAndDownfall(0.7f, 0.8f)
->setLeafFoliageWaterSkyColor(
eMinecraftColour_Grass_Forest,
eMinecraftColour_Foliage_Forest,
eMinecraftColour_Water_Forest,
eMinecraftColour_Sky_Forest);
MethodParametersEffect
setName(wstring)Display nameSets m_name for the biome
setColor(int)Hex RGBMap color on the debug view
setTemperatureAndDownfall(float, float)temp, downfallControls snow, rain, grass/foliage tint
setDepthAndScale(float, float)depth, scaleTerrain height; negative = water, 0.1 = flat, 1.5 = mountains
setLeafColor(int)Hex RGBLeaf block tint override
setSnowCovered()NoneTurns on snow cover (also triggered by temp < 0.15)
setNoRain()NoneDisables precipitation entirely
setLeafFoliageWaterSkyColor(...)Four eMinecraftColour valuesConsole-specific color table entries for grass, foliage, water, sky

Set in the Biome base constructor:

PropertyDefault
topMaterialTile::grass_Id
materialTile::dirt_Id
leafColor0x4EE031
temperature0.5f
downfall0.5f
depth0.1f
scale0.3f
_hasRaintrue

Temperature controls a few different things:

  • Snow: Biomes with temperature < 0.15 and rain enabled get snow instead of rain (hasSnow())
  • Rain: Disabled when setNoRain() is called or when hasSnow() returns true
  • Humid: Biomes with downfall > 0.85 are considered humid (isHumid())

Override the default grass/dirt surface by setting topMaterial and material in your constructor:

MyBiome::MyBiome(int id) : Biome(id)
{
// Sand surface like desert
this->topMaterial = (byte) Tile::sand_Id;
this->material = (byte) Tile::sand_Id;
}

The Biome base constructor adds default mob spawns for all biomes. There are six mob categories with separate lists:

ListCategoryDefault Contents
enemiesMobCategory::monsterSpider, Zombie, Skeleton, Creeper, Slime, Enderman
friendliesMobCategory::creatureSheep (12), Pig (10), Cow (8)
friendlies_chickenMobCategory::creature_chickenChicken (10)
friendlies_wolfMobCategory::creature_wolfEmpty by default
friendlies_mushroomcowMobCategory::creature_mushroomcowEmpty by default
waterFriendliesMobCategory::waterCreatureSquid (10)

The MobSpawnerData constructor takes (eINSTANCEOF mobClass, int weight, int minCount, int maxCount).

The DesertBiome clears all friendly mob lists:

DesertBiome::DesertBiome(int id) : Biome(id)
{
friendlies.clear();
friendlies_chicken.clear();
friendlies_wolf.clear();
// Desert surface
this->topMaterial = (byte) Tile::sand_Id;
this->material = (byte) Tile::sand_Id;
}

The ForestBiome adds wolves to the wolf-specific spawn list:

ForestBiome::ForestBiome(int id) : Biome(id)
{
friendlies_wolf.push_back(
new MobSpawnerData(eTYPE_WOLF, 5, 4, 4));
decorator->treeCount = 10;
decorator->grassCount = 2;
}

BiomeDecorator handles placing ores, trees, flowers, grass, reeds, cacti, and other features during chunk generation. You configure it by changing the count fields in your biome’s constructor.

FieldDefaultControls
treeCount0Trees per chunk (set to -999 to disable)
flowerCount2Yellow flowers
grassCount1Tall grass patches
deadBushCount0Dead bushes
mushroomCount0Mushroom placement
reedsCount0Sugar cane
cactusCount0Cacti
waterlilyCount0Lily pads
hugeMushrooms0Huge mushrooms
sandCount3Sand deposits
clayCount1Clay deposits
gravelCount1Gravel deposits
liquidstrueUnderground lava/water pockets

Here’s the exact order that BiomeDecorator::decorate() places things. This order matters because later features can overwrite earlier ones.

  1. Ores (decorateOres())
  2. Sand deposits (sandCount times, placed at surface)
  3. Clay deposits (clayCount times, placed at surface)
  4. Gravel deposits (gravelCount times, placed at surface)
  5. Trees (treeCount times, +1 bonus tree 10% of the time)
  6. Huge mushrooms (hugeMushrooms times)
  7. Flowers (flowerCount times, yellow flower + 25% chance of rose each)
  8. Grass (grassCount times, uses biome->getGrassFeature())
  9. Dead bushes (deadBushCount times)
  10. Water lilies (waterlilyCount times)
  11. Mushrooms (mushroomCount times, 25% chance brown + 12.5% chance red each)
  12. Extra mushrooms (always: 25% chance brown, 12.5% chance red, once each)
  13. Reeds (reedsCount times + always 10 extra attempts)
  14. Pumpkins (1 in 32 chance per chunk)
  15. Cacti (cactusCount times)
  16. Water springs (50 attempts if liquids is true)
  17. Lava springs (20 attempts if liquids is true)

Note: reeds always get 10 extra placement attempts on top of reedsCount. And mushrooms get a baseline placement (one brown, one red attempt) even if mushroomCount is 0.

The decorator places ores through decorateOres(). Each ore type has a vein size (set in _init()) and a spawn count/depth:

OreVein SizeAttempts per ChunkY Range
Dirt32 blocks200 to genDepth (128)
Gravel32 blocks100 to 128
Coal16 blocks200 to 128
Iron8 blocks200 to 64
Gold8 blocks20 to 32
Redstone7 blocks80 to 16
Diamond7 blocks10 to 16
Lapis Lazuli6 blocks1centered at y=16, spread 16

Lapis uses decorateDepthAverage() instead of decorateDepthSpan(). This means it uses a triangle distribution centered on y=16, so it’s most common around that level and gets rarer above and below.

4J added level->setInstaTick(true) around ore generation as a performance optimization. This prevents block update events from firing while ores are being placed.

The decorator places ores at these rates through decorateOres(). The feature objects are:

FeatureOre Type
coalOreFeatureCoal
ironOreFeatureIron
goldOreFeatureGold
redStoneOreFeatureRedstone
diamondOreFeatureDiamond
lapisOreFeatureLapis Lazuli

The BiomeDecorator provides three methods for ore-style placement that you can use in custom decorators:

// Place count times at random x/z, random y between y0 and y1
void decorateDepthSpan(int count, Feature *feature, int y0, int y1);
// Place count times at random x/z, y centered on yMid with triangle distribution
void decorateDepthAverage(int count, Feature *feature, int yMid, int ySpan);
// Shorthand for decorateDepthSpan with y0=0, y1=genDepth
void decorate(int count, Feature *feature);

decorateDepthAverage uses random->nextInt(ySpan) + random->nextInt(ySpan) + (yMid - ySpan). Adding two random values creates a triangle distribution that peaks at yMid. This is how lapis lazuli concentrates around y=16.

When liquids is true (the default), the decorator places:

  • 50 water springs: at random y using random->nextInt(random->nextInt(genDepth - 8) + 8). The nested nextInt makes springs more common at lower Y values.
  • 20 lava springs: at random y using triple-nested nextInt, making them even more biased toward the bottom of the world.

Set decorator->liquids = false in your biome constructor to turn off underground springs entirely.

Override getTreeFeature() to control which trees spawn. The returned Feature gets used once per tree attempt:

Feature *MyBiome::getTreeFeature(Random *random)
{
// 20% birch, 10% fancy oak, 70% normal oak
if (random->nextInt(5) == 0)
{
return new BirchFeature(false);
}
if (random->nextInt(10) == 0)
{
return new BasicTree(false);
}
return new TreeFeature(false);
}

Available tree features: TreeFeature (normal oak), BasicTree (fancy oak), BirchFeature, SwampTreeFeature, MegaTreeFeature (large 2x2 jungle trees).

For decoration beyond what the decorator handles, override decorate():

void MyBiome::decorate(Level *level, Random *random, int xo, int zo)
{
// Run standard decoration first
Biome::decorate(level, random, xo, zo);
// Add custom features (e.g., 1-in-1000 chance of a well)
if (random->nextInt(1000) == 0)
{
int x = xo + random->nextInt(16) + 8;
int z = zo + random->nextInt(16) + 8;
Feature *well = new DesertWellFeature();
well->place(level, random, x,
level->getHeightmap(x, z) + 1, z);
delete well;
}
}

In Biome::staticCtor() (Minecraft.World/Biome.cpp), add your biome with a unique ID:

// 1. Add a static pointer in Biome.h:
static Biome *myBiome;
// 2. Initialize it in Biome.cpp (top of file):
Biome *Biome::myBiome = NULL;
// 3. Create in staticCtor():
Biome::myBiome = (new MyBiome(23))
->setColor(0x336633)
->setName(L"My Biome")
->setTemperatureAndDownfall(0.6f, 0.7f)
->setDepthAndScale(0.2f, 0.5f)
->setLeafFoliageWaterSkyColor(
eMinecraftColour_Grass_Forest,
eMinecraftColour_Foliage_Forest,
eMinecraftColour_Water_Forest,
eMinecraftColour_Sky_Forest);
// 4. Update BIOME_COUNT:
static const int BIOME_COUNT = 24;

The Biome constructor automatically registers the biome in the Biome::biomes[256] array at the given ID index.

The four eMinecraftColour values point to entries in the console-specific color table. For a new biome, you’ll need to add matching color entries in the colour definitions, or just reuse existing biome colors.

The world generation layer pipeline decides where biomes show up. The pipeline is built in Layer::getDefaultLayers() (Minecraft.World/Layer.cpp).

IslandLayer # Initial ocean/land noise
FuzzyZoomLayer # Upscale with noise
AddIslandLayer ×3 # Add more land masses
AddSnowLayer # Mark cold regions
ZoomLayer ×N # Progressive upscaling
BiomeInitLayer # Assign biome IDs to land cells
RegionHillsLayer # Add hill variants
ZoomLayer ×4-6 # Final upscaling (4 normal, 6 large biomes)
AddMushroomIslandLayer + GrowMushroomIslandLayer
ShoreLayer # Add beaches along coastlines
SwampRiversLayer # Swamp river features
SmoothLayer # Reduce noise artifacts
RiverMixerLayer # Overlay rivers onto biome map
VoronoiZoom # Final precision zoom for block-level lookup

BiomeInitLayer (Minecraft.World/BiomeInitLayer.cpp) is where land cells get assigned to specific biomes. It has a startBiomes array:

BiomeInitLayer::BiomeInitLayer(__int64 seed,
shared_ptr<Layer> parent, LevelType *levelType)
: Layer(seed)
{
this->parent = parent;
if (levelType == LevelType::lvl_normal_1_1)
{
startBiomes = BiomeArray(6);
startBiomes[0] = Biome::desert;
startBiomes[1] = Biome::forest;
startBiomes[2] = Biome::extremeHills;
startBiomes[3] = Biome::swampland;
startBiomes[4] = Biome::plains;
startBiomes[5] = Biome::taiga;
}
else
{
startBiomes = BiomeArray(7);
// Same as above, plus:
startBiomes[6] = Biome::jungle;
}
}

To get your biome into world generation, add it to the startBiomes array:

// For the default level type:
startBiomes = BiomeArray(8);
startBiomes[0] = Biome::desert;
startBiomes[1] = Biome::forest;
startBiomes[2] = Biome::extremeHills;
startBiomes[3] = Biome::swampland;
startBiomes[4] = Biome::plains;
startBiomes[5] = Biome::taiga;
startBiomes[6] = Biome::jungle;
startBiomes[7] = Biome::myBiome; // Your new biome

During getArea(), each land cell picks a random biome from this array with equal probability using nextRandom(startBiomes.length).

If you want more complex biome placement logic (like biomes that only appear near certain other biomes), you can create a new Layer subclass:

class MyCustomLayer : public Layer
{
public:
MyCustomLayer(__int64 seed, shared_ptr<Layer> parent)
: Layer(seed)
{
this->parent = parent;
}
intArray getArea(int xo, int yo, int w, int h)
{
intArray input = parent->getArea(xo, yo, w, h);
intArray result = IntCache::allocate(w * h);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
initRandom(x + xo, y + yo);
int old = input[x + y * w];
// Custom logic: replace plains with your biome
// 10% of the time
if (old == Biome::plains->id
&& nextRandom(10) == 0)
{
result[x + y * w] = Biome::myBiome->id;
}
else
{
result[x + y * w] = old;
}
}
}
return result;
}
};

Insert it into the pipeline in Layer::getDefaultLayers():

biomeLayer = shared_ptr<Layer>(
new BiomeInitLayer(200, biomeLayer, levelType));
// Add your custom layer after BiomeInitLayer:
biomeLayer = shared_ptr<Layer>(
new MyCustomLayer(201, biomeLayer));

The zoomLevel variable in getDefaultLayers() controls biome scale: 4 for normal worlds, 6 for large biomes (LevelType::lvl_largeBiomes). Your biome will automatically scale with this setting since the zoom layers run after biome selection.

IDBiomeSubclass
0OceanOceanBiome
1PlainsPlainsBiome
2DesertDesertBiome
3Extreme HillsExtremeHillsBiome
4ForestForestBiome
5TaigaTaigaBiome
6SwamplandSwampBiome
7RiverRiverBiome
8Hell (Nether)HellBiome
9Sky (The End)TheEndBiome
10Frozen OceanOceanBiome
11Frozen RiverRiverBiome
12Ice PlainsIceBiome
13Ice MountainsIceBiome
14Mushroom IslandMushroomIslandBiome
15Mushroom Island ShoreMushroomIslandBiome
16BeachBeachBiome
17Desert HillsDesertBiome
18Forest HillsForestBiome
19Taiga HillsTaigaBiome
20Extreme Hills EdgeExtremeHillsBiome
21JungleJungleBiome
22Jungle HillsJungleBiome

Here’s a detailed look at what each biome subclass actually configures. Use these as reference when building your own.

The simplest biome. No subclass overrides at all. Just uses the base Biome defaults: grass surface, 0 trees, 2 flowers, 1 grass. Temperature 0.8, downfall 0.4.

  • Surface: sand on sand
  • Clears all friendly mob lists (no animals)
  • Decorator: deadBushCount = 2, reedsCount = 50, cactusCount = 10
  • No rain (setNoRain())
  • Temperature 2.0, downfall 0.0
  • Also has the desert well (1 in 1000 chance per chunk in decorate())
  • Adds wolves to friendlies_wolf (weight 5, group 4)
  • Decorator: treeCount = 10, grassCount = 2
  • getTreeFeature(): 20% birch, 80% normal oak
  • Adds wolves
  • Decorator: treeCount = 10, grassCount = 1
  • Surface: grass/dirt (default)
  • Snow covered for Taiga Hills variant
  • getTreeFeature(): 33% tall spruce, 67% normal spruce

The most complex decorator setup of any vanilla biome:

  • Decorator: treeCount = 2, flowerCount = -999 (disabled!), deadBushCount = 1, mushroomCount = 8, reedsCount = 10, waterlilyCount = 4
  • getTreeFeature(): always returns SwampTreeFeature
  • Custom decorate() adds extra reeds (10 bonus attempts on top of the 10 base) and extra mushrooms

Setting flowerCount to -999 is a clever hack. Since the flower loop runs flowerCount times, a negative number means zero iterations. The constant -999 is used instead of 0 to make the intent clear.

The densest biome by far:

  • Decorator: treeCount = 50, grassCount = 25, flowerCount = 4
  • Adds ocelots to enemies list (weight 2, group 1-1)
  • Adds chickens to friendlies_chicken (weight 10, group 4)
  • Custom getTreeFeature() with four tree types:
    • 10% chance: huge jungle tree (MegaTreeFeature, 2x2 trunk)
    • Of the remaining 90%: 50% shrub, 33.3% normal oak, 16.7% jungle tree
  • Custom decorate() adds 50 vine placements after the standard decoration
  • Temperature 1.2, downfall 0.9 (humid)
  • Snow covered
  • Temperature 0.0, downfall 0.5
  • Decorator: default (minimal vegetation)
  • Surface: grass/dirt with snow on top
  • Surface: mycelium on dirt
  • Clears all enemy and friendly mob lists
  • Adds mooshroom cows to friendlies_mushroomcow (weight 8, group 4-8)
  • Decorator: hugeMushrooms = 1, flowerCount = 0, grassCount = 0
  • Temperature 0.2, downfall 0.3
  • Adds default biome emerald ore feature (not through the standard decorator)
  • Standard decorator settings
  • Depth: -1.0 (deep water)
  • Scale: 0.1 (flat ocean floor)
  • No subclass customization beyond depth/scale
  • Surface: sand on sand
  • Depth: 0.0, scale: 0.1 (flat at sea level)
  • Depth: -0.5 (shallow water)
  • Scale: 0.0 (flat)

The layer pipeline uses a custom random number generator. Each layer gets a 64-bit seed that combines the world seed with the layer’s own seed. Here’s how it works:

void Layer::initWorldGenSeed(__int64 seed) {
worldGenSeed = seed;
worldGenSeed *= worldGenSeed * 6364136223846793005LL + 1442695040888963407LL;
worldGenSeed += baseSeed;
worldGenSeed *= worldGenSeed * 6364136223846793005LL + 1442695040888963407LL;
worldGenSeed += baseSeed;
worldGenSeed *= worldGenSeed * 6364136223846793005LL + 1442695040888963407LL;
worldGenSeed += baseSeed;
}

Then for each position, it re-seeds with the X/Z coordinates:

void Layer::initRandom(__int64 x, __int64 z) {
chunkSeed = worldGenSeed;
chunkSeed *= chunkSeed * 6364136223846793005LL + 1442695040888963407LL;
chunkSeed += x;
chunkSeed *= chunkSeed * 6364136223846793005LL + 1442695040888963407LL;
chunkSeed += z;
chunkSeed *= chunkSeed * 6364136223846793005LL + 1442695040888963407LL;
chunkSeed += x;
chunkSeed *= chunkSeed * 6364136223846793005LL + 1442695040888963407LL;
chunkSeed += z;
}

The nextRandom(int bound) method then extracts values from this seed. On PS Vita, there’s an optimization that uses bitwise AND instead of modulo when the bound is a power of 2.

This deterministic seeding is what makes world generation reproducible from the same seed. Every layer, at every position, always produces the same result for a given world seed.

Each biome has four color table entries for grass, foliage, water, and sky. These are set through setLeafFoliageWaterSkyColor():

Biome *Biome::setLeafFoliageWaterSkyColor(
eMinecraftColour grass,
eMinecraftColour foliage,
eMinecraftColour water,
eMinecraftColour sky)

The getSkyColor(), getGrassColor(), getFolageColor(), and getWaterColor() methods all read from the ColourTable using these enum values. If the colour table returns 0 (no entry), the code falls back to a temperature-based calculation.

For a new biome, you can either:

  1. Reuse existing biome color entries (pass eMinecraftColour_Grass_Forest etc.)
  2. Add new entries to the ColourTable string array and binary data

Option 1 is simpler and works fine if your biome’s colors are similar to an existing one. Option 2 gives you full control but requires editing the binary color data that ships with the texture pack.