Skip to content

Tutorial

Exlll edited this page Jul 9, 2022 · 16 revisions

Introduction

This tutorial is intended to show most features of this library by going step-by-step through the following example.

Let's say that we want to create a configuration for the following imaginary game:

  • A game of two teams, where one team is only allowed to place blocks and the other is only allowed to break them.
  • Some blocks are not allowed to be placed.
  • The participants in a team can either have a member or a leader role.
  • The participants are described by their UUID, name, and role.
  • The game has a moderator that is described by its UUID, name, and email.
  • The winning team wins a prize while the losers get one of several consolation items.
  • The game takes place in an area.
  • The game can only be played during a specific period (start and end date).
  • Some information should only be used internally and not be written to the configuration file.
  • All fields should be formatted uppercase.

Please note that this is meant to be an example to show most features of this library. You most likely wouldn't want to model a game or configuration like this.

Final configuration

Our final configuration will look like this:

# The game config for our imaginary game!
# Valid color codes are: &4, &c, &e

# This message is displayed to the winner team
WIN_MESSAGE: '&4YOU WON!'
# This message is displayed to the losers
LOSE_MESSAGE: '&c...you lost!'
FIRST_PRIZE: |
  ==: org.bukkit.inventory.ItemStack
  v: 3105
  type: DIAMOND_AXE
  meta:
    ==: ItemMeta
    meta-type: UNSPECIFIC
    enchants:
      DIG_SPEED: 5
      MENDING: 1
      DURABILITY: 3
CONSOLATION_PRIZES:
  - |
    ==: org.bukkit.inventory.ItemStack
    v: 3105
    type: STICK
    amount: 2
  - |
    ==: org.bukkit.inventory.ItemStack
    v: 3105
    type: ROTTEN_FLESH
    amount: 3
  - |
    ==: org.bukkit.inventory.ItemStack
    v: 3105
    type: CARROT
    amount: 4
START_DATE: 2022-01-01
END_DATE: 2022-12-31
FORBIDDEN_BLOCKS:
  - LAVA
  - BARRIER
MODERATOR:
  UUID: a45e0532-27f4-4883-aa1c-dc9ac988e3cd
  NAME: Mod
  # The moderators email
  # It must be valid!
  EMAIL: [email protected]
TEAMS:
  BLOCK_PLACE:
    - UUID: 8b41f31f-a663-4d1e-b08f-29bf2cf6ab57
      NAME: Eve
      TEAM_ROLE: LEADER
    - UUID: 768f3e0f-7bc4-49f1-b72d-d50895e3699e
      NAME: Dave
      TEAM_ROLE: MEMBER
  BLOCK_BREAK:
    - UUID: 08a253d1-490c-42d6-b01f-681d3a6584b1
      NAME: Alice
      TEAM_ROLE: LEADER
    - UUID: 8672ab2b-bbd9-4be4-8eeb-1b3770b94ae9
      NAME: Bob
      TEAM_ROLE: MEMBER
ARENA_RADIUS: 10
ARENA_CENTER: world;0;0

# Authors: Exlll

Steps

1. Create configuration

The first thing we have to do is to create a class and annotate it with @Configuration.

@Configuration
public final class GameConfig {}

2. Add a win and lose message

Then we can add the messages that are displayed to the winning and losing team. Because winMessage and loseMessage are strings, we can just add two fields with the same name and annotate them with @Comment.

@Configuration
public final class GameConfig {
    @Comment("This message is displayed to the winner team")
    private String winMessage = "&4YOU WON!";
    @Comment("This message is displayed to the losers")
    private String loseMessage = "&c...you lost!";
}

3. Define prizes

Next we define the prizes for the winning and losing team. Since we want to choose a random item for the losing team, we define several items in a list.

@Configuration
public final class GameConfig {
    // ...
    private ItemStack firstPrize = initFirstPrize();
    private List<ItemStack> consolationPrizes = List.of(
            new ItemStack(Material.STICK, 2),
            new ItemStack(Material.ROTTEN_FLESH, 3),
            new ItemStack(Material.CARROT, 4)
    );

    private ItemStack initFirstPrize() {
        ItemStack stack = new ItemStack(Material.DIAMOND_AXE);
        stack.addEnchantment(Enchantment.DURABILITY, 3);
        stack.addEnchantment(Enchantment.DIG_SPEED, 5);
        stack.addEnchantment(Enchantment.MENDING, 1);
        return stack;
    }
}

4. Define the period in which the game is allowed to be played

The period in which the game is allowed to be played is given by a start and an end date.

@Configuration
public final class GameConfig {
    // ...
    private LocalDate startDate = LocalDate.of(2022, Month.JANUARY, 1);
    private LocalDate endDate = LocalDate.of(2022, Month.DECEMBER, 31);
}

5. Add a set of blocks that are not allowed to be placed.

We don't want the users to place lava or barrier blocks which we can identify by their Material type.

