Codecs

Revision as of 02:31, 27 November 2020 by ChampionAsh5357 (talk | contribs) (Formatting Fixes)

Codecs are an abstraction layer around DynamicOps which allow objects to be serialized and deserialized in different contexts such as JSON or NBT. It creates an easy way to read and interpret objects without the need of manual labor.

Codec Serialization and Deserialization

Codec Serialization and Deserialization is handled through two main methods: Codec#encodeStart and Codec#parse respectively. Each of these methods returns a DataResult which holds the encoded object type or the decoded object class. An Optional of the resulting output can be grabbed via DataResult#result. If a custom message should be specified along with the error message, that can be specified using DataResult#resultOrPartial. Alternatively,DataResult#getOrThrowcan be used which grabs the instance directly instead of an optional.

Creating a Codec for a Class

Let's assume there is the following class structure that a codec should be created for:

public class ExampleCodecClass {

    private final int field_1;
    private final List<BlockPos> field_2;
    private final Item field_3;

    public ExampleCodecClass(int field_1, List<BlockPos> field_2, Item field_3) {...}
}

For each basic object instance, a codec can be constructed using RecordCodecBuilder::create. This takes in a function that converts an Instance of an object, which is a group of codecs for each serializable field, to an App, which is an unary type constructor for allowing algorithms to be generalized using generics.

public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> {
    return ...;
});

To add a list of valid codecs, which is denoted by Pwhere n is the number of fields in the instance,Instance#groupis used which takes in codecs converted into an App of some kind. This example will examine three such scenarios.

First, there is a primitive integer field. All primitive codecs are declared within the Codec class along with a few extra primitive streams (in this case we will use Codec#INT). To convert this codec into a valid key-pair form, the parameter name needs to be specified. This can be done using Codec#fieldOf which will take in a string which represents the key of this field. This will convert the codec into a MapCodec which as the name states creates a key-value pair to deserialize the instance from. From there, how to serialize the instance from the class object must also be specified. This can be done using MapCodec#forGetter which takes in a function that converts the class object to the type instance, hence the getter method name. This creates a RecordCodecBuilder which will be the final state of the codec as it is an instance of App.

public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> {
    return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1),
      ...)...;
});

Next, there is a list of BlockPos which has a premade codec within itself. However, a Codec needs to be converted into a Codec. Luckily, there are a few helpers within the codec class that allows some of these conversions to be trivial. In this case, Codec#listOf will convert a codec of some generic into a list of that generic. The process for attaching the codec is exactly the same.

public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> {
    return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1),
      BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2),
      ...)...;
});

A few other notable mentions that might be used within a codec:

Method Description
intRange Creates a integer codec with a valid inclusive range.
floatRange Creates a float codec with a valid inclusive range.
doubleRange Creates a double codec with a valid inclusive range.
pair Create a pair using two codecs.
either Creates an either (an object with some fallback object) using two codecs.
unboundedMap Creates a map using two codecs.

Last, there is a Item. This is optional and should default to Items#AIR when not defined. Here, there will be two techniques used to grab the associated codec. By default, a Registry is an instance of a codec. Therefore, the codec can be grabbed by specifying the registry instance (e.g. Registry#ITEM). What if there is no vanilla registry instance however? Then, another codec method can be used: Codec#xmap. This allows the associated object to be mapped to another object. A function specifies mapping the associated object to the new object for decoding and vice versa for encoding. For example, the ResourceLocation codec can be mapped to an Item through the forge registry instance.

To define a field as optional, Codec#optionalFieldOf should be used. One instance holds the value as an Optional while the other allows a defined default value.

public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> {
    return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1),
      BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2),
      ResourceLocation.CODEC.xmap(loc -> ForgeRegistries.ITEMS.getValue(loc), item -> item.getRegistryName()).optionalFieldOf("field_3", Items.AIR).forGetter(obj -> obj.field_3))...;
});

Now, there is a product P3 as there is three parameters. To convert this into an App, the method P#apply should be called. This takes in an Applicative which our builder is an instance of and a function that returns the class object given the specified wrapped arguments. Creating a new constructor is one way of creating the outputted codec for the class object.

public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> {
    return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1),
      BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2),
      ResourceLocation.CODEC.xmap(loc -> ForgeRegistries.ITEMS.getValue(loc), item -> item.getRegistryName()).optionalFieldOf("field_3", Items.AIR).forGetter(obj -> obj.field_3))
      .apply(builder, ExampleCodecClass::new);
});

Limitations

Codecs are required to abide by a String-Object key-pair. Any codec that does not have a String key will throw an error during encoding and decoding.

A group can have at most 16 inner codecs normally. This limitation is specified by the number of product generic classes available.