From a2bb17cf40f1ee28a735e6c082310b78d2fce3ae Mon Sep 17 00:00:00 2001
From: RedstoneParadox <spinjitzu00@gmail.com>
Date: Fri, 13 Nov 2020 10:22:02 -0800
Subject: [PATCH] End Biomes Api [1.16] (#830)

* Create EndBiomes.java

* Revert "Create EndBiomes.java"

This reverts commit 4d1736fad99e1be0921ef92d9576e8f503e64be4.

* Revert "Revert "Create EndBiomes.java""

This reverts commit 673c508d825a0eb095f94ff9b5cb88d1b77e395c.

* Rename ContinentalBiomeEntry to WeightedBiomeEntry

* Complete API for adding biomes to the end

* Mixin to end biome source

* Revert "Rename ContinentalBiomeEntry to WeightedBiomeEntry"

This reverts commit 849197e15d18c26af2a6f2aa1766b7d73d9a7144.

* Revert "Revert "Rename ContinentalBiomeEntry to WeightedBiomeEntry""

This reverts commit c2aa4ab097eb3daed71e82d4e77142671ed04316.

* Create SimpleLayerRandomnessSource for use with end biome source mixin

* Renamed MAIN region to MAIN_ISLANDS

* Create test mod

* Added pickFromNoise method to WeightedBiomePicker

* Javadoc and licensing info

* Comply with checkstyle

* Internalize EndRegion

* Added stretching when getting the biome for the end

* Rename EndBiomes to TheEndBiomes to be consistent with yarn mappings

* Fix typo

Courtesy of i509VBC

Co-authored-by: i509VCB <i509vcb@gmail.com>

* Add @Unique to private fields in MixinTheEndBiomeSource

* Made end test biome consistent in style with previous test

Courtesy of i509VBC

Co-authored-by: i509VCB <i509vcb@gmail.com>

* Fix imports

* Didn't know test mods needed to follow the checkstyle

* SimpleLayerRandomnessSource's random is now use to instantiate its noise sampler

* A very important note

* Update javadoc for adding biomes to the Main Island region

* Remove method that is leftover from when EndRegion was part of the API

* Ported 1.16.1 biomes-api-v1 to 1.16.2.
Public API changes:
- Removed flagging biomes as suitable for player spawns since that is now handled in the Biome Builder.
- Changed API over to RegistryKey's, not because it's necessary, but because it is more ergonomic since Vanilla Biomes in BuiltInBiomes are now all exposed as keys, rather than instances.

* Increase memory size to fix build failures.

* Add the ability to add modded dimensions to the regions of the end.

* Hopefully fixed everything that went wrong during the rebase

* Update fabric-biome-api-v1.mixins.json

* Finally got custom end biomes to generate

* Thanks for nothing, GitHub Desktop

* Bad GitHub Desktop

* Bad newline no cookie

* Got everything to follow the checkstyle

* Bring branch up-to-date with main repo (#11)

* Revert "Bring branch up-to-date with main repo (#11)"

This reverts commit dc471062410ec2de0899a7520584b16f5ddb88f1.

* Remove some stuff that's not supposed to be there

* Fix git silliness

* Update FabricBiomeTest.java

* TheEndBiomes is now Deprecated to match OverworldBiomes and NetherBiomes

* Update some checkNotNull messages

* Change the noise scale when replacing end biomes

* Bad space no cookie

* Remove unnecessary code

* Remove unused imports

* Set up InternalBiomeData to treat End Midlands and End Barrens as border biomes

* Changed the API to reflect midlands and barrens biomes being considered border biomes

* Start work on getting the new system fully working

* Finally got everything working and cleaned up the Javadoc

* Fixed checkstyle violations

* Fix checkstyle violations again

* Drop fabric_ prefix

* Calling SimpleLayerRandomnessSource#nextInt() now throws an exception

Co-authored-by: i509VCB <i509vcb@gmail.com>
Co-authored-by: Sebastian Hartte <sebastian@hartte.de>
---
 .../fabric/api/biome/v1/TheEndBiomes.java     | 104 ++++++++++++++++++
 .../fabric/impl/biome/InternalBiomeData.java  |  57 +++++++++-
 .../fabric/impl/biome/InternalBiomeUtils.java |   4 +-
 .../biome/SimpleLayerRandomnessSource.java    |  41 +++++++
 ...iomeEntry.java => WeightedBiomeEntry.java} |   4 +-
 .../impl/biome/WeightedBiomePicker.java       |  12 +-
 .../mixin/biome/MixinTheEndBiomeSource.java   |  81 ++++++++++++++
 .../resources/fabric-biome-api-v1.mixins.json |   1 +
 .../fabric/test/biome/FabricBiomeTest.java    |  57 +++++++++-
 9 files changed, 348 insertions(+), 13 deletions(-)
 create mode 100644 fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/api/biome/v1/TheEndBiomes.java
 create mode 100644 fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/SimpleLayerRandomnessSource.java
 rename fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/{ContinentalBiomeEntry.java => WeightedBiomeEntry.java} (90%)
 create mode 100644 fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/mixin/biome/MixinTheEndBiomeSource.java

diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/api/biome/v1/TheEndBiomes.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/api/biome/v1/TheEndBiomes.java
new file mode 100644
index 000000000..d334b3290
--- /dev/null
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/api/biome/v1/TheEndBiomes.java
@@ -0,0 +1,104 @@
+/*
+ * 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.biome.v1;
+
+import net.minecraft.util.registry.RegistryKey;
+import net.minecraft.world.biome.Biome;
+import net.minecraft.world.biome.BiomeKeys;
+
+import net.fabricmc.fabric.impl.biome.InternalBiomeData;
+
+/**
+ * API that exposes some internals of the minecraft default biome source for The End.
+ *
+ * @deprecated Experimental feature, may be removed or changed without further notice.
+ * Because of the volatility of world generation in Minecraft 1.16, this API is marked experimental
+ * since it is likely to change in future Minecraft versions.
+ */
+@Deprecated
+public final class TheEndBiomes {
+	private TheEndBiomes() { }
+
+	/**
+	 * <p>Adds the biome as a main end island biome with the specified weight; note that this includes the main island
+	 * and some of the land encircling the empty space. Note that adding a biome to this region could potentially mess
+	 * with the generation of the center island and cause it to generate incorrectly; this method only exists for
+	 * consistency.</p>
+	 *
+	 * @param biome the biome to be 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.
+	 * Vanilla biomes have a weight of 1.0
+	 */
+	public static void addMainIslandBiome(RegistryKey<Biome> biome, double weight) {
+		InternalBiomeData.addEndBiomeReplacement(BiomeKeys.THE_END, biome, weight);
+	}
+
+	/**
+	 * <p>Adds the biome as an end highlands biome with the specified weight. End Highlands biomes make up the
+	 * center region of the large outer islands in The End.</p>
+	 *
+	 * @param biome the biome to be 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.
+	 * The vanilla biome has a weight of 1.0.
+	 */
+	public static void addHighlandsBiome(RegistryKey<Biome> biome, double weight) {
+		InternalBiomeData.addEndBiomeReplacement(BiomeKeys.END_HIGHLANDS, biome, weight);
+	}
+
+	/**
+	 * <p>Adds a custom biome as a small end islands biome with the specified weight; small end island biomes
+	 * make up the smaller islands in between the larger islands of the end.</p>
+	 *
+	 * @param biome the biome to be 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.
+	 * The vanilla biome has a weight of 1.0.
+	 */
+	public static void addSmallIslandsBiome(RegistryKey<Biome> biome, double weight) {
+		InternalBiomeData.addEndBiomeReplacement(BiomeKeys.SMALL_END_ISLANDS, biome, weight);
+	}
+
+	/**
+	 * <p>Adds the biome as an end midlands of the parent end highlands biome. End Midlands make up the area on
+	 * the large outer islands between the highlands and the barrens and are similar to edge biomes in the
+	 * overworld.</p>
+	 *
+	 * @param highlands The highlands biome to where the midlands biome is added
+	 * @param midlands the biome to be added as a midlands 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.
+	 * The vanilla biome has a weight of 1.0.
+	 */
+	public static void addMidlandsBiome(RegistryKey<Biome> highlands, RegistryKey<Biome> midlands, double weight) {
+		InternalBiomeData.addEndMidlandsReplacement(highlands, midlands, weight);
+	}
+
+	/**
+	 * Ands the barrens to with the specified weight to the end barrens regions which correspond to the End Barrens barrens.
+	 *
+	 * @param highlands The highlands biome to where the barrends biome is added
+	 * @param barrens the biome to be added as a barrens 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.
+	 * The vanilla biome has a weight of 1.0.
+	 */
+	public static void addBarrensBiome(RegistryKey<Biome> highlands, RegistryKey<Biome> barrens, double weight) {
+		InternalBiomeData.addEndBarrensReplacement(highlands, barrens, weight);
+	}
+}
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeData.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeData.java
index b6827bd81..4d696c210 100644
--- a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeData.java
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeData.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -48,15 +49,28 @@ public final class InternalBiomeData {
 	}
 
 	private static final EnumMap<OverworldClimate, WeightedBiomePicker> OVERWORLD_MODDED_CONTINENTAL_BIOME_PICKERS = new EnumMap<>(OverworldClimate.class);
-	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_HILLS_MAP = new HashMap<>();
-	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_SHORE_MAP = new HashMap<>();
-	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_EDGE_MAP = new HashMap<>();
-	private static final Map<RegistryKey<Biome>, VariantTransformer> OVERWORLD_VARIANT_TRANSFORMERS = new HashMap<>();
-	private static final Map<RegistryKey<Biome>, RegistryKey<Biome>> OVERWORLD_RIVER_MAP = new HashMap<>();
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_HILLS_MAP = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_SHORE_MAP = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> OVERWORLD_EDGE_MAP = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, VariantTransformer> OVERWORLD_VARIANT_TRANSFORMERS = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, RegistryKey<Biome>> OVERWORLD_RIVER_MAP = new IdentityHashMap<>();
 
 	private static final Set<RegistryKey<Biome>> NETHER_BIOMES = new HashSet<>();
 	private static final Map<RegistryKey<Biome>, Biome.MixedNoisePoint> NETHER_BIOME_NOISE_POINTS = new HashMap<>();
 
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> END_BIOMES_MAP = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> END_MIDLANDS_MAP = new IdentityHashMap<>();
+	private static final Map<RegistryKey<Biome>, WeightedBiomePicker> END_BARRENS_MAP = new IdentityHashMap<>();
+
+	static {
+		END_BIOMES_MAP.computeIfAbsent(BiomeKeys.THE_END, key -> new WeightedBiomePicker()).addBiome(BiomeKeys.THE_END, 1.0);
+		END_BIOMES_MAP.computeIfAbsent(BiomeKeys.END_HIGHLANDS, key -> new WeightedBiomePicker()).addBiome(BiomeKeys.END_HIGHLANDS, 1.0);
+		END_BIOMES_MAP.computeIfAbsent(BiomeKeys.SMALL_END_ISLANDS, key -> new WeightedBiomePicker()).addBiome(BiomeKeys.SMALL_END_ISLANDS, 1.0);
+
+		END_MIDLANDS_MAP.computeIfAbsent(BiomeKeys.END_HIGHLANDS, key -> new WeightedBiomePicker()).addBiome(BiomeKeys.END_MIDLANDS, 1.0);
+		END_BARRENS_MAP.computeIfAbsent(BiomeKeys.END_HIGHLANDS, key -> new WeightedBiomePicker()).addBiome(BiomeKeys.END_BARRENS, 1.0);
+	}
+
 	public static void addOverworldContinentalBiome(OverworldClimate climate, RegistryKey<Biome> biome, double weight) {
 		Preconditions.checkArgument(climate != null, "Climate is null");
 		Preconditions.checkArgument(biome != null, "Biome is null");
@@ -144,6 +158,27 @@ public final class InternalBiomeData {
 		NETHER_BIOMES.clear(); // Reset cached overall biome list
 	}
 
+	public static void addEndBiomeReplacement(RegistryKey<Biome> replaced, RegistryKey<Biome> variant, double weight) {
+		Preconditions.checkNotNull(replaced, "replaced biome is null");
+		Preconditions.checkNotNull(variant, "variant biome is null");
+		Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (got %s)", weight);
+		END_BIOMES_MAP.computeIfAbsent(replaced, key -> new WeightedBiomePicker()).addBiome(variant, weight);
+	}
+
+	public static void addEndMidlandsReplacement(RegistryKey<Biome> highlands, RegistryKey<Biome> midlands, double weight) {
+		Preconditions.checkNotNull(highlands, "highlands biome is null");
+		Preconditions.checkNotNull(midlands, "midlands biome is null");
+		Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (got %s)", weight);
+		END_MIDLANDS_MAP.computeIfAbsent(highlands, key -> new WeightedBiomePicker()).addBiome(midlands, weight);
+	}
+
+	public static void addEndBarrensReplacement(RegistryKey<Biome> highlands, RegistryKey<Biome> barrens, double weight) {
+		Preconditions.checkNotNull(highlands, "highlands biome is null");
+		Preconditions.checkNotNull(barrens, "midlands biome is null");
+		Preconditions.checkArgument(weight > 0.0, "Weight is less than or equal to 0.0 (got %s)", weight);
+		END_BARRENS_MAP.computeIfAbsent(highlands, key -> new WeightedBiomePicker()).addBiome(barrens, weight);
+	}
+
 	public static Map<RegistryKey<Biome>, WeightedBiomePicker> getOverworldHills() {
 		return OVERWORLD_HILLS_MAP;
 	}
@@ -184,6 +219,18 @@ public final class InternalBiomeData {
 		return NETHER_BIOMES.contains(biome);
 	}
 
+	public static Map<RegistryKey<Biome>, WeightedBiomePicker> getEndBiomesMap() {
+		return END_BIOMES_MAP;
+	}
+
+	public static Map<RegistryKey<Biome>, WeightedBiomePicker> getEndMidlandsMap() {
+		return END_MIDLANDS_MAP;
+	}
+
+	public static Map<RegistryKey<Biome>, WeightedBiomePicker> getEndBarrensMap() {
+		return END_BARRENS_MAP;
+	}
+
 	private static class DefaultHillsData {
 		private static final ImmutableMap<RegistryKey<Biome>, RegistryKey<Biome>> DEFAULT_HILLS;
 
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeUtils.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeUtils.java
index 6cfb4c82f..da10da0a6 100644
--- a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeUtils.java
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/InternalBiomeUtils.java
@@ -93,7 +93,7 @@ public final class InternalBiomeUtils {
 		return biome != null && biome.getCategory() == Biome.Category.OCEAN;
 	}
 
-	public static int searchForBiome(double reqWeightSum, int vanillaArrayWeight, List<ContinentalBiomeEntry> moddedBiomes) {
+	public static int searchForBiome(double reqWeightSum, int vanillaArrayWeight, List<WeightedBiomeEntry> moddedBiomes) {
 		reqWeightSum -= vanillaArrayWeight;
 		int low = 0;
 		int high = moddedBiomes.size() - 1;
@@ -152,7 +152,7 @@ public final class InternalBiomeUtils {
 		} else {
 			// Modded biome; use a binary search, and then transform accordingly.
 
-			ContinentalBiomeEntry found = picker.search(reqWeightSum - vanillaArrayWeight);
+			WeightedBiomeEntry found = picker.search(reqWeightSum - vanillaArrayWeight);
 
 			result.accept(transformBiome(random, found.getBiome(), climate));
 		}
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/SimpleLayerRandomnessSource.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/SimpleLayerRandomnessSource.java
new file mode 100644
index 000000000..8d7925037
--- /dev/null
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/SimpleLayerRandomnessSource.java
@@ -0,0 +1,41 @@
+/*
+ * 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.biome;
+
+import java.util.Random;
+
+import net.minecraft.util.math.noise.PerlinNoiseSampler;
+import net.minecraft.world.biome.layer.util.LayerRandomnessSource;
+
+public class SimpleLayerRandomnessSource implements LayerRandomnessSource {
+	private final PerlinNoiseSampler sampler;
+
+	public SimpleLayerRandomnessSource(long seed) {
+		Random random = new Random(seed);
+		this.sampler = new PerlinNoiseSampler(random);
+	}
+
+	@Override
+	public int nextInt(int bound) {
+		throw new UnsupportedOperationException("SimpleLayerRandomnessSource does not support calling nextInt(int).");
+	}
+
+	@Override
+	public PerlinNoiseSampler getNoiseSampler() {
+		return sampler;
+	}
+}
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/ContinentalBiomeEntry.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomeEntry.java
similarity index 90%
rename from fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/ContinentalBiomeEntry.java
rename to fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomeEntry.java
index 6fdf53317..b62a5464b 100644
--- a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/ContinentalBiomeEntry.java
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomeEntry.java
@@ -22,7 +22,7 @@ import net.minecraft.world.biome.Biome;
 /**
  * Represents a biome and its corresponding weight.
  */
-final class ContinentalBiomeEntry {
+final class WeightedBiomeEntry {
 	private final RegistryKey<Biome> biome;
 	private final double weight;
 	private final double upperWeightBound;
@@ -32,7 +32,7 @@ final class ContinentalBiomeEntry {
 	 * @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 RegistryKey<Biome> biome, final double weight, final double upperWeightBound) {
+	WeightedBiomeEntry(final RegistryKey<Biome> biome, final double weight, final double upperWeightBound) {
 		this.biome = biome;
 		this.weight = weight;
 		this.upperWeightBound = upperWeightBound;
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomePicker.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomePicker.java
index 1a8ae9141..f212e8a31 100644
--- a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomePicker.java
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/impl/biome/WeightedBiomePicker.java
@@ -30,7 +30,7 @@ import net.minecraft.world.biome.layer.util.LayerRandomnessSource;
  */
 public final class WeightedBiomePicker {
 	private double currentTotal;
-	private List<ContinentalBiomeEntry> entries;
+	private final List<WeightedBiomeEntry> entries;
 
 	WeightedBiomePicker() {
 		currentTotal = 0;
@@ -40,7 +40,7 @@ public final class WeightedBiomePicker {
 	void addBiome(final RegistryKey<Biome> biome, final double weight) {
 		currentTotal += weight;
 
-		entries.add(new ContinentalBiomeEntry(biome, weight, currentTotal));
+		entries.add(new WeightedBiomeEntry(biome, weight, currentTotal));
 	}
 
 	double getCurrentWeightTotal() {
@@ -53,13 +53,19 @@ public final class WeightedBiomePicker {
 		return search(target).getBiome();
 	}
 
+	public RegistryKey<Biome> pickFromNoise(LayerRandomnessSource source, double x, double y, double z) {
+		double target = Math.abs(source.getNoiseSampler().sample(x, y, z, 0.0, 0.0)) * getCurrentWeightTotal();
+
+		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) {
+	WeightedBiomeEntry 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");
diff --git a/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/mixin/biome/MixinTheEndBiomeSource.java b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/mixin/biome/MixinTheEndBiomeSource.java
new file mode 100644
index 000000000..859fba626
--- /dev/null
+++ b/fabric-biome-api-v1/src/main/java/net/fabricmc/fabric/mixin/biome/MixinTheEndBiomeSource.java
@@ -0,0 +1,81 @@
+/*
+ * 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.biome;
+
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.world.biome.BiomeKeys;
+import net.minecraft.world.biome.Biome;
+import net.minecraft.util.registry.Registry;
+import net.minecraft.util.registry.RegistryKey;
+import net.minecraft.world.biome.layer.util.LayerRandomnessSource;
+import net.minecraft.world.biome.source.TheEndBiomeSource;
+
+import net.fabricmc.fabric.impl.biome.InternalBiomeData;
+import net.fabricmc.fabric.impl.biome.SimpleLayerRandomnessSource;
+import net.fabricmc.fabric.impl.biome.WeightedBiomePicker;
+
+@Mixin(TheEndBiomeSource.class)
+public class MixinTheEndBiomeSource {
+	@Shadow
+	@Final
+	private Registry<Biome> biomeRegistry;
+	@Shadow
+	@Final
+	private long seed;
+	@Unique
+	private LayerRandomnessSource randomnessSource = new SimpleLayerRandomnessSource(seed);
+
+	@Inject(method = "getBiomeForNoiseGen", at = @At("RETURN"), cancellable = true)
+	private void getWeightedEndBiome(int biomeX, int biomeY, int biomeZ, CallbackInfoReturnable<Biome> cir) {
+		Biome vanillaBiome = cir.getReturnValue();
+
+		// Since all vanilla biomes are added to the registry, this will never fail.
+		// If not all vanilla biomes are added to the registry, this code will fail
+		// either way.
+		RegistryKey<Biome> vanillaKey = biomeRegistry.getKey(vanillaBiome).get();
+		RegistryKey<Biome> replacementKey;
+
+		if (vanillaKey == BiomeKeys.END_MIDLANDS || vanillaKey == BiomeKeys.END_BARRENS) {
+			// Since the pickers are statically populated by InternalBiomeData, picker will never be null.
+			WeightedBiomePicker highlandsPicker = InternalBiomeData.getEndBiomesMap().get(BiomeKeys.END_HIGHLANDS);
+			RegistryKey<Biome> highlandsKey = highlandsPicker.pickFromNoise(randomnessSource, biomeX/64.0, 0, biomeZ/64.0);
+
+			if (vanillaKey == BiomeKeys.END_MIDLANDS) {
+				// Since the pickers are statically populated by InternalBiomeData, picker will never be null.
+				WeightedBiomePicker midlandsPicker = InternalBiomeData.getEndMidlandsMap().get(highlandsKey);
+				replacementKey = midlandsPicker.pickFromNoise(randomnessSource, biomeX/64.0, 0, biomeZ/64.0);
+			} else {
+				// Since the pickers are statically populated by InternalBiomeData, picker will never be null.
+				WeightedBiomePicker barrensPicker = InternalBiomeData.getEndBarrensMap().get(highlandsKey);
+				replacementKey = barrensPicker.pickFromNoise(randomnessSource, biomeX/64.0, 0, biomeZ/64.0);
+			}
+		} else {
+			// Since the pickers are statically populated by InternalBiomeData, picker will never be null.
+			WeightedBiomePicker picker = InternalBiomeData.getEndBiomesMap().get(vanillaKey);
+			replacementKey = picker.pickFromNoise(randomnessSource, biomeX/64.0, 0, biomeZ/64.0);
+		}
+
+		cir.setReturnValue(biomeRegistry.get(replacementKey));
+	}
+}
diff --git a/fabric-biome-api-v1/src/main/resources/fabric-biome-api-v1.mixins.json b/fabric-biome-api-v1/src/main/resources/fabric-biome-api-v1.mixins.json
index 8c6825455..fb64bfffa 100644
--- a/fabric-biome-api-v1/src/main/resources/fabric-biome-api-v1.mixins.json
+++ b/fabric-biome-api-v1/src/main/resources/fabric-biome-api-v1.mixins.json
@@ -20,6 +20,7 @@
     "MixinAddRiversLayer",
     "MixinMultiNoiseBiomeSource",
     "MixinSetBaseBiomesLayer",
+    "MixinTheEndBiomeSource",
     "MultiNoiseBiomeSourceAccessor",
     "VanillaLayeredBiomeSourceAccessor"
   ],
diff --git a/fabric-biome-api-v1/src/testmod/java/net/fabricmc/fabric/test/biome/FabricBiomeTest.java b/fabric-biome-api-v1/src/testmod/java/net/fabricmc/fabric/test/biome/FabricBiomeTest.java
index e26ce65d8..10dce1e70 100644
--- a/fabric-biome-api-v1/src/testmod/java/net/fabricmc/fabric/test/biome/FabricBiomeTest.java
+++ b/fabric-biome-api-v1/src/testmod/java/net/fabricmc/fabric/test/biome/FabricBiomeTest.java
@@ -16,14 +16,28 @@
 
 package net.fabricmc.fabric.test.biome;
 
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.sound.BiomeMoodSound;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.registry.BuiltinRegistries;
 import net.minecraft.util.registry.Registry;
 import net.minecraft.util.registry.RegistryKey;
 import net.minecraft.world.biome.Biome;
+import net.minecraft.world.biome.BiomeEffects;
 import net.minecraft.world.biome.BiomeKeys;
 import net.minecraft.world.biome.DefaultBiomeCreator;
+import net.minecraft.world.biome.GenerationSettings;
+import net.minecraft.world.biome.SpawnSettings;
+import net.minecraft.world.gen.GenerationStep;
+import net.minecraft.world.gen.feature.ConfiguredFeatures;
+import net.minecraft.world.gen.feature.ConfiguredStructureFeatures;
+import net.minecraft.world.gen.feature.DefaultBiomeFeatures;
 import net.minecraft.world.gen.surfacebuilder.ConfiguredSurfaceBuilders;
+import net.minecraft.world.gen.surfacebuilder.ConfiguredSurfaceBuilder;
+import net.minecraft.world.gen.surfacebuilder.SurfaceBuilder;
+import net.minecraft.world.gen.surfacebuilder.SurfaceConfig;
+import net.minecraft.world.gen.surfacebuilder.TernarySurfaceConfig;
 
 import net.fabricmc.api.ModInitializer;
 import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
@@ -32,6 +46,7 @@ import net.fabricmc.fabric.api.biome.v1.ModificationPhase;
 import net.fabricmc.fabric.api.biome.v1.NetherBiomes;
 import net.fabricmc.fabric.api.biome.v1.OverworldBiomes;
 import net.fabricmc.fabric.api.biome.v1.OverworldClimate;
+import net.fabricmc.fabric.api.biome.v1.TheEndBiomes;
 
 /**
  * <b>NOTES FOR TESTING:</b>
@@ -47,8 +62,13 @@ public class FabricBiomeTest implements ModInitializer {
 	public static final String MOD_ID = "fabric-biome-api-v1-testmod";
 
 	private static final RegistryKey<Biome> TEST_CRIMSON_FOREST = RegistryKey.of(Registry.BIOME_KEY, new Identifier(MOD_ID, "test_crimson_forest"));
-
 	private static final RegistryKey<Biome> CUSTOM_PLAINS = RegistryKey.of(Registry.BIOME_KEY, new Identifier(MOD_ID, "custom_plains"));
+	private static final RegistryKey<Biome> TEST_END_HIGHLANDS = RegistryKey.of(Registry.BIOME_KEY, new Identifier(MOD_ID, "test_end_highlands"));
+	private static final RegistryKey<Biome> TEST_END_MIDLANDS = RegistryKey.of(Registry.BIOME_KEY, new Identifier(MOD_ID, "test_end_midlands"));
+	private static final RegistryKey<Biome> TEST_END_BARRRENS = RegistryKey.of(Registry.BIOME_KEY, new Identifier(MOD_ID, "test_end_barrens"));
+
+	private static BlockState STONE = Blocks.STONE.getDefaultState();
+	private static ConfiguredSurfaceBuilder<TernarySurfaceConfig> TEST_END_SURFACE_BUILDER = register("end", SurfaceBuilder.DEFAULT.withConfig(new TernarySurfaceConfig(STONE, STONE, STONE)));
 
 	@Override
 	public void onInitialize() {
@@ -60,6 +80,15 @@ public class FabricBiomeTest implements ModInitializer {
 		Registry.register(BuiltinRegistries.BIOME, CUSTOM_PLAINS.getValue(), DefaultBiomeCreator.createPlains(false));
 		OverworldBiomes.addBiomeVariant(BiomeKeys.PLAINS, CUSTOM_PLAINS, 1);
 
+		Registry.register(BuiltinRegistries.BIOME, TEST_END_HIGHLANDS.getValue(), createEndHighlands());
+		Registry.register(BuiltinRegistries.BIOME, TEST_END_MIDLANDS.getValue(), createEndMidlands());
+		Registry.register(BuiltinRegistries.BIOME, TEST_END_BARRRENS.getValue(), createEndBarrens());
+		// TESTING HINT: to get to the end:
+		// /execute in minecraft:the_end run tp @s 0 90 0
+		TheEndBiomes.addHighlandsBiome(TEST_END_HIGHLANDS, 5.0);
+		TheEndBiomes.addMidlandsBiome(TEST_END_HIGHLANDS, TEST_END_MIDLANDS, 1.0);
+		TheEndBiomes.addBarrensBiome(TEST_END_HIGHLANDS, TEST_END_BARRRENS, 1.0);
+
 		OverworldBiomes.addEdgeBiome(BiomeKeys.PLAINS, BiomeKeys.END_BARRENS, 0.9);
 
 		OverworldBiomes.addShoreBiome(BiomeKeys.FOREST, BiomeKeys.NETHER_WASTES, 0.9);
@@ -80,4 +109,30 @@ public class FabricBiomeTest implements ModInitializer {
 							context.getGenerationSettings().setBuiltInSurfaceBuilder(ConfiguredSurfaceBuilders.CRIMSON_FOREST);
 						});
 	}
+
+	// These are used for testing the spacing of custom end biomes.
+	private static Biome createEndHighlands() {
+		GenerationSettings.Builder builder = (new GenerationSettings.Builder()).surfaceBuilder(TEST_END_SURFACE_BUILDER).structureFeature(ConfiguredStructureFeatures.END_CITY).feature(GenerationStep.Feature.SURFACE_STRUCTURES, ConfiguredFeatures.END_GATEWAY).feature(GenerationStep.Feature.VEGETAL_DECORATION, ConfiguredFeatures.CHORUS_PLANT);
+		return composeEndSpawnSettings(builder);
+	}
+
+	public static Biome createEndMidlands() {
+		GenerationSettings.Builder builder = (new GenerationSettings.Builder()).surfaceBuilder(TEST_END_SURFACE_BUILDER).structureFeature(ConfiguredStructureFeatures.END_CITY);
+		return composeEndSpawnSettings(builder);
+	}
+
+	public static Biome createEndBarrens() {
+		GenerationSettings.Builder builder = (new GenerationSettings.Builder()).surfaceBuilder(TEST_END_SURFACE_BUILDER);
+		return composeEndSpawnSettings(builder);
+	}
+
+	private static Biome composeEndSpawnSettings(GenerationSettings.Builder builder) {
+		SpawnSettings.Builder builder2 = new SpawnSettings.Builder();
+		DefaultBiomeFeatures.addEndMobs(builder2);
+		return (new Biome.Builder()).precipitation(Biome.Precipitation.NONE).category(Biome.Category.THEEND).depth(0.1F).scale(0.2F).temperature(0.5F).downfall(0.5F).effects((new BiomeEffects.Builder()).waterColor(4159204).waterFogColor(329011).fogColor(10518688).skyColor(0).moodSound(BiomeMoodSound.CAVE).build()).spawnSettings(builder2.build()).generationSettings(builder.build()).build();
+	}
+
+	private static <SC extends SurfaceConfig> ConfiguredSurfaceBuilder<SC> register(String id, ConfiguredSurfaceBuilder<SC> configuredSurfaceBuilder) {
+		return BuiltinRegistries.add(BuiltinRegistries.CONFIGURED_SURFACE_BUILDER, id, configuredSurfaceBuilder);
+	}
 }