From 4a675e0433fcf8e331619fba0a0b01e336f01162 Mon Sep 17 00:00:00 2001
From: tastybento <tastybento@wasteofplastic.com>
Date: Wed, 21 Dec 2022 11:09:16 -0800
Subject: [PATCH] Custom biomes and vanilla biomes mixed

This covers the overworld. Next is to do the nether.
---
 src/main/java/world/bentobox/boxed/Boxed.java | 132 +++++------
 .../AbstractBoxedBiomeProvider.java           |   9 +-
 .../AbstractBoxedChunkGenerator.java          | 182 +++++++++++++++
 .../generators/AbstractCopyBiomeProvider.java |   5 +-
 .../generators/AbstractSeedBiomeProvider.java |  82 ++++++-
 .../boxed/generators/BoxedBlockPopulator.java |  13 +-
 .../boxed/generators/BoxedChunkGenerator.java | 211 +-----------------
 .../generators/BoxedSeedChunkGenerator.java   |  40 +++-
 .../boxed/generators/SeedBiomeGenerator.java  |   5 +-
 src/main/resources/biomes.yml                 |  25 +--
 10 files changed, 390 insertions(+), 314 deletions(-)
 create mode 100644 src/main/java/world/bentobox/boxed/generators/AbstractBoxedChunkGenerator.java

diff --git a/src/main/java/world/bentobox/boxed/Boxed.java b/src/main/java/world/bentobox/boxed/Boxed.java
index 977b813..8baba9c 100644
--- a/src/main/java/world/bentobox/boxed/Boxed.java
+++ b/src/main/java/world/bentobox/boxed/Boxed.java
@@ -22,10 +22,12 @@ import world.bentobox.bentobox.api.flags.Flag;
 import world.bentobox.bentobox.api.flags.Flag.Mode;
 import world.bentobox.bentobox.api.flags.Flag.Type;
 import world.bentobox.bentobox.managers.RanksManager;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator;
 import world.bentobox.boxed.generators.BoxedBiomeGenerator;
 import world.bentobox.boxed.generators.BoxedBlockPopulator;
 import world.bentobox.boxed.generators.BoxedChunkGenerator;
 import world.bentobox.boxed.generators.BoxedSeedChunkGenerator;
+import world.bentobox.boxed.generators.SeedBiomeGenerator;
 import world.bentobox.boxed.listeners.AdvancementListener;
 import world.bentobox.boxed.listeners.EnderPearlListener;
 
