Skip to content
This repository has been archived by the owner on Oct 6, 2024. It is now read-only.

Using properties

Rutger Kok edited this page Jul 7, 2020 · 8 revisions

Your world generator probably has several switches that a server admin can use to customize the terrain. WorldGeneratorApi provides a system for storing these properties.

Setting everything up

For this tutorial, we won't start from scratch. Instead, we use a simple plugin called Pancake that generates flat lands. We created this plugin during the previous tutorial. If you want, you can continue working from there. However, you can also just download a ZIP file I created for you.

Starting from a premade ZIP file

Download the starting point (ZIP)

Extract the ZIP somewhere. This is a Maven project, instructions on building it and importing it in Eclispe/IntelliJ can be found here. After you have imported the plugin,

There are two classes included. PancakeGenerator generates the actual terrain, while PancakeMain starts the plugin and registers PancakeGenerator. If you don't understand what's going on, please refer back to the previous tutorial.

If you start a world with Pancake set as the terrain generator, then you'll end up with a flat world. Still, caves, trees, ores, villages, strongholds, etc. should generate normally.

Making the terrain height configurable

We want the server admin to be able to specify how high the terrain is. We could just use an ordinary variable for that. However, WorldGeneratorApi provides a nice property system for inter-plugin communication. Although it is currently not possible, the idea is that the server admin can use WorldGeneratorApi to change properties while the server is running, and immediately see the effect. If you'd design your own configuration system, that will not be possible. So it's best to use the properties system.

We first start by updating the PancakeGenerator class. Copy-paste following code, replacing the existing class:

public class PancakeGenerator implements BaseTerrainGenerator {

    private final WorldRef world;
    private final PancakeConfig config;

    PancakeGenerator(WorldRef world, PancakeConfig config) {
        this.world = world;
        this.config = config;
    }

    @Override
    public int getHeight(BiomeGenerator biomeGenerator, int x, int z, HeightType type) {
        return (int) config.height.get(world); // (1);
    }

    @Override
    public void setBlocksInChunk(GeneratingChunk chunk) {
        int height = (int) config.height.get(world); // (2)

        chunk.getBlocksForChunk().setRegion(0, 0, 0, CHUNK_SIZE, height, CHUNK_SIZE, Material.STONE);
    }

}

There are two changes, marked with // (1) and // (2). In both cases, the value of the terrain height is read from a configuration object (we still need to create this object, so currently the code won't compile). We ask the configuration system for the current value of the dirt height for our world.

Note how we store the value as a float instead of a more appropriate type. Currently, WorldGeneratorApi supports two types of properties: floats and objects. Floats seem to be the most appropriate of the two. However, this means that a conversion to an int is necessary.

In the lines that follow, the value is used. Stone is now generated from y = 0 to y = height. Another change is that there is now a constructor written down. This constructor stores the current world and the configuration object. Let's write this configuration object. Create a new class called PancakeConfig and paste the following code in it.

package nl.rutgerkok.pancakeworldgenerator;

import org.bukkit.NamespacedKey;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.Plugin;

import nl.rutgerkok.worldgeneratorapi.WorldRef;
import nl.rutgerkok.worldgeneratorapi.property.FloatProperty;
import nl.rutgerkok.worldgeneratorapi.property.PropertyRegistry;

public class PancakeConfig {
    private static final String HEIGHT = "height";

    public final FloatProperty height;

    public PancakeConfig(Plugin plugin, PropertyRegistry registry) {
        height = registry.getFloat(new NamespacedKey(plugin, HEIGHT), 63);
    }

    public void readConfig(WorldRef world, FileConfiguration fileConfiguration) {
        height.setWorldDefault(world, (float) fileConfiguration.getDouble(HEIGHT, height.get(world)));
    }
}

Change the package declaration if necessary. The constructor initializes the settings and the readConfig method is used to actually read the configuration values from a YAML file. For now, this class just contains one setting, but hopefully it is clear how to extend it to read other settings.

