-
Notifications
You must be signed in to change notification settings - Fork 0
Persistence and you
AsynCore is powered by deserialization and serialization of data into binary. Blocks are serialized and deserialized from the disk when a chunk/world is loaded/unloaded, items are deserialized, invoked, and serialized for events, and BlockTrackers are serialized and deserialized alongside the chunk.
The serialization at the heart of AsynCore is YAJSLib, which is a serialization library I wrote designed to be future safe. a "Persistent" is just another name for a deserializer/serializer, with a few fancy pieces like Persistent#blank
this is where all persistent should be registered, so anything trying to serialize/deserialize your object can be noticed.
here, you can serialize an object if you have it's version id, this is really only useful if you want to serialize something with a very specific version, say, maybe serializing something to the disk should be different from sending it to the client. But for AsynCore, this is basically useless.
This is the method to look for a serializer for a given class, it returns null if it can't find one, and you can tell it to look for superclasses of the class for serializers. For example, when serializing a HashMap, you can register a persistent for Map, and just let the persistent registry find it. You likely won't need to use this method, as serialization/deserialization is handled for you most of the time, or is abstracted in classes like PersistentOutput
This is the really only relevant method you need, so we'll go in-depth here.
the version hash is just a unique id for every persistent, this id is used to make sure that when you deserialize a class, you deserialize it with the same deserializer that was used to serialize it in the first place, otherwise, if you changed your code, for example, added a field, you would reach the end of the stream because you serialized your data without that integer before!
void register(
Class type, // this is the class you are registering a persistent for
Persistent // a persistent is the serializer for the class
)
You can create your own persistent class for each object you want to serialize, but if you are writing the class yourself, you have a few other options to reduce the clutter.
the empty persistent is a pre-made persistent for classes that don't have any data to serialize, it's simply a placeholder.
persistentRegistry.register(MyClass.class, new EmptyPersistent(<random id>, MyClass::new))
this persistent is for when you want to serialize an object, but don't want to make a seperate class, and just shove the serialization methods at the bottom or something, without cluttering your code too much.
let's take a look at some examples
public class MyClass {
private int myVar; // we have a field
public MyClass(int myVar) { // and a non-default constructor
this.myVar = myVar;
}
}
we want to serialize our class, so let's add a serialization method
public class MyClass {
private int myVar; // we have a field
public MyClass(int myVar) { // and a non-default constructor
this.myVar = myVar;
}
@Writer(389129129L) // a random id, just like in our empty persistent, or any other one
public void write( // the name of the method is irrelavent
PersistentOutput out // all annotated persistent methods **must** have only one parameter, (PersistentOutput/PersistentInput)
) throws IOException { // unchecked exceptions because we're lazy
out.writeInt(myVar); // serialize myVar to the disk
}
}
now we have to tell the annotated persistent how to deserialize our class, so let's add a deserializer
public class MyClass {
private int myVar; // we have a field
public MyClass(int myVar) { // and a non-default constructor
this.myVar = myVar;
}
@Writer(389129129L)
public void write(PersistentOutput out) throws IOException {
out.writeInt(myVar);
}
@Reader(389129129L) // same id as the writer
public void read(PersistentInput in) throws IOException {
myVar = in.readInt(); // and we read the integer
}
}
wonderful, now let's register it in the registry
persistentRegistry.register(MyClass.class, new AnnotatedPersistent<>(
() -> new MyClass(0), // this is the default "constructor" the persistent will use when serializing your object, it will create a new MyClass via that constructor, and use reflection to call the read method on it
MyClass.class,
389129129L // this has to be the same id as the reader and writer
)
);
This could be its own section, but I think this is a good place to put it.
Now, let's say our wonderful program has been chugging along, and we decided MyClass
is in desperate need of an upgrade, and we want to add a new field! But all our data has been serialized with a single integer, if we just go and willynilly, add a readX to our deserialization method, we'll run out of data!
Here's a visual example to show what I'm talking about
// In our data, we wrote 4 bytes, the 4 bytes in the integer we serialized.
// |---myVar---|
data [0, 0, 0, 0]
// but now we want to add a new variable, lets say, a float
// so when we try to read our data...
data[0, 0, 0, 0]
readInt (myVar)
data[] // now our data is empty
readFloat (newVar) // exception, our old data is incompatible with our new one, because our old data only has 4 bytes in it!
So this is where YAJSLib shines, the version hash allows you to have 2 or more serialization methods for the same class.
public class MyClass {
private int myVar;
private float newVar; // new variable
@Deprecated // old constructor should only be used for deserializing old data
public MyClass(int myVar) { // and a non-default constructor
this.myVar = myVar;
}
public MyClass(int myVar, float newVar) {...} // new constructor
@Writer(489092091209L) // a new id
public void write(PersistentOutput out) throws IOException {
out.writeFloat(newVar);
out.writeInt(myVar); // we can even switch up the order
}
@Reader(489092091209L)
public void read(PersistentInput input) throws IOException {
newVar = input.readFloat();
myVar = input.readInt();
}
// we still keep the old serialization method, because we still need to know how to deserialize old data
@Writer(389129129L)
public void writeOld(PersistentOutput out) throws IOException {
out.writeInt(myVar); // the serializer should remain untouched
}
@Reader(389129129L)
public void readOld(PersistentInput in) throws IOException {
myVar = in.readInt();
// sometimes, we want our variables to have a different default value, other than 3, we can either
// make the constructor do it, or do it in here, I put it in here just to show you it
this.newVar = 3f;
}
}
and we can now register our new persistent
persistentRegistry.register(MyClass.class, new AnnotatedPersistent<>(() -> new MyClass(0), MyClass.class, 389129129L)); // we still have to have our old persistent
// order matters, our new persistent **must** be registered *after* the old one, so that it is used instead
persistentRegistry.register(MyClass.class, new AnnotatedPersistent<>(() -> new MyClass(0, 3f), MyClass.class, 489092091209L));
And that's about it, there are a few other things in YAJSLib, but AsynCore handles most of them for you