-
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: 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
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. 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);
}
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.
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!
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.
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.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());
}
}
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());
}
}