@@ -48,13 +50,15 @@ public class Boxed extends GameModeAddon {
     private static final String SEED = "seed";
     private static final String NETHER = "_nether";
     private static final String THE_END = "_the_end";
+    private static final String BASE = "_base";
 
     // Settings
     private Settings settings;
-    private BoxedChunkGenerator chunkGenerator;
+    private AbstractBoxedChunkGenerator chunkGenerator;
     private final Config<Settings> configObject = new Config<>(this, Settings.class);
     private AdvancementsManager advManager;
-    private BoxedChunkGenerator netherChunkGenerator;
+    private AbstractBoxedChunkGenerator netherChunkGenerator;
+    private World baseWorld;
     private World seedWorld;
     private World seedWorldNether;
     //private World seedWorldEnd;
@@ -146,22 +150,70 @@ public class Boxed extends GameModeAddon {
 
     @Override
     public void createWorlds() {
-        // Create seed world
+        String worldName = settings.getWorldName().toLowerCase();
+        // Create overworld
+        createOverWorld(worldName);
+
+        // Make the nether if it does not exist
+        if (settings.isNetherGenerate()) {
+            createNether(worldName);
+        }
+        /*
+        // Make the end if it does not exist
+        if (settings.isEndGenerate()) {
+          //TODO
+         */
+    }
+
+    private void createNether(String worldName) {
+        log("Creating Boxed Seed Nether world ...");
+        seedWorldNether = WorldCreator
+                .name(SEED + NETHER)
+                .generator(new BoxedSeedChunkGenerator(this, Environment.NETHER))
+                .environment(Environment.NETHER)
+                .seed(getSettings().getSeed())
+                .createWorld();
+        seedWorldNether.setDifficulty(Difficulty.EASY); // No damage wanted in this world.
+
+        copyChunks(seedWorldNether, this.netherChunkGenerator);
+
+        if (getServer().getWorld(worldName + NETHER) == null) {
+            log("Creating Boxed's Nether...");
+        }
+        netherWorld = getWorld(worldName, World.Environment.NETHER);
+    }
+
+    private void createOverWorld(String worldName) {
+        // Create vanilla seed world
         log("Creating Boxed Seed world ...");
+        // This creates a vanilla base world with biomes
+        AbstractBoxedChunkGenerator seedBaseGen = new BoxedSeedChunkGenerator(this, Environment.NORMAL);
+        baseWorld = WorldCreator
+                .name(SEED+BASE)
+                .generator(seedBaseGen)
+                .environment(Environment.NORMAL)
+                .seed(getSettings().getSeed())
+                .createWorld();
+        baseWorld.setDifficulty(Difficulty.PEACEFUL);
+        baseWorld.setSpawnLocation(settings.getSeedX(), 64, settings.getSeedZ());
+        copyChunks(baseWorld, seedBaseGen);
+        // Create seed world
+        // This copies a base world with custom biomes
+        log("Creating Boxed Biomed world ...");
+
         seedWorld = WorldCreator
                 .name(SEED)
-                .generator(new BoxedSeedChunkGenerator(this, Environment.NORMAL))
+                .generator(new BoxedSeedChunkGenerator(this, Environment.NORMAL, new SeedBiomeGenerator(this, seedBaseGen)))
                 .environment(Environment.NORMAL)
                 .seed(getSettings().getSeed())
                 .createWorld();
         seedWorld.setDifficulty(Difficulty.EASY);
-        copyChunks(seedWorld);
+
         seedWorld.setSpawnLocation(settings.getSeedX(), 64, settings.getSeedZ());
 
+        copyChunks(seedWorld, chunkGenerator);
+
 
-        // Unload seed world
-        //Bukkit.getServer().unloadWorld("seed", false);
-        String worldName = settings.getWorldName().toLowerCase();
 
         if (getServer().getWorld(worldName) == null) {
             log("Creating Boxed world ...");
@@ -170,70 +222,20 @@ public class Boxed extends GameModeAddon {
         // Create the world if it does not exist
         islandWorld = getWorld(worldName, World.Environment.NORMAL);
 
-        // Make the nether if it does not exist
-        if (settings.isNetherGenerate()) {
-            log("Creating Boxed Seed Nether world ...");
-            // Copy regions
-            /*
-            boolean newWorld = Bukkit.getWorld(SEED + NETHER) == null;
-            if (newWorld) {
-                // New world
-                File root = new File(getDataFolder(), "../../../..");
-                BentoBox.getInstance().logDebug("Absolute path " + root.getAbsolutePath());
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.18.18.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.18.19.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.18.20.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.19.18.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.19.19.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.19.20.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.20.18.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.20.19.mca", root, false, false);
-                this.saveResource("worlds/seed_nether/DIM-1/region/r.20.20.mca", root, false, false);
-            }*/
-
-            seedWorldNether = WorldCreator
-                    .name(SEED + NETHER)
-                    .generator(new BoxedSeedChunkGenerator(this, Environment.NETHER))
-                    .environment(Environment.NETHER)
-                    .seed(getSettings().getSeed())
-                    .createWorld();
-            seedWorldNether.setDifficulty(Difficulty.EASY); // No damage wanted in this world.
-
-
-
-
-            copyChunks(seedWorldNether);
-
-            if (getServer().getWorld(worldName + NETHER) == null) {
-                log("Creating Boxed's Nether...");
-            }
-            netherWorld = getWorld(worldName, World.Environment.NETHER);
-        }
-        /*
-        // Make the end if it does not exist
-        if (settings.isEndGenerate()) {
-            if (getServer().getWorld(worldName + THE_END) == null) {
-                log("Creating Boxed's End World...");
-            }
-            endWorld = settings.isEndIslands() ? getWorld(worldName, World.Environment.THE_END) : getWorld(worldName, World.Environment.THE_END);
-        }
-         */
     }
 
     /**
      * Copies chunks from the seed world so they can be pasted in the game world
-     * @param seedWorld - source world
+     * @param world - source world
+     * @param gen - generator to store the chunks
      */
-    private void copyChunks(World seedWorld) {
-        BoxedChunkGenerator gen;
+    private void copyChunks(World world, AbstractBoxedChunkGenerator gen) {
         int startX = 0;
         int startZ = 0;
-        if (seedWorld.getEnvironment().equals(Environment.NORMAL)) {
-            gen = chunkGenerator;
+        if (world.getEnvironment().equals(Environment.NORMAL)) {
             startX = this.settings.getSeedX() >> 4;
             startZ = this.settings.getSeedZ() >> 4;
         } else {
-            gen = netherChunkGenerator;
             startX = this.settings.getNetherSeedX() >> 4;
             startZ = this.settings.getNetherSeedZ() >> 4;
         }
@@ -245,12 +247,12 @@ public class Boxed extends GameModeAddon {
         int last = 0;
         for (int x = -size; x <= size; x ++) {
             for (int z = -size; z <= size; z++) {
-                gen.setChunk(x, z, seedWorld.getChunkAt(startX + x, startZ + z));
+                gen.setChunk(x, z, world.getChunkAt(startX + x, startZ + z));
                 count++;
                 int p = (int) (count / percent * 100);
                 if (p % 10 == 0 && p != last) {
                     last = p;
-                    this.log("Storing seed chunks for " + seedWorld.getEnvironment() + " " + p + "% done");
+                    this.log("Storing seed chunks for " + world.getEnvironment() + " " + p + "% done");
                 }
 
             }
@@ -262,7 +264,7 @@ public class Boxed extends GameModeAddon {
      * @param env - nether, normal, or end
      * @return the chunkGenerator for the environment
      */
-    public BoxedChunkGenerator getChunkGenerator(Environment env) {
+    public AbstractBoxedChunkGenerator getChunkGenerator(Environment env) {
         if (env.equals(Environment.NORMAL)) {
             return chunkGenerator;
         }
diff --git a/src/main/java/world/bentobox/boxed/generators/AbstractBoxedBiomeProvider.java b/src/main/java/world/bentobox/boxed/generators/AbstractBoxedBiomeProvider.java
index ce7da0f..9ea9eb3 100644
--- a/src/main/java/world/bentobox/boxed/generators/AbstractBoxedBiomeProvider.java
+++ b/src/main/java/world/bentobox/boxed/generators/AbstractBoxedBiomeProvider.java
@@ -83,11 +83,10 @@ public abstract class AbstractBoxedBiomeProvider extends BiomeProvider {
 
     @Override
     public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) {
-        int chunkX = (int)((double)x/16);
-        int chunkZ = (int)((double)z/16);
-        int size = (int)(dist / 16D); // Convert to chunk
-        chunkX = BoxedChunkGenerator.repeatCalc(chunkX, size);
-        chunkZ = BoxedChunkGenerator.repeatCalc(chunkZ, size);
+        int chunkX = x >> 4;
+        int chunkZ = z >> 4;
+        chunkX = BoxedChunkGenerator.repeatCalc(chunkX);
+        chunkZ = BoxedChunkGenerator.repeatCalc(chunkZ);
         ChunkSnapshot c = addon.getChunkGenerator(worldInfo.getEnvironment()).getChunk(chunkX, chunkZ);
 
         if (c != null) {
diff --git a/src/main/java/world/bentobox/boxed/generators/AbstractBoxedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/AbstractBoxedChunkGenerator.java
new file mode 100644
index 0000000..349856c
--- /dev/null
+++ b/src/main/java/world/bentobox/boxed/generators/AbstractBoxedChunkGenerator.java
@@ -0,0 +1,182 @@
+package world.bentobox.boxed.generators;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+
+import org.bukkit.Chunk;
+import org.bukkit.ChunkSnapshot;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Banner;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.block.Sign;
+import org.bukkit.entity.AbstractHorse;
+import org.bukkit.entity.Ageable;
+import org.bukkit.entity.ChestedHorse;
+import org.bukkit.entity.Horse;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Tameable;
+import org.bukkit.entity.Villager;
+import org.bukkit.generator.BiomeProvider;
+import org.bukkit.generator.BlockPopulator;
+import org.bukkit.generator.ChunkGenerator;
+import org.bukkit.generator.WorldInfo;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.material.Attachable;
+import org.bukkit.material.Colorable;
+import org.bukkit.util.Vector;
+
+import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock;
+import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner;
+import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity;
+import world.bentobox.bentobox.util.Pair;
+import world.bentobox.boxed.Boxed;
+
+/**
+ * Chunk generator for all environments
+ * @author tastybento
+ *
+ */
+public abstract class AbstractBoxedChunkGenerator extends ChunkGenerator {
+
+    protected final Boxed addon;
+    protected static int size;
+    protected Map<Pair<Integer, Integer>, ChunkStore> chunks = new HashMap<>();
+    public record ChunkStore(ChunkSnapshot snapshot, List<EntityData> bpEnts, List<ChestData> chests) {};
+    public record EntityData(Vector relativeLoc, BlueprintEntity entity) {};
+    public record ChestData(Vector relativeLoc, BlueprintBlock chest) {};
+
+    //private final WorldRef wordRefNether;
+
+    public AbstractBoxedChunkGenerator(Boxed addon) {
+        this.addon = addon;
+        size = (int)(addon.getSettings().getIslandDistance() / 16D); // Size is chunks
+    }
+
+    /**
+     * Save a chunk
+     * @param z - chunk z coord
+     * @param x - chunk x coord
+     * @param chunk the chunk to set
+     */
+    public void setChunk(int x, int z, Chunk chunk) {
+        chunks.put(new Pair<>(x, z), new ChunkStore(chunk.getChunkSnapshot(false, true, false), getEnts(chunk), getChests(chunk)));
+    }
+
+    protected abstract List<EntityData> getEnts(Chunk chunk);
+
+    protected abstract List<ChestData> getChests(Chunk chunk);
+
+    /**
+     * @param x chunk x
+     * @param z chunk z
+     * @return chunk snapshot or null if there is none
+     */
+    public ChunkSnapshot getChunk(int x, int z) {
+        return chunks.get(new Pair<>(x, z)).snapshot;
+    }
+
+    @Override
+    public boolean canSpawn(World world, int x, int z)
+    {
+        return true;
+    }
+
+    @Override
+    public void generateNoise(WorldInfo worldInfo, Random r, int chunkX, int chunkZ, ChunkData cd) {
+
+        int height = worldInfo.getMaxHeight();
+        int minY = worldInfo.getMinHeight();
+        int xx = repeatCalc(chunkX);
+        int zz = repeatCalc(chunkZ);
+        Pair<Integer, Integer> coords = new Pair<>(xx, zz);
+        if (!chunks.containsKey(coords)) {
+            // This should never be needed because islands should abut each other
+            cd.setRegion(0, minY, 0, 16, 0, 16, Material.WATER);
+            return;
+        }
+        // Copy the chunk
+        ChunkSnapshot chunk = chunks.get(coords).snapshot;
+        copyChunkVerbatim(cd, chunk, minY, height);
+
+    }
+
+    private void copyChunkVerbatim(ChunkData cd, ChunkSnapshot chunk, int minY, int height) {
+        for (int x = 0; x < 16; x ++) {
+            for (int z = 0; z < 16; z++) {
+                for (int y = minY; y < height; y++) {
+                    cd.setBlock(x, y, z, chunk.getBlockData(x, y, z));
+                }
+            }
+        }
+    }
+
+    /**
+     * Calculates the repeating value for a given size
+     * @param chunkCoord chunk coord
+     * @return mapped chunk coord
+     */
+    public static int repeatCalc(int chunkCoord) {
+        int xx;
+        if (chunkCoord > 0) {
+            xx = Math.floorMod(chunkCoord + size, size*2) - size;
+        } else {
+            xx = Math.floorMod(chunkCoord - size, -size*2) + size;
+        }
+        return xx;
+    }
+
+    /**
+     * @return the chunks
+     */
+    public Map<Pair<Integer, Integer>, ChunkStore> getChunks() {
+        return chunks;
+    }
+
+    @Override
+    public boolean shouldGenerateNoise() {
+        return false;
+    }
+
+    @Override
+    public boolean shouldGenerateSurface() {
+
+        return false;
+    }
+
+    @Override
+    public boolean shouldGenerateCaves() {
+        return false;
+        //return this.addon.getSettings().isGenerateCaves();
+    }
+
+    @Override
+    public boolean shouldGenerateDecorations() {
+        return false;
+        //return this.addon.getSettings().isGenerateDecorations();
+    }
+
+    @Override
+    public boolean shouldGenerateMobs() {
+        return this.addon.getSettings().isGenerateMobs();
+    }
+
+    @Override
+    public boolean shouldGenerateStructures() {
+        return false;
+        //return this.addon.getSettings().isAllowStructures();
+    }
+
+}
diff --git a/src/main/java/world/bentobox/boxed/generators/AbstractCopyBiomeProvider.java b/src/main/java/world/bentobox/boxed/generators/AbstractCopyBiomeProvider.java
index 23c3840..d011239 100644
--- a/src/main/java/world/bentobox/boxed/generators/AbstractCopyBiomeProvider.java
+++ b/src/main/java/world/bentobox/boxed/generators/AbstractCopyBiomeProvider.java
@@ -33,9 +33,8 @@ public abstract class AbstractCopyBiomeProvider extends BiomeProvider {
     public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) {
         int chunkX = (int)((double)x/16);
         int chunkZ = (int)((double)z/16);
-        int size = (int)(dist / 16D); // Convert to chunk
-        chunkX = BoxedChunkGenerator.repeatCalc(chunkX, size);
-        chunkZ = BoxedChunkGenerator.repeatCalc(chunkZ, size);
+        chunkX = BoxedChunkGenerator.repeatCalc(chunkX);
+        chunkZ = BoxedChunkGenerator.repeatCalc(chunkZ);
         ChunkSnapshot c = addon.getChunkGenerator(worldInfo.getEnvironment()).getChunk(chunkX, chunkZ);
 
         if (c != null) {
diff --git a/src/main/java/world/bentobox/boxed/generators/AbstractSeedBiomeProvider.java b/src/main/java/world/bentobox/boxed/generators/AbstractSeedBiomeProvider.java
index 52c0ad1..d675c80 100644
--- a/src/main/java/world/bentobox/boxed/generators/AbstractSeedBiomeProvider.java
+++ b/src/main/java/world/bentobox/boxed/generators/AbstractSeedBiomeProvider.java
@@ -4,13 +4,16 @@ import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
+import org.bukkit.ChunkSnapshot;
 import org.bukkit.World.Environment;
 import org.bukkit.block.Biome;
 import org.bukkit.block.BlockFace;
@@ -18,9 +21,13 @@ import org.bukkit.configuration.file.YamlConfiguration;
 import org.bukkit.generator.BiomeProvider;
 import org.bukkit.generator.WorldInfo;
 import org.bukkit.util.Vector;
+import org.eclipse.jdt.annotation.NonNull;
 
 import com.google.common.base.Enums;
 
+import world.bentobox.bentobox.BentoBox;
+import world.bentobox.bentobox.util.Pair;
+import world.bentobox.bentobox.util.Util;
 import world.bentobox.boxed.Boxed;
 
 /**
@@ -31,6 +38,7 @@ import world.bentobox.boxed.Boxed;
 public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
 
     private static final Map<Environment, String> ENV_MAP;
+    private static final int DEPTH = 50;
 
     static {
         Map<Environment, String> e = new EnumMap<>(Environment.class);
@@ -48,11 +56,13 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
     private final int offsetX;
     private final int offsetZ;
     protected final Map<BlockFace, SortedMap<Double, Biome>> quadrants;
+    private final AbstractBoxedChunkGenerator seedGen;
 
 
-    protected AbstractSeedBiomeProvider(Boxed boxed, Environment env, Biome defaultBiome) {
+    protected AbstractSeedBiomeProvider(Boxed boxed, Environment env, Biome defaultBiome, AbstractBoxedChunkGenerator seedGen) {
         this.addon = boxed;
         this.defaultBiome = defaultBiome;
+        this.seedGen = seedGen;
         dist = addon.getSettings().getIslandDistance();
         offsetX = addon.getSettings().getIslandXOffset();
         offsetZ = addon.getSettings().getIslandZOffset();
@@ -75,34 +85,78 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
         quadrants.put(BlockFace.SOUTH_WEST, southWest);
     }
 
-    private Biome getBiome(BlockFace dir, double d) {
+    private Biome getQuadrantBiome(BlockFace dir, double d) {
         Entry<Double, Biome> en = ((TreeMap<Double, Biome>) quadrants.get(dir)).ceilingEntry(d);
-        return en == null ? defaultBiome : en.getValue();
+        return en == null ? null : en.getValue();
     }
 
     @Override
     public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) {
-        return getMappedBiome(x,z);
+        // Custom biomes are not 3D yet
+        if (y < DEPTH) {
+            Biome result = getVanillaBiome(worldInfo, x, y, z);
+            return Objects.requireNonNull(result);
+        }
+        Biome result = getMappedBiome(x,z);
+        if (result == null || result.equals(Biome.CUSTOM)) {
+            result = getVanillaBiome(worldInfo, x, y, z);
+
+        }
+        return Objects.requireNonNull(result);
     }
 
+    private @NonNull Biome getVanillaBiome(WorldInfo worldInfo, int x, int y, int z) {
+        // Vanilla biomes
+        int chunkX = BoxedChunkGenerator.repeatCalc(x >> 4);
+        int chunkZ = BoxedChunkGenerator.repeatCalc(z >> 4);
+        ChunkSnapshot snapshot = this.seedGen.getChunk(chunkX, chunkZ);
+        if (snapshot == null) {
+            return defaultBiome;
+        }
+        int xx = Math.floorMod(x, 16);
+        int zz = Math.floorMod(z, 16);
+        int yy = Math.max(Math.min(y * 4, worldInfo.getMaxHeight()), worldInfo.getMinHeight()); // To handle bug in Spigot
+
+        Biome b = snapshot.getBiome(xx, yy, zz);
+        if (y > DEPTH )
+            BentoBox.getInstance().logDebug("Returning vanilla biome " + b + " for " + worldInfo.getName() + "  " + x + " " + y + " " + z);
+        return Objects.requireNonNull(b);
+    }
+
+    private Map<Pair<Integer, Integer>, Biome> biomeCache = new HashMap<>();
+    /**
+     * Get the mapped 2D biome at position x,z
+     * @param x - block coord
+     * @param z - block coord
+     * @return Biome
+     */
     private Biome getMappedBiome(int x, int z) {
         /*
          * Biomes go around the island centers
          *
          */
+        Biome result = biomeCache.get((new Pair<Integer, Integer>(x,z)));
+        if (result != null) {
+            return result;
+        }
         Vector s = new Vector(x, 0, z);
         Vector l = getClosestIsland(s);
-        double dis = l.distanceSquared(s);
-        double d = dis / (dist * dist);
+        BentoBox.getInstance().logDebug("Closest island is " + Util.xyz(l));
+        double dis = l.distance(s);
+        double d = dis / dist; // Normalize
         Vector direction = s.subtract(l);
         if (direction.getBlockX() <= 0 && direction.getBlockZ() <= 0) {
-            return getBiome(BlockFace.NORTH_WEST, d);
+            result = getQuadrantBiome(BlockFace.NORTH_WEST, d);
         } else if (direction.getBlockX() > 0 && direction.getBlockZ() <= 0) {
-            return getBiome(BlockFace.NORTH_EAST, d);
+            result = getQuadrantBiome(BlockFace.NORTH_EAST, d);
         } else if (direction.getBlockX() <= 0 && direction.getBlockZ() > 0) {
-            return getBiome(BlockFace.SOUTH_WEST, d);
+            result = getQuadrantBiome(BlockFace.SOUTH_WEST, d);
+        } else {
+            result = getQuadrantBiome(BlockFace.SOUTH_EAST, d);
         }
-        return getBiome(BlockFace.SOUTH_EAST, d);
+        biomeCache.put(new Pair<Integer, Integer>(x,z), result);
+        return result;
+
     }
 
     @Override
@@ -111,6 +165,11 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
         return Arrays.stream(Biome.values()).filter(b -> !b.equals(Biome.CUSTOM)).toList();
     }
 
+    /**
+     * Get the island center closest to this vector
+     * @param v - vector
+     * @return island center vector (no y value)
+     */
     private Vector getClosestIsland(Vector v) {
         int d = dist * 2;
         long x = Math.round((double) v.getBlockX() / d) * d + offsetX;
@@ -129,8 +188,9 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
                 try {
                     double d = Double.parseDouble(split[0]);
                     Biome biome = Enums.getIfPresent(Biome.class, split[1].toUpperCase(Locale.ENGLISH)).orNull();
-                    if (biome == null && !split[1].toUpperCase(Locale.ENGLISH).equalsIgnoreCase("default")) {
+                    if (biome == null) {
                         addon.logError(split[1].toUpperCase(Locale.ENGLISH) + " is an unknown biome on this server.");
+                        result.put(d, Biome.CUSTOM);
                     } else {
                         // A biome of null means that no alternative biome should be applied
                         result.put(d, biome);
diff --git a/src/main/java/world/bentobox/boxed/generators/BoxedBlockPopulator.java b/src/main/java/world/bentobox/boxed/generators/BoxedBlockPopulator.java
index 8cf3c4c..cf34c60 100644
--- a/src/main/java/world/bentobox/boxed/generators/BoxedBlockPopulator.java
+++ b/src/main/java/world/bentobox/boxed/generators/BoxedBlockPopulator.java
@@ -6,10 +6,8 @@ import java.util.Random;
 
 import org.bukkit.Bukkit;
 import org.bukkit.Location;
-import org.bukkit.Material;
 import org.bukkit.World;
 import org.bukkit.block.Banner;
-import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.CreatureSpawner;
 import org.bukkit.entity.Entity;
@@ -20,13 +18,12 @@ import org.bukkit.inventory.Inventory;
 import org.bukkit.inventory.InventoryHolder;
 import org.bukkit.util.Vector;
 
-import world.bentobox.bentobox.BentoBox;
 import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock;
 import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner;
 import world.bentobox.bentobox.util.Pair;
 import world.bentobox.boxed.Boxed;
-import world.bentobox.boxed.generators.BoxedChunkGenerator.ChestData;
-import world.bentobox.boxed.generators.BoxedChunkGenerator.ChunkStore;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator.ChestData;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator.ChunkStore;
 
 /**
  * @author tastybento
@@ -35,14 +32,12 @@ import world.bentobox.boxed.generators.BoxedChunkGenerator.ChunkStore;
 public class BoxedBlockPopulator extends BlockPopulator {
 
     private Boxed addon;
-    private int size;
 
     /**
      * @param addon
      */
     public BoxedBlockPopulator(Boxed addon) {
         this.addon = addon;
-        this.size = (int)(addon.getSettings().getIslandDistance() / 16D); // Size is chunks
 
     }
 
@@ -54,8 +49,8 @@ public class BoxedBlockPopulator extends BlockPopulator {
         World world = Bukkit.getWorld(worldInfo.getUID());
         int height = worldInfo.getMaxHeight();
         int minY = worldInfo.getMinHeight();
-        int xx = BoxedChunkGenerator.repeatCalc(chunkX, size);
-        int zz = BoxedChunkGenerator.repeatCalc(chunkZ, size);
+        int xx = BoxedChunkGenerator.repeatCalc(chunkX);
+        int zz = BoxedChunkGenerator.repeatCalc(chunkZ);
         Pair<Integer, Integer> coords = new Pair<>(xx, zz);
         if (chunks.containsKey(coords)) {
             //// BentoBox.getInstance().logDebug("Populating ");
diff --git a/src/main/java/world/bentobox/boxed/generators/BoxedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/BoxedChunkGenerator.java
index 0163a51..f4b70c4 100644
--- a/src/main/java/world/bentobox/boxed/generators/BoxedChunkGenerator.java
+++ b/src/main/java/world/bentobox/boxed/generators/BoxedChunkGenerator.java
@@ -43,26 +43,19 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner;
 import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity;
 import world.bentobox.bentobox.util.Pair;
 import world.bentobox.boxed.Boxed;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator.ChestData;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator.ChunkStore;
+import world.bentobox.boxed.generators.AbstractBoxedChunkGenerator.EntityData;
 
 /**
  * Chunk generator for all environments
  * @author tastybento
  *
  */
-public class BoxedChunkGenerator extends ChunkGenerator {
-
-    private final Boxed addon;
-    private final int size;
-    private Map<Pair<Integer, Integer>, ChunkStore> chunks = new HashMap<>();
-    public record ChunkStore(ChunkSnapshot snapshot, List<EntityData> bpEnts, List<ChestData> chests) {};
-    public record EntityData(Vector relativeLoc, BlueprintEntity entity) {};
-    public record ChestData(Vector relativeLoc, BlueprintBlock chest) {};
-
-    //private final WorldRef wordRefNether;
+public class BoxedChunkGenerator extends AbstractBoxedChunkGenerator {
 
     public BoxedChunkGenerator(Boxed addon) {
-        this.addon = addon;
-        this.size = (int)(addon.getSettings().getIslandDistance() / 16D); // Size is chunks
+        super(addon);
     }
 
     @Override
@@ -76,169 +69,22 @@ public class BoxedChunkGenerator extends ChunkGenerator {
         return world.getPopulators();
     }
 
-    /**
-     * Save a chunk
-     * @param z - chunk z coord
-     * @param x - chunk x coord
-     * @param chunk the chunk to set
-     */
-    public void setChunk(int x, int z, Chunk chunk) {
-        List<LivingEntity> ents = Arrays.stream(chunk.getEntities())
+    @Override
+    protected List<EntityData> getEnts(Chunk chunk) {
+        return this.setEntities(Arrays.stream(chunk.getEntities())
                 .filter(Objects::nonNull)
                 .filter(e -> !(e instanceof Player))
                 .filter(e -> e instanceof LivingEntity)
                 .map(LivingEntity.class::cast)
-                .toList();
-        // Grab entities
-        List<EntityData> bpEnts = this.setEntities(ents);
-        // Grab tile entities
-        List<ChestData> chests = Arrays.stream(chunk.getTileEntities()).map(t -> new ChestData(getLocInChunk(t.getLocation()), this.getBluePrintBlock(t.getBlock()))).toList();
-        chunks.put(new Pair<>(x, z), new ChunkStore(chunk.getChunkSnapshot(false, true, false), bpEnts, chests));
-    }
-
-    /**
-     * @param x chunk x
-     * @param z chunk z
-     * @return chunk snapshot or null if there is none
-     */
-    public ChunkSnapshot getChunk(int x, int z) {
-        return chunks.get(new Pair<>(x, z)).snapshot;
+                .toList());
     }
 
     @Override
-    public boolean canSpawn(World world, int x, int z)
-    {
-        return true;
-    }
-
-    @Override
-    public void generateNoise(WorldInfo worldInfo, Random r, int chunkX, int chunkZ, ChunkData cd) {
-
-        int height = worldInfo.getMaxHeight();
-        int minY = worldInfo.getMinHeight();
-        int xx = repeatCalc(chunkX, size);
-        int zz = repeatCalc(chunkZ, size);
-        Pair<Integer, Integer> coords = new Pair<>(xx, zz);
-        if (!chunks.containsKey(coords)) {
-            // This should never be needed because islands should abut each other
-            cd.setRegion(0, minY, 0, 16, 0, 16, Material.WATER);
-            return;
-        }
-        // Copy the chunk
-        ChunkSnapshot chunk = chunks.get(coords).snapshot;
-        copyChunkVerbatim(cd, chunk, minY, height);
-
-    }
-
-    private void copyChunkVerbatim(ChunkData cd, ChunkSnapshot chunk, int minY, int height) {
-        for (int x = 0; x < 16; x ++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = minY; y < height; y++) {
-                    cd.setBlock(x, y, z, chunk.getBlockData(x, y, z));
-                }
-            }
-        }
-    }
-
-    /*
-    private void copyChunk(ChunkData cd, ChunkSnapshot chunk, int minY, int height) {
-        for (int x = 0; x < 16; x ++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = minY; y < height; y++) {
-                    Material m = chunk.getBlockType(x, y, z);
-                    // Handle blocks that occur naturally in water
-                    if (isInWater(m)) {
-                        cd.setBlock(x, y, z, Material.WATER);
-                    } else {
-                        // Handle liquids and default blocks
-                        switch (m) {
-                        case WATER, LAVA, NETHERRACK, STONE, END_STONE -> cd.setBlock(x, y, z, m);
-                        default ->
-                        // Most other blocks
-                        cd.setBlock(x, y, z, isGround(m) ? Material.STONE : Material.AIR);
-                        }
-                    }
-                }
-            }
-        }
-    }
-     */
-    /**
-     * Calculates the repeating value for a given size
-     * @param chunkCoord chunk coord
-     * @param s size
-     * @return mapped chunk coord
-     */
-    public static int repeatCalc(int chunkCoord, int s) {
-        int xx;
-        if (chunkCoord > 0) {
-            xx = Math.floorMod(chunkCoord + s, s*2) - s;
-        } else {
-            xx = Math.floorMod(chunkCoord - s, -s*2) + s;
-        }
-        return xx;
-    }
-
-    /**
-     * @return the chunks
-     */
-    public Map<Pair<Integer, Integer>, ChunkStore> getChunks() {
-        return chunks;
-    }
-    /*
-    private static boolean isInWater(Material m) {
-        return switch (m) {
-        // Underwater plants
-        case KELP, KELP_PLANT, SEAGRASS, BUBBLE_COLUMN, BUBBLE_CORAL, BUBBLE_CORAL_BLOCK, BUBBLE_CORAL_FAN,
-        BUBBLE_CORAL_WALL_FAN, DEAD_BRAIN_CORAL, DEAD_BRAIN_CORAL_BLOCK, DEAD_BRAIN_CORAL_FAN,
-        DEAD_BRAIN_CORAL_WALL_FAN, DEAD_BUBBLE_CORAL, DEAD_BUBBLE_CORAL_BLOCK, DEAD_BUBBLE_CORAL_FAN,
-        DEAD_BUBBLE_CORAL_WALL_FAN, DEAD_BUSH, DEAD_FIRE_CORAL, DEAD_FIRE_CORAL_BLOCK, DEAD_FIRE_CORAL_FAN,
-        DEAD_FIRE_CORAL_WALL_FAN, DEAD_HORN_CORAL, DEAD_HORN_CORAL_BLOCK, DEAD_HORN_CORAL_FAN,
-        DEAD_HORN_CORAL_WALL_FAN, DEAD_TUBE_CORAL, DEAD_TUBE_CORAL_BLOCK, DEAD_TUBE_CORAL_FAN,
-        DEAD_TUBE_CORAL_WALL_FAN, FIRE_CORAL, FIRE_CORAL_BLOCK, FIRE_CORAL_FAN, FIRE_CORAL_WALL_FAN,
-        HORN_CORAL, HORN_CORAL_BLOCK, HORN_CORAL_FAN, HORN_CORAL_WALL_FAN, TUBE_CORAL, TUBE_CORAL_BLOCK,
-        TUBE_CORAL_FAN, TUBE_CORAL_WALL_FAN, TALL_SEAGRASS -> true;
-        default -> false;
-        };
+    protected List<ChestData> getChests(Chunk chunk) {
+        return Arrays.stream(chunk.getTileEntities()).map(t -> new ChestData(getLocInChunk(t.getLocation()), this.getBluePrintBlock(t.getBlock()))).toList();
     }
 
 
-    private static boolean isGround(Material m) {
-        if (m.isAir() || m.isBurnable() || !m.isSolid()) return false;
-        return switch (m) {
-        case ANDESITE, BEDROCK, CALCITE, CLAY, COAL_ORE, COARSE_DIRT, COBBLESTONE, COPPER_ORE, DEEPSLATE,
-        DEEPSLATE_COAL_ORE, DEEPSLATE_COPPER_ORE, DEEPSLATE_DIAMOND_ORE, DEEPSLATE_EMERALD_ORE,
-        DEEPSLATE_GOLD_ORE, DEEPSLATE_IRON_ORE, DEEPSLATE_LAPIS_ORE, DEEPSLATE_REDSTONE_ORE, DIAMOND_ORE,
-        DIORITE, DIRT, DIRT_PATH, DRIPSTONE_BLOCK, EMERALD_ORE, END_STONE, FARMLAND, GLOWSTONE, GOLD_ORE,
-        GRANITE, GRASS_BLOCK, IRON_ORE, MAGMA_BLOCK, MYCELIUM, NETHERITE_BLOCK, NETHERRACK, RED_SAND,
-        RED_SANDSTONE, ROOTED_DIRT, SAND, SANDSTONE, SOUL_SAND, SOUL_SOIL, STONE, TERRACOTTA, AMETHYST_BLOCK,
-        AMETHYST_CLUSTER, AMETHYST_SHARD, BASALT, BLACKSTONE, BLACK_CONCRETE, BLACK_GLAZED_TERRACOTTA,
-        BLACK_TERRACOTTA, BLUE_CONCRETE, BLUE_GLAZED_TERRACOTTA, BLUE_TERRACOTTA, BONE_BLOCK, BROWN_CONCRETE,
-        BROWN_GLAZED_TERRACOTTA, BROWN_TERRACOTTA, BUDDING_AMETHYST, CHISELED_DEEPSLATE,
-        CHISELED_NETHER_BRICKS, CHISELED_POLISHED_BLACKSTONE, CHISELED_QUARTZ_BLOCK, CHISELED_RED_SANDSTONE,
-        CHISELED_SANDSTONE, CHISELED_STONE_BRICKS, COAL_BLOCK, COBBLED_DEEPSLATE, CRYING_OBSIDIAN,
-        CUT_RED_SANDSTONE, CUT_RED_SANDSTONE_SLAB, CUT_SANDSTONE, CUT_SANDSTONE_SLAB, CYAN_CONCRETE,
-        CYAN_GLAZED_TERRACOTTA, CYAN_TERRACOTTA, DEEPSLATE_BRICKS, DIAMOND_BLOCK, ECHO_SHARD, EMERALD_BLOCK,
-        GOLD_BLOCK, GRAVEL, GRAY_CONCRETE, GRAY_GLAZED_TERRACOTTA, GRAY_TERRACOTTA, GREEN_CONCRETE,
-        GREEN_GLAZED_TERRACOTTA, GREEN_TERRACOTTA, INFESTED_CHISELED_STONE_BRICKS, INFESTED_COBBLESTONE,
-        INFESTED_CRACKED_STONE_BRICKS, INFESTED_DEEPSLATE, INFESTED_MOSSY_STONE_BRICKS, INFESTED_STONE,
-        INFESTED_STONE_BRICKS, LAPIS_ORE, LARGE_AMETHYST_BUD, LIGHT_BLUE_CONCRETE,
-        LIGHT_BLUE_GLAZED_TERRACOTTA, LIGHT_BLUE_TERRACOTTA, LIGHT_GRAY_CONCRETE,
-        LIGHT_GRAY_GLAZED_TERRACOTTA, LIGHT_GRAY_TERRACOTTA, LIME_CONCRETE, LIME_GLAZED_TERRACOTTA,
-        LIME_TERRACOTTA, MAGENTA_CONCRETE, MAGENTA_GLAZED_TERRACOTTA, MAGENTA_TERRACOTTA, MOSSY_COBBLESTONE,
-        MUD, NETHERITE_SCRAP, NETHER_GOLD_ORE, NETHER_QUARTZ_ORE, OBSIDIAN, ORANGE_CONCRETE,
-        ORANGE_GLAZED_TERRACOTTA, ORANGE_TERRACOTTA, PACKED_MUD, PINK_CONCRETE, PINK_GLAZED_TERRACOTTA,
-        PINK_TERRACOTTA, PODZOL, POLISHED_ANDESITE, POLISHED_BASALT, POLISHED_BLACKSTONE,
-        POLISHED_DEEPSLATE, POLISHED_DIORITE, POLISHED_GRANITE, PURPLE_CONCRETE, PURPLE_GLAZED_TERRACOTTA,
-        PURPLE_TERRACOTTA, PURPUR_BLOCK, QUARTZ_BLOCK, RAW_COPPER_BLOCK, RAW_GOLD_BLOCK, RAW_IRON_BLOCK,
-        REDSTONE_BLOCK, REDSTONE_ORE, RED_CONCRETE, RED_GLAZED_TERRACOTTA, RED_TERRACOTTA, SMOOTH_BASALT,
-        SMOOTH_QUARTZ, SMOOTH_RED_SANDSTONE, SMOOTH_SANDSTONE, SMOOTH_STONE, TUFF, WARPED_HYPHAE,
-        WARPED_NYLIUM, WHITE_CONCRETE, WHITE_GLAZED_TERRACOTTA, WHITE_TERRACOTTA, YELLOW_CONCRETE,
-        YELLOW_GLAZED_TERRACOTTA, YELLOW_TERRACOTTA -> true;
-        default -> false;
-        };
-    }
-     */
     private List<EntityData> setEntities(Collection<LivingEntity> entities) {
         List<EntityData> bpEnts = new ArrayList<>();
         for (LivingEntity entity: entities) {
@@ -350,39 +196,4 @@ public class BoxedChunkGenerator extends ChunkGenerator {
         return cs;
     }
 
-
-    @Override
-    public boolean shouldGenerateNoise() {
-        return false;
-    }
-
-    @Override
-    public boolean shouldGenerateSurface() {
-
-        return false;
-    }
-
-    @Override
-    public boolean shouldGenerateCaves() {
-        return false;
-        //return this.addon.getSettings().isGenerateCaves();
-    }
-
-    @Override
-    public boolean shouldGenerateDecorations() {
-        return false;
-        //return this.addon.getSettings().isGenerateDecorations();
-    }
-
-    @Override
-    public boolean shouldGenerateMobs() {
-        return this.addon.getSettings().isGenerateMobs();
-    }
-
-    @Override
-    public boolean shouldGenerateStructures() {
-        return false;
-        //return this.addon.getSettings().isAllowStructures();
-    }
-
 }
diff --git a/src/main/java/world/bentobox/boxed/generators/BoxedSeedChunkGenerator.java b/src/main/java/world/bentobox/boxed/generators/BoxedSeedChunkGenerator.java
index 965de77..c5c5154 100644
--- a/src/main/java/world/bentobox/boxed/generators/BoxedSeedChunkGenerator.java
+++ b/src/main/java/world/bentobox/boxed/generators/BoxedSeedChunkGenerator.java
@@ -1,5 +1,8 @@
 package world.bentobox.boxed.generators;
 
+import java.util.List;
+
+import org.bukkit.Chunk;
 import org.bukkit.World.Environment;
 import org.bukkit.generator.BiomeProvider;
 import org.bukkit.generator.ChunkGenerator;
@@ -12,24 +15,37 @@ import world.bentobox.boxed.Boxed;
  * @author tastybento
  *
  */
-public class BoxedSeedChunkGenerator extends ChunkGenerator {
+public class BoxedSeedChunkGenerator extends AbstractBoxedChunkGenerator {
 
-    private final BiomeProvider seedBiomeProvider;
+    private final BiomeProvider biomeProvider;
     private final Environment env;
 
     /**
-     * @param env
-     * @param seedBiomeProvider
+     * @param boxed - addon
+     * @param env - environment
      */
     public BoxedSeedChunkGenerator(Boxed boxed, Environment env) {
-        this.seedBiomeProvider = new SeedBiomeGenerator(boxed);
+        super(boxed);
+        this.biomeProvider = null;
+        this.env = env;
+    }
+
+    /**
+     * @param boxed - addon
+     * @param env - environment
+     * @param bp - biome provider
+     */
+    public BoxedSeedChunkGenerator(Boxed boxed, Environment env, BiomeProvider bp) {
+        super(boxed);
+        this.biomeProvider = bp;
         this.env = env;
     }
 
 
     @Override
     public BiomeProvider getDefaultBiomeProvider(WorldInfo worldInfo) {
-        return seedBiomeProvider;
+        // If null then vanilla biomes are used
+        return biomeProvider;
     }
 
     @Override
@@ -62,4 +78,16 @@ public class BoxedSeedChunkGenerator extends ChunkGenerator {
     public boolean shouldGenerateStructures() {
         return env.equals(Environment.NETHER); // We allow structures in the Nether
     }
+
+    @Override
+    protected List<EntityData> getEnts(Chunk chunk) {
+        // These won't be stored
+        return null;
+    }
+
+    @Override
+    protected List<ChestData> getChests(Chunk chunk) {
+        // These won't be stored
+        return null;
+    }
 }
diff --git a/src/main/java/world/bentobox/boxed/generators/SeedBiomeGenerator.java b/src/main/java/world/bentobox/boxed/generators/SeedBiomeGenerator.java
index f4bcf25..d257e0a 100644
--- a/src/main/java/world/bentobox/boxed/generators/SeedBiomeGenerator.java
+++ b/src/main/java/world/bentobox/boxed/generators/SeedBiomeGenerator.java
@@ -2,6 +2,7 @@ package world.bentobox.boxed.generators;
 
 import org.bukkit.World.Environment;
 import org.bukkit.block.Biome;
+import org.bukkit.generator.BiomeProvider;
 
 import world.bentobox.boxed.Boxed;
 
@@ -11,8 +12,8 @@ import world.bentobox.boxed.Boxed;
  */
 public class SeedBiomeGenerator extends AbstractSeedBiomeProvider {
 
-    public SeedBiomeGenerator(Boxed boxed) {
-        super(boxed, Environment.NORMAL, Biome.OCEAN);
+    public SeedBiomeGenerator(Boxed boxed, AbstractBoxedChunkGenerator seedGen) {
+        super(boxed, Environment.NORMAL, Biome.PLAINS, seedGen);
     }
 
 }
\ No newline at end of file
diff --git a/src/main/resources/biomes.yml b/src/main/resources/biomes.yml
index 5ffeab7..619ba94 100644
--- a/src/main/resources/biomes.yml
+++ b/src/main/resources/biomes.yml
@@ -2,27 +2,25 @@
 distribution:
   overworld:
     north-east:
-    - 0.05:PLAINS
+    - 0.05:CUSTOM
     - 0.1:DESERT
     - 0.2:SAVANNA
     - 0.5:SPARSE_JUNGLE
     - 0.65:JUNGLE
     - 0.8:BAMBOO_JUNGLE
     - 1.0:MANGROVE_SWAMP
+    - 2.0:CUSTOM
     south-east:
-    - 0.05:PLAINS
-    - 0.8:SUNFLOWER_PLAINS
+    - 0.05:CUSTOM
+    - 0.08:SUNFLOWER_PLAINS
     - 0.2:FLOWER_FOREST
     - 0.3:SAVANNA
     - 0.4:BEACH
-    - 0.5:COLD_OCEAN
+    - 0.5:CUSTOM
     north-west:
-    - 0.05:PLAINS
-    - 0.8:WARM_OCEAN
-    - 1.5:COLD_OCEAN
-    - 2.0:OCEAN
+    - 2.0:CUSTOM
     south-west:
-    - 0.04:PLAINS
+    - 0.04:CUSTOM
     - 0.05:DARK_FOREST
     - 0.06:BIRCH_FOREST
     - 0.07:FOREST
@@ -34,9 +32,10 @@ distribution:
     - 0.7:TAIGA
     - 1.1:SNOWY_PLAINS
     - 2.1:SNOWY_TAIGA
+    - 2.2:CUSTOM
   nether:
     north-east:
-    - 0.03:NETHER_WASTES
+    - 0.03:CUSTOM
     - 0.14:CRIMSON_FOREST
     - 0.26:NETHER_WASTES
     - 0.51:WARPED_FOREST
@@ -46,7 +45,7 @@ distribution:
     - 1.6:BASALT_DELTAS
     - 2.1:NETHER_WASTES
     south-east:
-    - 0.03:NETHER_WASTES
+    - 0.03:CUSTOM
     - 0.05:CRIMSON_FOREST
     - 0.23:NETHER_WASTES
     - 0.48:WARPED_FOREST
@@ -56,7 +55,7 @@ distribution:
     - 1.9:BASALT_DELTAS
     - 2.0:NETHER_WASTES
     north-west:
-    - 0.03:NETHER_WASTES
+    - 0.03:CUSTOM
     - 0.15:CRIMSON_FOREST
     - 0.20:NETHER_WASTES
     - 0.3:WARPED_FOREST
@@ -66,7 +65,7 @@ distribution:
     - 1.5:BASALT_DELTAS
     - 2.0:NETHER_WASTES
     south-west:
-    - 0.03:NETHER_WASTES
+    - 0.03:CUSTOM
     - 0.11:CRIMSON_FOREST
     - 0.22:NETHER_WASTES
     - 0.51:WARPED_FOREST