Jump to content

Recommended Posts

Posted

Hello,

I recently started implementation of Codec based serialization for my data objects and ran into some minor issues. I have some external utility class holding few parameters which is used by one data object (single parameter). The data object codecs are loaded via registry lookup and this extra parameter is mapped by extra key, which I would like to remove (even though I think that won't be possible as it could possibly cause issues when multiple objects are inlined, but in my case it's just one which would work). I have checked several documentations about codecs, but I have not found anything useful (is there any documentation by Mojang? I found only the DFU repository, which is for version 1.x, while MC is already on 5.x).

I'll share code snippets which should explain the situation better than I do..

 

So I have some common class like this for example

public class MyObj {

    public static final Codec<MyObj> CODEC = RecordCodecBuilder.create(i -> i.group(
        Codec.INT.fieldOf("i").forGetter(MyObj::i),
        Codec.INT.fieldOf("j").forGetter(MyObj::j)
    ).apply(i, MyObj::new));
}

which I want to be loaded from format like this

{
    "type": "namespace:id",
    "i": 1,
    "j": 2
}

The actual codec inside the following class is mapped via Codec#dispatch method. My current implementation has it like this:

public class DataObj {

    public static final Codec<DataObj> CODEC = MyObj.CODEC.fieldOf("key").xmap(DataObj::new, t -> t.myObj).codec();
    private final MyObj myObj;

    private DataObj(MyObj myObj) {
        this.myObj = myObj;
    }
}

which expects to be in another Map object like this

{
    "type": "namespace:id",
    "key": {
        "i": 1,
        "j": 2
    }
}

Is there any way to inline it without the key parameter?

Posted

I don't really follow what you are trying to do from that description. 🙂 

But if you want to change the codec based on a "type" field, look at Codec.partialDispatch() or one of its simplified helpers that call it.

The basic idea is you tell it the name of type field, and then some functions that tell it how to map

type -> Codec<value> and value -> type

 

Here's a "simple" example I knocked up. I obviously don't recommend using the class name in a real world example as the type discriminator. 

You can use any "primitive" type as the typeCodec, e.g. a ResourceLocation

public class TypedCodecRegistry<TYPE, VALUE> {

    // The codec for the type field
    private final Codec<TYPE> typeCodec;
    // The mapping of value -> type
    private final Function<VALUE, TYPE> typeFunction;
    // type -> real codec
    private final Map<TYPE, Codec<? extends VALUE>> CODECS = new HashMap<>();

    public TypedCodecRegistry(Codec<TYPE> typeCodec, Function<VALUE, TYPE> typeFunction) {
        this.typeCodec = typeCodec;
        this.typeFunction = typeFunction;
    }

    public Codec<VALUE> codec() {
        return this.typeCodec.partialDispatch(
                "type",  // The name of the type field
                // given a value what is its type?
                value -> DataResult.success(this.typeFunction.apply(value)),
                // look up the codec for a type or its an error
                type -> Optional.ofNullable(this.CODECS.get(type)).map(DataResult::success).orElse(DataResult.error("Unknown type " + type)));
    }

    public void register(TYPE type, Codec<? extends VALUE> codec) {
        this.CODECS.put(type, codec);
    }

    public static void main(String[] args) {
        // type is the class's simple name
        var registry = new TypedCodecRegistry<>(Codec.STRING, value -> value.getClass().getSimpleName());
        var codec = registry.codec();

        registry.register(Integer.class.getSimpleName(), Codec.INT);
        registry.register(String.class.getSimpleName(), Codec.STRING);
        registry.register(ResourceLocation.class.getSimpleName(), ResourceLocation.CODEC);
        System.out.println(encode(codec, "hello world"));
        System.out.println(encode(codec, 42));
        System.out.println(encode(codec, new ResourceLocation("minecraft:dirt")));
    }

    public static String encode(Codec<Object> codec, Object value) {
        return codec.encodeStart(JsonOps.INSTANCE, value).getOrThrow(false, System.err::println).toString();
    }
}

It outputs

Quote

{"type":"String","value":"hello world"}

{"type":"Integer","value":42}

{"type":"ResourceLocation","value":"minecraft:dirt"}

 

Boilerplate:

If you don't post your logs/debug.log we can't help you. For curseforge you need to enable the forge debug.log in its minecraft settings. You should also post your crash report if you have one.

If there is no error in the log file and you don't have a crash report then post the launcher_log.txt from the minecraft folder. Again for curseforge this will be in your curseforge/minecraft/Install

Large files should be posted to a file sharing site like https://gist.github.com  You should also read the support forum sticky post.