The NamespacedKey class is used to give your setting a unique name, based on the name of your plugin and on DIRT_HEIGHT. In my case, the name of the setting became pancake:dirt_height.

Before the plugin actually works, we need to update the main class to read the configuration.

public class PancakeMain extends JavaPlugin {

    private PancakeConfig pancakeConfig;
    private WorldGeneratorApi api;

    @Override
    public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {

        WorldRef world = WorldRef.ofName(worldName);
        return api.createCustomGenerator(world, generator -> {

            pancakeConfig.readConfig(world, getConfig());
            generator.setBaseTerrainGenerator(new PancakeGenerator(world, pancakeConfig));
            this.getLogger().info("Enabled the Pancake world generator for world \"" + worldName + "\"");

        });

    }

    @Override
    public void onEnable() {

        api = WorldGeneratorApi.getInstance(this, 1, 0);
        pancakeConfig = new PancakeConfig(this, api.getPropertyRegistry());

    }

}

We have now added an onEnable method, which connects to WorldGeneratorApi and initializes the configuration file. Previously, the API was initialized in getDefaultWorldGenerator(...), but the call has been moved so that we can also use the API for initializing the configuration class.

At this point, you can manually create a plugins/Pancake/config.yml file with the following contents:

height: 150

(If the name in the plugin.yml is not Pancake but something else, then the config.yml should of course not be placed in the Pancake folder, but somewhere else.) Then, restart the server and notice how new terrain now has more layers of terrain.

Making the server write a configuration file for you

This approach is not very user-friendly: it is better to pre-create the configuration file, so that the server admin knows that something can be edited.

Let's add the following method to the PancakeConfig class:

    public void writeConfig(WorldRef world, FileConfiguration fileConfiguration) {
        fileConfiguration.set(HEIGHT, height.get(world));
    }

This sets the value in the configuration file to the actual value that was read. If the user placed height: not_a_number in the configuration file, then that will get corrected to height: 63.0.

Now, we only need to actually call this method and save the configuration file. That can be done by changing the getDefaultWorldGenerator method in the PancakeMain class.

    @Override
    public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {

        WorldRef world = WorldRef.ofName(worldName);
        return api.createCustomGenerator(world, generator -> {

            pancakeConfig.readConfig(world, getConfig());
            generator.setBaseTerrainGenerator(new PancakeGenerator(world, pancakeConfig));

            // The following two lines are new
            pancakeConfig.writeConfig(world, getConfig());
            saveConfig();

            this.getLogger().info("Enabled the Pancake world generator for world \"" + worldName + "\"");

        });

    }

saveConfig() is a method of Bukkit.

Phew, we're done!

Making changes to properties while the server is running

There is a /worldgeneratorapi reload command. This resets the world generator and then runs all world generator initialization code again. For Pancake, this means that the configuration file will be read from disk again, written back to disk, and the message "Enabled the Pancake world generator for world \"" + worldName + "\"" will be printed again.

You can also directly inspect and change the properties, without reloading everything. If you run /worldgeneratorapi get world pancake:height, the current height will be printed for the world named world. (If your world has a different name than simply world, use that name in the command.) If you run /worldgeneratorapi set world pancake:height 40, the property will be updated to 40 for the world named world. Note that this updated value will be reset upon reload or server restart. After all, Pancake only saves the configuration file directly after reading it, so it will never write this changed value to disk. Therefore, these commands are great for debugging and terrain optimalization.

More information

Besides FloatProperty, there is also the more general Property<T>, where T can be any object. The API is the same, except that you need to use PropertyRegistry.getProperty(...) instead of PropertyRegistry.getFloat(...). You can use this class to store more complex configuration properties.

Here, we have used world-specific properties. It is also possible to use biome-specific properties. Look at the other methods in the FloatProperty class for instructions. It is even possible to provide properties that are just valid for a single biome in a single world.

In this tutorial, we used a custom-created property, with the key new NamespacedKey(plugin, HEIGHT) (this evaluates to "pancake:height".