Fabric Biomes API ()

Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: coderbot16 <coderbot16@gmail.com>
This commit is contained in:
valoeghese 2019-07-03 20:53:25 +02:00 committed by asie
parent 43028fa68d
commit 896c7fbb2d
19 changed files with 1286 additions and 0 deletions

View file

@ -0,0 +1,2 @@
archivesBaseName = "fabric-biomes-v1"
version = getSubprojectVersion(project, "0.1.0")

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.biomes.v1;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.minecraft.world.biome.Biome;
/**
* General API that applies to all biome sources
*/
public final class FabricBiomes {
private FabricBiomes() {
}
/**
* Allows players to naturally spawn in this biome
*
* @param biome a biome the player should be able to spawn in
*/
public static void addSpawnBiome(Biome biome) {
InternalBiomeData.addSpawnBiome(biome);
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.biomes.v1;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.minecraft.world.biome.Biome;
/**
* API that exposes some internals of the minecraft default biome source for the overworld
*/
public final class OverworldBiomes {
private OverworldBiomes() {
}
/**
* Adds the biome to the specified climate group, with the specified weight. This is only for the biomes that make up the initial continents in generation.
*
* @param biome the biome to be added
* @param climate the climate group whereto the biome is added
* @param weight the weight of the entry. The weight in this method corresponds to its selection likelihood, with
* heavier biomes being more likely to be selected and lighter biomes being selected with less likelihood.
* @see OverworldClimate for a list of vanilla biome weights
*/
public static void addContinentalBiome(Biome biome, OverworldClimate climate, double weight) {
InternalBiomeData.addOverworldContinentalBiome(climate, biome, weight);
}
/**
* Adds the biome as a hills variant of the parent biome, with the specified weight
*
* @param parent the biome to where the hills variant is added
* @param hills the biome to be set as a hills variant
* @param weight the weight of the entry. The weight in this method corresponds to its selection likelihood, with
* heavier biomes being more likely to be selected and lighter biomes being selected with less likelihood.
* Mods should use 1.0 as the default/normal weight.
*/
public static void addHillsBiome(Biome parent, Biome hills, double weight) {
InternalBiomeData.addOverworldHillsBiome(parent, hills, weight);
}
/**
* Adds the biome as a shore/beach biome for the parent biome, with the specified weight
*
* @param parent the base biome to where the shore biome is added
* @param shore the biome to be added as a shore biome
* @param weight the weight of the entry. The weight in this method corresponds to its selection likelihood, with
* heavier biomes being more likely to be selected and lighter biomes being selected with less likelihood.
* Mods should use 1.0 as the default/normal weight.
*/
public static void addShoreBiome(Biome parent, Biome shore, double weight) {
InternalBiomeData.addOverworldShoreBiome(parent, shore, weight);
}
/**
* Adds the biome as an an edge biome (excluding as a beach) of the parent biome, with the specified weight
*
* @param parent the base biome to where the edge biome is added
* @param edge the biome to be added as an edge biome
* @param weight the weight of the entry. The weight in this method corresponds to its selection likelihood, with
* heavier biomes being more likely to be selected and lighter biomes being selected with less likelihood.
* Mods should use 1.0 as the default/normal weight.
*/
public static void addEdgeBiome(Biome parent, Biome edge, double weight) {
InternalBiomeData.addOverworldEdgeBiome(parent, edge, weight);
}
/**
* Adds a 'variant' biome which replaces another biome on occasion.
* For example, addBiomeVariant(Biomes.JUNGLE, Biomes.DESERT, 0.2) will replace 20% of jungles with deserts.
* This method is rather useful for replacing biomes not generated through standard methods, such as oceans,
* deep oceans, jungles, mushroom islands, etc. When replacing ocean and deep ocean biomes, one must specify
* the biome without temperature (Biomes.OCEAN / Biomes.DEEP_OCEAN) only, as ocean temperatures have not been
* assigned; additionally, one must not specify climates for oceans, deep oceans, or mushroom islands, as they do not have
* any climate assigned at this point in the generation.
*
* @param replaced the base biome that is replaced by a variant
* @param variant the biome to be added as a variant
* @param chance the chance of replacement of the biome into the variant
* @param climates the climates in which the variants will occur in (none listed = add variant to all climates)
*/
public static void addBiomeVariant(Biome replaced, Biome variant, double chance, OverworldClimate... climates) {
InternalBiomeData.addOverworldBiomeReplacement(replaced, variant, chance, climates);
}
/**
* Sets the river type that will generate in the biome. If null is passed as the river biome, then rivers will not
* generate in this biome.
*
* @param parent the base biome in which the river biome is to be set
* @param river the river biome for this biome
*/
public static void setRiverBiome(Biome parent, Biome river) {
InternalBiomeData.setOverworldRiverBiome(parent, river);
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.biomes.v1;
/**
* Represents the climates of biomes on the overworld continents
*/
public enum OverworldClimate {
/**
* Includes Snowy Tundra (with a weight of 3) and Snowy Taiga (with a weight of 1)
*/
SNOWY,
/**
* Includes Forest, Taiga, Mountains, and Plains (all with weights of 1)
*/
COOL,
/**
* Includes Forest, Dark Forest, Mountains, Plains, Birch Forest, and Swamp (all with weights of 1)
*/
TEMPERATE,
/**
* Includes Desert (with a weight of 3), Savanna (with a weight of 2), and Plains (with a weight of 1)
*/
DRY
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import net.minecraft.world.biome.Biome;
/**
* Represents a biome variant and its corresponding chance
*/
final class BiomeVariant {
private final Biome variant;
private final double chance;
/**
* @param variant the variant biome
* @param chance the chance of replacement of the biome into the variant
*/
BiomeVariant(final Biome variant, final double chance) {
this.variant = variant;
this.chance = chance;
}
/**
* @return the variant biome
*/
Biome getVariant() {
return variant;
}
/**
* @return the chance of replacement of the biome into the variant
*/
double getChance() {
return chance;
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import net.minecraft.world.biome.Biome;
/**
* Represents a biome and its corresponding weight
*/
final class ContinentalBiomeEntry {
private final Biome biome;
private final double weight;
private final double upperWeightBound;
/**
* @param biome the biome
* @param weight how often a biome will be chosen
* @param upperWeightBound the upper weight bound within the context of the other entries, used for the binary search
*/
ContinentalBiomeEntry(final Biome biome, final double weight, final double upperWeightBound) {
this.biome = biome;
this.weight = weight;
this.upperWeightBound = upperWeightBound;
}
Biome getBiome() {
return biome;
}
double getWeight() {
return weight;
}
/**
* @return the upper weight boundary for the search
*/
double getUpperWeightBound() {
return upperWeightBound;
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import net.fabricmc.fabric.api.biomes.v1.OverworldClimate;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.Biomes;
import net.minecraft.world.biome.layer.BiomeLayers;
import java.util.*;
/**
* Lists and maps for internal use only! Stores data that is used by the various mixins into the world generation
*/
public final class InternalBiomeData {
private InternalBiomeData() {
}
private static final EnumMap<OverworldClimate, WeightedBiomePicker> OVERWORLD_MODDED_CONTINENTAL_BIOME_PICKERS = new EnumMap<>(OverworldClimate.class);
private static final Map<Biome, WeightedBiomePicker> OVERWORLD_HILLS_MAP = new HashMap<>();
private static final Map<Biome, WeightedBiomePicker> OVERWORLD_SHORE_MAP = new HashMap<>();
private static final Map<Biome, WeightedBiomePicker> OVERWORLD_EDGE_MAP = new HashMap<>();
private static final Map<Biome, VariantTransformer> OVERWORLD_VARIANT_TRANSFORMERS = new HashMap<>();
private static final Map<Biome, Biome> OVERWORLD_RIVER_MAP = new HashMap<>();
private static final List<Biome> OVERWORLD_INJECTED_BIOMES = new ArrayList<>();
private static final Set<Biome> SPAWN_BIOMES = new HashSet<>();
public static void addOverworldContinentalBiome(OverworldClimate climate, Biome biome, double weight) {
Preconditions.checkArgument(climate != null, "Climate is null");
Preconditions.checkArgument(biome != null, "Biome is null");
Preconditions.checkArgument(!Double.isNaN(weight), "Weight is NaN");
Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (%s)", weight);
OVERWORLD_MODDED_CONTINENTAL_BIOME_PICKERS.computeIfAbsent(climate, k -> new WeightedBiomePicker()).addBiome(biome, weight);
OVERWORLD_INJECTED_BIOMES.add(biome);
}
public static void addOverworldHillsBiome(Biome primary, Biome hills, double weight) {
Preconditions.checkArgument(primary != null, "Primary biome is null");
Preconditions.checkArgument(hills != null, "Hills biome is null");
Preconditions.checkArgument(!Double.isNaN(weight), "Weight is NaN");
Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (%s)", weight);
OVERWORLD_HILLS_MAP.computeIfAbsent(primary, biome -> DefaultHillsData.injectDefaultHills(primary, new WeightedBiomePicker())).addBiome(hills, weight);
OVERWORLD_INJECTED_BIOMES.add(hills);
}
public static void addOverworldShoreBiome(Biome primary, Biome shore, double weight) {
Preconditions.checkArgument(primary != null, "Primary biome is null");
Preconditions.checkArgument(shore != null, "Shore biome is null");
Preconditions.checkArgument(!Double.isNaN(weight), "Weight is NaN");
Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (%s)", weight);
OVERWORLD_SHORE_MAP.computeIfAbsent(primary, biome -> new WeightedBiomePicker()).addBiome(shore, weight);
OVERWORLD_INJECTED_BIOMES.add(shore);
}
public static void addOverworldEdgeBiome(Biome primary, Biome edge, double weight) {
Preconditions.checkArgument(primary != null, "Primary biome is null");
Preconditions.checkArgument(edge != null, "Edge biome is null");
Preconditions.checkArgument(!Double.isNaN(weight), "Weight is NaN");
Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (%s)", weight);
OVERWORLD_EDGE_MAP.computeIfAbsent(primary, biome -> new WeightedBiomePicker()).addBiome(edge, weight);
OVERWORLD_INJECTED_BIOMES.add(edge);
}
public static void addOverworldBiomeReplacement(Biome replaced, Biome variant, double chance, OverworldClimate[] climates) {
Preconditions.checkArgument(replaced != null, "Replaced biome is null");
Preconditions.checkArgument(variant != null, "Variant biome is null");
Preconditions.checkArgument(chance > 0 && chance <= 1, "Chance is not greater than 0 or less than or equal to 1");
OVERWORLD_VARIANT_TRANSFORMERS.computeIfAbsent(replaced, biome -> new VariantTransformer()).addBiome(variant, chance, climates);
OVERWORLD_INJECTED_BIOMES.add(variant);
}
public static void setOverworldRiverBiome(Biome primary, Biome river) {
Preconditions.checkArgument(primary != null, "Primary biome is null");
OVERWORLD_RIVER_MAP.put(primary, river);
if (river != null) {
OVERWORLD_INJECTED_BIOMES.add(river);
}
}
public static void addSpawnBiome(Biome biome) {
Preconditions.checkArgument(biome != null, "Biome is null");
SPAWN_BIOMES.add(biome);
}
public static List<Biome> getOverworldInjectedBiomes() {
return OVERWORLD_INJECTED_BIOMES;
}
public static Set<Biome> getSpawnBiomes() {
return SPAWN_BIOMES;
}
public static Map<Biome, WeightedBiomePicker> getOverworldHills() {
return OVERWORLD_HILLS_MAP;
}
public static Map<Biome, WeightedBiomePicker> getOverworldShores() {
return OVERWORLD_SHORE_MAP;
}
public static Map<Biome, WeightedBiomePicker> getOverworldEdges() {
return OVERWORLD_EDGE_MAP;
}
public static Map<Biome, Biome> getOverworldRivers() {
return OVERWORLD_RIVER_MAP;
}
public static EnumMap<OverworldClimate, WeightedBiomePicker> getOverworldModdedContinentalBiomePickers() {
return OVERWORLD_MODDED_CONTINENTAL_BIOME_PICKERS;
}
public static Map<Biome, VariantTransformer> getOverworldVariantTransformers() {
return OVERWORLD_VARIANT_TRANSFORMERS;
}
private static class DefaultHillsData {
private static final ImmutableMap<Biome, Biome> DEFAULT_HILLS;
static WeightedBiomePicker injectDefaultHills(Biome base, WeightedBiomePicker picker) {
Biome defaultHill = DEFAULT_HILLS.get(base);
if (defaultHill != null) {
picker.addBiome(defaultHill, 1);
} else if (BiomeLayers.areSimilar(Registry.BIOME.getRawId(base), Registry.BIOME.getRawId(Biomes.WOODED_BADLANDS_PLATEAU))) {
picker.addBiome(Biomes.BADLANDS, 1);
} else if (base == Biomes.DEEP_OCEAN || base == Biomes.DEEP_LUKEWARM_OCEAN || base == Biomes.DEEP_COLD_OCEAN) {
picker.addBiome(Biomes.PLAINS, 1);
picker.addBiome(Biomes.FOREST, 1);
} else if (base == Biomes.DEEP_FROZEN_OCEAN) {
// Note: Vanilla Deep Frozen Oceans only have a 1/3 chance of having default hills.
// This is a clever trick that ensures that when a mod adds hills with a weight of 1, the 1/3 chance is fulfilled.
// 0.5 + 1.0 = 1.5, and 0.5 / 1.5 = 1/3.
picker.addBiome(Biomes.PLAINS, 0.25);
picker.addBiome(Biomes.FOREST, 0.25);
} else if (base == Biomes.PLAINS) {
picker.addBiome(Biomes.WOODED_HILLS, 1);
picker.addBiome(Biomes.FOREST, 2);
}
return picker;
}
static {
ImmutableMap.Builder<Biome, Biome> builder = ImmutableMap.builder();
builder.put(Biomes.DESERT, Biomes.DESERT_HILLS);
builder.put(Biomes.FOREST, Biomes.WOODED_HILLS);
builder.put(Biomes.BIRCH_FOREST, Biomes.BIRCH_FOREST_HILLS);
builder.put(Biomes.DARK_FOREST, Biomes.PLAINS);
builder.put(Biomes.TAIGA, Biomes.TAIGA_HILLS);
builder.put(Biomes.GIANT_TREE_TAIGA, Biomes.GIANT_TREE_TAIGA_HILLS);
builder.put(Biomes.SNOWY_TAIGA, Biomes.SNOWY_TAIGA_HILLS);
builder.put(Biomes.SNOWY_TUNDRA, Biomes.SNOWY_MOUNTAINS);
builder.put(Biomes.JUNGLE, Biomes.JUNGLE_HILLS);
builder.put(Biomes.BAMBOO_JUNGLE, Biomes.BAMBOO_JUNGLE_HILLS);
builder.put(Biomes.OCEAN, Biomes.DEEP_OCEAN);
builder.put(Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN);
builder.put(Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN);
builder.put(Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN);
builder.put(Biomes.MOUNTAINS, Biomes.WOODED_MOUNTAINS);
builder.put(Biomes.SAVANNA, Biomes.SAVANNA_PLATEAU);
DEFAULT_HILLS = builder.build();
}
}
}

View file

@ -0,0 +1,144 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldClimate;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import java.util.List;
import java.util.Map;
import java.util.function.IntConsumer;
/**
* Internal utilities used for biome sampling
*/
public final class InternalBiomeUtils {
private InternalBiomeUtils() {
}
/**
* @param north raw id of the biome to the north
* @param east raw id of the biome to the east
* @param south raw id of the biome to the south
* @param west raw id of the biome to the west
* @param center central biome that comparisons are relative to
* @return whether the central biome is an edge of a biome
*/
public static boolean isEdge(int north, int east, int south, int west, int center) {
return areUnsimilar(center, north) || areUnsimilar(center, east) || areUnsimilar(center, south) || areUnsimilar(center, west);
}
/**
* @param mainBiomeId the main raw biome id in comparison
* @param secondaryBiomeId the secondary raw biome id in comparison
* @return whether the two biomes are unsimilar
*/
private static boolean areUnsimilar(int mainBiomeId, int secondaryBiomeId) {
if (mainBiomeId == secondaryBiomeId) { // for efficiency, determine if the ids are equal first
return false;
} else {
Biome secondaryBiome = Registry.BIOME.get(secondaryBiomeId);
Biome mainBiome = Registry.BIOME.get(mainBiomeId);
boolean isUnsimilar = secondaryBiome.hasParent() ? !(mainBiomeId == Registry.BIOME.getRawId(Registry.BIOME.get(new Identifier(secondaryBiome.getParent())))) : true;
isUnsimilar = isUnsimilar && (mainBiome.hasParent() ? !(secondaryBiomeId == Registry.BIOME.getRawId(Registry.BIOME.get(new Identifier(mainBiome.getParent())))) : true);
return isUnsimilar;
}
}
/**
* @param north raw id of the biome to the north
* @param east raw id of the biome to the east
* @param south raw id of the biome to the south
* @param west raw id of the biome to the west
* @return whether a biome in any direction is an ocean around the central biome
*/
public static boolean neighborsOcean(int north, int east, int south, int west) {
return isOceanBiome(north) || isOceanBiome(east) || isOceanBiome(south) || isOceanBiome(west);
}
private static boolean isOceanBiome(int id) {
Biome biome = Registry.BIOME.get(id);
return biome != null && biome.getCategory() == Biome.Category.OCEAN;
}
public static int searchForBiome(double reqWeightSum, int vanillaArrayWeight, List<ContinentalBiomeEntry> moddedBiomes) {
reqWeightSum -= vanillaArrayWeight;
int low = 0;
int high = moddedBiomes.size() - 1;
while (low < high) {
int mid = (high + low) >>> 1;
if (reqWeightSum < moddedBiomes.get(mid).getUpperWeightBound()) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
}
/**
* Potentially transforms a biome into its variants based on the provided randomness source.
*
* @param random The randomness source
* @param existing The base biome
* @param climate The climate in which the biome resides, or null to indicate an unknown climate
* @return The potentially transformed biome
*/
public static int transformBiome(LayerRandomnessSource random, Biome existing, OverworldClimate climate) {
Map<Biome, VariantTransformer> overworldVariantTransformers = InternalBiomeData.getOverworldVariantTransformers();
VariantTransformer transformer = overworldVariantTransformers.get(existing);
if (transformer != null) {
return Registry.BIOME.getRawId(transformer.transformBiome(existing, random, climate));
}
return Registry.BIOME.getRawId(existing);
}
public static void injectBiomesIntoClimate(LayerRandomnessSource random, int[] vanillaArray, OverworldClimate climate, IntConsumer result) {
WeightedBiomePicker picker = InternalBiomeData.getOverworldModdedContinentalBiomePickers().get(climate);
if (picker == null || picker.getCurrentWeightTotal() <= 0.0) {
// Return early, there are no modded biomes.
// Since we don't pass any values to the IntConsumer, this falls through to vanilla logic.
// Thus, this prevents Fabric from changing vanilla biome selection behavior without biome mods in this case.
return;
}
int vanillaArrayWeight = vanillaArray.length;
double reqWeightSum = (double) random.nextInt(Integer.MAX_VALUE) * (vanillaArray.length + picker.getCurrentWeightTotal()) / Integer.MAX_VALUE;
if (reqWeightSum < vanillaArray.length) {
// Vanilla biome; look it up from the vanilla array and transform accordingly.
result.accept(transformBiome(random, Registry.BIOME.get(vanillaArray[(int) reqWeightSum]), climate));
} else {
// Modded biome; use a binary search, and then transform accordingly.
ContinentalBiomeEntry found = picker.search(reqWeightSum - vanillaArrayWeight);
result.accept(transformBiome(random, found.getBiome(), climate));
}
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldClimate;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Deals with picking variants for you
*/
final class VariantTransformer {
private final SubTransformer defaultTransformer = new SubTransformer();
private final Map<OverworldClimate, SubTransformer> transformers = new HashMap<>();
/**
* @param variant the variant that the replaced biome is replaced with
* @param chance the chance of replacement of the biome into the variant
* @param climates the climates that the variant can replace the base biome in, empty/null indicates all climates
*/
void addBiome(Biome variant, double chance, OverworldClimate[] climates) {
if (climates == null || climates.length == 0) {
defaultTransformer.addBiome(variant, chance);
climates = OverworldClimate.values();
}
for (OverworldClimate climate : climates) {
transformers.computeIfAbsent(climate, c -> new SubTransformer()).addBiome(variant, chance);
}
}
/**
* Transforms a biome into a variant randomly depending on its chance
*
* @param replaced biome to transform
* @param random the {@link LayerRandomnessSource} from the layer
* @return the transformed biome
*/
Biome transformBiome(Biome replaced, LayerRandomnessSource random, OverworldClimate climate) {
if (climate == null) {
return defaultTransformer.transformBiome(replaced, random);
}
SubTransformer transformer = transformers.get(climate);
if (transformer != null) {
return transformer.transformBiome(replaced, random);
} else {
return replaced;
}
}
static final class SubTransformer {
private final List<BiomeVariant> variants = new ArrayList<>();
/**
* @param variant the variant that the replaced biome is replaced with
* @param chance the chance of replacement of the biome into the variant
*/
private void addBiome(Biome variant, double chance) {
variants.add(new BiomeVariant(variant, chance));
}
/**
* Transforms a biome into a variant randomly depending on its chance
*
* @param replaced biome to transform
* @param random the {@link LayerRandomnessSource} from the layer
* @return the transformed biome
*/
private Biome transformBiome(Biome replaced, LayerRandomnessSource random) {
for (BiomeVariant variant : variants) {
if (random.nextInt(Integer.MAX_VALUE) < variant.getChance() * Integer.MAX_VALUE) {
return variant.getVariant();
}
}
return replaced;
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.biomes;
import com.google.common.base.Preconditions;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import java.util.ArrayList;
import java.util.List;
/**
* Picks biomes with arbitrary double weights using a binary search.
*/
public final class WeightedBiomePicker {
private double currentTotal;
private List<ContinentalBiomeEntry> entries;
WeightedBiomePicker() {
currentTotal = 0;
entries = new ArrayList<>();
}
void addBiome(final Biome biome, final double weight) {
currentTotal += weight;
entries.add(new ContinentalBiomeEntry(biome, weight, currentTotal));
}
double getCurrentWeightTotal() {
return currentTotal;
}
public Biome pickRandom(LayerRandomnessSource random) {
double target = (double) random.nextInt(Integer.MAX_VALUE) * getCurrentWeightTotal() / Integer.MAX_VALUE;
return search(target).getBiome();
}
/**
* Searches with the specified target value
*
* @param target The target value, must satisfy the constraint 0 <= target <= currentTotal
* @return The result of the search
*/
ContinentalBiomeEntry search(final double target) {
// Sanity checks, fail fast if stuff is going wrong.
Preconditions.checkArgument(target <= currentTotal, "The provided target value for biome selection must be less than or equal to the weight total");
Preconditions.checkArgument(target >= 0, "The provided target value for biome selection cannot be negative");
int low = 0;
int high = entries.size() - 1;
while (low < high) {
int mid = (high + low) >>> 1;
if (target < entries.get(mid).getUpperWeightBound()) {
high = mid;
} else {
low = mid + 1;
}
}
return entries.get(low);
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldBiomes;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.fabricmc.fabric.impl.biomes.InternalBiomeUtils;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.AddEdgeBiomesLayer;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Adds edges and shores specified in {@link OverworldBiomes#addEdgeBiome(Biome, Biome, double)} and {@link OverworldBiomes#addShoreBiome(Biome, Biome, double)} to the edges layer
*/
@Mixin(AddEdgeBiomesLayer.class)
public class MixinAddEdgeBiomesLayer {
@Inject(at = @At("HEAD"), method = "sample", cancellable = true)
private void sample(LayerRandomnessSource rand, int north, int east, int south, int west, int center, CallbackInfoReturnable<Integer> info) {
Biome centerBiome = Registry.BIOME.get(center);
if (InternalBiomeData.getOverworldShores().containsKey(centerBiome) && InternalBiomeUtils.neighborsOcean(north, east, south, west)) {
info.setReturnValue(Registry.BIOME.getRawId(InternalBiomeData.getOverworldShores().get(centerBiome).pickRandom(rand)));
} else if (InternalBiomeData.getOverworldEdges().containsKey(centerBiome) && InternalBiomeUtils.isEdge(north, east, south, west, center)) {
info.setReturnValue(Registry.BIOME.getRawId(InternalBiomeData.getOverworldEdges().get(centerBiome).pickRandom(rand)));
}
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldBiomes;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.fabricmc.fabric.impl.biomes.WeightedBiomePicker;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.AddHillsLayer;
import net.minecraft.world.biome.layer.BiomeLayers;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import net.minecraft.world.biome.layer.LayerSampler;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Injects hills biomes specified from {@link OverworldBiomes#addHillsBiome(Biome, Biome, double)}into the default hills layer
*/
@Mixin(AddHillsLayer.class)
public class MixinAddHillsLayer {
@Inject(at = @At("HEAD"), method = "sample", cancellable = true)
private void sample(LayerRandomnessSource rand, LayerSampler biomeSampler, LayerSampler noiseSampler, int chunkX, int chunkZ, CallbackInfoReturnable<Integer> info) {
if (InternalBiomeData.getOverworldHills().isEmpty()) {
// No use doing anything if there are no hills registered. Fall through to vanilla logic.
return;
}
final int biomeId = biomeSampler.sample(chunkX, chunkZ);
int noiseSample = noiseSampler.sample(chunkX, chunkZ);
int processedNoiseSample = (noiseSample - 2) % 29;
final Biome biome = Registry.BIOME.get(biomeId);
WeightedBiomePicker hillPicker = InternalBiomeData.getOverworldHills().get(biome);
if (hillPicker == null) {
// No hills for this biome, fall through to vanilla logic.
return;
}
if (rand.nextInt(3) == 0 || processedNoiseSample == 0) {
int biomeReturn = Registry.BIOME.getRawId(hillPicker.pickRandom(rand));
Biome parent;
if (processedNoiseSample == 0 && biomeReturn != biomeId) {
parent = Biome.getParentBiome(Registry.BIOME.get(biomeReturn));
biomeReturn = parent == null ? biomeId : Registry.BIOME.getRawId(parent);
}
if (biomeReturn != biomeId) {
int similarity = 0;
if (BiomeLayers.areSimilar(biomeSampler.sample(chunkX, chunkZ - 1), biomeId)) {
++similarity;
}
if (BiomeLayers.areSimilar(biomeSampler.sample(chunkX + 1, chunkZ), biomeId)) {
++similarity;
}
if (BiomeLayers.areSimilar(biomeSampler.sample(chunkX - 1, chunkZ), biomeId)) {
++similarity;
}
if (BiomeLayers.areSimilar(biomeSampler.sample(chunkX, chunkZ + 1), biomeId)) {
++similarity;
}
if (similarity >= 3) {
info.setReturnValue(biomeReturn);
return;
}
}
}
// Cancel vanilla logic.
info.setReturnValue(biomeId);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldBiomes;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.AddRiversLayer;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import net.minecraft.world.biome.layer.LayerSampler;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
/**
* Sets river biomes specified with {@link OverworldBiomes#setRiverBiome(Biome, Biome)}
*/
@Mixin(AddRiversLayer.class)
public class MixinAddRiversLayer {
@Shadow
@Final
private static int RIVER_ID;
@Inject(at = @At("HEAD"), method = "sample", cancellable = true)
private void sample(LayerRandomnessSource rand, LayerSampler landSampler, LayerSampler riverSampler, int x, int z, CallbackInfoReturnable<Integer> info) {
int landBiomeId = landSampler.sample(x, z);
Biome landBiome = Registry.BIOME.get(landBiomeId);
int riverBiomeId = riverSampler.sample(x, z);
Map<Biome, Biome> overworldRivers = InternalBiomeData.getOverworldRivers();
if (overworldRivers.containsKey(landBiome) && riverBiomeId == RIVER_ID) {
Biome riverBiome = overworldRivers.get(landBiome);
info.setReturnValue(riverBiome == null ? landBiomeId : Registry.BIOME.getRawId(riverBiome));
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.source.BiomeSource;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Adds spawn biomes to the base {@link BiomeSource} class.
*/
@Mixin(BiomeSource.class)
public class MixinBiomeSource {
@Shadow
@Final
private static List<Biome> SPAWN_BIOMES;
@Inject(at = @At("RETURN"), cancellable = true, method = "getSpawnBiomes")
private void getSpawnBiomes(CallbackInfoReturnable<List<Biome>> info) {
Set<Biome> biomes = new LinkedHashSet<>(info.getReturnValue());
if (biomes.addAll(InternalBiomeData.getSpawnBiomes())) {
info.setReturnValue(new ArrayList<>(biomes));
}
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.api.biomes.v1.OverworldClimate;
import net.fabricmc.fabric.impl.biomes.InternalBiomeUtils;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.layer.LayerRandomnessSource;
import net.minecraft.world.biome.layer.SetBaseBiomesLayer;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Injects biomes into the arrays of biomes in the {@link SetBaseBiomesLayer}
*/
@Mixin(SetBaseBiomesLayer.class)
public class MixinSetBaseBiomesLayer {
@Shadow
@Final
@Mutable
private static int[] SNOWY_BIOMES;
@Shadow
@Final
@Mutable
private static int[] COOL_BIOMES;
@Shadow
@Final
@Mutable
private static int[] TEMPERATE_BIOMES;
@Shadow
@Final
@Mutable
private static int[] DRY_BIOMES;
@Shadow
@Final
private static int WOODED_BADLANDS_PLATEAU_ID;
@Shadow
@Final
private static int BADLANDS_PLATEAU_ID;
@Shadow
@Final
private static int JUNGLE_ID;
@Shadow
@Final
private static int GIANT_TREE_TAIGA_ID;
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/world/biome/layer/SetBaseBiomesLayer;chosenGroup1:[I"), method = "sample", cancellable = true)
private void injectDryBiomes(LayerRandomnessSource random, int value, CallbackInfoReturnable<Integer> info) {
InternalBiomeUtils.injectBiomesIntoClimate(random, DRY_BIOMES, OverworldClimate.DRY, info::setReturnValue);
}
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/world/biome/layer/SetBaseBiomesLayer;TEMPERATE_BIOMES:[I"), method = "sample", cancellable = true)
private void injectTemperateBiomes(LayerRandomnessSource random, int value, CallbackInfoReturnable<Integer> info) {
InternalBiomeUtils.injectBiomesIntoClimate(random, TEMPERATE_BIOMES, OverworldClimate.TEMPERATE, info::setReturnValue);
}
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/world/biome/layer/SetBaseBiomesLayer;SNOWY_BIOMES:[I"), method = "sample", cancellable = true)
private void injectSnowyBiomes(LayerRandomnessSource random, int value, CallbackInfoReturnable<Integer> info) {
InternalBiomeUtils.injectBiomesIntoClimate(random, SNOWY_BIOMES, OverworldClimate.SNOWY, info::setReturnValue);
}
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/world/biome/layer/SetBaseBiomesLayer;COOL_BIOMES:[I"), method = "sample", cancellable = true)
private void injectCoolBiomes(LayerRandomnessSource random, int value, CallbackInfoReturnable<Integer> info) {
InternalBiomeUtils.injectBiomesIntoClimate(random, COOL_BIOMES, OverworldClimate.COOL, info::setReturnValue);
}
@Inject(at = @At("RETURN"), method = "sample", cancellable = true)
private void transformVariants(LayerRandomnessSource random, int value, CallbackInfoReturnable<Integer> info) {
int biomeId = info.getReturnValueI();
Biome biome = Registry.BIOME.get(biomeId);
// Determine what special case this is...
OverworldClimate climate;
if (biomeId == BADLANDS_PLATEAU_ID || biomeId == WOODED_BADLANDS_PLATEAU_ID) {
climate = OverworldClimate.DRY;
} else if (biomeId == JUNGLE_ID) {
climate = OverworldClimate.TEMPERATE;
} else if (biomeId == GIANT_TREE_TAIGA_ID) {
climate = OverworldClimate.TEMPERATE;
} else {
climate = null;
}
info.setReturnValue(InternalBiomeUtils.transformBiome(random, biome, climate));
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.biomes;
import net.fabricmc.fabric.impl.biomes.InternalBiomeData;
import net.minecraft.block.BlockState;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.source.VanillaLayeredBiomeSource;
import net.minecraft.world.gen.feature.StructureFeature;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
import java.util.Set;
/**
* Adds the biomes in world gen to the array for the vanilla layered biome source.
* This helps with {@link VanillaLayeredBiomeSource#hasStructureFeature(StructureFeature)} returning correctly for modded biomes as well as in {@link VanillaLayeredBiomeSource#getTopMaterials()}}
*/
@Mixin(VanillaLayeredBiomeSource.class)
public class MixinVanillaLayeredBiomeSource {
@Shadow
@Final
@Mutable
private Biome[] biomes;
@Unique
private int injectionCount;
@Inject(at = @At("HEAD"), method = "hasStructureFeature")
private void hasStructureFeature(CallbackInfoReturnable<Boolean> info) {
updateInjections();
}
@Inject(at = @At("HEAD"), method = "getTopMaterials")
private void getTopMaterials(CallbackInfoReturnable<Set<BlockState>> info) {
updateInjections();
}
@Unique
private void updateInjections() {
List<Biome> injectedBiomes = InternalBiomeData.getOverworldInjectedBiomes();
int currentSize = injectedBiomes.size();
if (this.injectionCount < currentSize) {
List<Biome> toInject = injectedBiomes.subList(injectionCount, currentSize - 1);
Biome[] oldBiomes = this.biomes;
this.biomes = new Biome[oldBiomes.length + toInject.size()];
System.arraycopy(oldBiomes, 0, this.biomes, 0, oldBiomes.length);
int index = oldBiomes.length;
for (Biome injected : toInject) {
biomes[index++] = injected;
}
injectionCount += toInject.size();
}
}
}

View file

@ -0,0 +1,16 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.biomes",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinAddEdgeBiomesLayer",
"MixinAddHillsLayer",
"MixinAddRiversLayer",
"MixinBiomeSource",
"MixinSetBaseBiomesLayer",
"MixinVanillaLayeredBiomeSource"
],
"injectors": {
"defaultRequire": 1
}
}

View file

@ -0,0 +1,18 @@
{
"schemaVersion": 1,
"id": "fabric-biomes-v1",
"version": "${version}",
"name": "fabric-biomes-v1",
"license": "Apache-2.0",
"environment": "*",
"depends": {
"fabricloader": ">=0.4.0"
},
"mixins": [
"fabric-biomes-v1.mixins.json"
]
}

View file

@ -14,6 +14,7 @@ rootProject.name = "fabric-api"
include 'fabric-api-base'
include 'fabric-biomes-v1'
include 'fabric-commands-v0'
include 'fabric-containers-v0'
include 'fabric-content-registries-v0'