-
Notifications
You must be signed in to change notification settings - Fork 17
Tutorial
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.
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
The first thing we have to do is to create a class and annotate it with @Configuration
.
@Configuration
public final class GameConfig {}
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!";
}
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;
}
}
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);
}
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);
}
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() {}
}
}
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.
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.
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";
}
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());
}
}
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());
}
}