Resource How to create custom biomes

Discussion in 'Spigot Plugin Development' started by CasperSwagerman, Jun 22, 2021.

  1. Hello everyone, 1.16 added support for custom biomes via data packs. These custom biomes can have their own colours: for example sky, water, leaves or fog colours, without requiring a texturepack. You actually don't need a data pack to do it though. You can actually register custom biomes using a bunch of NMS. Here is a tutorial on how to do it in 1.17:

    We start off by creating a ResourceKey for your own custom biome:
    Code (Java):

    Server server = Bukkit.getServer();
    CraftServer craftserver = (CraftServer)server;
    dedicatedserver = craftserver.getServer();
    ResourceKey<BiomeBase> newKey = ResourceKey.a(IRegistry.aO, new MinecraftKey("test", "fancybiome"))
    newKey will be used to register your own biome by the name: "test:fancybiome" (test will replace the normal "minecraft:"). Make sure both names are in lowercase.

    Now that we have the ResourceKey to register your own biome, let's actually start creating it.

    We start off by retrieving the BiomeBase object (this holds all the information of a biome) of a normal forest biome. We do this so we can just copy the more complicated objects (like mob spawning or terrain generation) to our new biome. Making everything ourselves is unnecessary:
    Code (Java):
    ResourceKey<BiomeBase> oldKey = ResourceKey.a(IRegistry.aO, new MinecraftKey("minecraft", "forest"));
    IRegistryWritable<BiomeBase> registrywritable = dedicatedserver.getCustomRegistry().b(IRegistry.aO);
    BiomeBase forestbiome = registrywritable.a(oldKey);
    Now let's create a new object of BiomeBase.a. The BiomeBase.a class works as a BiomeBase builder. Let's also copy some data from the forest biome to our own biome. Some fields are private, so we are going to use reflection.
    Code (Java):
    BiomeBase.a newBiome = new BiomeBase.a();
    newBiome.a(forestbiome.t());
    newBiome.a(forestbiome.c());

    Field biomeSettingMobsField = BiomeBase.class.getDeclaredField("m");
    biomeSettingMobsField.setAccessible(true);
    BiomeSettingsMobs biomeSettingMobs = (BiomeSettingsMobs) biomeSettingMobsField.get(forestbiome);
    newBiome.a(biomeSettingMobs);

    Field biomeSettingGenField = BiomeBase.class.getDeclaredField("l");
    biomeSettingGenField.setAccessible(true);
    BiomeSettingsGeneration biomeSettingGen = (BiomeSettingsGeneration) biomeSettingGenField.get(forestbiome);
    newBiome.a(biomeSettingGen);
    It's time to start putting in our own stuff. The values below are the default values. If you don't care about them, just leave them like this:
    Code (Java):
    newBiome.a(0.2F); //Depth of biome
    newBiome.b(0.05F); //Scale of biome
    newBiome.c(0.7F); //Temperature of biome
    newBiome.d(0.8F); //Downfall of biome
    newBiome.a(BiomeBase.TemperatureModifier.a); //BiomeBase.TemperatureModifier.a will make your biome normal, BiomeBase.TemperatureModifier.b will make your biome frozen
    Your biome is almost set up now. The only thing that's left is giving our biome our own colours! This is a very tricky part. The BiomeBase object (or BiomeBase.a) contains a separate object called BiomeFog that contains all the information about biome colours. This object is kinda similar to the BiomeBase object: you create one by using a builder class called BiomeFog.a. Let's create one.
    Code (Java):
    BiomeFog.a newFog = new BiomeFog.a();
    newFog.a(GrassColor.a); //This doesn't affect the actual final grass color, just leave this line as it is or you will get errors
    Now that our BiomeFog object is set up, we can start putting in the colours. Minecraft uses decimal colour codes. You can easily change a hex colour code into a decimal colour code by using this simple line:
    Code (Java):
    Integer.parseInt("your hex colour code here",16); //Dont forget to remove the # in front of the hex color code
    Now that you have some decimal color codes ready, let's put them into the BiomeFog.a object:
    Code (Java):
    //Necessary values; removing them will break your biome
    newFog.a(your decimal colorcode here); //fogcolor
    newFog.b(your decimal colorcode here); //water color
    newFog.c(your decimal colorcode here); //water fog color
    newFog.d(your decimal colorcode here); //sky color

    //Unnecessary values; can be removed safely if you don't want to change them
    newFog.e(your decimal colorcode here) //foliage color (leaves, fines and more)
    newFog.f(your decimal colorcode here) //grass blocks color
    The BiomeFog.a object is now done. We can now put it inside the BiomeBase.a object:
    Code (Java):
    newBiome.a(newFog.a());
    Now that our BiomeBase.a object is done, let's add it to the server's registry:
    Code (Java):
    dedicatedserver.getCustomRegistry().b(IRegistry.aO).a(newKey, newBiome.a(), Lifecycle.stable());
    That's it! You will now have added your own biome to your server! All this code should be called in onEnable(), every time the servers starts. The server won't save the registered biome on its own, so you will need to register it every startup. To make sure the server doesn't delete the biome out of the world, put this in your plugin.yml:
    Code (YAML):
    load: STARTUP
    Your plugin will now load before the worlds are loaded. If you don't register your custom biome before the world loads, Minecraft will delete your custom biome out of the world.

    Now.... how do you actually use your new biome? The Spigot API (1.17) doesn't currently support custom biomes (you know what to do spigot devs ;)), so I wrote my own methods to change biomes in a world:
    Code (Java):
    @Override
        public boolean setBiome(String newBiomeName, Chunk c) {
       
            BiomeBase base;
            IRegistryWritable<BiomeBase> registrywritable = dedicatedserver.getCustomRegistry().b(IRegistry.aO);
       
            ResourceKey<BiomeBase> rkey = ResourceKey.a(IRegistry.aO, new MinecraftKey(newBiomeName.toLowerCase()));
            base = registrywritable.a(rkey);
            if(base == null) {
                if(newBiomeName.contains(":")) {
                    ResourceKey<BiomeBase> newrkey = ResourceKey.a(IRegistry.aO, new MinecraftKey(newBiomeName.split(":")[0].toLowerCase(), newBiomeName.split(":")[1].toLowerCase()));
                    base = registrywritable.a(newrkey);
                    if(base == null) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
       
            World w = ((CraftWorld)c.getWorld()).getHandle();
       
            for (int x = 0; x <= 15; x++) {
                for (int z = 0; z <= 15; z++) {
                    for(int y = 0; y <= c.getWorld().getMaxHeight(); y++) {
                   
                        setBiome(c.getX() * 16 + x, y, c.getZ() * 16 + z, w, base);
                    }
                }
            }
            refreshChunksForAll(c);
            return true;
        }

        @Override
        public boolean setBiome(String newBiomeName, Location l) {
            BiomeBase base;
            IRegistryWritable<BiomeBase> registrywritable = dedicatedserver.getCustomRegistry().b(IRegistry.aO);
       
            ResourceKey<BiomeBase> rkey = ResourceKey.a(IRegistry.aO, new MinecraftKey(newBiomeName.toLowerCase()));
            base = registrywritable.a(rkey);
            if(base == null) {
                if(newBiomeName.contains(":")) {
                    ResourceKey<BiomeBase> newrkey = ResourceKey.a(IRegistry.aO, new MinecraftKey(newBiomeName.split(":")[0].toLowerCase(), newBiomeName.split(":")[1].toLowerCase()));
                    base = registrywritable.a(newrkey);
                    if(base == null) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
       
            setBiome(l.getBlockX(), l.getBlockY(), l.getBlockZ(), ((CraftWorld)l.getWorld()).getHandle(), base);
            refreshChunksForAll(l.getChunk());
            return true;
        }
     
        private void setBiome(int x, int y, int z, World w, BiomeBase bb) {
              BlockPosition pos = new BlockPosition(x, 0, z);
         
              if (w.isLoaded(pos)) {
             
                 net.minecraft.world.level.chunk.Chunk chunk = w.getChunkAtWorldCoords(pos);
                 if (chunk != null) {
                 
                    chunk.getBiomeIndex().setBiome(x >> 2, y >> 2, z >> 2, bb);
                    chunk.markDirty();
                 }
              }
          }

    private void refreshChunksForAll(Chunk chunk) {
            net.minecraft.world.level.chunk.Chunk c = ((CraftChunk)chunk).getHandle();
            for (Player player : chunk.getWorld().getPlayers()) {
                if (player.isOnline()) {
                    if((player.getLocation().distance(chunk.getBlock(0, 0, 0).getLocation()) < (Bukkit.getServer().getViewDistance() * 16))) {
                        ((CraftPlayer) player).getHandle().b.sendPacket(new PacketPlayOutMapChunk(c));
                    }
                }
            }
        }
     
    You world generator people of course have your own way of doing this. If you ever need the BiomeBase object of your own biome, call newBiome.a(); on your custom biome object.

    The result you might ask? This:
    [​IMG]

    Thank you for reading my tutorial. Hope you learned something and I'm looking forward to more people using custom biomes!

    Edit: added missing code
     
    #1 CasperSwagerman, Jun 22, 2021
    Last edited: Jul 30, 2021 at 10:19 AM
    • Winner x 11
    • Useful x 8
    • Agree x 2
    • Like x 1
    • Informative x 1
    • Creative x 1
  2. This is insane, I was thinking about this when they announced custom biomes in datapacks, but this is nuts. I just hate playing around with NMS because it is so messy and the reflection required to make it work is just too much effort. I hope that the spigot devs see this and add support so we all can make our plugins clean and leave the messy stuff in the backend. This is amazing and you did gods work leaving comments on what each obfuscated method did!
     
    • Friendly Friendly x 1
  3. Hey, tour resources is amazing ! :love:

    Btw do you know how to prevent a biome from generation in 1.16.5 ? I asked it here but it seems that nobody can answer me :cry:
     
  4. Thank you ! I should be able to update my plugin now with your tutorial :)

    Have a question, how did you found that code:
    Code (Text):
    dedicatedserver.getCustomRegistry().b(IRegistry.aO).a(newKey, newBiome.a(), Lifecycle.stable());
     
     
  5. What does the method
    Code (Text):
    refreshChunksForAll(Chunk chunk)
    look like?

    Something like?
    Code (Text):

    private void refreshChunksForAll(Chunk chunk) {
        net.minecraft.world.level.chunk.Chunk c = ((CraftChunk)chunk).getHandle();
        for (Player player : chunk.getWorld().getPlayers()) {
            if (player.isOnline()) {
                ((CraftPlayer)player).getHandle().b.sendPacket(new PacketPlayOutMapChunk(c));
            }
        }
    }
     
     
    #5 eccentric, Jun 23, 2021
    Last edited: Jun 23, 2021
    • Agree Agree x 1
  6. you just made my day


    If I had to guess

    World#refreshChunk(X,Z)
     
    #6 Fahlur, Jun 23, 2021
    Last edited: Jun 23, 2021
  7. Code (Text):
    boolean refreshChunk(int x, int z)
    Deprecated.
    This method is not guaranteed to work suitably across all client implementations.
     
  8. Amazing! Custom biomes have such a big potential for cool projects, it was a shame nobody knew how to use them. I figured out by comparing deobfuscated classes to the normal classes, found out which object holds the biome registry through some digging and then reverse-engineered a way to get the object via DedicatedServer. If you also want to support 1.16 in your plugin: IRegistry.aO (obfuscated version of IRegistry.BIOME_REGISTRY) is IRegistry.ay in 1.16. I think some other fields changed too (like in BiomeBase). I don't have that on hand now though.
     
  9. I see, I forgot to include it. All this code comes directly out of my upcoming premium plugin, so posting only the code used to create biomes was a bit of a struggle. You always have to resend the chunk when changing biomes (that's why setbiome in worldedit requires you to relog) or they wouldn't appear on the client.

    You were very close though, make sure to add an extra line before you send the packet to avoid unnecessarily sending packets:
    Code (Java):
    if((p.getLocation().distance(c.getBlock(0, 0, 0).getLocation()) < (Bukkit.getServer().getViewDistance() * 16)))
    This makes sure that only the players in range get the packet.

    Edit: definitely don't use World#refreshChunk(X,Z)
     
    • Like Like x 1
    • Informative Informative x 1
  10. Thanks for that :)

    I have been adding custom dimensions/biomes with datapacks for my TARDIS plugin, but I have been finding that after server restarts the custom biomes in my worlds are being reported as
    Code (Text):
    minecraft:ocean
    and they lose their custom sky/water/foliage colours and I get rain in my desert biomes etc. Would registering the biomes at STARTUP help prevent his do you think? Or is it happening because Spigot doesn't support custom biomes in it's Biome enum?
     
  11. If you don't load the biomes at STARTUP, the world loader won't recognise the biome in your world and it will just replace it with something else. My way around this was to use packets, so the biomes aren't actually changed on the server. This also makes it very easy if the biome change is only temporary and want to avoid saving all original biomes yourself to change them back later. If you want to change the biome permanently, I would choose to just register at STARTUP. If it's only temporary, go for the packet approach.
     
  12. Well I have the same issue, I don't understand how the server is registering custom biome.

    I set my custom biome on start like you did:
    Code (Text):
    Server server = Bukkit.getServer();
            CraftServer craftserver = (CraftServer)server;
            MinecraftServer ms = craftserver.getHandle().getServer().F().getMinecraftServer();
            ResourceKey<BiomeBase> newKey = ResourceKey.a(IRegistry.aO, new MinecraftKey("test", "fancybiome"));
            IRegistryWritable<BiomeBase> registrywritable = ms.getCustomRegistry().b(IRegistry.aO);
            BiomeBase forestbiome = registrywritable.a(Biomes.e); //Biomes.e is forest biome

            BiomeBase.a newBiome = new BiomeBase.a();
            newBiome.a(forestbiome.t());
            newBiome.a(forestbiome.c());

            try {
                Field biomeSettingMobsField = BiomeBase.class.getDeclaredField("m");
                biomeSettingMobsField.setAccessible(true);
                BiomeSettingsMobs biomeSettingMobs = (BiomeSettingsMobs) biomeSettingMobsField.get(forestbiome);
                newBiome.a(biomeSettingMobs);

                Field biomeSettingGenField = BiomeBase.class.getDeclaredField("l");
                biomeSettingGenField.setAccessible(true);
                BiomeSettingsGeneration biomeSettingGen = (BiomeSettingsGeneration) biomeSettingGenField.get(forestbiome);
                newBiome.a(biomeSettingGen);

            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }

            newBiome.a(0.2F); //Depth of biome
            newBiome.b(0.05F); //Scale of biome
            newBiome.c(0.7F); //Temperature of biome
            newBiome.d(0.8F); //Downfall of biome
            newBiome.a(BiomeBase.TemperatureModifier.a); //BiomeBase.TemperatureModifier.a will make your biome normal, BiomeBase.TemperatureModifier.b will make your biome frozen
            BiomeFog.a newFog = new BiomeFog.a();
            newFog.a(BiomeFog.GrassColor.a); //This doesn't affect the actual final grass color, just leave this line as it is or you will get errors

            //Necessary values; removing them will break your biome
            newFog.a(Integer.parseInt("fcba03", 16)); //fogcolor
            newFog.b(Integer.parseInt("fcba03", 16)); //water color
            newFog.c(Integer.parseInt("fcba03", 16)); //water fog color
            newFog.d(Integer.parseInt("fcba03", 16)); //sky color

    //Unnecessary values; can be removed safely if you don't want to change them
            newFog.e(Integer.parseInt("fcba03", 16)); //foliage color (leaves, fines and more)
            newFog.f(Integer.parseInt("fcba03", 16)); //grass blocks color

            newBiome.a(newFog.a());
            ms.getCustomRegistry().b(IRegistry.aO).a(newKey, newBiome.a(), Lifecycle.stable());

    I have a command to tell me if the biome is registered:

    Code (Text):
     ms.getCustomRegistry().d(IRegistry.aO).forEach(biomeBase -> {
                 System.out.println("biome id " + ms.getCustomRegistry().d(IRegistry.aO).getKey(biomeBase) + " " +  ms.getCustomRegistry().d(IRegistry.aO).getId(biomeBase) );
            });
    I listen incoming map chunk packet to change all biome with the new biome id:

    Code (Text):
      manager.addPacketListener(new PacketAdapter(instance, PacketType.Play.Server.MAP_CHUNK) {
                @Override
                public void onPacketSending(PacketEvent event) {
                    PacketContainer packet = event.getPacket();
                    int[] biomeIDs = packet.getIntegerArrays().read(0);
                    Arrays.fill(biomeIDs, 176); //id of custombiome
                    packet.getIntegerArrays().write(0, biomeIDs);
                }
            });
    When I enter in F3 mode, the biome is minecraft : ocean
    I think this is because the server could not find the registered biome.

    So I tried to add the custom biome elsewhere:

    Like inside the biome registery:
    Code (Text):
      Reflection.getMethod(BiomeRegistry.class,"a",int.class,ResourceKey.class,BiomeBase.class)
                    .invoke(BiomeRegistry.class,176,newKey,newBiome.a());
    or inside the worldchunkmanager class:

    Code (Text):
        WorldServer ws = ((CraftWorld) player.getWorld()).getHandle();
            net.minecraft.world.level.chunk.ChunkGenerator chunkgenerator = ws.getChunkProvider().getChunkGenerator();
            WorldChunkManager worldchunkmanager = chunkgenerator.getWorldChunkManager();
            List<BiomeBase> list = new ArrayList<>(worldchunkmanager.b());
            list.add(ms.getCustomRegistry().d(IRegistry.aO).fromId(176));
            Reflection.getField(worldchunkmanager.getClass(),"d", List.class).set(worldchunkmanager,list);
    id is fine but the minecraftkey is null when I iterate hover the list:

    Code (Text):
     worldchunkmanager.b().forEach(b -> {
                System.out.println("biome id " + ms.getCustomRegistry().d(IRegistry.aO).getKey(b) + " "
                        + ms.getCustomRegistry().d(IRegistry.aO).getId(b));

            });
     
  13. Hmmm that's weird. I just pasted your exact code in an empty plugin and added my setBiome method, and I got this:

    [​IMG]

    As you can see on the right, it does show the correct name for me. Are you doing this on a spigot server? Because I'm not sure about paper or others. Otherwise, try running build tools again and create a new server as a test. Maybe it's another part of your plugin that is intervening? Or another plugin like multiverse (haven't tested)?
     
  14. Yes your setbiome method is modifying the chunk:
    Code (Text):
     chunk.getBiomeIndex().setBiome(x >> 2, y >> 2, z >> 2, bb);
    chunk.markDirty();
    And I can't use that, I only want packet to not affect the world.
    Because the biome is a forest biome with custom colors, it means when you generate new chunk all your biomes become forest.
     
  15. Code (Java):
    public int[] biomepacket = new int[1024]; // packet is filled in class constructor
    private Class<?> chunkPacket = Reflection1_17.getClass("net.minecraft.network.protocol.game.PacketPlayOutMapChunk");
    private FieldAccessor<int[]> biomesfield = Reflection1_17.getField("net.minecraft.network.protocol.game.PacketPlayOutMapChunk", int[].class, 0);

    protocol = new TinyProtocol(main) {
        @Override
        public Object onPacketOutAsync(Player reciever, Channel channel, Object packet) {
            if (chunkPacket.isInstance(packet)) {
                biomesfield.set(packet, biomepacket);
                return packet;
            }
        }
    }
    This is how I did it with packets in my premium plugin and it works fine too on 1.17. I think your issue may have to do with your server setup, other plugins or maybe protocollib. That's all I can think of right now.
     
  16. I am not sure to understand ?
    You replace the int array with the empty array biomepacket ?

    EDIT: my bad did not see the comment
     
  17. I fill it in the class constructor. But since it's my premium plugin, I don't want to include other code. I fill it with ID 176 just like you do, and it works fine.
     
  18. Yes no problem,
    So you do not use the setBiome method ?
     
  19. Thank you so much, it's working fine now, my mistake:

    I did not see the error on startup:
    Code (Text):
    Cannot invoke "net.minecraft.server.level.WorldServer.getMinecraftServer()" because the return value of "net.minecraft.server.dedicated.DedicatedServer.F()" is null
    caused by
    Code (Text):

      MinecraftServer ms = craftserver.getHandle().getServer().F().getMinecraftServer();
     
    now replaced with:

    Code (Text):
    Server server = Bukkit.getServer();
            CraftServer craftserver = (CraftServer)server;
            DedicatedServer ds = craftserver.getHandle().getServer();
     
    • Like Like x 1
  20. Awesome! I only wrote the setBiome method for this tutorial, so everyone has an easy way of using their custom biomes :)