This is an old revision of the document!
Table of Contents
Adding Biomes
NOTE: There is a ongoing pull request on Fabric API that adds biome API for 1.16.2. Please use this tutorial as a workaround until it gets merged.
Introduction
There are 3 steps that are required to add a biome to the world.
- Creating a biome
- Registering a biome
- Adding a biome to a climate zone in the world
In this tutorial, we will add new biome called obsiland biome, whose surface is covered with obsidians.
Creating a Biome
To create a biome, use Biome.Builder
and configure properties.
Missing one property will likely cause the game to crash.
It is recommended to look at vanilla biomes (created in DefaultBiomeCreator
) as examples.
public class ExampleMod implements ModInitializer { // SurfaceBuilder defines how the surface of your biome looks. // We use custom surface builder for our biome to cover surface with obsidians. private static final ConfiguredSurfaceBuilder<TernarySurfaceConfig> OBSIDIAN_SURFACE_BUILDER = SurfaceBuilder.DEFAULT .withConfig(new TernarySurfaceConfig( Blocks.OBSIDIAN.getDefaultState(), Blocks.DIRT.getDefaultState(), Blocks.GRAVEL.getDefaultState())); private static final Biome OBSILAND = createObsiland(); private static Biome createObsiland() { // We specify what entities spawn and what features generate in the biome. // Aside from some structures, trees, rocks, plants and // custom entities, these are mostly the same for each biome. // Vanilla configured features for biomes are defined in DefaultBiomeFeatures. SpawnSettings.Builder spawnSettings = new SpawnSettings.Builder(); DefaultBiomeFeatures.addFarmAnimals(spawnSettings); DefaultBiomeFeatures.addMonsters(spawnSettings, 95, 5, 100); GenerationSettings.Builder generationSettings = new GenerationSettings.Builder(); generationSettings.surfaceBuilder(OBSIDIAN_SURFACE_BUILDER); DefaultBiomeFeatures.addDefaultUndergroundStructures(generationSettings); DefaultBiomeFeatures.addLandCarvers(generationSettings); DefaultBiomeFeatures.addDefaultLakes(generationSettings); DefaultBiomeFeatures.addDungeons(generationSettings); DefaultBiomeFeatures.addMineables(generationSettings); DefaultBiomeFeatures.addDefaultOres(generationSettings); DefaultBiomeFeatures.addDefaultDisks(generationSettings); DefaultBiomeFeatures.addSprings(generationSettings); DefaultBiomeFeatures.addFrozenTopLayer(generationSettings); return (new Biome.Builder()) .precipitation(Biome.Precipitation.RAIN) .category(Biome.Category.NONE) .depth(0.125F) .scale(0.05F) .temperature(0.8F) .downfall(0.4F) .effects((new BiomeEffects.Builder()) .waterColor(0x3f76e4) .waterFogColor(0x050533) .fogColor(0xc0d8ff) .skyColor(0x77adff) .build()) .spawnSettings(spawnSettings.build()) .generationSettings(generationSettings.build()) .build(); } }
Registering Biomes
We register our biome at the entrypoint onInitialize
.
If you use your own surface builder, you will also have to register it.
public class ExampleMod implements ModInitializer { public static final RegistryKey<Biome> OBSILAND_KEY = RegistryKey.of(Registry.BIOME_KEY, new Identifier("tutorial", "obsiland")); @Override public void onInitialize() { Registry.register(BuiltinRegistries.CONFIGURED_SURFACE_BUILDER, new Identifier("tutorial", "obsidian"), OBSIDIAN_SURFACE_BUILDER); Registry.register(BuiltinRegistries.BIOME, OBSILAND_KEY.getValue(), OBSILAND); BuiltinBiomesAccessor.getRawIdMap().put(BuiltinRegistries.BIOME.getRawId(OBSILAND), OBSILAND_KEY); } }
@Mixin(BuiltinBiomes.class) public interface BuiltinBiomesAccessor { @Accessor("BY_RAW_ID") public static Int2ObjectMap<RegistryKey<Biome>> getRawIdMap() { throw new AssertionError(); } }
You should also give your biome a language entry in your en_us.json
file:
- src/main/resources/assets/modid/lang/en_us.json
{ "biome.tutorial.obsiland": "Obsiland" }
Adding a biome to a climate zone in the world
The vanilla biomes used in the overworld is defined in VanillaLayeredBiomeSource.BIOMES
.
We have to add our biome to it first.
public class ExampleMod implements ModInitializer { @Override public void onInitialize() { [...] // We have to copy existing List because it is immutable. List<RegistryKey<Biome>> biomes = new ArrayList<>(VanillaLayeredBiomeSourceAccessor.getBiomes()); biomes.add(OBSILAND_KEY); VanillaLayeredBiomeSourceAccessor.setBiomes(biomes); } }
@Mixin(VanillaLayeredBiomeSource.class) public interface VanillaLayeredBiomeSourceAccessor { @Accessor("BIOMES") public static List<RegistryKey<Biome>> getBiomes() { throw new AssertionError(); } @Accessor("BIOMES") public static void setBiomes(List<RegistryKey<Biome>> biomes) { throw new AssertionError(); } }
Secondly, we need to specify the climate to which the biome is added.
In this tutorial, we will add the custom biome to the temperate climate as an example.
We modify SetBaseBiomesLayer.TEMPERATE_BIOMES
to accomplish this.
public class ExampleMod implements ModInitializer { @Override public void onInitialize() { [...] // SetBaseBiomesLayer.TEMPERATE_BIOMES is an array of raw biome IDs. // We have to get the raw id of our biome and append it to TEMPERATE_BIOMES. SetBaseBiomesLayerAccessor.setTemperateBiomes( ArrayUtils.add(SetBaseBiomesLayerAccessor.getTemperateBiomes(), BuiltinRegistries.BIOME.getRawId(OBSILAND))); } }
@Mixin(SetBaseBiomesLayer.class) public interface SetBaseBiomesLayerAccessor { @Accessor("TEMPERATE_BIOMES") public static int[] getTemperateBiomes() { throw new AssertionError(); } @Accessor("TEMPERATE_BIOMES") public static void setTemperateBiomes(int[] biomes) { throw new AssertionError(); } }