@Configuration
public final class GameConfig {
    // ...
    private Set<Material> forbiddenBlocks = Set.of(Material.LAVA, Material.BARRIER);
}

6. Model participants and moderators

A user is defined by their UUID and name. A participant additionally has a role in the team and a moderator an email. To model that, we can create a User class and subclass it. We also need to annotate the User class with @Configuration. However, the subclasses don't need to be annotated.

We can add constructors to initialize these classes. Every configuration must have a default constructor, though, so we have to add one, too. That constructor can be private.

@Configuration
public final class GameConfig {
    // ...
    enum Role {MEMBER, LEADER}

    @Configuration
    public static class User {
        private UUID uuid;
        private String name;

        public User(UUID uuid, String name) {/* initialize */}
        private User() {}
    }

    public static final class Participant extends User {
        private Role teamRole;

        public Participant(UUID uuid, String name, Role teamRole) {/* initialize */}
        private Participant() {}
    }

    public static final class Moderator extends User {
        @Comment({"The moderators email", "It must be valid!"})
        private String email;

        public Moderator(UUID uuid, String name, String email) {/* initialize */}
        private Moderator() {}
    }
}

7. Model the teams and add a moderator

A team is described by its permission and list of participants. We could implement a new class to model that but instead we are going the easy route and will just map the permission to a list of participants:

@Configuration
public final class GameConfig {
    // ...
    private Moderator moderator = new Moderator(UUID.randomUUID(), "Mod", "[email protected]");
    private Map<Permission, List<Participant>> teams = Map.of(
            Permission.BLOCK_BREAK,
            List.of(
                    new Participant(UUID.randomUUID(), "Alice", Role.LEADER),
                    new Participant(UUID.randomUUID(), "Bob", Role.MEMBER)
            ),
            Permission.BLOCK_PLACE, 
            List.of(
                    new Participant(UUID.randomUUID(), "Eve", Role.LEADER),
                    new Participant(UUID.randomUUID(), "Dave", Role.MEMBER)
            )
    );
    
    enum Permission {BLOCK_BREAK, BLOCK_PLACE}
}

NOTE:

You cannot write User moderator = new Moderator(...)! As described in the README, serializers are selected by the type of the field, which in this case is User. That means that if you do this, only the fields of the User class will be written but the email will not.

8. Add arena

While we also could model the arena as its own class, we will just add the fields directly to our GameConfig. An arena is defined by its center, a Location, and radius, an int. We can add both of these directly to the configuration because Location is one of the Bukkit types that can be serialized of out the box.

@Configuration
public final class GameConfig {
    // ...
    private int arenaRadius = 10;
    private Location arenaCenter = new Location(Bukkit.getWorld("world"), 0, 0, 0);
}

However, because we don't like how Location is serialized by default, we are going to write a custom serializer for it.

@Configuration
public final class GameConfig {
    // ...
    static final class LocationStringConverter implements Serializer<Location, String> {
        @Override
        public String serialize(Location location) {
            String worldName = location.getWorld().getName();
            int blockX = location.getBlockX();
            int blockZ = location.getBlockZ();
            return worldName + ";" + blockX + ";" + blockZ;
        }

        @Override
        public Location deserialize(String s) {
            String[] split = s.split(";");
            World world = Bukkit.getWorld(split[0]);
            int x = Integer.parseInt(split[1]);
            int z = Integer.parseInt(split[2]);
            return new Location(world, x, 0, z);
        }
    }
}

This serializer needs to be added to a ConfigurationProperties object. We are going to do that in the last step.

9. Add internal fields

We also wanted to add some internal fields which should be ignored when the configuration is serialized. One way to do this is to make the fields final, static, transient, or to annotate them with @Ignore. A second approach is to write a custom FieldFilter and add to the ConfigurationProperties object.

A FieldFilter is simply a predicate that takes a Field and returns true when the field should be serialized and false otherwise.

Let's add two internal fields. Will will add a FieldFilter that filters out fields that start with the word internal in the next section.

@Configuration
public final class GameConfig {
    // ...
    private int internal1 = 20;
    private String internal2 = "30";
}

11. Use GameConfig

With that, our GameConfig is pretty much ready to use. The final step is to configure a YamlConfigurationProperties object and use it to save our config.

Because we want to serialize Bukkit classes in our config, we have to the use ConfigLib.BUKKIT_DEFAULT_PROPERTIES object from the configlib-paper artifact as our starting point. We add a header and footer, change the formatting, and add a field filter. With that we are done.

