-
-
Notifications
You must be signed in to change notification settings - Fork 34
Modded Biome Slices System
In the release of Minecraft 1.18: The Caves & Cliffs Part 2, Mojang completely reworked the Overworld and Nether's distribution of biomes, and it gets praised for its intricate and data-driven generation. Unfortunately, the system Mojang uses for picking the biomes in the Overworld and Nether (and, to a more significant extent, all dimensions) is difficult to manipulate in a compatibility-friendly way. The core issue here is the BiomeSource
class, used for selecting and storing the possible biomes in a level/dimension. An unknown amount of BiomeSource
subclasses can exist, each with unique ways of selecting biomes, making it impossible to reliably modify the biome distribution of any source.
Blueprint offers a solution to this problem for noise-based biome sources (the most common style of biome distribution). This system is named, as you will see, fittingly, the Modded Biome Slices System. The rest of this page details the system's core components, guiding developers on how to make the most of it.
HEADS UP: This page assumes you know what a BiomeSource
is. If you don't know, please watch this video section by slicedlime.
To begin, a Modded Biome Slice is a weighted slice/area with nearly complete control over the biomes within it.
Each Modded Biome Slice comprises four key components: levels
, weight
, and provider
.
Understanding what each of these components does is essential to understanding future concepts.
-
levels
: The array of level stem keys to pick which dimensions this slice will generate in. Level stem keys identify a specific dimension, such as"minecraft:overworld"
. -
weight
: The probabilistic weight of this slice determines its rarity, stored as an integer. Lower weights make this slice rarer, and higher weights make it more common. A slice's theoretical proportion of influence on a dimension's biomes equals the slice's weight divided by the sum of all other slice weights in the dimension. We say "influence" in addition to "proportion" because a slice may relinquish its territory at a block position. Further information about this "relinquish" behavior is in the section about the Original Source Marker Biome. -
provider
: AModdedBiomeProvider
provides the distribution of the biomes within this slice. Modded Biome Providers are complicated, so they deserve a section.
To have Blueprint load a slice, a .json
file representation of the slice goes into a blueprint/modded_biome_slices
folder in a resources data folder.
You can find examples of .json
slice representations in Blueprint's test mod's resources.
JSON Skeleton of a Modded Biome Slice:
{
"levels": [],
"provider": {
"type": "?"
},
"weight": 1
}
"provider"
is always an object containing "type"
, but the "type"
determines the rest of the required data in the object.
The ModdedBiomeProvider
interface is in Blueprint's BiomeUtil
class. ModdedBiomeProvider
instances are essentially used to mimic BiomeSource
instances, which is why they are in BiomeUtil
. However, the ModdedBiomeProvider
interface is known for its usage in the ModdedBiomeSlice
class.
ModdedBiomeProvider
has three instance methods: getNoiseBiome()
, getAdditionalPossibleBiomes()
, and codec()
.
-
getNoiseBiome()
Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler, BiomeSource original, Registry<Biome> registry)
- This method functions very similar to
getNoiseBiome()
inBiomeSource
. - The
original
parameter accepts theBiomeSource
in which this provider is being used. - The
registry
parameter accepts the server's biome registry. - All the other parts of this method function the same as
getNoiseBiome()
inBiomeSource
.
-
getAdditionalPossibleBiomes()
Set<Holder<Biome>> getAdditionalPossibleBiomes(Registry<Biome> registry)
- Because these providers are considered additions to existing
BiomeSource
instances, this method is a variant of thepossibleBiomes()
method inBiomeSource
. - The
registry
parameter accepts the server's biome registry. - The return value is the set of all biomes that can get returned from this provider's
getNoiseBiome()
method.
-
codec()
Codec<? extends ModdedBiomeProvider> codec()
- Returns a
Codec
instance that can serialize and deserialize instances of this provider. - Serialization is only used by Blueprint for data generation and is optional if you don't intend for data generators.
- Deserialization is required as it is used during the loading phases of the server in Blueprint to load the configurations of slices into the game.
- All
Codec
instances returned by this method should also be registered inBiomeUtil
for safety purposes. - To register your own
Codec
instances, you can call theregisterBiomeProvider()
method inBiomeUtil
.
Understanding all of the above is not very important if you do not intend to make your own ModdedBiomeProvider
implementations.
For most modders, Blueprint's built-in implementations will suffice. Those are the following:
- Has no possible configurations and simply has the same biomes as the
BiomeSource
in which it's being used. - This is used by Blueprint as the "vanilla" slice.
{
"type": "blueprint:original"
}
- Works like
MultiNoiseBiomeSource
, with the difference being it has an optional"areas"
map. See this page to learn how Multi-Noise biome sources work. - The
"areas"
map remaps the biomes in entries in the multi-noise biomes array. This is mainly for easier edit access for users. For example, a slice has variants of the forest biome, such as"mod_id:warm_forest"
and"mod_id:cold_forest"
. Users could then edit the"warm_forest"
mapping to change or remove the "warm forest" biome in the slice. The"areas"
map is optional but highly recommended to make it easier for users to change your slice. - The
"only_map_from_areas"
boolean determines if the provider should only map a biome key to a biome key from the"areas"
map. When this is false, the provider will try to find the biome belonging to a biome key if the biome key is not in the"areas"
map. - When the provider cannot map a biome key, it will default to the Original Source Marker biome.
- Particularly useful for any dimension that uses multi-noise, such as the Overworld and the Nether.
{
"type": "blueprint:multi_noise",
"areas": {
"mod_id:biome_key": "mod_id:biome_key"
},
"biomes": [
"biome": "mod_id:biome_key",
"parameters": {
"continentalness": 0.0,
"depth": 0.0,
"erosion": 0.0,
"humidity": 0.0,
"offset": 0.0,
"temperature": 0.0,
"weirdness": 0.0
}
],
"only_map_from_areas": true
}
- Configured by a
List<Pair<HolderSet<Biome>, BiomeSource>>
instance where each element is a pair of a set of biomes and a biome source to use when overlaying any biome in that set. - Particularly useful for just replacing specific biomes, such as in The End.
{
"type": "blueprint:overlay",
"overlays": [
{
"biome_source": {}
"matches_biomes": "#mod_id:biome_holderset"
}
]
}
- Configured by a
BiomeSource
instance and simply uses that source's corresponding methods. - Useful if there's a custom
BiomeSource
type that you wish to use.
{
"type": "blueprint:biome_source",
"biome_source": {}
}
Now that we've established what Modded Biome Slices are and what they're made of, we can discuss how they're used.
For Blueprint to nicely distribute everyone's modifications to a biome source, it uses a new biome source, the ModdedBiomeSource
. This biome source works by overlaying an original biome source with the slices. For now, the biome source is limited to distributing slices in a noise-blob-like pattern, similar to the old way how Minecraft distributed its biomes through randomized noise zooming. If the future demands it, the ModdedBiomeSource
may get reworked to allow more types of slice distribution.
The image above is a simulation of the distribution used in ModdedBiomeSource
, where each noise blob is colored corresponding to a separate slice.
Red, green, and blue are separate slices, each with a weight of 10. The other slices have a weight of 3, so they're smaller. We chose to distribute the slices in this style because it's fast, easy to interact and maintain on a core technical level, offers good configuration, and fits well with most noise-based biome distributions. You can imagine an existing dimension's biomes overlayed with this slice distribution, where a high-weight slice uses the dimension's original biomes. Other slices would have custom distributions—a natural flow of original biomes into mod-affected areas.
The ModdedBiomeSource
seems simple on a surface level but is not perfect and quite complex internally. The algorithm used to evaluate the slice at a block position is custom but is based on zoom-based noise. In zoom-based noise, each position is given an initial value, and a "zoom-out" process gets done to spread each value into irregular, random shapes that look natural. More info under this page's Grown Biomes section gives detailed information on the process. This style of noise got used substantially in Minecraft versions before 1.18. Blueprint's algorithm takes slightly different approaches, zooming the coordinates alone and then picking a slice at the zoomed coordinate.
So why doesn't it always work great?
- Only so much room for every slice.
- Some people will want their slices to be different sizes or shapes.
Mitigations for these issues:
- Modded Biome Slices "relinquish" territory they do not need, allowing them to free up room for other slices. For example, a slice with only biomes in cold areas will not impact the territory of a slice with only biomes elsewhere.
- Still, there's only so much room in the world to share, but players can configure individual dimension slice sizes (more detail below).
Configuring Slice Sizes per Dimension
To configure a dimension's slice size, you have to add values under the [modded_biome_slice_sizes]
in Blueprint's common config. If you don't know where mod configs are, you can find them in your Minecraft app data's config/
folder.
Sample [modded_biome_slice_sizes]
section in Blueprint's common config:
#The modded biome slice sizes for dimensions
#Blueprint's Modded Biome Slice System allows for datapacks and mods to add new biome areas to any dimension
#Changing the size values will affect the size of all modded biome areas in their respected dimension
#If a slice size isn't a positive integer, it will get ignored and the default slice size will get used instead
[modded_biome_slice_sizes]
#For example, the overworld's slice size would be formatted like this
"minecraft:overworld" = 8
#If the slice size for a dimension isn't defined, this value will get used for that dimension
default = 8
If you've read the sections above, you've heard of the ability for slices to "relinquish" their territory at a block position. However, you have yet to learn what that means or how the Original Source Marker biome gets involved. Blueprint introduces a new concept: marking a position within a slice to use the original biome source. Biome providers return the Original Source Marker biome to mark a position. The marker will cause a re-rolling process instead, allowing slices to relinquish their territory so that other randomly selected slices can get more territory. This also assists modders in having their biome frequency be more precise because they don't need their slices taking up space that's mostly default. Most importantly, the marker improves the compatibility with other modded biome systems because any position using the original biome source will let other systems run, such as Terrablender, guaranteeing they get proper configurable territory alongside Blueprint.
You can reference the Original Source Marker biome in your mods from the BlueprintBiomes
class.
Another crucial detail is that the compatibility granted by ModdedBiomeSource
only holds if you follow the protocol of the Original Source Marker! Whenever you don't need to change a biome at a position or are placing a vanilla biome at its normal position, please use the Original Source Marker instead.
Now that you've, ideally, read all the sections above, we can discuss making your life easier when it comes to making modded biome slices. The .json
files for slices are complicated and generally quite long, so data generators are potent here, not to mention, who doesn't like spending less time?
Modded Biome Slices are a Datapack Registry, so you can use the DatapackBuiltinEntriesProvider
class for generating slice files.
- Create a
DatapackBuiltinEntriesProvider
extension.
public final class ExampleDatapackBuiltinEntriesProvider extends DatapackBuiltinEntriesProvider {
private static final RegistrySetBuilder BUILDER = new RegistrySetBuilder();
public ExampleDatapackBuiltinEntriesProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries, BUILDER, Set.of(MY_MOD_ID));
}
}
- Bootstrap biomes to use in your slices.
public final class ExampleDatapackBuiltinEntriesProvider extends DatapackBuiltinEntriesProvider {
// Bootstrap the biomes you want to use in your slices. Vanilla biomes are automatically bootstrapped.
// In this example, we bootstrapped Blueprint's biomes, which contain the Original Source Marker biome.
private static final RegistrySetBuilder BUILDER = new RegistrySetBuilder()
.add(Registries.BIOME, BlueprintDatapackBuiltinEntriesProvider::bootstrapBiomes);
public ExampleDatapackBuiltinEntriesProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries, BUILDER, Set.of(MY_MOD_ID));
}
}
- Bootstrap your slices.
public final class ExampleDatapackBuiltinEntriesProvider extends DatapackBuiltinEntriesProvider {
private static final RegistrySetBuilder BUILDER = new RegistrySetBuilder()
.add(Registries.BIOME, BlueprintDatapackBuiltinEntriesProvider::bootstrapBiomes)
.add(BlueprintDataPackRegistries.MODDED_BIOME_SLICES, ExampleDatapackBuiltinEntriesProvider::bootstrapSlices);
public ExampleDatapackBuiltinEntriesProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries, BUILDER, Set.of(MY_MOD_ID));
}
private static void bootstrapSlices(BootstapContext<ModdedBiomeSlice> context) {
var biomes = context.lookup(Registries.BIOME);
// Tells the generator to generate a slice weighing 50 that will provide a checkboard-based biome source in biomes with end cities in the end.
// Just a silly little example!
context.register(
sliceKey("end_checkerboard"),
new ModdedBiomeSlice(
50,
new BiomeUtil.OverlayModdedBiomeProvider(
List.of(
Pair.of(
biomes.getOrThrow(BiomeTags.HAS_END_CITY),
new CheckerboardColumnBiomeSource(biomes.getOrThrow(BiomeTags.HAS_STRONGHOLD), 2)
)
)
),
LevelStem.END
)
);
}
private static ResourceKey<ModdedBiomeSlice> sliceKey(String name) {
return ResourceKey.create(BlueprintDataPackRegistries.MODDED_BIOME_SLICES, new ResourceLocation(MY_MOD_ID, name));
}
}
- Add your provider to Forge's
GatherDataEvent
.
private void dataSetup(GatherDataEvent event) {
boolean includeServer = event.includeServer();
DataGenerator generator = event.getGenerator();
PackOutput packOutput = generator.getPackOutput();
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();
generator.addProvider(includeServer, new ExampleDatapackBuiltinEntriesProvider(packOutput, lookupProvider));
}
- Run the data generator and enjoy your fresh, hot, and ready slices!
Now that was all cool, but you might still wonder how to generate multi-noise-based slices for more complicated distributions, such as in the Overworld and Nether. Unfortunately, that's a very long question because multi-noise is vanilla's beast. However, we will provide a bit of guidance.
The MultiNoiseModdedBiomeProvider
is what we recommend using for providing multi-noise distribution within your slice. We talked about this before.
- Replace the biomes you want to replace.
- Replace all biomes you don't want in your slice with the Original Source Marker (
BlueprintBiomes.ORIGINAL_SOURCE_MARKER
). - If you want, make more complex and low-level changes to the class.
ResourceKey<Biome> hotForestsArea = ResourceKey.create(Registries.BIOME, new ResourceLocation(MY_MOD_ID, "hot_forests"));
// Maps hot forests to the crimson forest biome
BiomeUtil.MultiNoiseModdedBiomeProvider.builder()
.area(hotForestsArea, Biomes.CRIMSON_FOREST)
.biomes(consumer -> {
new OverworldBiomeBuilder().addBiomes(pair -> {
if (pair.getSecond() == Biomes.FOREST && pair.getFirst().temperature().min() >= 0.2F) {
pair = Pair.of(pair.getFirst(), hotForestsArea);
}
consumer.accept(pair);
});
})
.build()
Replace specific biomes and use a parameter list preset. This is the most limiting but simple method. For example:
var parameterLists = context.lookup(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
// Replaces ocean biome with the small end islands biome
BiomeUtil.MultiNoiseModdedBiomeProvider.builder()
.area(Biomes.OCEAN, Biomes.SMALL_END_ISLANDS)
.biomes(parameterLists.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD))
.build()
Manually add your biome entries. This is the most flexible way, but it can be very tedious. For example:
Climate.Parameter zero = Climate.Parameter.point(0.0F);
// Generates Crimson Forests underground
BiomeUtil.MultiNoiseModdedBiomeProvider.builder()
.biomes(consumer -> {
consumer.accept(Pair.of(Climate.parameters(0, 0, 0, 0, 0, 0, 0), BlueprintBiomes.ORIGINAL_SOURCE_MARKER));
consumer.accept(Pair.of(Climate.parameters(zero, zero, zero, zero, Climate.Parameter.span(0.3F, 1.0F), zero, 0.0F), Biomes.CRIMSON_FOREST));
})
.onlyMapFromAreas(false)
.build()