-
Notifications
You must be signed in to change notification settings - Fork 2
Config files
One of the features of this library is a dynamic configuration file utility which allows the direct saving and loading of data classes.
The way you create a data class with this utility is by implementing the ISerializable interface. An example of this is below:
public class MyDataClass implements ISerializable {
@Serialized
public String name = "Bob";
@Override
public void onDeserialize() {
// Do stuff when it's finished deserializing
// Allows you to act on data as soon as it's loaded
}
@Override
public void onPreSerialize() {
// Do stuff right before it serializes
// Allows you to change data before it saves
}
}
This is a very basic example which shows how to implement the class, as well as how to mark a variable for serialization / deserialization. Let's analyze:
-
MyDataClass
implementsISerializable
- The variable we want to save and load has the
@Serialized
annotation present - We have the default methods for modifying data as soon as it's available, as well as changing it before it's saved
What kind of variable types can be serialized?
- All data types that are available in Bukkit's YAML system
- String
- int
- double
- float
- etc
- Another class that implements
ISerializable
(more on that later) - Any variable type that has been made compatible via an
ISerializationHandler
(more on that later)
To create a nested data class it's actually quite easy. Let's create a new data class: public class PlayerData implements ISerializable {
@Serialized
public int score = 0;
@Serialized
public double health = 20.0;
@Override
public void onDeserialize() {
}
@Override
public void onPreSerialize() {
}
}
Now if we modify the original data class we can nest the new data class inside: public class MyDataClass implements ISerializable {
@Serialized
public String name = "Bob";
@Serialized
public PlayerData data;
@Override
public void onDeserialize() {
// Do stuff when it's finished deserializing
// Allows you to act on data as soon as it's loaded
}
@Override
public void onPreSerialize() {
// Do stuff right before it serializes
// Allows you to change data before it saves
}
}
The serialize
and deserialize
methods will call themselves recursively to deal with the nested ISerializable
types.
There is a handy annotation available to tell the ConfigManager what to do when it can't load a variable (for whatever reason): OnFail
. To use this annotation, let's go back to our data classes. We want to make it so that if the PlayerData
class fails to load that we're just going to stop the whole process and return null
. To do that, all we have to add is one line:
public class MyDataClass implements ISerializable {
@Serialized
public String name = "Bob";
@Serialized
@OnFail(FailResponse.CANCEL_LOAD)
public PlayerData data;
@Override
public void onDeserialize() {
// Do stuff when it's finished deserializing
// Allows you to act on data as soon as it's loaded
}
@Override
public void onPreSerialize() {
// Do stuff right before it serializes
// Allows you to change data before it saves
}
}
Note that right above the PlayerData
variable data
we added an OnFail
annotation, telling ConfigManager
to cancel the loading of the data if this fails to be loaded. All options are shown here. By default the ConfigManager
won't even give an error if a variable fails to load because there can be very normal reasons for a variable not to load, and usually you don't want an error. But if you do, you can add @OnFail(FailResponse.CONSOLE_ERR)
to your variable.
You might be wondering how you can add support for ConfigManager
to be able to handle pre-exisiting classes. Enter: ISerializationHandler
While a bit daunting, this class is rather easy to use. Let's take an example here, this is the VectorSerializer
class which comes with PerceiveCore
by default:
public class VectorSerializer implements ISerializationHandler<Vector> {
@Override
public Map<String, Object> serialize(Vector obj) {
Map<String, Object> map = new HashMap<>();
map.put("x", obj.getX());
map.put("y", obj.getY());
map.put("z", obj.getZ());
return map;
}
@Override
public Vector deserialize(Map<String, Object> map) {
double x = (double) map.get("x");
double y = (double) map.get("y");
double z = (double) map.get("z");
return new Vector(x, y, z);
}
}
What's this doing?
- We implement
ISerializationHandler
and give it a type parameter ofVector
- We override the interface's methods
-
serialize
: We create a map and put our vector's x, y, and z values into it, then return the map -
deserialize
: We get our values from the map, casting them as we go. We then return a new vector based on those values
-
This is the whole Vector
serializer class, but there is one more step you'll have to do: you must register the handler. This can be done like this:
ConfigManager.registerSerializationHandler(Vector.class, new VectorSerializer());
Obviously replacing Vector.class
with the proper class you want to add support for and VectorSerializer
with your ISerializationHandler
implementation.
A couple notes:
ConfigManager
gives instances ofISerializable
priority over classes that have serialization handlers, so creating a serialization handler for yourISerializable
class is pointless and a waste of time.ClassCastException
s have special errors in the console mentioning that an invalid type was encountered, saying what it was and what it should be. Might be helpful.
Before a data class can be saved or loaded you're going to need a ConfigManager
instance. To create this it's as simple as ConfigManager cm = new ConfigManager(pluginInstance);
, of course assuming that pluginInstance
is an instance of JavaPlugin.
Now, you have a few options for saving and loading data. Let's start with saving data:
// Assumptions: cm is a ConfigManager instance, config is a YamlConfiguration instance, and section is a configuration section in config
MyDataClass data = new MyDataClass();
cm.save(data, "someConfig.yml"); // Saves the data to a file
cm.save(data, config); // Saves the data to a config object
cm.save(data, config, "someConfig.yml"); // Saves the data to a config and then saves the config to a file
cm.save(data, section); // Saves the data to a ConfigurationSection
These are the 4 available methods for saving your data classes. As for loading, it's very much the same:
// Assumptions: cm is a ConfigManager instance, config is a YamlConfiguration instance, and section is a configuration section in config
MyDataClass data;
data = cm.load(MyDataClass.class, "someConfig.yml"); // Loads the data from a file
data = cm.load(MyDataClass.class, config); // Loads the data from a config object
data = cm.load(MyDataClass.class, section); // Loads the data from a ConfigurationSection
There isn't much to say about either the loading or the saving, they're both pretty self explanatory. For the loading you just provide it a class so it knows what it's "template" is. Also there is a type parameter for cm.load
but it isn't necessary as I'm pretty sure every IDE can figure out the type parameter from context.
Note by Rayzr: I write this late at night when I was exhausted and my hand is broken and I'm trying to do this typing on an iPod so admittedly this last section isn't the greatest. I'll fix it later.