1.17.x Changing vanilla biomes

Discussion in 'Spigot Plugin Development' started by EOT3000, Jul 19, 2021.

  1. Hello, I'd wanted to make a world with any generation, but with biomes in different places from vanilla. I decided that the easiest way to do this is by using NMS to change where specific biomes should be, based on an image of the biomes.

    This worked mostly well, however there were some issues, mostly with oceans and other water. The biomes listed are correct, but what generates there isn't.

    For example, here is an ocean here where there should be plains.
    upload_2021-7-19_9-28-26.png

    The other issue I found was that restarting the server causes chunk borders, though this can be fixed by pre generating the server
    upload_2021-7-19_9-34-19.png

    Here is the code that I used:

    Code (Text):

    import com.mojang.serialization.Codec;
    import com.mojang.serialization.Lifecycle;
    import com.mojang.serialization.codecs.RecordCodecBuilder;
    import net.minecraft.core.IRegistry;
    import net.minecraft.core.IRegistryWritable;
    import net.minecraft.resources.RegistryLookupCodec;
    import net.minecraft.server.dedicated.DedicatedServer;
    import net.minecraft.server.level.WorldServer;
    import net.minecraft.world.level.biome.BiomeBase;
    import net.minecraft.world.level.biome.WorldChunkManager;
    import net.minecraft.world.level.chunk.ChunkGenerator;
    import org.bukkit.Bukkit;
    import org.bukkit.block.Biome;
    import org.bukkit.craftbukkit.v1_17_R1.CraftServer;
    import org.bukkit.craftbukkit.v1_17_R1.CraftWorld;
    import org.bukkit.craftbukkit.v1_17_R1.block.CraftBlock;

    import javax.imageio.ImageIO;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.lang.reflect.Field;
    import java.util.List;

    public class CustomChunkManager extends WorldChunkManager {
        public static final Codec<CustomChunkManager> e = RecordCodecBuilder.create((instance) -> {
            return instance.group(Codec.LONG.fieldOf("seed").stable().forGetter((overworldBiomeSource) -> {
                return overworldBiomeSource.seed;
            }), Codec.BOOL.optionalFieldOf("legacy_biome_init_layer", false, Lifecycle.stable()).forGetter((overworldBiomeSource) -> {
                return false;
            }), Codec.BOOL.fieldOf("large_biomes").orElse(false).stable().forGetter((overworldBiomeSource) -> {
                return false;
            }), RegistryLookupCodec.a(IRegistry.aO).forGetter((overworldBiomeSource) -> {
                return overworldBiomeSource.registry;
            })).apply(instance, instance.stable((a,b,c,d) -> {
                // getStuff is a method to get all biomes on the map+void
                return new CustomChunkManager(GeneratorPlugin.getStuff());
            }));
        });

        private BufferedImage biomeImage;
        IRegistryWritable<BiomeBase> registry;

        private long seed;

        public CustomChunkManager(List<BiomeBase> biomes) {
            super(biomes);

            DedicatedServer dedicatedServer = ((CraftServer) Bukkit.getServer()).getServer();

            // Registry that contains all biomes
            registry = dedicatedServer.getCustomRegistry().b(IRegistry.aO);

            try {
                // Read the image which contains the biomes
               
                biomeImage = ImageIO.read(new File("map.png"));
            } catch (Exception e) {
                e.printStackTrace();
            }

            setGenerator(((CraftWorld) Bukkit.getWorld("world")).getHandle());
        }

        public void setGenerator(final WorldServer world) {
            try {
                ChunkGenerator generator = world.getChunkProvider().getChunkGenerator();
                Class clazz = ChunkGenerator.class;

                // Field for biomes
                Field field = clazz.getDeclaredField("c");

                // Field for population
                Field field2 = clazz.getDeclaredField("b");

                field.setAccessible(true);
                field2.setAccessible(true);

                field.set(generator, this);
                field2.set(generator, this);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        protected Codec<? extends WorldChunkManager> a() {
            return e;
        }

        @Override
        public WorldChunkManager a(long seed) {
            return new CustomChunkManager(this.d);
        }


        // Return the biome found on the image, or void if out of bounds
        @Override
        public BiomeBase getBiome(int biomeX, int biomeY, int biomeZ) {
            try {
                return CraftBlock.biomeToBiomeBase(registry, CustomBiome.getBiome(biomeImage.getRGB(biomeX + 1024, biomeZ + 1024)).biome);
            } catch (Exception e) {
                //
            }

            return CraftBlock.biomeToBiomeBase(registry, Biome.THE_VOID);
        }
    }
     
     
  2. why use nms? you can change without nms.

    Chunk#.setBiome();
     

  3. Code (Text):
    @EventHandler
    public void chunkLoadEvent(ChunkLoadEvent event) {
        Chunk chunk = event.getChunk();

        if (event.isNewChunk()) {
            for (int x = 0; x < 16; x++) {
                for (int y = 0; y < 256; y++) {
                    for (int z = 0; z < 16; z++) {
                        chunk.getWorld().setBiome(chunk.getX()*16+x, y, chunk.getZ()*16+x, Biome.PLAINS);
                    }
                }
            }
        }
    }
     
    Something like this? I tried that and it didn't do anything except change what the biome is. I want to change the biome along with what generated there
     
  4. If it has already been set, it is necessary to force update the chunk with NMS.