public final class GamePlugin extends JavaPlugin {
    @Override
    public void onEnable() {
        YamlConfigurationProperties properties = ConfigLib.BUKKIT_DEFAULT_PROPERTIES.toBuilder()
                .header(
                        """
                        The game config for our imaginary game!
                        Valid color codes are: &4, &c, &e
                        """
                )
                .footer("Authors: Exlll")
                .addSerializer(Location.class, new GameConfig.LocationStringConverter())
                .setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
                .setFieldFilter(field -> !field.getName().startsWith("internal"))
                .build();

        Path configFile = new File(getDataFolder(), "config.yml").toPath();

        GameConfig config = Configurations.updateYamlConfiguration(
                configFile,
                GameConfig.class,
                properties
        );

        System.out.println(config.getWinMessage());
        System.out.println(config.getLoseMessage());
    }
}

Full example

import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.Serializer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;

import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@Configuration
public final class GameConfig {
    @Comment("This message is displayed to the winner team")
    private String winMessage = "&4YOU WON!";
    @Comment("This message is displayed to the losers")
    private String loseMessage = "&c...you lost!";
    private ItemStack firstPrize = initFirstPrize();
    private List<ItemStack> consolationPrizes = List.of(
            new ItemStack(Material.STICK, 2),
            new ItemStack(Material.ROTTEN_FLESH, 3),
            new ItemStack(Material.CARROT, 4)
    );
    private LocalDate startDate = LocalDate.of(2022, Month.JANUARY, 1);
    private LocalDate endDate = LocalDate.of(2022, Month.DECEMBER, 31);
    private Set<Material> forbiddenBlocks = Set.of(Material.LAVA, Material.BARRIER);
    private Moderator moderator = new Moderator(UUID.randomUUID(), "Mod", "[email protected]");
    private Map<Permission, List<Participant>> teams = Map.of(
            Permission.BLOCK_BREAK,
            List.of(
                    new Participant(UUID.randomUUID(), "Alice", Role.LEADER),
                    new Participant(UUID.randomUUID(), "Bob", Role.MEMBER)
            ),
            Permission.BLOCK_PLACE,
            List.of(
                    new Participant(UUID.randomUUID(), "Eve", Role.LEADER),
                    new Participant(UUID.randomUUID(), "Dave", Role.MEMBER)
            )
    );
    private int arenaRadius = 10;
    private Location arenaCenter = new Location(Bukkit.getWorld("world"), 0, 0, 0);
    private int internal1 = 20;
    private String internal2 = "30";

    private ItemStack initFirstPrize() {
        ItemStack stack = new ItemStack(Material.DIAMOND_AXE);
        stack.addEnchantment(Enchantment.DURABILITY, 3);
        stack.addEnchantment(Enchantment.DIG_SPEED, 5);
        stack.addEnchantment(Enchantment.MENDING, 1);
        return stack;
    }

    enum Role {MEMBER, LEADER}

    @Configuration
    public static class User {
        private UUID uuid;
        private String name;

        public User(UUID uuid, String name) {
            this.uuid = uuid;
            this.name = name;
        }

        private User() {}
    }

    public static final class Participant extends User {
        private Role teamRole;

        public Participant(UUID uuid, String name, Role teamRole) {
            super(uuid, name);
            this.teamRole = teamRole;
        }

        private Participant() {}
    }

    public static final class Moderator extends User {
        @Comment({"The moderators email", "It must be valid!"})
        private String email;

        public Moderator(UUID uuid, String name, String email) {
            super(uuid, name);
            this.email = email;
        }

        private Moderator() {}
    }

    enum Permission {BLOCK_BREAK, BLOCK_PLACE}

    static final class LocationStringConverter implements Serializer<Location, String> {
        @Override
        public String serialize(Location location) {
            String worldName = location.getWorld().getName();
            int blockX = location.getBlockX();
            int blockZ = location.getBlockZ();
            return worldName + ";" + blockX + ";" + blockZ;
        }

        @Override
        public Location deserialize(String s) {
            String[] split = s.split(";");
            World world = Bukkit.getWorld(split[0]);
            int x = Integer.parseInt(split[1]);
            int z = Integer.parseInt(split[2]);
            return new Location(world, x, 0, z);
        }
    }
    // GETTERS ...
}
import de.exlll.configlib.ConfigLib;
import de.exlll.configlib.Configurations;
import de.exlll.configlib.FieldFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import org.bukkit.Location;
import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.nio.file.Path;

public final class GamePlugin extends JavaPlugin {
    @Override
    public void onEnable() {
        YamlConfigurationProperties properties = ConfigLib.BUKKIT_DEFAULT_PROPERTIES.toBuilder()
                .header(
                        """
                        The game config for our imaginary game!
                        Valid color codes are: &4, &c, &e
                        """
                )
                .footer("Authors: Exlll")
                .addSerializer(Location.class, new GameConfig.LocationStringConverter())
                .setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
                .setFieldFilter(field -> !field.getName().startsWith("internal"))
                .build();

        Path configFile = new File(getDataFolder(), "config.yml").toPath();

        GameConfig config = Configurations.updateYamlConfiguration(
                configFile,
                GameConfig.class,
                properties
        );

        System.out.println(config.getWinMessage());
        System.out.println(config.getLoseMessage());
    }
}
Clone this wiki locally