Skip to content

Persistence and you

Devan-Kerman edited this page Mar 1, 2020 · 3 revisions

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

The persistent registry

this is where all persistent should be registered, so anything trying to serialize/deserialize your object can be noticed.

PersistentRegistry#fromId

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.

PersistentRegistry#forClass

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

PersistentRegistry#register

This is the really only relevant method you need, so we'll go in-depth here.

Registering Persistents

version hash

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
)

Pre-Made Persistent

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.

EmptyPersistent

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))

AnnotatedPersistent

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
   )
);

Versioning

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