- Notifications
You must be signed in to change notification settings - Fork 28
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 teamWIN_MESSAGE: '&4YOU WON!'# This message is displayed to the losersLOSE_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: 3CONSOLATION_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: 4START_DATE: 2022-01-01END_DATE: 2022-12-31FORBIDDEN_BLOCKS:
- LAVA
- BARRIERMODERATOR:
UUID: 3fc1e4c3-0d6a-4342-a159-4b0fbd78cda8NAME: Mod# The moderators email# It must be valid!EMAIL: mod@admin.comTEAMS:
BLOCK_PLACE:
- UUID: 5621f3b8-cbab-4571-ba97-a4ff3da59b33NAME: EveTEAM_ROLE: LEADER
- UUID: 5b0c7fc3-2da6-48a1-bc06-1b4daa0f6881NAME: DaveTEAM_ROLE: MEMBERBLOCK_BREAK:
- UUID: a4bc6c3e-8159-431d-abde-0b19697d5505NAME: AliceTEAM_ROLE: LEADER
- UUID: e3a2fcdb-a9be-4396-ad43-32a8339220b3NAME: BobTEAM_ROLE: MEMBERARENA:
ARENA_RADIUS: 10# The world and x and z coordinates of the arena.ARENA_CENTER: world;0;0# Authors: ExlllThe first thing we have to do is to create a class and annotate it with @Configuration.
@ConfigurationpublicfinalclassGameConfig {}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.
@ConfigurationpublicfinalclassGameConfig {
@Comment("This message is displayed to the winner team")
privateStringwinMessage = "&4YOU WON!";
@Comment("This message is displayed to the losers")
privateStringloseMessage = "&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.
@ConfigurationpublicfinalclassGameConfig {
// ...privateItemStackfirstPrize = initFirstPrize();
privateList<ItemStack> consolationPrizes = List.of(
newItemStack(Material.STICK, 2),
newItemStack(Material.ROTTEN_FLESH, 3),
newItemStack(Material.CARROT, 4)
);
privateItemStackinitFirstPrize() {
ItemStackstack = newItemStack(Material.DIAMOND_AXE);
stack.addEnchantment(Enchantment.DURABILITY, 3);
stack.addEnchantment(Enchantment.DIG_SPEED, 5);
stack.addEnchantment(Enchantment.MENDING, 1);
returnstack;
}
}The period in which the game is allowed to be played is given by a start and an end date.
@ConfigurationpublicfinalclassGameConfig {
// ...privateLocalDatestartDate = LocalDate.of(2022, Month.JANUARY, 1);
privateLocalDateendDate = 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.
@ConfigurationpublicfinalclassGameConfig {
// ...privateSet<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.
@ConfigurationpublicfinalclassGameConfig {
// ...enumRole {MEMBER, LEADER}
@ConfigurationpublicstaticclassUser {
privateUUIDuuid;
privateStringname;
publicUser(UUIDuuid, Stringname) {/* initialize */}
privateUser() {}
}
publicstaticfinalclassParticipantextendsUser {
privateRoleteamRole;
publicParticipant(UUIDuuid, Stringname, RoleteamRole) {/* initialize */}
privateParticipant() {}
}
publicstaticfinalclassModeratorextendsUser {
@Comment({"The moderators email", "It must be valid!"})
privateStringemail;
publicModerator(UUIDuuid, Stringname, Stringemail) {/* initialize */}
privateModerator() {}
}
}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:
@ConfigurationpublicfinalclassGameConfig {
// ...privateModeratormoderator = newModerator(UUID.randomUUID(), "Mod", "mod@admin.com");
privateMap<Permission, List<Participant>> teams = Map.of(
Permission.BLOCK_BREAK,
List.of(
newParticipant(UUID.randomUUID(), "Alice", Role.LEADER),
newParticipant(UUID.randomUUID(), "Bob", Role.MEMBER)
),
Permission.BLOCK_PLACE, List.of(
newParticipant(UUID.randomUUID(), "Eve", Role.LEADER),
newParticipant(UUID.randomUUID(), "Dave", Role.MEMBER)
)
);
enumPermission {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.
@ConfigurationpublicfinalclassGameConfig {
// ...privateArenaarena = newArena(10, newLocation(Bukkit.getWorld("world"), 0, 0, 0));
recordArena(
intarenaRadius,
@Comment("The world and x and z coordinates of the arena.")
LocationarenaCenter
) {}
}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.
@ConfigurationpublicfinalclassGameConfig {
// ...staticfinalclassLocationStringSerializerimplementsSerializer<Location, String> {
@OverridepublicStringserialize(Locationlocation) {
StringworldName = location.getWorld().getName();
intblockX = location.getBlockX();
intblockZ = location.getBlockZ();
returnworldName + ";" + blockX + ";" + blockZ;
}
@OverridepublicLocationdeserialize(Strings) {
String[] split = s.split(";");
Worldworld = Bukkit.getWorld(split[0]);
intx = Integer.parseInt(split[1]);
intz = Integer.parseInt(split[2]);
returnnewLocation(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.
@ConfigurationpublicfinalclassGameConfig {
// ...privateintinternal1 = 20;
privateStringinternal2 = "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.
publicfinalclassGamePluginextendsJavaPlugin {
@OverridepublicvoidonEnable() {
YamlConfigurationPropertiesproperties = 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, newGameConfig.LocationStringSerializer())
.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
.setFieldFilter(field -> !field.getName().startsWith("internal"))
.build();
PathconfigFile = newFile(getDataFolder(), "config.yml").toPath();
GameConfigconfig = YamlConfigurations.update(
configFile,
GameConfig.class,
properties
);
System.out.println(config.getWinMessage());
System.out.println(config.getLoseMessage());
System.out.println(config.getArena());
}
}importde.exlll.configlib.Comment;
importde.exlll.configlib.Configuration;
importde.exlll.configlib.Serializer;
importorg.bukkit.Bukkit;
importorg.bukkit.Location;
importorg.bukkit.Material;
importorg.bukkit.World;
importorg.bukkit.enchantments.Enchantment;
importorg.bukkit.inventory.ItemStack;
importjava.time.LocalDate;
importjava.time.Month;
importjava.util.List;
importjava.util.Map;
importjava.util.Set;
importjava.util.UUID;
@ConfigurationpublicfinalclassGameConfig {
@Comment("This message is displayed to the winner team")
privateStringwinMessage = "&4YOU WON!";
@Comment("This message is displayed to the losers")
privateStringloseMessage = "&c...you lost!";
privateItemStackfirstPrize = initFirstPrize();
privateList<ItemStack> consolationPrizes = List.of(
newItemStack(Material.STICK, 2),
newItemStack(Material.ROTTEN_FLESH, 3),
newItemStack(Material.CARROT, 4)
);
privateLocalDatestartDate = LocalDate.of(2022, Month.JANUARY, 1);
privateLocalDateendDate = LocalDate.of(2022, Month.DECEMBER, 31);
privateSet<Material> forbiddenBlocks = Set.of(Material.LAVA, Material.BARRIER);
privateModeratormoderator = newModerator(UUID.randomUUID(), "Mod", "mod@admin.com");
privateMap<Permission, List<Participant>> teams = Map.of(
Permission.BLOCK_BREAK,
List.of(
newParticipant(UUID.randomUUID(), "Alice", Role.LEADER),
newParticipant(UUID.randomUUID(), "Bob", Role.MEMBER)
),
Permission.BLOCK_PLACE,
List.of(
newParticipant(UUID.randomUUID(), "Eve", Role.LEADER),
newParticipant(UUID.randomUUID(), "Dave", Role.MEMBER)
)
);
privateArenaarena = newArena(10, newLocation(Bukkit.getWorld("world"), 0, 0, 0));
privateintinternal1 = 20;
privateStringinternal2 = "30";
privateItemStackinitFirstPrize() {
ItemStackstack = newItemStack(Material.DIAMOND_AXE);
stack.addEnchantment(Enchantment.DURABILITY, 3);
stack.addEnchantment(Enchantment.DIG_SPEED, 5);
stack.addEnchantment(Enchantment.MENDING, 1);
returnstack;
}
enumRole {MEMBER, LEADER}
@ConfigurationpublicstaticclassUser {
privateUUIDuuid;
privateStringname;
publicUser(UUIDuuid, Stringname) {
this.uuid = uuid;
this.name = name;
}
privateUser() {}
}
publicstaticfinalclassParticipantextendsUser {
privateRoleteamRole;
publicParticipant(UUIDuuid, Stringname, RoleteamRole) {
super(uuid, name);
this.teamRole = teamRole;
}
privateParticipant() {}
}
publicstaticfinalclassModeratorextendsUser {
@Comment({"The moderators email", "It must be valid!"})
privateStringemail;
publicModerator(UUIDuuid, Stringname, Stringemail) {
super(uuid, name);
this.email = email;
}
privateModerator() {}
}
enumPermission {BLOCK_BREAK, BLOCK_PLACE}
staticfinalclassLocationStringSerializerimplementsSerializer<Location, String> {
@OverridepublicStringserialize(Locationlocation) {
StringworldName = location.getWorld().getName();
intblockX = location.getBlockX();
intblockZ = location.getBlockZ();
returnworldName + ";" + blockX + ";" + blockZ;
}
@OverridepublicLocationdeserialize(Strings) {
String[] split = s.split(";");
Worldworld = Bukkit.getWorld(split[0]);
intx = Integer.parseInt(split[1]);
intz = Integer.parseInt(split[2]);
returnnewLocation(world, x, 0, z);
}
}
recordArena(
intarenaRadius,
@Comment("The world and x and z coordinates of the arena.")
LocationarenaCenter
) {}
// GETTERS ...
}importde.exlll.configlib.ConfigLib;
importde.exlll.configlib.NameFormatters;
importde.exlll.configlib.YamlConfigurationProperties;
importde.exlll.configlib.YamlConfigurations;
importorg.bukkit.Location;
importorg.bukkit.plugin.java.JavaPlugin;
importjava.io.File;
importjava.nio.file.Path;
publicfinalclassGamePluginextendsJavaPlugin {
@OverridepublicvoidonEnable() {
YamlConfigurationPropertiesproperties = 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, newGameConfig.LocationStringSerializer())
.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
.setFieldFilter(field -> !field.getName().startsWith("internal"))
.build();
PathconfigFile = newFile(getDataFolder(), "config.yml").toPath();
GameConfigconfig = YamlConfigurations.update(
configFile,
GameConfig.class,
properties
);
System.out.println(config.getWinMessage());
System.out.println(config.getLoseMessage());
System.out.println(config.getArena());
}
}