Skip to content

Tutorial

Exlll edited this page Jul 24, 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: 3fc1e4c3-0d6a-4342-a159-4b0fbd78cda8
  NAME: Mod
  # The moderators email
  # It must be valid!
  EMAIL: [email protected]
TEAMS:
  BLOCK_PLACE:
    - UUID: 5621f3b8-cbab-4571-ba97-a4ff3da59b33
      NAME: Eve
      TEAM_ROLE: LEADER
    - UUID: 5b0c7fc3-2da6-48a1-bc06-1b4daa0f6881
      NAME: Dave
      TEAM_ROLE: MEMBER
  BLOCK_BREAK:
    - UUID: a4bc6c3e-8159-431d-abde-0b19697d5505
      NAME: Alice
      TEAM_ROLE: LEADER
    - UUID: e3a2fcdb-a9be-4396-ad43-32a8339220b3
      NAME: Bob
      TEAM_ROLE: MEMBER
ARENA:
  ARENA_RADIUS: 10
  # The world and x and z coordinates of the arena.
  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. Material is an enum type, and you can use any Java enum type with this library.

@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

Since this library supports Java records, we can easily model our arena as one. The arena is defined by its center, a Location, and radius, an int. We can add both of these directly as record components because Location is one of the Bukkit types that can be serialized of out the box.

@Configuration
public final class GameConfig {
    // ...
    private Arena arena = new Arena(10, new Location(Bukkit.getWorld("world"), 0, 0, 0));

    record Arena(
            int arenaRadius,
            @Comment("The world and x and z coordinates of the arena.")
            Location arenaCenter
    ) {}
}

Note that records don't need to be annotated with @Configuration. Also note that record components can be commented as well!

9. Add custom Location serializer

Because we don't like how Location is serialized by default, we are going to write a custom serializer for it. We can do so by implementing the Serializer interface.

To identify the center of our arena, we just need the world as well as the x- and z-coordinates.

@Configuration
public final class GameConfig {
    // ...
    static final class LocationStringSerializer 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);
        }
    }
}

Now that we defined a custom serializer, it still needs to be added to a ConfigurationProperties object. We are going to do that in the last step.

10. 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.LocationStringSerializer())
                .setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
                .setFieldFilter(field -> !field.getName().startsWith("internal"))
                .build();

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

        GameConfig config = YamlConfigurations.update(
                configFile,
                GameConfig.class,
                properties
        );

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

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 Arena arena = new Arena(10, 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 LocationStringSerializer 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);
        }
    }

    record Arena(
            int arenaRadius,
            @Comment("The world and x and z coordinates of the arena.")
            Location arenaCenter
    ) {}
    
    // GETTERS ...
}
import de.exlll.configlib.ConfigLib;
import de.exlll.configlib.NameFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurations;
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.LocationStringSerializer())
                .setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
                .setFieldFilter(field -> !field.getName().startsWith("internal"))
                .build();

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

        GameConfig config = YamlConfigurations.update(
                configFile,
                GameConfig.class,
                properties
        );

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