Skip to main content

serialization

The main purpose of WrapperLib is to avoid manually writing code to convert information from java objects to json, nbt, or byte buffer and back again. You end up with a lot of boiler plate code for writing out the data as a list of primitives using different, but very similar, methods depending on the target data type. I find this situation really annoying because you end up with a massive amount of code that doesn't actually relate to the unique features of your mod. It's just boiler plate noise for moving bytes around.

WrapperLib approaches the task differently. First, your data object is converted into a JSON string by Gson (Google's library). Then, for sending over the network, we just call FriendlyByteBuf#writeUft on that string. For saving to NBT, we just call CompoundTag#putString on that string. To get the object back we just read out the one string and use Gson to convert it back into an object of a given class. Helpfully the Gson library that does the heavy lifting is included with Minecraft so you don't have to bundle it and make your jar bigger.

Gson is quite a popular library so you can read up on its docs to learn more about what types of objects it can deal with. Primitives, strings, enums, nested objects (where all fields are supported), collections, arrays, and maps are all fair game. For example, the data structure below would work fine. Even above 1.16, the version of Gson shipped with Minecraft (Gson 2.8.9) does NOT support records. Your class cannot have final fields.

enum C { D, E, F; }

class B {
int d;
C e;
Double f;
}

class A {
String a;
Map<String, List<B>> b;
B c;
}

Included Type Adapters‚Äč

You can register special serialization code for types that require more interesting logic than just looping through the object's fields and trying to serialize them individually. The following vanilla types are natively supported.

  • CompoundTag
  • ItemStack
  • Item
  • Block
  • BlockEntityType
  • EntityType
  • Enchantment
  • MobEffect
  • Potion
  • Fluid

Map Woes‚Äč

When converting maps to json, the keys always use their toString method instead of their registered type adapter. So their type adapter must be able to cope with turing that string back into the correct object. You can fix this by calling GsonBuilder#enableComplexMapKeySerialization. Instead of being represented as a map of strings to objects they would be represented by an array of object pairs where the first is a key and the second is the value. I don't do this by default because I think it looks uglier since most of the time the keys I end up using will serialize reasonably.

UUID and registry objects (using RegistryObjectTypeAdapterFactory) will work as map keys already.

Adding Type Adapters‚Äč

  • Use RegistryObjectTypeAdapterFactory.add(Class<T>, Registry<T>) to add type adapter factory that will represent a registry object as a resource location string.
  • Use JsonHelper#addTypeAdapter
  • Use JsonHelper#addTypeAdapterFactory

Be aware that a TypeAdapter will only run for exactly the class it's registered for. You must use a TypeAdapterFactory to run for arbitrary subclasses as well.

Doing any of these will effect the serialization used by network, data, and config. This is perfectly fine to do if you are shadowing WrapperLib. If you are not shadowing, you run the risk of overlapping with other mods trying to do the same thing if you register an adapter for a class not added by your mod.

It becomes a problem if the two mods type adapters represent the data in different ways. Imagine Mod A saves some data, with a custom adapter, in Format A. Then the player installs Mod B and restarts the world. Mod B overrides Mod A's adapter and so the saved data is read expecting Format B which throws an exception so the default values are used and the previously saved data is lost. It would work as long as the player kept their mod list the same, never adding or removing mods from a world, that register overlapping type adapters. It would also work for networking. It only gets risky when you save to disk, then change mods, then try to load.

Both DataWrapper and ConfigWrapper provide a withGson method that allows you to specify a Gson object that will be used only by that individual wrapper instance (including for syncing it over the network). You can get a GsonBuilder with my included type adapters by calling JsonHelper.get().newBuilder().

Scary Objects‚Äč

You must be very careful when writing type adapters for vanilla game objects. They often rely on objects being exactly the expected instance and a newly created object with the same field values is not sufficiently equal. Extra care is required to retrieve the correct object upon loading data (or receiving a packet) instead of creating a new one.

Registry Objects‚Äč

For example, registry objects rely on things being exactly the same object as was registered originally. If you take the apple item and recreate a new item object with all the same field values, that won't be an apple. So sending just field values over the network won't work. Instead you must get the registry name of the object, send that over the network, and then the receiving side can retrieve the correct instance registered with that name instead of recreating a new object.

Since these registry names are stable across world reloads they can be safely stored in data/config and still point to the same object (unless the mod that adds it has been removed). My RegistryObjectTypeAdapterFactory class can handle this for you.

Entities‚Äč

Entities have a similar problem. They have separate instances on the server and client side and recreating a new object with the same field values does not make it the same entity that's actually in the world.

You must get the entity id, send that over the network, and retrieve the correct object from the world. This id system is what vanilla uses to keep entities in sync across the client and the server. However, these network ids are not consistent across world loads. They are just based on the order that the entities are added to the world.

Entities also have a uuid that is consistent throughout the life of the entity. However, entities cannot be retrieved by uuid from a ClientLevel, only a ServerLevel (unless you use an access widener on Level#getEntities but even then there's no guarantee the entity will actually be loaded an individual client even if you know it exists on the server).

So using their id is suitable for networking and their uuid is suitable for server side only saved data. ClientLevel does allow retrieving players by uuid but again you run the risk of that player not being loaded on that client. I think it will often be easier to just include the (id or uuid) in your data structure and deal with retrieving the object manually on a case by case basis than dealing with writing situational type adapters.

Same story for Block Entities, you have to send the block position and retrieve it from the world again. Could write a type adapter for that tho if you feel inspired.