Posted (edited)

Not exactly that, I most likely worded that poorly.

I have already implemented different codec mappings using the #dispatch functions, this question is more about codec creation for one value.

I'll try to explain my current code in greater detail.

 

I currently have one registry of objects which are called RewardTransformerType and it's class looks like this

public final class RewardTransformerType<V, T extends RewardTransformer<V>> implements IdentifierHolder, Predicate<Class<?>> {

    public static final Codec<RewardTransformer<?>> CODEC = QuestingRegistries.REWARD_TRANSFORMERS.dispatch("type", RewardTransformer::getType, type -> type.codec);
    private final ResourceLocation identifier;
    private final Codec<T> codec;
    private final Class<V> type;

    public RewardTransformerType(ResourceLocation identifier, Codec<T> codec, Class<V> type) {
        this.identifier = identifier;
        this.codec = codec;
        this.type = type;
    }

    @Override
    public ResourceLocation getIdentifier() {
        return identifier;
    }

    @Override
    public boolean test(Class<?> aClass) {
        return aClass.equals(this.type);
    }
}

And one type implementation which looks like this

public class CountByAttributeTransformer implements RewardTransformer<Integer> {

    public static final Codec<CountByAttributeTransformer> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            OutputModifier.CODEC.fieldOf("modifier").forGetter(t -> t.modifier)
    ).apply(instance, CountByAttributeTransformer::new));
    private final OutputModifier modifier;

    public CountByAttributeTransformer(OutputModifier modifier) {
        this.modifier = modifier;
    }

    @Override
    public Integer adjust(Integer originalValue, Player player, Quest quest) {
        return PlayerData.get(player).map(data -> {
            IAttributeProvider provider = data.getAttributes();
            return (int) Math.round(this.modifier.getModifiedValue(provider, originalValue));
        }).orElse(originalValue);
    }

    @Override
    public RewardTransformerType<?, ?> getType() {
        return QuestRegistry.COUNT_BY_ATTRIBUTE_TRANSFORMER;
    }
}

As you can see, currently the Codec for CountByAttributeTransformer class uses codec from OutputModifier class, which looks like this

public static final Codec<OutputModifier> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            ResourceLocation.CODEC.flatXmap(location -> {
                IAttributeId id = Attribs.find(location);
                return id == null ? DataResult.error("Unknown attribute " + location) : DataResult.success(id);
            }, attributeId -> attributeId == null ? DataResult.error("Attribute is null") : DataResult.success(attributeId.getId()))
                    .fieldOf("attribute").forGetter(t -> t.attributeId),
            ResourceLocation.CODEC.flatXmap(location -> {
                IModifierOp op = AttributeOps.find(location);
                return op == null ? DataResult.error("Unknown operation " + location) : DataResult.success(op);
            }, operation -> operation == null ? DataResult.error("Operation is null") : DataResult.success(operation.getId()))
                    .fieldOf("operation").forGetter(t -> t.operator)
    ).apply(instance, OutputModifier::new));

 

My problem with this is that when I want to serialize/deserialize it, result looks like this

{
    "type": "namespace:id",
    "modifier": {
        "attribute": "namespace:id",
        "operation": "namespace:id"
    }
}

and I would like to get this result instead

{
    "type": "namespace:id",
    "attribute": "namespace:id",
    "operation": "namespace:id"
}

 

So my question is how to adjust the codec in CountByAttribute class so I can get the wanted result (if it is even possible). I have also tried using this codec, but that was resulting in "Not an JSON object" error, so thats wrong approach too

public static final Codec<CountByAttributeTransformer> CODEC = OutputModifier.CODEC.xmap(CountByAttributeTransformer::new, t -> t.modifier);

 

Is there a way to get to the second result (without creating duplicate codec for OutputModifier class)?

Edited by Toma™
Posted

Can you please stop posting snippets and show all the relevant code (preferably on github), so I don't have to guess what you are doing and waste my time like I did above.

e.g. (and it is only one example) what does this do:

Quote

QuestingRegistries.REWARD_TRANSFORMERS

Without seeing all the code, I can't tell why your xmap solution doesn't work, you don't show where/how it is referenced/used.

Boilerplate:

If you don't post your logs/debug.log we can't help you. For curseforge you need to enable the forge debug.log in its minecraft settings. You should also post your crash report if you have one.

If there is no error in the log file and you don't have a crash report then post the launcher_log.txt from the minecraft folder. Again for curseforge this will be in your curseforge/minecraft/Install

Large files should be posted to a file sharing site like https://gist.github.com  You should also read the support forum sticky post.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Announcements



×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.