From fd28a6b01db3fcf64e272338058def72b2e69fb1 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Fri, 28 Jun 2024 20:21:46 -0500 Subject: [PATCH 01/76] Initial work on structures system --- .../structured/CodecInterpreter.java | 30 +++++++++++++ .../codecextras/structured/Interpreter.java | 24 ++++++++++ .../codecextras/structured/Key.java | 18 ++++++++ .../structured/KeyStoringInterpreter.java | 18 ++++++++ .../codecextras/structured/Keys.java | 44 +++++++++++++++++++ .../codecextras/structured/Structure.java | 41 +++++++++++++++++ 6 files changed, 175 insertions(+) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Key.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Keys.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Structure.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java new file mode 100644 index 0000000..586c93e --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -0,0 +1,30 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; + +import java.util.List; + +public class CodecInterpreter extends KeyStoringInterpreter { + public CodecInterpreter(Keys keys) { + super(keys.join(Keys.builder() + .add(Interpreter.STRING, new CodecHolder<>(Codec.STRING)) + .build() + )); + } + + @Override + public DataResult>> list(App single) { + return DataResult.success(new CodecHolder<>(CodecHolder.unbox(single).codec.listOf())); + } + + public record CodecHolder(Codec codec) implements App { + public static final class Mu implements K1 {} + + static CodecHolder unbox(App box) { + return (CodecHolder) box; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java new file mode 100644 index 0000000..412eff7 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -0,0 +1,24 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.DataResult; + +import java.util.List; + +public interface Interpreter { + DataResult>> list(App single); + + DataResult> keyed(Key key); + + Key UNIT = Key.create("UNIT"); + Key BOOL = Key.create("BOOL"); + Key BYTE = Key.create("BYTE"); + Key SHORT = Key.create("SHORT"); + Key INT = Key.create("INT"); + Key LONG = Key.create("LONG"); + Key FLOAT = Key.create("FLOAT"); + Key DOUBLE = Key.create("DOUBLE"); + Key STRING = Key.create("STRING"); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key.java b/src/main/java/dev/lukebemish/codecextras/structured/Key.java new file mode 100644 index 0000000..d8440e5 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key.java @@ -0,0 +1,18 @@ +package dev.lukebemish.codecextras.structured; + +public final class Key { + private final String name; + + private Key(String name) { + this.name = name; + } + + public static Key create(String name) { + var className = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName(); + return new Key<>(className + ":" + name); + } + + public String name() { + return name; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java new file mode 100644 index 0000000..bc3bd62 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java @@ -0,0 +1,18 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; + +public abstract class KeyStoringInterpreter implements Interpreter { + private final Keys keys; + + protected KeyStoringInterpreter(Keys keys) { + this.keys = keys; + } + + @Override + public DataResult> keyed(Key key) { + return keys.get(key).map(DataResult::success).orElse(DataResult.error(() -> "Unknown key "+key.name())); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java new file mode 100644 index 0000000..43be6a8 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -0,0 +1,44 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; + +public final class Keys { + private final IdentityHashMap, App> keys; + + private Keys(IdentityHashMap, App> keys) { + this.keys = keys; + } + + @SuppressWarnings("unchecked") + public Optional> get(Key key) { + return Optional.ofNullable((App) keys.get(key)); + } + + public static Builder builder() { + return new Builder<>(); + } + + public Keys join(Keys other) { + var map = new IdentityHashMap<>(this.keys); + map.putAll(other.keys); + return new Keys<>(map); + } + + public final static class Builder { + private final Map, App> keys = new IdentityHashMap<>(); + + public Builder add(Key key, App value) { + keys.put(key, value); + return this; + } + + public Keys build() { + return new Keys<>(new IdentityHashMap<>(keys)); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java new file mode 100644 index 0000000..f784b0b --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -0,0 +1,41 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.DataResult; + +import java.util.List; + +public interface Structure { + DataResult> interpret(Interpreter interpreter); + + default Structure> listOf() { + var outer = this; + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + return outer.interpret(interpreter).flatMap(interpreter::list); + } + }; + } + + static Structure keyed(Key key) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.keyed(key); + } + }; + } + + Structure UNIT = keyed(Interpreter.UNIT); + Structure BOOL = keyed(Interpreter.BOOL); + Structure BYTE = keyed(Interpreter.BYTE); + Structure SHORT = keyed(Interpreter.SHORT); + Structure INT = keyed(Interpreter.INT); + Structure LONG = keyed(Interpreter.LONG); + Structure FLOAT = keyed(Interpreter.FLOAT); + Structure DOUBLE = keyed(Interpreter.DOUBLE); + Structure STRING = keyed(Interpreter.STRING); +} From 597157b9f57af27f09208b54c390dadb5a7a2ae1 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Fri, 28 Jun 2024 21:26:05 -0500 Subject: [PATCH 02/76] Add StreamCodecInterpreter and tests --- .../structured/CodecInterpreter.java | 44 ++++++-- .../codecextras/structured/Interpreter.java | 3 + .../structured/RecordStructure.java | 83 ++++++++++++++ .../codecextras/structured/Structure.java | 4 + .../structured/StructuredMapCodec.java | 90 +++++++++++++++ .../codecextras/structured/package-info.java | 6 + .../structured/StreamCodecInterpreter.java | 103 ++++++++++++++++++ .../stream/structured/package-info.java | 6 + .../codecextras/test/record/package-info.java | 4 + .../test/structured/TestStructured.java | 42 +++++++ .../test/structured/package-info.java | 4 + 11 files changed, 379 insertions(+), 10 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/package-info.java create mode 100644 src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java create mode 100644 src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/record/package-info.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/package-info.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 586c93e..98acd1e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -2,29 +2,53 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import java.util.List; +import java.util.function.Function; -public class CodecInterpreter extends KeyStoringInterpreter { - public CodecInterpreter(Keys keys) { - super(keys.join(Keys.builder() - .add(Interpreter.STRING, new CodecHolder<>(Codec.STRING)) - .build() +public class CodecInterpreter extends KeyStoringInterpreter { + public CodecInterpreter(Keys keys) { + super(keys.join(Keys.builder() + .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) + .add(Interpreter.BOOL, new Holder<>(Codec.BOOL)) + .add(Interpreter.BYTE, new Holder<>(Codec.BYTE)) + .add(Interpreter.SHORT, new Holder<>(Codec.SHORT)) + .add(Interpreter.INT, new Holder<>(Codec.INT)) + .add(Interpreter.LONG, new Holder<>(Codec.LONG)) + .add(Interpreter.FLOAT, new Holder<>(Codec.FLOAT)) + .add(Interpreter.DOUBLE, new Holder<>(Codec.DOUBLE)) + .add(Interpreter.STRING, new Holder<>(Codec.STRING)) + .build() )); } + public CodecInterpreter() { + this(Keys.builder().build()); + } + @Override - public DataResult>> list(App single) { - return DataResult.success(new CodecHolder<>(CodecHolder.unbox(single).codec.listOf())); + public DataResult>> list(App single) { + return DataResult.success(new Holder<>(Holder.unbox(single).codec.listOf())); + } + + @Override + public DataResult> record(List> fields, Function creator) { + return StructuredMapCodec.of(fields, creator, this, CodecInterpreter::unbox) + .map(mapCodec -> new Holder<>(mapCodec.codec())); + } + + public static Codec unbox(App box) { + return Holder.unbox(box).codec(); } - public record CodecHolder(Codec codec) implements App { + public record Holder(Codec codec) implements App { public static final class Mu implements K1 {} - static CodecHolder unbox(App box) { - return (CodecHolder) box; + static Holder unbox(App box) { + return (Holder) box; } } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 412eff7..198f1e3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -6,12 +6,15 @@ import com.mojang.serialization.DataResult; import java.util.List; +import java.util.function.Function; public interface Interpreter { DataResult>> list(App single); DataResult> keyed(Key key); + DataResult> record(List> fields, Function creator); + Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); Key BYTE = Key.create("BYTE"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java new file mode 100644 index 0000000..712d363 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -0,0 +1,83 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class RecordStructure { + private final List> fields = new ArrayList<>(); + + public static final class Container { + private final Key[] keys; + private final Object[] array; + + private Container(Key[] keys, Object[] array) { + this.array = array; + this.keys = keys; + } + + @SuppressWarnings("unchecked") + public T get(Key key) { + if (key.count >= array.length || key != keys[key.count]) { + throw new IllegalArgumentException("Key does not belong to the container"); + } + return (T) array[key.count]; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final List values = new ArrayList<>(); + private final List> keys = new ArrayList<>(); + + private Builder() {} + + public void add(Key key, T value) { + keys.add(key); + values.add(value); + } + + public Container build() { + return new Container(keys.toArray(new Key[0]), values.toArray()); + } + } + } + + public static final class Key { + private final int count; + + private Key(int i) { + this.count = i; + } + } + + public record Field(String name, Structure structure, Function getter, Key key) {} + + public Key add(String name, Structure structure, Function getter) { + var key = new Key(fields.size()); + fields.add(new Field<>(name, structure, getter, key)); + return key; + } + + static Structure create(RecordStructure.Builder builder) { + RecordStructure instance = new RecordStructure<>(); + var creator = builder.build(instance); + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.record(instance.fields, creator); + } + }; + } + + @FunctionalInterface + public interface Builder { + Function build(RecordStructure builder); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index f784b0b..fd6f730 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -29,6 +29,10 @@ public DataResult> interpret(Interpreter interpre }; } + static Structure record(RecordStructure.Builder builder) { + return RecordStructure.create(builder); + } + Structure UNIT = keyed(Interpreter.UNIT); Structure BOOL = keyed(Interpreter.BOOL); Structure BYTE = keyed(Interpreter.BYTE); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java new file mode 100644 index 0000000..cfb9df2 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -0,0 +1,90 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +class StructuredMapCodec extends MapCodec { + private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} + + private final List> fields; + private final Function creator; + + private StructuredMapCodec(List> fields, Function creator) { + this.fields = fields; + this.creator = creator; + } + + public interface Unboxer { + Codec unbox(App box); + } + + public static DataResult> of(List> fields, Function creator, Interpreter interpreter, Unboxer unboxer) { + var mapCodecFields = new ArrayList>(); + for (var field : fields) { + DataResult> result = recordSingleField(field, mapCodecFields, interpreter, unboxer); + if (result != null) return result; + } + return DataResult.success(new StructuredMapCodec<>(mapCodecFields, creator)); + } + + private static @Nullable DataResult> recordSingleField(RecordStructure.Field field, ArrayList> mapCodecFields, Interpreter interpreter, Unboxer unboxer) { + var result = field.structure().interpret(interpreter); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + mapCodecFields.add(new StructuredMapCodec.Field<>(unboxer.unbox(result.result().orElseThrow()).fieldOf(field.name()), field.key(), field.getter())); + return null; + } + + @Override + public Stream keys(DynamicOps ops) { + return fields.stream().flatMap(f -> f.codec().keys(ops)); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + var builder = RecordStructure.Container.builder(); + for (var field : fields) { + DataResult result = singleField(ops, input, field, builder); + if (result != null) return result; + } + return DataResult.success(creator.apply(builder.build())); + } + + private static @Nullable DataResult singleField(DynamicOps ops, MapLike input, Field field, RecordStructure.Container.Builder builder) { + var key = field.key(); + var codec = field.codec(); + var result = codec.decode(ops, input); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + builder.add(key, result.result().orElseThrow()); + return null; + } + + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + for (var field : fields) { + prefix = encodeSingleField(input, ops, prefix, field); + } + return prefix; + } + + private RecordBuilder encodeSingleField(A input, DynamicOps ops, RecordBuilder prefix, Field field) { + var codec = field.codec(); + var value = field.getter().apply(input); + return codec.encode(value, ops, prefix); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/package-info.java new file mode 100644 index 0000000..d2476f7 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.structured; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java new file mode 100644 index 0000000..623c6e2 --- /dev/null +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -0,0 +1,103 @@ +package dev.lukebemish.codecextras.stream.mutable.dev.lukebemish.codecextras.stream.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.RecordStructure; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class StreamCodecInterpreter extends KeyStoringInterpreter> { + public StreamCodecInterpreter(Keys> keys) { + super(keys.join(Keys.>builder() + .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) + .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL)) + .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE)) + .add(Interpreter.SHORT, new Holder<>(ByteBufCodecs.SHORT)) + .add(Interpreter.INT, new Holder<>(ByteBufCodecs.VAR_INT)) + .add(Interpreter.LONG, new Holder<>(ByteBufCodecs.VAR_LONG)) + .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT)) + .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE)) + .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8)) + .build() + )); + } + + public StreamCodecInterpreter() { + this(Keys.>builder().build()); + } + + @Override + public DataResult, List>> list(App, A> single) { + return DataResult.success(new Holder<>(list(unbox(single)))); + } + + private static StreamCodec> list(StreamCodec elementCodec) { + return ByteBufCodecs.list().apply(elementCodec); + } + + @Override + public DataResult, A>> record(List> fields, Function creator) { + var streamFields = new ArrayList>(); + for (var field : fields) { + DataResult, A>> result = recordSingleField(field, streamFields); + if (result != null) return result; + } + return DataResult.success(new Holder<>(StreamCodec.of( + (buf, data) -> { + for (var field : streamFields) { + encodeSingleField(buf, field, data); + } + }, + buf -> { + var builder = RecordStructure.Container.builder(); + for (var field : streamFields) { + decodeSingleField(buf, field, builder); + } + return creator.apply(builder.build()); + } + ))); + } + + private static void encodeSingleField(B buf, Field field, A data) { + field.codec.encode(buf, field.getter.apply(data)); + } + + private static void decodeSingleField(B buf, Field field, RecordStructure.Container.Builder builder) { + var value = field.codec.decode(buf); + builder.add(field.key(), value); + } + + private @Nullable DataResult, A>> recordSingleField(RecordStructure.Field field, ArrayList> streamFields) { + var result = field.structure().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + streamFields.add(new Field<>(unbox(result.result().orElseThrow()), field.key(), field.getter())); + return null; + } + + public static StreamCodec unbox(App, T> box) { + return Holder.unbox(box).streamCodec(); + } + + public record Holder(StreamCodec streamCodec) implements App, T> { + public static final class Mu implements K1 {} + + static StreamCodecInterpreter.Holder unbox(App, T> box) { + return (StreamCodecInterpreter.Holder) box; + } + } + + private record Field(StreamCodec codec, RecordStructure.Key key, Function getter) {} +} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java new file mode 100644 index 0000000..57a5da3 --- /dev/null +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.stream.mutable.dev.lukebemish.codecextras.stream.structured; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/dev/lukebemish/codecextras/test/record/package-info.java b/src/test/java/dev/lukebemish/codecextras/test/record/package-info.java new file mode 100644 index 0000000..faf86ef --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/record/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.record; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java new file mode 100644 index 0000000..16973bc --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -0,0 +1,42 @@ +package dev.lukebemish.codecextras.test.structured; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.test.CodecAssertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class TestStructured { + private record TestRecord(int a, String b, List c) { + private static final Structure STRUCTURE = Structure.record(i -> { + var a = i.add("a", Structure.INT, TestRecord::a); + var b = i.add("b", Structure.STRING, TestRecord::b); + var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); + return container -> new TestRecord(container.get(a), container.get(b), container.get(c)); + }); + + private static final Codec CODEC = CodecInterpreter.unbox(STRUCTURE.interpret(new CodecInterpreter()).getOrThrow()); + } + + private final String json = """ + { + "a": 1, + "b": "test", + "c": [true, false, true] + }"""; + + private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true)); + + @Test + void testDecodingCodec() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, record, TestRecord.CODEC); + } + + @Test + void testEncodingCodec() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, record, json, TestRecord.CODEC); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/package-info.java b/src/test/java/dev/lukebemish/codecextras/test/structured/package-info.java new file mode 100644 index 0000000..8bf892b --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.structured; + +import org.jspecify.annotations.NullMarked; From eeb99cb3fb5ada7694a7599905e05b82bababeeb Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Fri, 28 Jun 2024 21:34:42 -0500 Subject: [PATCH 03/76] Add MapCodecInterpreter --- .../codecextras/structured/Keys.java | 10 ++++ .../structured/MapCodecInterpreter.java | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index 43be6a8..33e99b0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -19,6 +19,16 @@ public Optional> get(Key key) { return Optional.ofNullable((App) keys.get(key)); } + public Keys map(Converter converter) { + var map = new IdentityHashMap, App>(); + keys.forEach((key, value) -> map.put(key, converter.convert(value))); + return new Keys<>(map); + } + + public interface Converter { + App convert(App input); + } + public static Builder builder() { return new Builder<>(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java new file mode 100644 index 0000000..107732b --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -0,0 +1,50 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; + +import java.util.List; +import java.util.function.Function; + +public class MapCodecInterpreter extends KeyStoringInterpreter { + private final CodecInterpreter codecInterpreter; + + public MapCodecInterpreter(Keys keys, Keys codecKeys) { + super(keys); + this.codecInterpreter = new CodecInterpreter(codecKeys.join(keys.map(new Keys.Converter<>() { + @Override + public App convert(App app) { + return new CodecInterpreter.Holder<>(unbox(app).codec()); + } + }))); + } + + public MapCodecInterpreter() { + this(Keys.builder().build(), Keys.builder().build()); + } + + @Override + public DataResult>> list(App single) { + return DataResult.error(() -> "Cannot make a MapCodec for a list"); + } + + @Override + public DataResult> record(List> fields, Function creator) { + return StructuredMapCodec.of(fields, creator, codecInterpreter, CodecInterpreter::unbox) + .map(Holder::new); + } + + public static MapCodec unbox(App box) { + return Holder.unbox(box).mapCodec(); + } + + public record Holder(MapCodec mapCodec) implements App { + public static final class Mu implements K1 {} + + static Holder unbox(App box) { + return (Holder) box; + } + } +} From fc625fbbb6eb29359e9451ce87c7ea67b38ac289 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Fri, 28 Jun 2024 21:37:45 -0500 Subject: [PATCH 04/76] Apply spotless --- .../codecextras/structured/CodecInterpreter.java | 1 - .../dev/lukebemish/codecextras/structured/Interpreter.java | 1 - .../java/dev/lukebemish/codecextras/structured/Keys.java | 1 - .../codecextras/structured/MapCodecInterpreter.java | 1 - .../lukebemish/codecextras/structured/RecordStructure.java | 1 - .../dev/lukebemish/codecextras/structured/Structure.java | 1 - .../codecextras/structured/StructuredMapCodec.java | 3 +-- .../stream/structured/StreamCodecInterpreter.java | 7 +++---- .../codecextras/test/structured/TestStructured.java | 3 +-- 9 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 98acd1e..1baae38 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -5,7 +5,6 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 198f1e3..e9aeb4c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index 33e99b0..396c781 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -2,7 +2,6 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; - import java.util.IdentityHashMap; import java.util.Map; import java.util.Optional; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 107732b..00eeeb0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index 712d363..1d66f47 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -3,7 +3,6 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; - import java.util.ArrayList; import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index fd6f730..27e6182 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; - import java.util.List; public interface Structure { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index cfb9df2..b83c3dd 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -8,12 +8,11 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; -import org.jetbrains.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; class StructuredMapCodec extends MapCodec { private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 623c6e2..f4a5d98 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -9,13 +9,12 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jetbrains.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { public StreamCodecInterpreter(Keys> keys) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 16973bc..9fe9d92 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -5,9 +5,8 @@ import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.test.CodecAssertions; -import org.junit.jupiter.api.Test; - import java.util.List; +import org.junit.jupiter.api.Test; class TestStructured { private record TestRecord(int a, String b, List c) { From c7f8d2bb028dc9d67f27ce29c6e7518adb5cf961 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Fri, 28 Jun 2024 22:20:21 -0500 Subject: [PATCH 05/76] Structured comment support and StreamCodec fixes --- .../codecextras/structured/Annotations.java | 56 +++++++++++++++++++ .../structured/CodecInterpreter.java | 3 +- .../codecextras/structured/Structure.java | 29 ++++++++++ .../structured/StructuredMapCodec.java | 7 ++- .../structured/StreamCodecInterpreter.java | 26 ++++----- 5 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Annotations.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java new file mode 100644 index 0000000..b1d43e6 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java @@ -0,0 +1,56 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.K1; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; + +public final class Annotations { + public static final Key COMMENT = Key.create("comment"); + + @SuppressWarnings("unchecked") + public Optional get(Key key) { + return Optional.ofNullable((A) keys.get(key)); + } + + public static Keys.Builder builder() { + return new Keys.Builder<>(); + } + + public Annotations join(Annotations other) { + var map = new IdentityHashMap<>(this.keys); + map.putAll(other.keys); + return new Annotations(map); + } + + public Annotations with(Key key, A value) { + var map = new IdentityHashMap<>(this.keys); + map.put(key, value); + return new Annotations(map); + } + + public static Annotations empty() { + return EMPTY; + } + + public final static class Builder { + private final Map, Object> keys = new IdentityHashMap<>(); + + public Builder add(Key key, A value) { + keys.put(key, value); + return this; + } + + public Annotations build() { + return new Annotations(new IdentityHashMap<>(keys)); + } + } + + private static final Annotations EMPTY = new Annotations(new IdentityHashMap<>()); + + private final IdentityHashMap, Object> keys; + + private Annotations(IdentityHashMap, Object> keys) { + this.keys = keys; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 1baae38..9a0d0ce 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -5,6 +5,7 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import java.util.List; import java.util.function.Function; @@ -30,7 +31,7 @@ public CodecInterpreter() { @Override public DataResult>> list(App single) { - return DataResult.success(new Holder<>(Holder.unbox(single).codec.listOf())); + return DataResult.success(new Holder<>(CommentFirstListCodec.of(Holder.unbox(single).codec))); } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 27e6182..6bb63f1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -8,6 +8,35 @@ public interface Structure { DataResult> interpret(Interpreter interpreter); + default Annotations annotations() { + return Annotations.empty(); + } + + default Structure annotate(Key key, T value) { + var outer = this; + var annotations = annotations().with(key, value); + return annotatedDelegatingStructure(outer, annotations); + } + + default Structure annotate(Annotations annotations) { + var outer = this; + var combined = annotations().join(annotations); + return annotatedDelegatingStructure(outer, combined); + } + + private static Structure annotatedDelegatingStructure(Structure outer, Annotations annotations) { + return new Structure() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return outer.interpret(interpreter); + } + + @Override + public Annotations annotations() { + return annotations; + } + }; + } default Structure> listOf() { var outer = this; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index b83c3dd..c59e5f7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -8,6 +8,7 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; +import dev.lukebemish.codecextras.comments.CommentMapCodec; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -43,7 +44,11 @@ public static DataResult> of(List(unboxer.unbox(result.result().orElseThrow()).fieldOf(field.name()), field.key(), field.getter())); + Codec fieldCodec = unboxer.unbox(result.result().orElseThrow()); + MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) + .map(comment -> CommentMapCodec.of(fieldCodec.fieldOf(field.name()), comment)) + .orElseGet(() -> fieldCodec.fieldOf(field.name())); + mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); return null; } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index f4a5d98..c2a3b74 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -20,14 +20,14 @@ public class StreamCodecInterpreter extends KeyStoringInterpr public StreamCodecInterpreter(Keys> keys) { super(keys.join(Keys.>builder() .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) - .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL)) - .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE)) - .add(Interpreter.SHORT, new Holder<>(ByteBufCodecs.SHORT)) - .add(Interpreter.INT, new Holder<>(ByteBufCodecs.VAR_INT)) - .add(Interpreter.LONG, new Holder<>(ByteBufCodecs.VAR_LONG)) - .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT)) - .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE)) - .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8)) + .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) + .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE.cast())) + .add(Interpreter.SHORT, new Holder<>(ByteBufCodecs.SHORT.cast())) + .add(Interpreter.INT, new Holder<>(ByteBufCodecs.VAR_INT.cast())) + .add(Interpreter.LONG, new Holder<>(ByteBufCodecs.VAR_LONG.cast())) + .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT.cast())) + .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE.cast())) + .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) .build() )); } @@ -38,7 +38,7 @@ public StreamCodecInterpreter() { @Override public DataResult, List>> list(App, A> single) { - return DataResult.success(new Holder<>(list(unbox(single)))); + return DataResult.success(new Holder<>(StreamCodecInterpreter.list(unbox(single)))); } private static StreamCodec> list(StreamCodec elementCodec) { @@ -86,14 +86,14 @@ private static void decodeSingleField(B buf, Field StreamCodec unbox(App, T> box) { + public static StreamCodec unbox(App, T> box) { return Holder.unbox(box).streamCodec(); } - public record Holder(StreamCodec streamCodec) implements App, T> { - public static final class Mu implements K1 {} + public record Holder(StreamCodec streamCodec) implements App, T> { + public static final class Mu implements K1 {} - static StreamCodecInterpreter.Holder unbox(App, T> box) { + static StreamCodecInterpreter.Holder unbox(App, T> box) { return (StreamCodecInterpreter.Holder) box; } } From 2b5b5135ea85803b025ea065aa055276c1501789 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 3 Jul 2024 23:42:08 -0500 Subject: [PATCH 06/76] Partial record structures and optional fields --- .../structured/CodecInterpreter.java | 6 ++ .../codecextras/structured/Interpreter.java | 3 + .../structured/MapCodecInterpreter.java | 7 ++ .../structured/RecordStructure.java | 88 +++++++++++++++++-- .../codecextras/structured/Structure.java | 30 +++++++ .../structured/StructuredMapCodec.java | 13 ++- .../structured/StreamCodecInterpreter.java | 48 ++++++++-- .../test/structured/TestStructured.java | 5 +- 8 files changed, 183 insertions(+), 17 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 9a0d0ce..4cd14c1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -6,6 +6,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; + import java.util.List; import java.util.function.Function; @@ -40,6 +41,11 @@ public DataResult> record(List .map(mapCodec -> new Holder<>(mapCodec.codec())); } + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + return null; + } + public static Codec unbox(App box) { return Holder.unbox(box).codec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index e9aeb4c..719981a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; + import java.util.List; import java.util.function.Function; @@ -14,6 +15,8 @@ public interface Interpreter { DataResult> record(List> fields, Function creator); + DataResult> flatXmap(App input, Function> deserializer, Function> serializer); + Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); Key BYTE = Key.create("BYTE"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 00eeeb0..1ad83f1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; + import java.util.List; import java.util.function.Function; @@ -35,6 +36,12 @@ public DataResult> record(List .map(Holder::new); } + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + var mapCodec = unbox(input); + return DataResult.success(new Holder<>(mapCodec.flatXmap(deserializer, serializer))); + } + public static MapCodec unbox(App box) { return Holder.unbox(box).mapCodec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index 1d66f47..b606490 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -3,12 +3,20 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; + import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; public class RecordStructure { private final List> fields = new ArrayList<>(); + private final Set fieldNames = new HashSet<>(); + private int count = 0; public static final class Container { private final Key[] keys; @@ -20,7 +28,7 @@ private Container(Key[] keys, Object[] array) { } @SuppressWarnings("unchecked") - public T get(Key key) { + private T get(Key key) { if (key.count >= array.length || key != keys[key.count]) { throw new IllegalArgumentException("Key does not belong to the container"); } @@ -48,22 +56,92 @@ public Container build() { } } - public static final class Key { + public static final class Key implements Function { private final int count; private Key(int i) { this.count = i; } + + @Override + public T apply(Container container) { + return container.get(this); + } + } + + public interface Field { + String name(); + + Structure structure(); + + Function getter(); + + Optional> missingBehavior(); + + Key key(); + + interface MissingBehavior { + Supplier missing(); + Predicate predicate(); + } } - public record Field(String name, Structure structure, Function getter, Key key) {} + private record MissingBehaviorImpl(Supplier missing, Predicate predicate) implements Field.MissingBehavior {} + + private record FieldImpl(String name, Structure structure, Function getter, Optional> missingBehavior, Key key) implements Field {} public Key add(String name, Structure structure, Function getter) { - var key = new Key(fields.size()); - fields.add(new Field<>(name, structure, getter, key)); + var key = new Key(count); + count++; + fields.add(new FieldImpl<>(name, structure, getter, Optional.empty(), key)); + fieldNames.add(name); return key; } + public Key> addOptional(String name, Structure structure, Function> getter) { + var key = new Key>(count); + count++; + fields.add(new FieldImpl<>( + name, + structure.flatXmap( + t -> DataResult.success(Optional.of(t)), + o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() ->"Optional default value not handed by interpreter")) + ), + getter, + Optional.of(new MissingBehaviorImpl<>(Optional::empty, Optional::isPresent)), + key + )); + fieldNames.add(name); + return key; + } + + public Key addOptional(String name, Structure structure, Function getter, Supplier defaultValue) { + var key = new Key(count); + count++; + fields.add(new FieldImpl<>(name, structure, getter, Optional.of(new MissingBehaviorImpl<>(defaultValue, t -> !t.equals(defaultValue))), key)); + fieldNames.add(name); + return key; + } + + public Function add(RecordStructure.Builder part, Function getter) { + RecordStructure partial = new RecordStructure<>(); + partial.count = this.count; + var creator = part.build(partial); + for (var field : partial.fields) { + partialField(getter, field); + } + return creator; + } + + private void partialField(Function getter, Field field) { + if (fieldNames.contains(field.name())) { + throw new IllegalArgumentException("Duplicate field name: " + field.name()); + } + fields.add(new FieldImpl<>(field.name(), field.structure(), a -> field.getter().apply(getter.apply(a)), field.missingBehavior(), field.key())); + count++; + fieldNames.add(field.name()); + } + static Structure create(RecordStructure.Builder builder) { RecordStructure instance = new RecordStructure<>(); var creator = builder.build(instance); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6bb63f1..b182f79 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,7 +4,11 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; + import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; public interface Structure { DataResult> interpret(Interpreter interpreter); @@ -48,6 +52,32 @@ public DataResult>> interpret(Interpreter in }; } + default RecordStructure.Builder fieldOf(String name) { + return builder -> builder.add(name, this, Function.identity()); + } + + default RecordStructure.Builder> optionalFieldOf(String name) { + return builder -> builder.addOptional(name, this, Function.identity()); + } + + default RecordStructure.Builder optionalFieldOf(String name, Supplier defaultValue) { + return builder -> builder.addOptional(name, this, Function.identity(), defaultValue); + } + + default Structure flatXmap(Function> deserializer, Function> serializer) { + var outer = this; + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); + } + }; + } + + default Structure xmap(Function deserializer, Function serializer) { + return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); + } + static Structure keyed(Key key) { return new Structure<>() { @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index c59e5f7..3694055 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -9,11 +9,13 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentMapCodec; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; -import org.jspecify.annotations.Nullable; class StructuredMapCodec extends MapCodec { private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} @@ -46,12 +48,19 @@ public static DataResult> of(List fieldCodec = unboxer.unbox(result.result().orElseThrow()); MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) - .map(comment -> CommentMapCodec.of(fieldCodec.fieldOf(field.name()), comment)) + .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) .orElseGet(() -> fieldCodec.fieldOf(field.name())); mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); return null; } + private static MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field) { + return field.missingBehavior().map(behavior -> fieldCodec.optionalFieldOf(field.name()).xmap( + optional -> optional.orElseGet(behavior.missing()), + value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() + )).orElseGet(() -> fieldCodec.fieldOf(field.name())); + } + @Override public Stream keys(DynamicOps ops) { return fields.stream().flatMap(f -> f.codec().keys(ops)); diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index c2a3b74..ee69f45 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -9,13 +9,15 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; import io.netty.buffer.ByteBuf; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + public class StreamCodecInterpreter extends KeyStoringInterpreter> { public StreamCodecInterpreter(Keys> keys) { super(keys.join(Keys.>builder() @@ -68,13 +70,43 @@ public DataResult, A>> record(List DataResult, Y>> flatXmap(App, X> input, Function> deserializer, Function> serializer) { + var streamCodec = unbox(input); + return DataResult.success(new Holder<>(streamCodec.map( + x -> deserializer.apply(x).getOrThrow(), + y -> serializer.apply(y).getOrThrow() + ))); + } + private static void encodeSingleField(B buf, Field field, A data) { - field.codec.encode(buf, field.getter.apply(data)); + var missingBehaviour = field.missingBehavior(); + if (missingBehaviour.isEmpty()) { + field.codec.encode(buf, field.getter.apply(data)); + } else { + var behavior = missingBehaviour.get(); + if (behavior.predicate().test(field.getter.apply(data))) { + buf.writeBoolean(true); + field.codec.encode(buf, field.getter.apply(data)); + } else { + buf.writeBoolean(false); + } + } } private static void decodeSingleField(B buf, Field field, RecordStructure.Container.Builder builder) { - var value = field.codec.decode(buf); - builder.add(field.key(), value); + var missingBehaviour = field.missingBehavior(); + if (missingBehaviour.isEmpty()) { + var value = field.codec.decode(buf); + builder.add(field.key(), value); + } else { + if (buf.readBoolean()) { + var value = field.codec.decode(buf); + builder.add(field.key(), value); + } else { + builder.add(field.key(), missingBehaviour.get().missing().get()); + } + } } private @Nullable DataResult, A>> recordSingleField(RecordStructure.Field field, ArrayList> streamFields) { @@ -82,7 +114,7 @@ private static void decodeSingleField(B buf, Field(unbox(result.result().orElseThrow()), field.key(), field.getter())); + streamFields.add(new Field<>(unbox(result.result().orElseThrow()), field.key(), field.getter(), field.missingBehavior())); return null; } @@ -98,5 +130,5 @@ static StreamCodecInterpreter.Holder unbox(App(StreamCodec codec, RecordStructure.Key key, Function getter) {} + private record Field(StreamCodec codec, RecordStructure.Key key, Function getter, Optional> missingBehavior) {} } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 9fe9d92..5abb61f 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -5,16 +5,17 @@ import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.test.CodecAssertions; -import java.util.List; import org.junit.jupiter.api.Test; +import java.util.List; + class TestStructured { private record TestRecord(int a, String b, List c) { private static final Structure STRUCTURE = Structure.record(i -> { var a = i.add("a", Structure.INT, TestRecord::a); var b = i.add("b", Structure.STRING, TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); - return container -> new TestRecord(container.get(a), container.get(b), container.get(c)); + return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container)); }); private static final Codec CODEC = CodecInterpreter.unbox(STRUCTURE.interpret(new CodecInterpreter()).getOrThrow()); From 177dde7357fce73cab7f68bd2f076e1a230c4a46 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 3 Jul 2024 23:47:43 -0500 Subject: [PATCH 07/76] Default interpret methods on all built-in interpreters --- .../codecextras/structured/CodecInterpreter.java | 7 ++++++- .../codecextras/structured/MapCodecInterpreter.java | 4 ++++ .../stream/structured/StreamCodecInterpreter.java | 5 +++++ .../codecextras/test/structured/TestStructured.java | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 4cd14c1..f34172f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -43,13 +43,18 @@ public DataResult> record(List @Override public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - return null; + var codec = Holder.unbox(input).codec(); + return DataResult.success(new Holder<>(codec.flatXmap(deserializer, serializer))); } public static Codec unbox(App box) { return Holder.unbox(box).codec(); } + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(CodecInterpreter::unbox); + } + public record Holder(Codec codec) implements App { public static final class Mu implements K1 {} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 1ad83f1..3412129 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -46,6 +46,10 @@ public static MapCodec unbox(App box) { return Holder.unbox(box).mapCodec(); } + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(MapCodecInterpreter::unbox); + } + public record Holder(MapCodec mapCodec) implements App { public static final class Mu implements K1 {} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index ee69f45..277d4cc 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -8,6 +8,7 @@ import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; +import dev.lukebemish.codecextras.structured.Structure; import io.netty.buffer.ByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; @@ -122,6 +123,10 @@ public static StreamCodec unbox(App, T return Holder.unbox(box).streamCodec(); } + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(StreamCodecInterpreter::unbox); + } + public record Holder(StreamCodec streamCodec) implements App, T> { public static final class Mu implements K1 {} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 5abb61f..86bb4ca 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -18,7 +18,7 @@ private record TestRecord(int a, String b, List c) { return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container)); }); - private static final Codec CODEC = CodecInterpreter.unbox(STRUCTURE.interpret(new CodecInterpreter()).getOrThrow()); + private static final Codec CODEC = new CodecInterpreter().interpret(STRUCTURE).getOrThrow(); } private final String json = """ From 87147e463c5ea6c3237c7239e111c75973faca35 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 4 Jul 2024 00:44:00 -0500 Subject: [PATCH 08/76] Add json schema interpreter --- .../codecextras/comments/CommentMapCodec.java | 2 +- .../structured/CodecInterpreter.java | 1 - .../codecextras/structured/Interpreter.java | 1 - .../structured/JsonSchemaInterpreter.java | 117 ++++++++++++++++++ .../structured/MapCodecInterpreter.java | 1 - .../structured/RecordStructure.java | 3 +- .../codecextras/structured/Structure.java | 1 - .../structured/StructuredMapCodec.java | 5 +- .../structured/StreamCodecInterpreter.java | 7 +- .../codecextras/test/CodecAssertions.java | 4 +- .../test/structured/TestStructured.java | 44 ++++++- 11 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java index 20579a9..90fdd2e 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java @@ -26,7 +26,7 @@ public static MapCodec of(MapCodec codec, String comment) { Map map = codec.keys(JsonOps.INSTANCE) .filter(json -> json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) .map(json -> json.getAsJsonPrimitive().getAsString()) - .collect(Collectors.toMap(Function.identity(), s -> comment)); + .collect(Collectors.toMap(Function.identity(), s -> comment, (a, b) -> a)); return of(codec, map); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index f34172f..beec19f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -6,7 +6,6 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 719981a..97511aa 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java new file mode 100644 index 0000000..bc89098 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -0,0 +1,117 @@ +package dev.lukebemish.codecextras.structured; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + +public class JsonSchemaInterpreter extends KeyStoringInterpreter { + private static final JsonObject OBJECT = new JsonObject(); + private static final JsonObject NUMBER = new JsonObject(); + private static final JsonObject STRING = new JsonObject(); + private static final JsonObject BOOLEAN = new JsonObject(); + private static final JsonObject INTEGER = new JsonObject(); + private static final JsonObject ARRAY = new JsonObject(); + + static { + OBJECT.addProperty("type", "object"); + NUMBER.addProperty("type", "number"); + STRING.addProperty("type", "string"); + BOOLEAN.addProperty("type", "boolean"); + INTEGER.addProperty("type", "integer"); + ARRAY.addProperty("type", "array"); + } + + public JsonSchemaInterpreter(Keys keys) { + super(keys.join(Keys.builder() + .add(Interpreter.UNIT, new Holder<>(OBJECT)) + .add(Interpreter.BOOL, new Holder<>(BOOLEAN)) + .add(Interpreter.BYTE, new Holder<>(INTEGER)) + .add(Interpreter.SHORT, new Holder<>(INTEGER)) + .add(Interpreter.INT, new Holder<>(INTEGER)) + .add(Interpreter.LONG, new Holder<>(INTEGER)) + .add(Interpreter.FLOAT, new Holder<>(NUMBER)) + .add(Interpreter.DOUBLE, new Holder<>(NUMBER)) + .add(Interpreter.STRING, new Holder<>(STRING)) + .build() + )); + } + + public JsonSchemaInterpreter() { + this(Keys.builder().build()); + } + + @Override + public DataResult>> list(App single) { + var object = copy(ARRAY); + object.add("items", unbox(single)); + return DataResult.success(new Holder<>(object)); + } + + @Override + public DataResult> record(List> fields, Function creator) { + var object = copy(OBJECT); + var properties = new JsonObject(); + var required = new JsonArray(); + for (RecordStructure.Field field : fields) { + Supplier error = singleField(field, properties, required); + if (error != null) { + return DataResult.error(error); + } + } + object.add("properties", properties); + object.add("required", required); + return DataResult.success(new Holder<>(object)); + } + + private @Nullable Supplier singleField(RecordStructure.Field field, JsonObject properties, JsonArray required) { + var partialResolt = field.structure().interpret(this); + if (partialResolt.isError()) { + return partialResolt.error().orElseThrow().messageSupplier(); + } + var fieldObject = copy(unbox(partialResolt.result().orElseThrow())); + + field.structure().annotations().get(Annotations.COMMENT).ifPresent(comment -> + fieldObject.addProperty("description", comment) + ); + + field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); + + properties.add(field.name(), fieldObject); + return null; + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + return DataResult.success(new Holder<>(unbox(input))); + } + + public static JsonObject unbox(App box) { + return Holder.unbox(box).jsonObject; + } + + public DataResult interpret(Structure structure) { + return structure.interpret(this).map(JsonSchemaInterpreter::unbox); + } + + public record Holder(JsonObject jsonObject) implements App { + public static final class Mu implements K1 {} + + static Holder unbox(App box) { + return (Holder) box; + } + } + + private JsonObject copy(JsonObject object) { + JsonObject copy = new JsonObject(); + for (String key : object.keySet()) { + copy.add(key, object.get(key)); + } + return copy; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 3412129..53afe66 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index b606490..ec80a2a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -3,7 +3,6 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -105,7 +104,7 @@ public Key> addOptional(String name, Structure structure, Fun name, structure.flatXmap( t -> DataResult.success(Optional.of(t)), - o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() ->"Optional default value not handed by interpreter")) + o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() ->"Optional default value not handled by interpreter")) ), getter, Optional.of(new MissingBehaviorImpl<>(Optional::empty, Optional::isPresent)), diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index b182f79..88009ea 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,7 +4,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; - import java.util.List; import java.util.Optional; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 3694055..0164b60 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -9,13 +9,12 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentMapCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; class StructuredMapCodec extends MapCodec { private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} @@ -49,7 +48,7 @@ public static DataResult> of(List fieldCodec = unboxer.unbox(result.result().orElseThrow()); MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) - .orElseGet(() -> fieldCodec.fieldOf(field.name())); + .orElseGet(() -> makeFieldCodec(fieldCodec, field)); mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); return null; } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 277d4cc..d6ad074 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -10,14 +10,13 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { public StreamCodecInterpreter(Keys> keys) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index 8044ffd..a7c2e21 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java +++ b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java @@ -19,7 +19,7 @@ public static void assertDecodes(DynamicOps jsonOps, String jso public static void assertDecodes(DynamicOps ops, T data, O expected, Codec codec) { DataResult dataResult = codec.parse(ops, data); - Assertions.assertTrue(dataResult.result().isPresent()); + Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); Assertions.assertEquals(expected, dataResult.result().get()); } @@ -31,7 +31,7 @@ public static void assertEncodes(DynamicOps jsonOps, O value, S public static void assertEncodes(DynamicOps ops, O value, T expected, Codec codec) { DataResult dataResult = codec.encodeStart(ops, value); - Assertions.assertTrue(dataResult.result().isPresent()); + Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); Assertions.assertEquals(expected, dataResult.result().get()); } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 86bb4ca..ed70f80 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -2,20 +2,23 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.Annotations; import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.test.CodecAssertions; -import org.junit.jupiter.api.Test; - import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; class TestStructured { - private record TestRecord(int a, String b, List c) { + private record TestRecord(int a, String b, List c, Optional d) { private static final Structure STRUCTURE = Structure.record(i -> { - var a = i.add("a", Structure.INT, TestRecord::a); + var a = i.add("a", Structure.INT.annotate(Annotations.COMMENT, "Field A"), TestRecord::a); var b = i.add("b", Structure.STRING, TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); - return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container)); + var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); + return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); }); private static final Codec CODEC = new CodecInterpreter().interpret(STRUCTURE).getOrThrow(); @@ -28,7 +31,31 @@ private record TestRecord(int a, String b, List c) { "c": [true, false, true] }"""; - private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true)); + private final String schema = """ + { + "type": "object", + "properties": { + "a": { + "type": "integer", + "description": "Field A" + }, + "b": { + "type": "string" + }, + "c": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "d": { + "type": "string" + } + }, + "required": ["a", "b", "c"] + }"""; + + private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty()); @Test void testDecodingCodec() { @@ -39,4 +66,9 @@ void testDecodingCodec() { void testEncodingCodec() { CodecAssertions.assertEncodes(JsonOps.INSTANCE, record, json, TestRecord.CODEC); } + + @Test + void testJsonSchema() { + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(TestRecord.STRUCTURE).getOrThrow().toString()); + } } From 2b5cd00f4c761fc9cf65675313a838ecf57d38c5 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 4 Jul 2024 00:52:41 -0500 Subject: [PATCH 09/76] Make structure test test fieldOf --- .../lukebemish/codecextras/test/structured/TestStructured.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index ed70f80..06c067c 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -15,7 +15,7 @@ class TestStructured { private record TestRecord(int a, String b, List c, Optional d) { private static final Structure STRUCTURE = Structure.record(i -> { var a = i.add("a", Structure.INT.annotate(Annotations.COMMENT, "Field A"), TestRecord::a); - var b = i.add("b", Structure.STRING, TestRecord::b); + var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); From 39411cec544c9bdd25fa9e4d35f5a96439f0651e Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 4 Jul 2024 21:05:38 -0500 Subject: [PATCH 10/76] Change how comment annotations are handled to be more flexible going forwards --- .../codecextras/comments/CommentMapCodec.java | 5 +++++ .../structured/CodecInterpreter.java | 6 ++++++ .../codecextras/structured/Interpreter.java | 2 ++ .../structured/JsonSchemaInterpreter.java | 16 +++++++++----- .../structured/MapCodecInterpreter.java | 11 ++++++++++ .../codecextras/structured/Structure.java | 21 ++++++++++++++++--- .../structured/StreamCodecInterpreter.java | 14 ++++++++++--- 7 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java index 90fdd2e..e089bd9 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java @@ -19,6 +19,11 @@ private CommentMapCodec(MapCodec delegate, Map comments) { } public static MapCodec of(MapCodec codec, Map comments) { + if (codec instanceof CommentMapCodec commentMapCodec) { + Map allComments = commentMapCodec.comments; + allComments.putAll(comments); + return new CommentMapCodec<>(commentMapCodec.delegate, allComments); + } return new CommentMapCodec<>(codec, comments); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index beec19f..b754676 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -46,6 +46,12 @@ public DataResult> flatXmap(App input, Fu return DataResult.success(new Holder<>(codec.flatXmap(deserializer, serializer))); } + @Override + public DataResult> annotate(App input, Annotations annotations) { + // No annotations handled here + return DataResult.success(input); + } + public static Codec unbox(App box) { return Holder.unbox(box).codec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 97511aa..090e976 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -16,6 +16,8 @@ public interface Interpreter { DataResult> flatXmap(App input, Function> deserializer, Function> serializer); + DataResult> annotate(App input, Annotations annotations); + Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); Key BYTE = Key.create("BYTE"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index bc89098..88aa5e8 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -5,10 +5,11 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; +import org.jspecify.annotations.Nullable; + import java.util.List; import java.util.function.Function; import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { private static final JsonObject OBJECT = new JsonObject(); @@ -76,10 +77,6 @@ public DataResult> record(List } var fieldObject = copy(unbox(partialResolt.result().orElseThrow())); - field.structure().annotations().get(Annotations.COMMENT).ifPresent(comment -> - fieldObject.addProperty("description", comment) - ); - field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); properties.add(field.name(), fieldObject); @@ -91,6 +88,15 @@ public DataResult> flatXmap(App input, Fu return DataResult.success(new Holder<>(unbox(input))); } + @Override + public DataResult> annotate(App input, Annotations annotations) { + var schema = copy(unbox(input)); + annotations.get(Annotations.COMMENT).ifPresent(comment -> { + schema.addProperty("description", comment); + }); + return DataResult.success(new Holder<>(schema)); + } + public static JsonObject unbox(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 53afe66..3f81958 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -4,6 +4,8 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; +import dev.lukebemish.codecextras.comments.CommentMapCodec; + import java.util.List; import java.util.function.Function; @@ -41,6 +43,15 @@ public DataResult> flatXmap(App input, Fu return DataResult.success(new Holder<>(mapCodec.flatXmap(deserializer, serializer))); } + @Override + public DataResult> annotate(App input, Annotations annotations) { + var mapCodec = new Object() { + MapCodec m = unbox(input); + }; + mapCodec.m = annotations.get(Annotations.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); + return DataResult.success(new Holder<>(mapCodec.m)); + } + public static MapCodec unbox(App box) { return Holder.unbox(box).mapCodec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 88009ea..580a22d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,6 +4,8 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import org.jspecify.annotations.Nullable; + import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -28,17 +30,30 @@ default Structure annotate(Annotations annotations) { } private static Structure annotatedDelegatingStructure(Structure outer, Annotations annotations) { - return new Structure() { + final class AnnotatedDelegatingStructure implements Structure { + final @Nullable AnnotatedDelegatingStructure delegate; + + AnnotatedDelegatingStructure(@Nullable AnnotatedDelegatingStructure delegate) { + this.delegate = delegate; + } + @Override public DataResult> interpret(Interpreter interpreter) { - return outer.interpret(interpreter); + var result = interpretNoAnnotations(interpreter); + return result.flatMap(r -> interpreter.annotate(r, annotations)); + } + + private DataResult> interpretNoAnnotations(Interpreter interpreter) { + return delegate != null ? delegate.interpretNoAnnotations(interpreter) : outer.interpret(interpreter); } @Override public Annotations annotations() { return annotations; } - }; + } + + return new AnnotatedDelegatingStructure(outer instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure ? annotatedDelegatingStructure : null); } default Structure> listOf() { diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index d6ad074..26de83a 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -4,19 +4,21 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.structured.Annotations; import dev.lukebemish.codecextras.structured.Interpreter; import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { public StreamCodecInterpreter(Keys> keys) { @@ -79,6 +81,12 @@ public DataResult, Y>> flatXmap(App, X> inp ))); } + @Override + public DataResult, A>> annotate(App, A> input, Annotations annotations) { + // No annotations handled here + return DataResult.success(input); + } + private static void encodeSingleField(B buf, Field field, A data) { var missingBehaviour = field.missingBehavior(); if (missingBehaviour.isEmpty()) { From 342f9b442df27ca7ccbbbbf5016ca027e189a00d Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 4 Jul 2024 21:09:06 -0500 Subject: [PATCH 11/76] Add description and title annotation keys, with description falling back to comment if missing --- .../structured/JsonSchemaInterpreter.java | 41 +++++++++++-------- .../structured/MapCodecInterpreter.java | 1 - .../codecextras/structured/Structure.java | 3 +- .../structured/StreamCodecInterpreter.java | 7 ++-- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 88aa5e8..7da9319 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -5,28 +5,14 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; -import org.jspecify.annotations.Nullable; - import java.util.List; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { - private static final JsonObject OBJECT = new JsonObject(); - private static final JsonObject NUMBER = new JsonObject(); - private static final JsonObject STRING = new JsonObject(); - private static final JsonObject BOOLEAN = new JsonObject(); - private static final JsonObject INTEGER = new JsonObject(); - private static final JsonObject ARRAY = new JsonObject(); - - static { - OBJECT.addProperty("type", "object"); - NUMBER.addProperty("type", "number"); - STRING.addProperty("type", "string"); - BOOLEAN.addProperty("type", "boolean"); - INTEGER.addProperty("type", "integer"); - ARRAY.addProperty("type", "array"); - } + public static final Key TITLE = Key.create("title"); + public static final Key DESCRIPTION = Key.create("description"); public JsonSchemaInterpreter(Keys keys) { super(keys.join(Keys.builder() @@ -91,9 +77,12 @@ public DataResult> flatXmap(App input, Fu @Override public DataResult> annotate(App input, Annotations annotations) { var schema = copy(unbox(input)); - annotations.get(Annotations.COMMENT).ifPresent(comment -> { + annotations.get(DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { schema.addProperty("description", comment); }); + annotations.get(TITLE).ifPresent(comment -> { + schema.addProperty("title", comment); + }); return DataResult.success(new Holder<>(schema)); } @@ -120,4 +109,20 @@ private JsonObject copy(JsonObject object) { } return copy; } + + private static final JsonObject OBJECT = new JsonObject(); + private static final JsonObject NUMBER = new JsonObject(); + private static final JsonObject STRING = new JsonObject(); + private static final JsonObject BOOLEAN = new JsonObject(); + private static final JsonObject INTEGER = new JsonObject(); + private static final JsonObject ARRAY = new JsonObject(); + + static { + OBJECT.addProperty("type", "object"); + NUMBER.addProperty("type", "number"); + STRING.addProperty("type", "string"); + BOOLEAN.addProperty("type", "boolean"); + INTEGER.addProperty("type", "integer"); + ARRAY.addProperty("type", "array"); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 3f81958..336f61d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -5,7 +5,6 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; - import java.util.List; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 580a22d..7379180 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,12 +4,11 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; -import org.jspecify.annotations.Nullable; - import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; public interface Structure { DataResult> interpret(Interpreter interpreter); diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 26de83a..3f7fa2f 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -11,14 +11,13 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { public StreamCodecInterpreter(Keys> keys) { From a8192e3c9ff98a8fe2b0a87f75a4d478fb2f7338 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 4 Jul 2024 21:12:06 -0500 Subject: [PATCH 12/76] Refactor where description and title annotation keys are stored and add javadoc --- .../codecextras/structured/Annotations.java | 11 +++++++++++ .../codecextras/structured/JsonSchemaInterpreter.java | 7 ++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java index b1d43e6..1e777a4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java @@ -6,7 +6,18 @@ import java.util.Optional; public final class Annotations { + /** + * A comment that a field in a structure should be serialized with. + */ public static final Key COMMENT = Key.create("comment"); + /** + * A human-readable title for a part of a structure. + */ + public static final Key TITLE = Key.create("title"); + /** + * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. + */ + public static final Key DESCRIPTION = Key.create("description"); @SuppressWarnings("unchecked") public Optional get(Key key) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 7da9319..3e4843e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -11,9 +11,6 @@ import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { - public static final Key TITLE = Key.create("title"); - public static final Key DESCRIPTION = Key.create("description"); - public JsonSchemaInterpreter(Keys keys) { super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(OBJECT)) @@ -77,10 +74,10 @@ public DataResult> flatXmap(App input, Fu @Override public DataResult> annotate(App input, Annotations annotations) { var schema = copy(unbox(input)); - annotations.get(DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { + annotations.get(Annotations.DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { schema.addProperty("description", comment); }); - annotations.get(TITLE).ifPresent(comment -> { + annotations.get(Annotations.TITLE).ifPresent(comment -> { schema.addProperty("title", comment); }); return DataResult.success(new Holder<>(schema)); From 829bf5cd417d413f184049acb34f52f7d8c6de21 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 7 Jul 2024 12:16:23 -0500 Subject: [PATCH 13/76] Update formatting --- .../codecextras/comments/CommentMapCodec.java | 208 ++++++------- .../codecextras/structured/Annotations.java | 94 +++--- .../structured/CodecInterpreter.java | 96 +++--- .../codecextras/structured/Interpreter.java | 28 +- .../structured/JsonSchemaInterpreter.java | 222 +++++++------- .../codecextras/structured/Key.java | 22 +- .../structured/KeyStoringInterpreter.java | 16 +- .../codecextras/structured/Keys.java | 86 +++--- .../structured/MapCodecInterpreter.java | 94 +++--- .../structured/RecordStructure.java | 286 +++++++++--------- .../codecextras/structured/Structure.java | 204 ++++++------- .../structured/StructuredMapCodec.java | 142 ++++----- .../structured/StreamCodecInterpreter.java | 244 +++++++-------- .../codecextras/test/CodecAssertions.java | 54 ++-- .../test/structured/TestStructured.java | 104 +++---- 15 files changed, 950 insertions(+), 950 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java index e089bd9..e801c2d 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java @@ -10,108 +10,108 @@ import java.util.stream.Stream; public final class CommentMapCodec extends MapCodec { - private final MapCodec delegate; - private final Map comments; - - private CommentMapCodec(MapCodec delegate, Map comments) { - this.delegate = delegate; - this.comments = comments; - } - - public static MapCodec of(MapCodec codec, Map comments) { - if (codec instanceof CommentMapCodec commentMapCodec) { - Map allComments = commentMapCodec.comments; - allComments.putAll(comments); - return new CommentMapCodec<>(commentMapCodec.delegate, allComments); - } - return new CommentMapCodec<>(codec, comments); - } - - public static MapCodec of(MapCodec codec, String comment) { - Map map = codec.keys(JsonOps.INSTANCE) - .filter(json -> json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) - .map(json -> json.getAsJsonPrimitive().getAsString()) - .collect(Collectors.toMap(Function.identity(), s -> comment, (a, b) -> a)); - return of(codec, map); - } - - @Override - public Stream keys(DynamicOps ops) { - return delegate.keys(ops); - } - - @Override - public DataResult decode(DynamicOps ops, MapLike input) { - return delegate.decode(ops, input); - } - - @Override - public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { - final RecordBuilder builder = delegate.encode(input, ops, prefix); - - return new RecordBuilder<>() { - RecordBuilder mutableBuilder = builder; - - @Override - public DynamicOps ops() { - return builder.ops(); - } - - @Override - public RecordBuilder add(T key, T value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder add(T key, DataResult value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder add(DataResult key, DataResult value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder withErrorsFrom(DataResult result) { - mutableBuilder = mutableBuilder.withErrorsFrom(result); - return this; - } - - @Override - public RecordBuilder setLifecycle(Lifecycle lifecycle) { - mutableBuilder = mutableBuilder.setLifecycle(lifecycle); - return this; - } - - @Override - public RecordBuilder mapError(UnaryOperator onError) { - mutableBuilder = mutableBuilder.mapError(onError); - return this; - } - - @Override - public DataResult build(T prefix) { - DataResult built = builder.build(prefix); - if (this.ops() instanceof AccompaniedOps accompaniedOps) { - Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); - if (commentOps.isPresent()) { - return built.flatMap(t -> - commentOps.get().commentToMap(t, comments.entrySet().stream().collect(Collectors.toMap(e -> - ops.createString(e.getKey()), e -> ops.createString(e.getValue())))) - ); - } - } - return built; - } - }; - } - - @Override - public String toString() { - return "CommentMapCodec["+delegate+"]"; - } + private final MapCodec delegate; + private final Map comments; + + private CommentMapCodec(MapCodec delegate, Map comments) { + this.delegate = delegate; + this.comments = comments; + } + + public static MapCodec of(MapCodec codec, Map comments) { + if (codec instanceof CommentMapCodec commentMapCodec) { + Map allComments = commentMapCodec.comments; + allComments.putAll(comments); + return new CommentMapCodec<>(commentMapCodec.delegate, allComments); + } + return new CommentMapCodec<>(codec, comments); + } + + public static MapCodec of(MapCodec codec, String comment) { + Map map = codec.keys(JsonOps.INSTANCE) + .filter(json -> json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) + .map(json -> json.getAsJsonPrimitive().getAsString()) + .collect(Collectors.toMap(Function.identity(), s -> comment, (a, b) -> a)); + return of(codec, map); + } + + @Override + public Stream keys(DynamicOps ops) { + return delegate.keys(ops); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + return delegate.decode(ops, input); + } + + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + final RecordBuilder builder = delegate.encode(input, ops, prefix); + + return new RecordBuilder<>() { + RecordBuilder mutableBuilder = builder; + + @Override + public DynamicOps ops() { + return builder.ops(); + } + + @Override + public RecordBuilder add(T key, T value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder add(T key, DataResult value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder add(DataResult key, DataResult value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder withErrorsFrom(DataResult result) { + mutableBuilder = mutableBuilder.withErrorsFrom(result); + return this; + } + + @Override + public RecordBuilder setLifecycle(Lifecycle lifecycle) { + mutableBuilder = mutableBuilder.setLifecycle(lifecycle); + return this; + } + + @Override + public RecordBuilder mapError(UnaryOperator onError) { + mutableBuilder = mutableBuilder.mapError(onError); + return this; + } + + @Override + public DataResult build(T prefix) { + DataResult built = builder.build(prefix); + if (this.ops() instanceof AccompaniedOps accompaniedOps) { + Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); + if (commentOps.isPresent()) { + return built.flatMap(t -> + commentOps.get().commentToMap(t, comments.entrySet().stream().collect(Collectors.toMap(e -> + ops.createString(e.getKey()), e -> ops.createString(e.getValue())))) + ); + } + } + return built; + } + }; + } + + @Override + public String toString() { + return "CommentMapCodec["+delegate+"]"; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java index 1e777a4..9a40c73 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java @@ -6,62 +6,62 @@ import java.util.Optional; public final class Annotations { - /** - * A comment that a field in a structure should be serialized with. - */ - public static final Key COMMENT = Key.create("comment"); - /** - * A human-readable title for a part of a structure. - */ - public static final Key TITLE = Key.create("title"); - /** - * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. - */ - public static final Key DESCRIPTION = Key.create("description"); + /** + * A comment that a field in a structure should be serialized with. + */ + public static final Key COMMENT = Key.create("comment"); + /** + * A human-readable title for a part of a structure. + */ + public static final Key TITLE = Key.create("title"); + /** + * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. + */ + public static final Key DESCRIPTION = Key.create("description"); - @SuppressWarnings("unchecked") - public Optional get(Key key) { - return Optional.ofNullable((A) keys.get(key)); - } + @SuppressWarnings("unchecked") + public Optional get(Key key) { + return Optional.ofNullable((A) keys.get(key)); + } - public static Keys.Builder builder() { - return new Keys.Builder<>(); - } + public static Keys.Builder builder() { + return new Keys.Builder<>(); + } - public Annotations join(Annotations other) { - var map = new IdentityHashMap<>(this.keys); - map.putAll(other.keys); - return new Annotations(map); - } + public Annotations join(Annotations other) { + var map = new IdentityHashMap<>(this.keys); + map.putAll(other.keys); + return new Annotations(map); + } - public Annotations with(Key key, A value) { - var map = new IdentityHashMap<>(this.keys); - map.put(key, value); - return new Annotations(map); - } + public Annotations with(Key key, A value) { + var map = new IdentityHashMap<>(this.keys); + map.put(key, value); + return new Annotations(map); + } - public static Annotations empty() { - return EMPTY; - } + public static Annotations empty() { + return EMPTY; + } - public final static class Builder { - private final Map, Object> keys = new IdentityHashMap<>(); + public final static class Builder { + private final Map, Object> keys = new IdentityHashMap<>(); - public Builder add(Key key, A value) { - keys.put(key, value); - return this; - } + public Builder add(Key key, A value) { + keys.put(key, value); + return this; + } - public Annotations build() { - return new Annotations(new IdentityHashMap<>(keys)); - } - } + public Annotations build() { + return new Annotations(new IdentityHashMap<>(keys)); + } + } - private static final Annotations EMPTY = new Annotations(new IdentityHashMap<>()); + private static final Annotations EMPTY = new Annotations(new IdentityHashMap<>()); - private final IdentityHashMap, Object> keys; + private final IdentityHashMap, Object> keys; - private Annotations(IdentityHashMap, Object> keys) { - this.keys = keys; - } + private Annotations(IdentityHashMap, Object> keys) { + this.keys = keys; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index b754676..50bddb7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -10,61 +10,61 @@ import java.util.function.Function; public class CodecInterpreter extends KeyStoringInterpreter { - public CodecInterpreter(Keys keys) { - super(keys.join(Keys.builder() - .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) - .add(Interpreter.BOOL, new Holder<>(Codec.BOOL)) - .add(Interpreter.BYTE, new Holder<>(Codec.BYTE)) - .add(Interpreter.SHORT, new Holder<>(Codec.SHORT)) - .add(Interpreter.INT, new Holder<>(Codec.INT)) - .add(Interpreter.LONG, new Holder<>(Codec.LONG)) - .add(Interpreter.FLOAT, new Holder<>(Codec.FLOAT)) - .add(Interpreter.DOUBLE, new Holder<>(Codec.DOUBLE)) - .add(Interpreter.STRING, new Holder<>(Codec.STRING)) - .build() - )); - } + public CodecInterpreter(Keys keys) { + super(keys.join(Keys.builder() + .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) + .add(Interpreter.BOOL, new Holder<>(Codec.BOOL)) + .add(Interpreter.BYTE, new Holder<>(Codec.BYTE)) + .add(Interpreter.SHORT, new Holder<>(Codec.SHORT)) + .add(Interpreter.INT, new Holder<>(Codec.INT)) + .add(Interpreter.LONG, new Holder<>(Codec.LONG)) + .add(Interpreter.FLOAT, new Holder<>(Codec.FLOAT)) + .add(Interpreter.DOUBLE, new Holder<>(Codec.DOUBLE)) + .add(Interpreter.STRING, new Holder<>(Codec.STRING)) + .build() + )); + } - public CodecInterpreter() { - this(Keys.builder().build()); - } + public CodecInterpreter() { + this(Keys.builder().build()); + } - @Override - public DataResult>> list(App single) { - return DataResult.success(new Holder<>(CommentFirstListCodec.of(Holder.unbox(single).codec))); - } + @Override + public DataResult>> list(App single) { + return DataResult.success(new Holder<>(CommentFirstListCodec.of(Holder.unbox(single).codec))); + } - @Override - public DataResult> record(List> fields, Function creator) { - return StructuredMapCodec.of(fields, creator, this, CodecInterpreter::unbox) - .map(mapCodec -> new Holder<>(mapCodec.codec())); - } + @Override + public DataResult> record(List> fields, Function creator) { + return StructuredMapCodec.of(fields, creator, this, CodecInterpreter::unbox) + .map(mapCodec -> new Holder<>(mapCodec.codec())); + } - @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - var codec = Holder.unbox(input).codec(); - return DataResult.success(new Holder<>(codec.flatXmap(deserializer, serializer))); - } + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + var codec = Holder.unbox(input).codec(); + return DataResult.success(new Holder<>(codec.flatXmap(deserializer, serializer))); + } - @Override - public DataResult> annotate(App input, Annotations annotations) { - // No annotations handled here - return DataResult.success(input); - } + @Override + public DataResult> annotate(App input, Annotations annotations) { + // No annotations handled here + return DataResult.success(input); + } - public static Codec unbox(App box) { - return Holder.unbox(box).codec(); - } + public static Codec unbox(App box) { + return Holder.unbox(box).codec(); + } - public DataResult> interpret(Structure structure) { - return structure.interpret(this).map(CodecInterpreter::unbox); - } + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(CodecInterpreter::unbox); + } - public record Holder(Codec codec) implements App { - public static final class Mu implements K1 {} + public record Holder(Codec codec) implements App { + public static final class Mu implements K1 {} - static Holder unbox(App box) { - return (Holder) box; - } - } + static Holder unbox(App box) { + return (Holder) box; + } + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 090e976..3c6d13a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -8,23 +8,23 @@ import java.util.function.Function; public interface Interpreter { - DataResult>> list(App single); + DataResult>> list(App single); - DataResult> keyed(Key key); + DataResult> keyed(Key key); - DataResult> record(List> fields, Function creator); + DataResult> record(List> fields, Function creator); - DataResult> flatXmap(App input, Function> deserializer, Function> serializer); + DataResult> flatXmap(App input, Function> deserializer, Function> serializer); - DataResult> annotate(App input, Annotations annotations); + DataResult> annotate(App input, Annotations annotations); - Key UNIT = Key.create("UNIT"); - Key BOOL = Key.create("BOOL"); - Key BYTE = Key.create("BYTE"); - Key SHORT = Key.create("SHORT"); - Key INT = Key.create("INT"); - Key LONG = Key.create("LONG"); - Key FLOAT = Key.create("FLOAT"); - Key DOUBLE = Key.create("DOUBLE"); - Key STRING = Key.create("STRING"); + Key UNIT = Key.create("UNIT"); + Key BOOL = Key.create("BOOL"); + Key BYTE = Key.create("BYTE"); + Key SHORT = Key.create("SHORT"); + Key INT = Key.create("INT"); + Key LONG = Key.create("LONG"); + Key FLOAT = Key.create("FLOAT"); + Key DOUBLE = Key.create("DOUBLE"); + Key STRING = Key.create("STRING"); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 3e4843e..57bd2f0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -11,115 +11,115 @@ import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { - public JsonSchemaInterpreter(Keys keys) { - super(keys.join(Keys.builder() - .add(Interpreter.UNIT, new Holder<>(OBJECT)) - .add(Interpreter.BOOL, new Holder<>(BOOLEAN)) - .add(Interpreter.BYTE, new Holder<>(INTEGER)) - .add(Interpreter.SHORT, new Holder<>(INTEGER)) - .add(Interpreter.INT, new Holder<>(INTEGER)) - .add(Interpreter.LONG, new Holder<>(INTEGER)) - .add(Interpreter.FLOAT, new Holder<>(NUMBER)) - .add(Interpreter.DOUBLE, new Holder<>(NUMBER)) - .add(Interpreter.STRING, new Holder<>(STRING)) - .build() - )); - } - - public JsonSchemaInterpreter() { - this(Keys.builder().build()); - } - - @Override - public DataResult>> list(App single) { - var object = copy(ARRAY); - object.add("items", unbox(single)); - return DataResult.success(new Holder<>(object)); - } - - @Override - public DataResult> record(List> fields, Function creator) { - var object = copy(OBJECT); - var properties = new JsonObject(); - var required = new JsonArray(); - for (RecordStructure.Field field : fields) { - Supplier error = singleField(field, properties, required); - if (error != null) { - return DataResult.error(error); - } - } - object.add("properties", properties); - object.add("required", required); - return DataResult.success(new Holder<>(object)); - } - - private @Nullable Supplier singleField(RecordStructure.Field field, JsonObject properties, JsonArray required) { - var partialResolt = field.structure().interpret(this); - if (partialResolt.isError()) { - return partialResolt.error().orElseThrow().messageSupplier(); - } - var fieldObject = copy(unbox(partialResolt.result().orElseThrow())); - - field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); - - properties.add(field.name(), fieldObject); - return null; - } - - @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - return DataResult.success(new Holder<>(unbox(input))); - } - - @Override - public DataResult> annotate(App input, Annotations annotations) { - var schema = copy(unbox(input)); - annotations.get(Annotations.DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { - schema.addProperty("description", comment); - }); - annotations.get(Annotations.TITLE).ifPresent(comment -> { - schema.addProperty("title", comment); - }); - return DataResult.success(new Holder<>(schema)); - } - - public static JsonObject unbox(App box) { - return Holder.unbox(box).jsonObject; - } - - public DataResult interpret(Structure structure) { - return structure.interpret(this).map(JsonSchemaInterpreter::unbox); - } - - public record Holder(JsonObject jsonObject) implements App { - public static final class Mu implements K1 {} - - static Holder unbox(App box) { - return (Holder) box; - } - } - - private JsonObject copy(JsonObject object) { - JsonObject copy = new JsonObject(); - for (String key : object.keySet()) { - copy.add(key, object.get(key)); - } - return copy; - } - - private static final JsonObject OBJECT = new JsonObject(); - private static final JsonObject NUMBER = new JsonObject(); - private static final JsonObject STRING = new JsonObject(); - private static final JsonObject BOOLEAN = new JsonObject(); - private static final JsonObject INTEGER = new JsonObject(); - private static final JsonObject ARRAY = new JsonObject(); - - static { - OBJECT.addProperty("type", "object"); - NUMBER.addProperty("type", "number"); - STRING.addProperty("type", "string"); - BOOLEAN.addProperty("type", "boolean"); - INTEGER.addProperty("type", "integer"); - ARRAY.addProperty("type", "array"); - } + public JsonSchemaInterpreter(Keys keys) { + super(keys.join(Keys.builder() + .add(Interpreter.UNIT, new Holder<>(OBJECT)) + .add(Interpreter.BOOL, new Holder<>(BOOLEAN)) + .add(Interpreter.BYTE, new Holder<>(INTEGER)) + .add(Interpreter.SHORT, new Holder<>(INTEGER)) + .add(Interpreter.INT, new Holder<>(INTEGER)) + .add(Interpreter.LONG, new Holder<>(INTEGER)) + .add(Interpreter.FLOAT, new Holder<>(NUMBER)) + .add(Interpreter.DOUBLE, new Holder<>(NUMBER)) + .add(Interpreter.STRING, new Holder<>(STRING)) + .build() + )); + } + + public JsonSchemaInterpreter() { + this(Keys.builder().build()); + } + + @Override + public DataResult>> list(App single) { + var object = copy(ARRAY); + object.add("items", unbox(single)); + return DataResult.success(new Holder<>(object)); + } + + @Override + public DataResult> record(List> fields, Function creator) { + var object = copy(OBJECT); + var properties = new JsonObject(); + var required = new JsonArray(); + for (RecordStructure.Field field : fields) { + Supplier error = singleField(field, properties, required); + if (error != null) { + return DataResult.error(error); + } + } + object.add("properties", properties); + object.add("required", required); + return DataResult.success(new Holder<>(object)); + } + + private @Nullable Supplier singleField(RecordStructure.Field field, JsonObject properties, JsonArray required) { + var partialResolt = field.structure().interpret(this); + if (partialResolt.isError()) { + return partialResolt.error().orElseThrow().messageSupplier(); + } + var fieldObject = copy(unbox(partialResolt.result().orElseThrow())); + + field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); + + properties.add(field.name(), fieldObject); + return null; + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + return DataResult.success(new Holder<>(unbox(input))); + } + + @Override + public DataResult> annotate(App input, Annotations annotations) { + var schema = copy(unbox(input)); + annotations.get(Annotations.DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { + schema.addProperty("description", comment); + }); + annotations.get(Annotations.TITLE).ifPresent(comment -> { + schema.addProperty("title", comment); + }); + return DataResult.success(new Holder<>(schema)); + } + + public static JsonObject unbox(App box) { + return Holder.unbox(box).jsonObject; + } + + public DataResult interpret(Structure structure) { + return structure.interpret(this).map(JsonSchemaInterpreter::unbox); + } + + public record Holder(JsonObject jsonObject) implements App { + public static final class Mu implements K1 {} + + static Holder unbox(App box) { + return (Holder) box; + } + } + + private JsonObject copy(JsonObject object) { + JsonObject copy = new JsonObject(); + for (String key : object.keySet()) { + copy.add(key, object.get(key)); + } + return copy; + } + + private static final JsonObject OBJECT = new JsonObject(); + private static final JsonObject NUMBER = new JsonObject(); + private static final JsonObject STRING = new JsonObject(); + private static final JsonObject BOOLEAN = new JsonObject(); + private static final JsonObject INTEGER = new JsonObject(); + private static final JsonObject ARRAY = new JsonObject(); + + static { + OBJECT.addProperty("type", "object"); + NUMBER.addProperty("type", "number"); + STRING.addProperty("type", "string"); + BOOLEAN.addProperty("type", "boolean"); + INTEGER.addProperty("type", "integer"); + ARRAY.addProperty("type", "array"); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key.java b/src/main/java/dev/lukebemish/codecextras/structured/Key.java index d8440e5..5cb84c4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Key.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key.java @@ -1,18 +1,18 @@ package dev.lukebemish.codecextras.structured; public final class Key { - private final String name; + private final String name; - private Key(String name) { - this.name = name; - } + private Key(String name) { + this.name = name; + } - public static Key create(String name) { - var className = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName(); - return new Key<>(className + ":" + name); - } + public static Key create(String name) { + var className = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName(); + return new Key<>(className + ":" + name); + } - public String name() { - return name; - } + public String name() { + return name; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java index bc3bd62..8037cfa 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java @@ -5,14 +5,14 @@ import com.mojang.serialization.DataResult; public abstract class KeyStoringInterpreter implements Interpreter { - private final Keys keys; + private final Keys keys; - protected KeyStoringInterpreter(Keys keys) { - this.keys = keys; - } + protected KeyStoringInterpreter(Keys keys) { + this.keys = keys; + } - @Override - public DataResult> keyed(Key key) { - return keys.get(key).map(DataResult::success).orElse(DataResult.error(() -> "Unknown key "+key.name())); - } + @Override + public DataResult> keyed(Key key) { + return keys.get(key).map(DataResult::success).orElse(DataResult.error(() -> "Unknown key "+key.name())); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index 396c781..4d7f360 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -7,47 +7,47 @@ import java.util.Optional; public final class Keys { - private final IdentityHashMap, App> keys; - - private Keys(IdentityHashMap, App> keys) { - this.keys = keys; - } - - @SuppressWarnings("unchecked") - public Optional> get(Key key) { - return Optional.ofNullable((App) keys.get(key)); - } - - public Keys map(Converter converter) { - var map = new IdentityHashMap, App>(); - keys.forEach((key, value) -> map.put(key, converter.convert(value))); - return new Keys<>(map); - } - - public interface Converter { - App convert(App input); - } - - public static Builder builder() { - return new Builder<>(); - } - - public Keys join(Keys other) { - var map = new IdentityHashMap<>(this.keys); - map.putAll(other.keys); - return new Keys<>(map); - } - - public final static class Builder { - private final Map, App> keys = new IdentityHashMap<>(); - - public Builder add(Key key, App value) { - keys.put(key, value); - return this; - } - - public Keys build() { - return new Keys<>(new IdentityHashMap<>(keys)); - } - } + private final IdentityHashMap, App> keys; + + private Keys(IdentityHashMap, App> keys) { + this.keys = keys; + } + + @SuppressWarnings("unchecked") + public Optional> get(Key key) { + return Optional.ofNullable((App) keys.get(key)); + } + + public Keys map(Converter converter) { + var map = new IdentityHashMap, App>(); + keys.forEach((key, value) -> map.put(key, converter.convert(value))); + return new Keys<>(map); + } + + public interface Converter { + App convert(App input); + } + + public static Builder builder() { + return new Builder<>(); + } + + public Keys join(Keys other) { + var map = new IdentityHashMap<>(this.keys); + map.putAll(other.keys); + return new Keys<>(map); + } + + public final static class Builder { + private final Map, App> keys = new IdentityHashMap<>(); + + public Builder add(Key key, App value) { + keys.put(key, value); + return this; + } + + public Keys build() { + return new Keys<>(new IdentityHashMap<>(keys)); + } + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 336f61d..6d974bd 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -9,61 +9,61 @@ import java.util.function.Function; public class MapCodecInterpreter extends KeyStoringInterpreter { - private final CodecInterpreter codecInterpreter; + private final CodecInterpreter codecInterpreter; - public MapCodecInterpreter(Keys keys, Keys codecKeys) { - super(keys); - this.codecInterpreter = new CodecInterpreter(codecKeys.join(keys.map(new Keys.Converter<>() { - @Override - public App convert(App app) { - return new CodecInterpreter.Holder<>(unbox(app).codec()); - } - }))); - } + public MapCodecInterpreter(Keys keys, Keys codecKeys) { + super(keys); + this.codecInterpreter = new CodecInterpreter(codecKeys.join(keys.map(new Keys.Converter<>() { + @Override + public App convert(App app) { + return new CodecInterpreter.Holder<>(unbox(app).codec()); + } + }))); + } - public MapCodecInterpreter() { - this(Keys.builder().build(), Keys.builder().build()); - } + public MapCodecInterpreter() { + this(Keys.builder().build(), Keys.builder().build()); + } - @Override - public DataResult>> list(App single) { - return DataResult.error(() -> "Cannot make a MapCodec for a list"); - } + @Override + public DataResult>> list(App single) { + return DataResult.error(() -> "Cannot make a MapCodec for a list"); + } - @Override - public DataResult> record(List> fields, Function creator) { - return StructuredMapCodec.of(fields, creator, codecInterpreter, CodecInterpreter::unbox) - .map(Holder::new); - } + @Override + public DataResult> record(List> fields, Function creator) { + return StructuredMapCodec.of(fields, creator, codecInterpreter, CodecInterpreter::unbox) + .map(Holder::new); + } - @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - var mapCodec = unbox(input); - return DataResult.success(new Holder<>(mapCodec.flatXmap(deserializer, serializer))); - } + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + var mapCodec = unbox(input); + return DataResult.success(new Holder<>(mapCodec.flatXmap(deserializer, serializer))); + } - @Override - public DataResult> annotate(App input, Annotations annotations) { - var mapCodec = new Object() { - MapCodec m = unbox(input); - }; - mapCodec.m = annotations.get(Annotations.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); - return DataResult.success(new Holder<>(mapCodec.m)); - } + @Override + public DataResult> annotate(App input, Annotations annotations) { + var mapCodec = new Object() { + MapCodec m = unbox(input); + }; + mapCodec.m = annotations.get(Annotations.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); + return DataResult.success(new Holder<>(mapCodec.m)); + } - public static MapCodec unbox(App box) { - return Holder.unbox(box).mapCodec(); - } + public static MapCodec unbox(App box) { + return Holder.unbox(box).mapCodec(); + } - public DataResult> interpret(Structure structure) { - return structure.interpret(this).map(MapCodecInterpreter::unbox); - } + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(MapCodecInterpreter::unbox); + } - public record Holder(MapCodec mapCodec) implements App { - public static final class Mu implements K1 {} + public record Holder(MapCodec mapCodec) implements App { + public static final class Mu implements K1 {} - static Holder unbox(App box) { - return (Holder) box; - } - } + static Holder unbox(App box) { + return (Holder) box; + } + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index ec80a2a..fbf522d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -13,147 +13,147 @@ import java.util.function.Supplier; public class RecordStructure { - private final List> fields = new ArrayList<>(); - private final Set fieldNames = new HashSet<>(); - private int count = 0; - - public static final class Container { - private final Key[] keys; - private final Object[] array; - - private Container(Key[] keys, Object[] array) { - this.array = array; - this.keys = keys; - } - - @SuppressWarnings("unchecked") - private T get(Key key) { - if (key.count >= array.length || key != keys[key.count]) { - throw new IllegalArgumentException("Key does not belong to the container"); - } - return (T) array[key.count]; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private final List values = new ArrayList<>(); - private final List> keys = new ArrayList<>(); - - private Builder() {} - - public void add(Key key, T value) { - keys.add(key); - values.add(value); - } - - public Container build() { - return new Container(keys.toArray(new Key[0]), values.toArray()); - } - } - } - - public static final class Key implements Function { - private final int count; - - private Key(int i) { - this.count = i; - } - - @Override - public T apply(Container container) { - return container.get(this); - } - } - - public interface Field { - String name(); - - Structure structure(); - - Function getter(); - - Optional> missingBehavior(); - - Key key(); - - interface MissingBehavior { - Supplier missing(); - Predicate predicate(); - } - } - - private record MissingBehaviorImpl(Supplier missing, Predicate predicate) implements Field.MissingBehavior {} - - private record FieldImpl(String name, Structure structure, Function getter, Optional> missingBehavior, Key key) implements Field {} - - public Key add(String name, Structure structure, Function getter) { - var key = new Key(count); - count++; - fields.add(new FieldImpl<>(name, structure, getter, Optional.empty(), key)); - fieldNames.add(name); - return key; - } - - public Key> addOptional(String name, Structure structure, Function> getter) { - var key = new Key>(count); - count++; - fields.add(new FieldImpl<>( - name, - structure.flatXmap( - t -> DataResult.success(Optional.of(t)), - o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() ->"Optional default value not handled by interpreter")) - ), - getter, - Optional.of(new MissingBehaviorImpl<>(Optional::empty, Optional::isPresent)), - key - )); - fieldNames.add(name); - return key; - } - - public Key addOptional(String name, Structure structure, Function getter, Supplier defaultValue) { - var key = new Key(count); - count++; - fields.add(new FieldImpl<>(name, structure, getter, Optional.of(new MissingBehaviorImpl<>(defaultValue, t -> !t.equals(defaultValue))), key)); - fieldNames.add(name); - return key; - } - - public Function add(RecordStructure.Builder part, Function getter) { - RecordStructure partial = new RecordStructure<>(); - partial.count = this.count; - var creator = part.build(partial); - for (var field : partial.fields) { - partialField(getter, field); - } - return creator; - } - - private void partialField(Function getter, Field field) { - if (fieldNames.contains(field.name())) { - throw new IllegalArgumentException("Duplicate field name: " + field.name()); - } - fields.add(new FieldImpl<>(field.name(), field.structure(), a -> field.getter().apply(getter.apply(a)), field.missingBehavior(), field.key())); - count++; - fieldNames.add(field.name()); - } - - static Structure create(RecordStructure.Builder builder) { - RecordStructure instance = new RecordStructure<>(); - var creator = builder.build(instance); - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return interpreter.record(instance.fields, creator); - } - }; - } - - @FunctionalInterface - public interface Builder { - Function build(RecordStructure builder); - } + private final List> fields = new ArrayList<>(); + private final Set fieldNames = new HashSet<>(); + private int count = 0; + + public static final class Container { + private final Key[] keys; + private final Object[] array; + + private Container(Key[] keys, Object[] array) { + this.array = array; + this.keys = keys; + } + + @SuppressWarnings("unchecked") + private T get(Key key) { + if (key.count >= array.length || key != keys[key.count]) { + throw new IllegalArgumentException("Key does not belong to the container"); + } + return (T) array[key.count]; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final List values = new ArrayList<>(); + private final List> keys = new ArrayList<>(); + + private Builder() {} + + public void add(Key key, T value) { + keys.add(key); + values.add(value); + } + + public Container build() { + return new Container(keys.toArray(new Key[0]), values.toArray()); + } + } + } + + public static final class Key implements Function { + private final int count; + + private Key(int i) { + this.count = i; + } + + @Override + public T apply(Container container) { + return container.get(this); + } + } + + public interface Field { + String name(); + + Structure structure(); + + Function getter(); + + Optional> missingBehavior(); + + Key key(); + + interface MissingBehavior { + Supplier missing(); + Predicate predicate(); + } + } + + private record MissingBehaviorImpl(Supplier missing, Predicate predicate) implements Field.MissingBehavior {} + + private record FieldImpl(String name, Structure structure, Function getter, Optional> missingBehavior, Key key) implements Field {} + + public Key add(String name, Structure structure, Function getter) { + var key = new Key(count); + count++; + fields.add(new FieldImpl<>(name, structure, getter, Optional.empty(), key)); + fieldNames.add(name); + return key; + } + + public Key> addOptional(String name, Structure structure, Function> getter) { + var key = new Key>(count); + count++; + fields.add(new FieldImpl<>( + name, + structure.flatXmap( + t -> DataResult.success(Optional.of(t)), + o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() ->"Optional default value not handled by interpreter")) + ), + getter, + Optional.of(new MissingBehaviorImpl<>(Optional::empty, Optional::isPresent)), + key + )); + fieldNames.add(name); + return key; + } + + public Key addOptional(String name, Structure structure, Function getter, Supplier defaultValue) { + var key = new Key(count); + count++; + fields.add(new FieldImpl<>(name, structure, getter, Optional.of(new MissingBehaviorImpl<>(defaultValue, t -> !t.equals(defaultValue))), key)); + fieldNames.add(name); + return key; + } + + public Function add(RecordStructure.Builder part, Function getter) { + RecordStructure partial = new RecordStructure<>(); + partial.count = this.count; + var creator = part.build(partial); + for (var field : partial.fields) { + partialField(getter, field); + } + return creator; + } + + private void partialField(Function getter, Field field) { + if (fieldNames.contains(field.name())) { + throw new IllegalArgumentException("Duplicate field name: " + field.name()); + } + fields.add(new FieldImpl<>(field.name(), field.structure(), a -> field.getter().apply(getter.apply(a)), field.missingBehavior(), field.key())); + count++; + fieldNames.add(field.name()); + } + + static Structure create(RecordStructure.Builder builder) { + RecordStructure instance = new RecordStructure<>(); + var creator = builder.build(instance); + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.record(instance.fields, creator); + } + }; + } + + @FunctionalInterface + public interface Builder { + Function build(RecordStructure builder); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 7379180..7117d07 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -11,106 +11,106 @@ import org.jspecify.annotations.Nullable; public interface Structure { - DataResult> interpret(Interpreter interpreter); - default Annotations annotations() { - return Annotations.empty(); - } - - default Structure annotate(Key key, T value) { - var outer = this; - var annotations = annotations().with(key, value); - return annotatedDelegatingStructure(outer, annotations); - } - - default Structure annotate(Annotations annotations) { - var outer = this; - var combined = annotations().join(annotations); - return annotatedDelegatingStructure(outer, combined); - } - - private static Structure annotatedDelegatingStructure(Structure outer, Annotations annotations) { - final class AnnotatedDelegatingStructure implements Structure { - final @Nullable AnnotatedDelegatingStructure delegate; - - AnnotatedDelegatingStructure(@Nullable AnnotatedDelegatingStructure delegate) { - this.delegate = delegate; - } - - @Override - public DataResult> interpret(Interpreter interpreter) { - var result = interpretNoAnnotations(interpreter); - return result.flatMap(r -> interpreter.annotate(r, annotations)); - } - - private DataResult> interpretNoAnnotations(Interpreter interpreter) { - return delegate != null ? delegate.interpretNoAnnotations(interpreter) : outer.interpret(interpreter); - } - - @Override - public Annotations annotations() { - return annotations; - } - } - - return new AnnotatedDelegatingStructure(outer instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure ? annotatedDelegatingStructure : null); - } - - default Structure> listOf() { - var outer = this; - return new Structure<>() { - @Override - public DataResult>> interpret(Interpreter interpreter) { - return outer.interpret(interpreter).flatMap(interpreter::list); - } - }; - } - - default RecordStructure.Builder fieldOf(String name) { - return builder -> builder.add(name, this, Function.identity()); - } - - default RecordStructure.Builder> optionalFieldOf(String name) { - return builder -> builder.addOptional(name, this, Function.identity()); - } - - default RecordStructure.Builder optionalFieldOf(String name, Supplier defaultValue) { - return builder -> builder.addOptional(name, this, Function.identity(), defaultValue); - } - - default Structure flatXmap(Function> deserializer, Function> serializer) { - var outer = this; - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); - } - }; - } - - default Structure xmap(Function deserializer, Function serializer) { - return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); - } - - static Structure keyed(Key key) { - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return interpreter.keyed(key); - } - }; - } - - static Structure record(RecordStructure.Builder builder) { - return RecordStructure.create(builder); - } - - Structure UNIT = keyed(Interpreter.UNIT); - Structure BOOL = keyed(Interpreter.BOOL); - Structure BYTE = keyed(Interpreter.BYTE); - Structure SHORT = keyed(Interpreter.SHORT); - Structure INT = keyed(Interpreter.INT); - Structure LONG = keyed(Interpreter.LONG); - Structure FLOAT = keyed(Interpreter.FLOAT); - Structure DOUBLE = keyed(Interpreter.DOUBLE); - Structure STRING = keyed(Interpreter.STRING); + DataResult> interpret(Interpreter interpreter); + default Annotations annotations() { + return Annotations.empty(); + } + + default Structure annotate(Key key, T value) { + var outer = this; + var annotations = annotations().with(key, value); + return annotatedDelegatingStructure(outer, annotations); + } + + default Structure annotate(Annotations annotations) { + var outer = this; + var combined = annotations().join(annotations); + return annotatedDelegatingStructure(outer, combined); + } + + private static Structure annotatedDelegatingStructure(Structure outer, Annotations annotations) { + final class AnnotatedDelegatingStructure implements Structure { + final @Nullable AnnotatedDelegatingStructure delegate; + + AnnotatedDelegatingStructure(@Nullable AnnotatedDelegatingStructure delegate) { + this.delegate = delegate; + } + + @Override + public DataResult> interpret(Interpreter interpreter) { + var result = interpretNoAnnotations(interpreter); + return result.flatMap(r -> interpreter.annotate(r, annotations)); + } + + private DataResult> interpretNoAnnotations(Interpreter interpreter) { + return delegate != null ? delegate.interpretNoAnnotations(interpreter) : outer.interpret(interpreter); + } + + @Override + public Annotations annotations() { + return annotations; + } + } + + return new AnnotatedDelegatingStructure(outer instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure ? annotatedDelegatingStructure : null); + } + + default Structure> listOf() { + var outer = this; + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + return outer.interpret(interpreter).flatMap(interpreter::list); + } + }; + } + + default RecordStructure.Builder fieldOf(String name) { + return builder -> builder.add(name, this, Function.identity()); + } + + default RecordStructure.Builder> optionalFieldOf(String name) { + return builder -> builder.addOptional(name, this, Function.identity()); + } + + default RecordStructure.Builder optionalFieldOf(String name, Supplier defaultValue) { + return builder -> builder.addOptional(name, this, Function.identity(), defaultValue); + } + + default Structure flatXmap(Function> deserializer, Function> serializer) { + var outer = this; + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); + } + }; + } + + default Structure xmap(Function deserializer, Function serializer) { + return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); + } + + static Structure keyed(Key key) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.keyed(key); + } + }; + } + + static Structure record(RecordStructure.Builder builder) { + return RecordStructure.create(builder); + } + + Structure UNIT = keyed(Interpreter.UNIT); + Structure BOOL = keyed(Interpreter.BOOL); + Structure BYTE = keyed(Interpreter.BYTE); + Structure SHORT = keyed(Interpreter.SHORT); + Structure INT = keyed(Interpreter.INT); + Structure LONG = keyed(Interpreter.LONG); + Structure FLOAT = keyed(Interpreter.FLOAT); + Structure DOUBLE = keyed(Interpreter.DOUBLE); + Structure STRING = keyed(Interpreter.STRING); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 0164b60..7bf9997 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -17,86 +17,86 @@ import org.jspecify.annotations.Nullable; class StructuredMapCodec extends MapCodec { - private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} + private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} - private final List> fields; - private final Function creator; + private final List> fields; + private final Function creator; - private StructuredMapCodec(List> fields, Function creator) { - this.fields = fields; - this.creator = creator; - } + private StructuredMapCodec(List> fields, Function creator) { + this.fields = fields; + this.creator = creator; + } - public interface Unboxer { - Codec unbox(App box); - } + public interface Unboxer { + Codec unbox(App box); + } - public static DataResult> of(List> fields, Function creator, Interpreter interpreter, Unboxer unboxer) { - var mapCodecFields = new ArrayList>(); - for (var field : fields) { - DataResult> result = recordSingleField(field, mapCodecFields, interpreter, unboxer); - if (result != null) return result; - } - return DataResult.success(new StructuredMapCodec<>(mapCodecFields, creator)); - } + public static DataResult> of(List> fields, Function creator, Interpreter interpreter, Unboxer unboxer) { + var mapCodecFields = new ArrayList>(); + for (var field : fields) { + DataResult> result = recordSingleField(field, mapCodecFields, interpreter, unboxer); + if (result != null) return result; + } + return DataResult.success(new StructuredMapCodec<>(mapCodecFields, creator)); + } - private static @Nullable DataResult> recordSingleField(RecordStructure.Field field, ArrayList> mapCodecFields, Interpreter interpreter, Unboxer unboxer) { - var result = field.structure().interpret(interpreter); - if (result.error().isPresent()) { - return DataResult.error(result.error().orElseThrow().messageSupplier()); - } - Codec fieldCodec = unboxer.unbox(result.result().orElseThrow()); - MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) - .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) - .orElseGet(() -> makeFieldCodec(fieldCodec, field)); - mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); - return null; - } + private static @Nullable DataResult> recordSingleField(RecordStructure.Field field, ArrayList> mapCodecFields, Interpreter interpreter, Unboxer unboxer) { + var result = field.structure().interpret(interpreter); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + Codec fieldCodec = unboxer.unbox(result.result().orElseThrow()); + MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) + .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) + .orElseGet(() -> makeFieldCodec(fieldCodec, field)); + mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); + return null; + } - private static MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field) { - return field.missingBehavior().map(behavior -> fieldCodec.optionalFieldOf(field.name()).xmap( - optional -> optional.orElseGet(behavior.missing()), - value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() - )).orElseGet(() -> fieldCodec.fieldOf(field.name())); - } + private static MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field) { + return field.missingBehavior().map(behavior -> fieldCodec.optionalFieldOf(field.name()).xmap( + optional -> optional.orElseGet(behavior.missing()), + value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() + )).orElseGet(() -> fieldCodec.fieldOf(field.name())); + } - @Override - public Stream keys(DynamicOps ops) { - return fields.stream().flatMap(f -> f.codec().keys(ops)); - } + @Override + public Stream keys(DynamicOps ops) { + return fields.stream().flatMap(f -> f.codec().keys(ops)); + } - @Override - public DataResult decode(DynamicOps ops, MapLike input) { - var builder = RecordStructure.Container.builder(); - for (var field : fields) { - DataResult result = singleField(ops, input, field, builder); - if (result != null) return result; - } - return DataResult.success(creator.apply(builder.build())); - } + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + var builder = RecordStructure.Container.builder(); + for (var field : fields) { + DataResult result = singleField(ops, input, field, builder); + if (result != null) return result; + } + return DataResult.success(creator.apply(builder.build())); + } - private static @Nullable DataResult singleField(DynamicOps ops, MapLike input, Field field, RecordStructure.Container.Builder builder) { - var key = field.key(); - var codec = field.codec(); - var result = codec.decode(ops, input); - if (result.error().isPresent()) { - return DataResult.error(result.error().orElseThrow().messageSupplier()); - } - builder.add(key, result.result().orElseThrow()); - return null; - } + private static @Nullable DataResult singleField(DynamicOps ops, MapLike input, Field field, RecordStructure.Container.Builder builder) { + var key = field.key(); + var codec = field.codec(); + var result = codec.decode(ops, input); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + builder.add(key, result.result().orElseThrow()); + return null; + } - @Override - public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { - for (var field : fields) { - prefix = encodeSingleField(input, ops, prefix, field); - } - return prefix; - } + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + for (var field : fields) { + prefix = encodeSingleField(input, ops, prefix, field); + } + return prefix; + } - private RecordBuilder encodeSingleField(A input, DynamicOps ops, RecordBuilder prefix, Field field) { - var codec = field.codec(); - var value = field.getter().apply(input); - return codec.encode(value, ops, prefix); - } + private RecordBuilder encodeSingleField(A input, DynamicOps ops, RecordBuilder prefix, Field field) { + var codec = field.codec(); + var value = field.getter().apply(input); + return codec.encode(value, ops, prefix); + } } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 3f7fa2f..3e2ebc6 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -20,126 +20,126 @@ import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { - public StreamCodecInterpreter(Keys> keys) { - super(keys.join(Keys.>builder() - .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) - .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) - .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE.cast())) - .add(Interpreter.SHORT, new Holder<>(ByteBufCodecs.SHORT.cast())) - .add(Interpreter.INT, new Holder<>(ByteBufCodecs.VAR_INT.cast())) - .add(Interpreter.LONG, new Holder<>(ByteBufCodecs.VAR_LONG.cast())) - .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT.cast())) - .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE.cast())) - .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) - .build() - )); - } - - public StreamCodecInterpreter() { - this(Keys.>builder().build()); - } - - @Override - public DataResult, List>> list(App, A> single) { - return DataResult.success(new Holder<>(StreamCodecInterpreter.list(unbox(single)))); - } - - private static StreamCodec> list(StreamCodec elementCodec) { - return ByteBufCodecs.list().apply(elementCodec); - } - - @Override - public DataResult, A>> record(List> fields, Function creator) { - var streamFields = new ArrayList>(); - for (var field : fields) { - DataResult, A>> result = recordSingleField(field, streamFields); - if (result != null) return result; - } - return DataResult.success(new Holder<>(StreamCodec.of( - (buf, data) -> { - for (var field : streamFields) { - encodeSingleField(buf, field, data); - } - }, - buf -> { - var builder = RecordStructure.Container.builder(); - for (var field : streamFields) { - decodeSingleField(buf, field, builder); - } - return creator.apply(builder.build()); - } - ))); - } - - @Override - public DataResult, Y>> flatXmap(App, X> input, Function> deserializer, Function> serializer) { - var streamCodec = unbox(input); - return DataResult.success(new Holder<>(streamCodec.map( - x -> deserializer.apply(x).getOrThrow(), - y -> serializer.apply(y).getOrThrow() - ))); - } - - @Override - public DataResult, A>> annotate(App, A> input, Annotations annotations) { - // No annotations handled here - return DataResult.success(input); - } - - private static void encodeSingleField(B buf, Field field, A data) { - var missingBehaviour = field.missingBehavior(); - if (missingBehaviour.isEmpty()) { - field.codec.encode(buf, field.getter.apply(data)); - } else { - var behavior = missingBehaviour.get(); - if (behavior.predicate().test(field.getter.apply(data))) { - buf.writeBoolean(true); - field.codec.encode(buf, field.getter.apply(data)); - } else { - buf.writeBoolean(false); - } - } - } - - private static void decodeSingleField(B buf, Field field, RecordStructure.Container.Builder builder) { - var missingBehaviour = field.missingBehavior(); - if (missingBehaviour.isEmpty()) { - var value = field.codec.decode(buf); - builder.add(field.key(), value); - } else { - if (buf.readBoolean()) { - var value = field.codec.decode(buf); - builder.add(field.key(), value); - } else { - builder.add(field.key(), missingBehaviour.get().missing().get()); - } - } - } - - private @Nullable DataResult, A>> recordSingleField(RecordStructure.Field field, ArrayList> streamFields) { - var result = field.structure().interpret(this); - if (result.error().isPresent()) { - return DataResult.error(result.error().orElseThrow().messageSupplier()); - } - streamFields.add(new Field<>(unbox(result.result().orElseThrow()), field.key(), field.getter(), field.missingBehavior())); - return null; - } - - public static StreamCodec unbox(App, T> box) { - return Holder.unbox(box).streamCodec(); - } - - public DataResult> interpret(Structure structure) { - return structure.interpret(this).map(StreamCodecInterpreter::unbox); - } - - public record Holder(StreamCodec streamCodec) implements App, T> { - public static final class Mu implements K1 {} - - static StreamCodecInterpreter.Holder unbox(App, T> box) { - return (StreamCodecInterpreter.Holder) box; - } - } - - private record Field(StreamCodec codec, RecordStructure.Key key, Function getter, Optional> missingBehavior) {} + public StreamCodecInterpreter(Keys> keys) { + super(keys.join(Keys.>builder() + .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) + .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) + .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE.cast())) + .add(Interpreter.SHORT, new Holder<>(ByteBufCodecs.SHORT.cast())) + .add(Interpreter.INT, new Holder<>(ByteBufCodecs.VAR_INT.cast())) + .add(Interpreter.LONG, new Holder<>(ByteBufCodecs.VAR_LONG.cast())) + .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT.cast())) + .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE.cast())) + .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) + .build() + )); + } + + public StreamCodecInterpreter() { + this(Keys.>builder().build()); + } + + @Override + public DataResult, List>> list(App, A> single) { + return DataResult.success(new Holder<>(StreamCodecInterpreter.list(unbox(single)))); + } + + private static StreamCodec> list(StreamCodec elementCodec) { + return ByteBufCodecs.list().apply(elementCodec); + } + + @Override + public DataResult, A>> record(List> fields, Function creator) { + var streamFields = new ArrayList>(); + for (var field : fields) { + DataResult, A>> result = recordSingleField(field, streamFields); + if (result != null) return result; + } + return DataResult.success(new Holder<>(StreamCodec.of( + (buf, data) -> { + for (var field : streamFields) { + encodeSingleField(buf, field, data); + } + }, + buf -> { + var builder = RecordStructure.Container.builder(); + for (var field : streamFields) { + decodeSingleField(buf, field, builder); + } + return creator.apply(builder.build()); + } + ))); + } + + @Override + public DataResult, Y>> flatXmap(App, X> input, Function> deserializer, Function> serializer) { + var streamCodec = unbox(input); + return DataResult.success(new Holder<>(streamCodec.map( + x -> deserializer.apply(x).getOrThrow(), + y -> serializer.apply(y).getOrThrow() + ))); + } + + @Override + public DataResult, A>> annotate(App, A> input, Annotations annotations) { + // No annotations handled here + return DataResult.success(input); + } + + private static void encodeSingleField(B buf, Field field, A data) { + var missingBehaviour = field.missingBehavior(); + if (missingBehaviour.isEmpty()) { + field.codec.encode(buf, field.getter.apply(data)); + } else { + var behavior = missingBehaviour.get(); + if (behavior.predicate().test(field.getter.apply(data))) { + buf.writeBoolean(true); + field.codec.encode(buf, field.getter.apply(data)); + } else { + buf.writeBoolean(false); + } + } + } + + private static void decodeSingleField(B buf, Field field, RecordStructure.Container.Builder builder) { + var missingBehaviour = field.missingBehavior(); + if (missingBehaviour.isEmpty()) { + var value = field.codec.decode(buf); + builder.add(field.key(), value); + } else { + if (buf.readBoolean()) { + var value = field.codec.decode(buf); + builder.add(field.key(), value); + } else { + builder.add(field.key(), missingBehaviour.get().missing().get()); + } + } + } + + private @Nullable DataResult, A>> recordSingleField(RecordStructure.Field field, ArrayList> streamFields) { + var result = field.structure().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().orElseThrow().messageSupplier()); + } + streamFields.add(new Field<>(unbox(result.result().orElseThrow()), field.key(), field.getter(), field.missingBehavior())); + return null; + } + + public static StreamCodec unbox(App, T> box) { + return Holder.unbox(box).streamCodec(); + } + + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(StreamCodecInterpreter::unbox); + } + + public record Holder(StreamCodec streamCodec) implements App, T> { + public static final class Mu implements K1 {} + + static StreamCodecInterpreter.Holder unbox(App, T> box) { + return (StreamCodecInterpreter.Holder) box; + } + } + + private record Field(StreamCodec codec, RecordStructure.Key key, Function getter, Optional> missingBehavior) {} } diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index a7c2e21..695ef0e 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java +++ b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java @@ -9,36 +9,36 @@ import org.junit.jupiter.api.Assertions; public final class CodecAssertions { - private CodecAssertions() {} + private CodecAssertions() {} - public static void assertDecodes(DynamicOps jsonOps, String json, O expected, Codec codec) { - Gson gson = new GsonBuilder().create(); - JsonElement jsonElement = gson.fromJson(json, JsonElement.class); - assertDecodes(jsonOps, jsonElement, expected, codec); - } + public static void assertDecodes(DynamicOps jsonOps, String json, O expected, Codec codec) { + Gson gson = new GsonBuilder().create(); + JsonElement jsonElement = gson.fromJson(json, JsonElement.class); + assertDecodes(jsonOps, jsonElement, expected, codec); + } - public static void assertDecodes(DynamicOps ops, T data, O expected, Codec codec) { - DataResult dataResult = codec.parse(ops, data); - Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); - Assertions.assertEquals(expected, dataResult.result().get()); - } + public static void assertDecodes(DynamicOps ops, T data, O expected, Codec codec) { + DataResult dataResult = codec.parse(ops, data); + Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); + Assertions.assertEquals(expected, dataResult.result().get()); + } - public static void assertEncodes(DynamicOps jsonOps, O value, String json, Codec codec) { - Gson gson = new GsonBuilder().create(); - JsonElement jsonElement = gson.fromJson(json, JsonElement.class); - assertEncodes(jsonOps, value, jsonElement, codec); - } + public static void assertEncodes(DynamicOps jsonOps, O value, String json, Codec codec) { + Gson gson = new GsonBuilder().create(); + JsonElement jsonElement = gson.fromJson(json, JsonElement.class); + assertEncodes(jsonOps, value, jsonElement, codec); + } - public static void assertEncodes(DynamicOps ops, O value, T expected, Codec codec) { - DataResult dataResult = codec.encodeStart(ops, value); - Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); - Assertions.assertEquals(expected, dataResult.result().get()); - } + public static void assertEncodes(DynamicOps ops, O value, T expected, Codec codec) { + DataResult dataResult = codec.encodeStart(ops, value); + Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); + Assertions.assertEquals(expected, dataResult.result().get()); + } - public static void assertJsonEquals(String expected, String actual) { - Gson gson = new GsonBuilder().create(); - JsonElement expectedElement = gson.fromJson(expected, JsonElement.class); - JsonElement actualElement = gson.fromJson(actual, JsonElement.class); - Assertions.assertEquals(expectedElement, actualElement); - } + public static void assertJsonEquals(String expected, String actual) { + Gson gson = new GsonBuilder().create(); + JsonElement expectedElement = gson.fromJson(expected, JsonElement.class); + JsonElement actualElement = gson.fromJson(actual, JsonElement.class); + Assertions.assertEquals(expectedElement, actualElement); + } } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 06c067c..82bc832 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -12,63 +12,63 @@ import org.junit.jupiter.api.Test; class TestStructured { - private record TestRecord(int a, String b, List c, Optional d) { - private static final Structure STRUCTURE = Structure.record(i -> { - var a = i.add("a", Structure.INT.annotate(Annotations.COMMENT, "Field A"), TestRecord::a); - var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); - var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); - var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); - return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); - }); + private record TestRecord(int a, String b, List c, Optional d) { + private static final Structure STRUCTURE = Structure.record(i -> { + var a = i.add("a", Structure.INT.annotate(Annotations.COMMENT, "Field A"), TestRecord::a); + var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); + var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); + var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); + return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); + }); - private static final Codec CODEC = new CodecInterpreter().interpret(STRUCTURE).getOrThrow(); - } + private static final Codec CODEC = new CodecInterpreter().interpret(STRUCTURE).getOrThrow(); + } - private final String json = """ - { - "a": 1, - "b": "test", - "c": [true, false, true] - }"""; + private final String json = """ + { + "a": 1, + "b": "test", + "c": [true, false, true] + }"""; - private final String schema = """ - { - "type": "object", - "properties": { - "a": { - "type": "integer", - "description": "Field A" - }, - "b": { - "type": "string" - }, - "c": { - "type": "array", - "items": { - "type": "boolean" - } - }, - "d": { - "type": "string" - } - }, - "required": ["a", "b", "c"] - }"""; + private final String schema = """ + { + "type": "object", + "properties": { + "a": { + "type": "integer", + "description": "Field A" + }, + "b": { + "type": "string" + }, + "c": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "d": { + "type": "string" + } + }, + "required": ["a", "b", "c"] + }"""; - private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty()); + private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty()); - @Test - void testDecodingCodec() { - CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, record, TestRecord.CODEC); - } + @Test + void testDecodingCodec() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, record, TestRecord.CODEC); + } - @Test - void testEncodingCodec() { - CodecAssertions.assertEncodes(JsonOps.INSTANCE, record, json, TestRecord.CODEC); - } + @Test + void testEncodingCodec() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, record, json, TestRecord.CODEC); + } - @Test - void testJsonSchema() { - CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(TestRecord.STRUCTURE).getOrThrow().toString()); - } + @Test + void testJsonSchema() { + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(TestRecord.STRUCTURE).getOrThrow().toString()); + } } From 3c59a1b546b909afd5c87c5f14b54c1af140eae5 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 22 Jul 2024 22:25:17 -0500 Subject: [PATCH 14/76] First attempt at parametric keys --- .../codecextras/structured/Annotation.java | 31 +++++++++ .../codecextras/structured/Annotations.java | 67 ------------------- .../structured/CodecInterpreter.java | 14 ++-- .../codecextras/structured/Interpreter.java | 5 +- .../structured/JsonSchemaInterpreter.java | 18 +++-- .../codecextras/structured/Key2.java | 18 +++++ .../structured/KeyStoringInterpreter.java | 15 ++++- .../codecextras/structured/Keys.java | 34 ++++++---- .../codecextras/structured/Keys2.java | 59 ++++++++++++++++ .../structured/MapCodecInterpreter.java | 34 ++++++++-- .../structured/ParametricKeyedValue.java | 18 +++++ .../codecextras/structured/Structure.java | 24 +++++-- .../structured/StructuredMapCodec.java | 2 +- .../codecextras/types/Identity.java | 12 ++++ .../codecextras/types/package-info.java | 6 ++ .../structured/StreamCodecInterpreter.java | 17 +++-- .../test/structured/TestStructured.java | 4 +- 17 files changed, 262 insertions(+), 116 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Annotation.java delete mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Annotations.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Key2.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Keys2.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/Identity.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/package-info.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java new file mode 100644 index 0000000..581b2b9 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java @@ -0,0 +1,31 @@ +package dev.lukebemish.codecextras.structured; + +import dev.lukebemish.codecextras.types.Identity; +import java.util.Optional; + +public class Annotation { + /** + * A comment that a field in a structure should be serialized with. + */ + public static final Key COMMENT = Key.create("comment"); + /** + * A human-readable title for a part of a structure. + */ + public static final Key TITLE = Key.create("title"); + /** + * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. + */ + public static final Key DESCRIPTION = Key.create("description"); + + public static Optional get(Keys keys, Key key) { + return keys.get(key).map(app -> Identity.unbox(app).value()); + } + + public static Keys empty() { + return EMPTY; + } + + private static final Keys EMPTY = Keys.builder().build(); + + private Annotation() {} +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java deleted file mode 100644 index 9a40c73..0000000 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotations.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.lukebemish.codecextras.structured; - -import com.mojang.datafixers.kinds.K1; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Optional; - -public final class Annotations { - /** - * A comment that a field in a structure should be serialized with. - */ - public static final Key COMMENT = Key.create("comment"); - /** - * A human-readable title for a part of a structure. - */ - public static final Key TITLE = Key.create("title"); - /** - * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. - */ - public static final Key DESCRIPTION = Key.create("description"); - - @SuppressWarnings("unchecked") - public Optional get(Key key) { - return Optional.ofNullable((A) keys.get(key)); - } - - public static Keys.Builder builder() { - return new Keys.Builder<>(); - } - - public Annotations join(Annotations other) { - var map = new IdentityHashMap<>(this.keys); - map.putAll(other.keys); - return new Annotations(map); - } - - public Annotations with(Key key, A value) { - var map = new IdentityHashMap<>(this.keys); - map.put(key, value); - return new Annotations(map); - } - - public static Annotations empty() { - return EMPTY; - } - - public final static class Builder { - private final Map, Object> keys = new IdentityHashMap<>(); - - public Builder add(Key key, A value) { - keys.put(key, value); - return this; - } - - public Annotations build() { - return new Annotations(new IdentityHashMap<>(keys)); - } - } - - private static final Annotations EMPTY = new Annotations(new IdentityHashMap<>()); - - private final IdentityHashMap, Object> keys; - - private Annotations(IdentityHashMap, Object> keys) { - this.keys = keys; - } -} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 50bddb7..778d82f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -6,12 +6,13 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.function.Function; public class CodecInterpreter extends KeyStoringInterpreter { - public CodecInterpreter(Keys keys) { - super(keys.join(Keys.builder() + public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { + super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) .add(Interpreter.BOOL, new Holder<>(Codec.BOOL)) .add(Interpreter.BYTE, new Holder<>(Codec.BYTE)) @@ -22,11 +23,14 @@ public CodecInterpreter(Keys keys) { .add(Interpreter.DOUBLE, new Holder<>(Codec.DOUBLE)) .add(Interpreter.STRING, new Holder<>(Codec.STRING)) .build() - )); + ), parametricKeys); } public CodecInterpreter() { - this(Keys.builder().build()); + this( + Keys.builder().build(), + Keys2., K1, K1>builder().build() + ); } @Override @@ -47,7 +51,7 @@ public DataResult> flatXmap(App input, Fu } @Override - public DataResult> annotate(App input, Annotations annotations) { + public DataResult> annotate(App input, Keys annotations) { // No annotations handled here return DataResult.success(input); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 3c6d13a..9df78fc 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.function.Function; @@ -16,7 +17,7 @@ public interface Interpreter { DataResult> flatXmap(App input, Function> deserializer, Function> serializer); - DataResult> annotate(App input, Annotations annotations); + DataResult> annotate(App input, Keys annotations); Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); @@ -27,4 +28,6 @@ public interface Interpreter { Key FLOAT = Key.create("FLOAT"); Key DOUBLE = Key.create("DOUBLE"); Key STRING = Key.create("STRING"); + + DataResult>> parametricallyKeyed(Key2 key, App parameter); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 57bd2f0..a37108d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -5,14 +5,15 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { - public JsonSchemaInterpreter(Keys keys) { - super(keys.join(Keys.builder() + public JsonSchemaInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { + super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(OBJECT)) .add(Interpreter.BOOL, new Holder<>(BOOLEAN)) .add(Interpreter.BYTE, new Holder<>(INTEGER)) @@ -23,11 +24,14 @@ public JsonSchemaInterpreter(Keys keys) { .add(Interpreter.DOUBLE, new Holder<>(NUMBER)) .add(Interpreter.STRING, new Holder<>(STRING)) .build() - )); + ), parametricKeys); } public JsonSchemaInterpreter() { - this(Keys.builder().build()); + this( + Keys.builder().build(), + Keys2., K1, K1>builder().build() + ); } @Override @@ -72,12 +76,12 @@ public DataResult> flatXmap(App input, Fu } @Override - public DataResult> annotate(App input, Annotations annotations) { + public DataResult> annotate(App input, Keys annotations) { var schema = copy(unbox(input)); - annotations.get(Annotations.DESCRIPTION).or(() -> annotations.get(Annotations.COMMENT)).ifPresent(comment -> { + Annotation.get(annotations, Annotation.DESCRIPTION).or(() -> Annotation.get(annotations, Annotation.COMMENT)).ifPresent(comment -> { schema.addProperty("description", comment); }); - annotations.get(Annotations.TITLE).ifPresent(comment -> { + Annotation.get(annotations, Annotation.TITLE).ifPresent(comment -> { schema.addProperty("title", comment); }); return DataResult.success(new Holder<>(schema)); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key2.java b/src/main/java/dev/lukebemish/codecextras/structured/Key2.java new file mode 100644 index 0000000..6c99f4f --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key2.java @@ -0,0 +1,18 @@ +package dev.lukebemish.codecextras.structured; + +public final class Key2 { + private final String name; + + private Key2(String name) { + this.name = name; + } + + public static Key2 create(String name) { + var className = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName(); + return new Key2<>(className + ":" + name); + } + + public String name() { + return name; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java index 8037cfa..3828ed2 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java @@ -5,14 +5,25 @@ import com.mojang.serialization.DataResult; public abstract class KeyStoringInterpreter implements Interpreter { - private final Keys keys; + private final Keys keys; + private final Keys2, K1, K1> parametricKeys; - protected KeyStoringInterpreter(Keys keys) { + protected KeyStoringInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { this.keys = keys; + this.parametricKeys = parametricKeys; } @Override public DataResult> keyed(Key key) { return keys.get(key).map(DataResult::success).orElse(DataResult.error(() -> "Unknown key "+key.name())); } + + @Override + public DataResult>> parametricallyKeyed(Key2 key, App parameter) { + return parametricKeys.get(key) + .map(ParametricKeyedValue::unbox) + .map(val -> val.converter().convert(parameter)) + .map(DataResult::success) + .orElse(DataResult.error(() -> "Unknown key "+key.name())); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index 4d7f360..fcf4d2e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -6,47 +6,53 @@ import java.util.Map; import java.util.Optional; -public final class Keys { - private final IdentityHashMap, App> keys; +public final class Keys { + private final IdentityHashMap, App> keys; - private Keys(IdentityHashMap, App> keys) { + private Keys(IdentityHashMap, App> keys) { this.keys = keys; } @SuppressWarnings("unchecked") - public Optional> get(Key key) { + public Optional> get(Key key) { return Optional.ofNullable((App) keys.get(key)); } - public Keys map(Converter converter) { - var map = new IdentityHashMap, App>(); + public Keys map(Converter converter) { + var map = new IdentityHashMap, App>(); keys.forEach((key, value) -> map.put(key, converter.convert(value))); return new Keys<>(map); } - public interface Converter { - App convert(App input); + public interface Converter { + App convert(App input); } - public static Builder builder() { + public static Builder builder() { return new Builder<>(); } - public Keys join(Keys other) { + public Keys join(Keys other) { var map = new IdentityHashMap<>(this.keys); map.putAll(other.keys); return new Keys<>(map); } - public final static class Builder { - private final Map, App> keys = new IdentityHashMap<>(); + public Keys with(Key key, App value) { + var map = new IdentityHashMap<>(this.keys); + map.put(key, value); + return new Keys<>(map); + } + + public final static class Builder { + private final Map, App> keys = new IdentityHashMap<>(); - public Builder add(Key key, App value) { + public Builder add(Key key, App value) { keys.put(key, value); return this; } - public Keys build() { + public Keys build() { return new Keys<>(new IdentityHashMap<>(keys)); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java new file mode 100644 index 0000000..cb77e34 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java @@ -0,0 +1,59 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K2; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; + +public final class Keys2 { + private final IdentityHashMap, App2> keys; + + private Keys2(IdentityHashMap, App2> keys) { + this.keys = keys; + } + + @SuppressWarnings("unchecked") + public Optional> get(Key2 key) { + return Optional.ofNullable((App2) keys.get(key)); + } + + public Keys2 map(Converter converter) { + var map = new IdentityHashMap, App2>(); + keys.forEach((key, value) -> map.put(key, converter.convert(value))); + return new Keys2<>(map); + } + + public interface Converter { + App2 convert(App2 input); + } + + public static Builder builder() { + return new Builder<>(); + } + + public Keys2 join(Keys2 other) { + var map = new IdentityHashMap<>(this.keys); + map.putAll(other.keys); + return new Keys2<>(map); + } + + public Keys2 with(Key2 key, App2 value) { + var map = new IdentityHashMap<>(this.keys); + map.put(key, value); + return new Keys2<>(map); + } + + public final static class Builder { + private final Map, App2> keys = new IdentityHashMap<>(); + + public Builder add(Key2 key, App2 value) { + keys.put(key, value); + return this; + } + + public Keys2 build() { + return new Keys2<>(new IdentityHashMap<>(keys)); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 6d974bd..4ed8d7b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -1,28 +1,52 @@ package dev.lukebemish.codecextras.structured; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.function.Function; public class MapCodecInterpreter extends KeyStoringInterpreter { private final CodecInterpreter codecInterpreter; - public MapCodecInterpreter(Keys keys, Keys codecKeys) { - super(keys); + public MapCodecInterpreter( + Keys keys, + Keys codecKeys, + Keys2, K1, K1> parametricKeys, + Keys2, K1, K1> parametricCodecKeys + ) { + super(keys, parametricKeys); this.codecInterpreter = new CodecInterpreter(codecKeys.join(keys.map(new Keys.Converter<>() { @Override public App convert(App app) { return new CodecInterpreter.Holder<>(unbox(app).codec()); } + })), parametricCodecKeys.join(parametricKeys.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { + @Override + public App2, A, B> convert(App2, A, B> input) { + var unboxed = ParametricKeyedValue.unbox(input); + return new ParametricKeyedValue<>(new ParametricKeyedValue.Converter() { + @Override + public App> convert(App parameter) { + var mapCodec = unbox(unboxed.converter().convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }); + } }))); } public MapCodecInterpreter() { - this(Keys.builder().build(), Keys.builder().build()); + this( + Keys.builder().build(), + Keys.builder().build(), + Keys2., K1, K1>builder().build(), + Keys2., K1, K1>builder().build() + ); } @Override @@ -43,11 +67,11 @@ public DataResult> flatXmap(App input, Fu } @Override - public DataResult> annotate(App input, Annotations annotations) { + public DataResult> annotate(App input, Keys annotations) { var mapCodec = new Object() { MapCodec m = unbox(input); }; - mapCodec.m = annotations.get(Annotations.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); + mapCodec.m = Annotation.get(annotations, Annotation.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); return DataResult.success(new Holder<>(mapCodec.m)); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java new file mode 100644 index 0000000..c20e18c --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java @@ -0,0 +1,18 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.kinds.K2; + +public record ParametricKeyedValue(Converter converter) implements App2, MuP, MuO> { + public static class Mu implements K2 {} + + public interface Converter { + App> convert(App parameter); + } + + public static ParametricKeyedValue unbox(App2, MuP, MuO> boxed) { + return (ParametricKeyedValue) boxed; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 7117d07..e82d0af 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -12,23 +13,23 @@ public interface Structure { DataResult> interpret(Interpreter interpreter); - default Annotations annotations() { - return Annotations.empty(); + default Keys annotations() { + return Annotation.empty(); } default Structure annotate(Key key, T value) { var outer = this; - var annotations = annotations().with(key, value); + var annotations = annotations().with(key, new Identity<>(value)); return annotatedDelegatingStructure(outer, annotations); } - default Structure annotate(Annotations annotations) { + default Structure annotate(Keys annotations) { var outer = this; var combined = annotations().join(annotations); return annotatedDelegatingStructure(outer, combined); } - private static Structure annotatedDelegatingStructure(Structure outer, Annotations annotations) { + private static Structure annotatedDelegatingStructure(Structure outer, Keys annotations) { final class AnnotatedDelegatingStructure implements Structure { final @Nullable AnnotatedDelegatingStructure delegate; @@ -47,7 +48,7 @@ private DataResult> interpretNoAnnotations(Interprete } @Override - public Annotations annotations() { + public Keys annotations() { return annotations; } } @@ -100,6 +101,17 @@ public DataResult> interpret(Interpreter interpre }; } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.parametricallyKeyed(key, parameter).flatMap(app -> + interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) + ); + } + }; + } + static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 7bf9997..0b1c520 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -46,7 +46,7 @@ public static DataResult> of(List fieldCodec = unboxer.unbox(result.result().orElseThrow()); - MapCodec fieldMapCodec = field.structure().annotations().get(Annotations.COMMENT) + MapCodec fieldMapCodec = Annotation.get(field.structure().annotations(), Annotation.COMMENT) .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) .orElseGet(() -> makeFieldCodec(fieldCodec, field)); mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); diff --git a/src/main/java/dev/lukebemish/codecextras/types/Identity.java b/src/main/java/dev/lukebemish/codecextras/types/Identity.java new file mode 100644 index 0000000..527768e --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/Identity.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +public record Identity(T value) implements App { + public static final class Mu implements K1 {} + + public static Identity unbox(App input) { + return (Identity) input; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/package-info.java b/src/main/java/dev/lukebemish/codecextras/types/package-info.java new file mode 100644 index 0000000..f0b7e5c --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.types; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 3e2ebc6..1a4ad09 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -4,12 +4,14 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; -import dev.lukebemish.codecextras.structured.Annotations; import dev.lukebemish.codecextras.structured.Interpreter; import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; import java.util.ArrayList; import java.util.List; @@ -20,8 +22,8 @@ import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter> { - public StreamCodecInterpreter(Keys> keys) { - super(keys.join(Keys.>builder() + public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { + super(keys.join(Keys., Object>builder() .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE.cast())) @@ -32,11 +34,14 @@ public StreamCodecInterpreter(Keys> keys) { .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE.cast())) .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) .build() - )); + ), parametricKeys); } public StreamCodecInterpreter() { - this(Keys.>builder().build()); + this( + Keys., Object>builder().build(), + Keys2.>, K1, K1>builder().build() + ); } @Override @@ -81,7 +86,7 @@ public DataResult, Y>> flatXmap(App, X> inp } @Override - public DataResult, A>> annotate(App, A> input, Annotations annotations) { + public DataResult, A>> annotate(App, A> input, Keys annotations) { // No annotations handled here return DataResult.success(input); } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 82bc832..90c4f5a 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -2,7 +2,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; -import dev.lukebemish.codecextras.structured.Annotations; +import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Structure; @@ -14,7 +14,7 @@ class TestStructured { private record TestRecord(int a, String b, List c, Optional d) { private static final Structure STRUCTURE = Structure.record(i -> { - var a = i.add("a", Structure.INT.annotate(Annotations.COMMENT, "Field A"), TestRecord::a); + var a = i.add("a", Structure.INT.annotate(Annotation.COMMENT, "Field A"), TestRecord::a); var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); From 10d2beff1cb93871db385b1dce67fcf5b69d5184 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 22 Jul 2024 22:42:03 -0500 Subject: [PATCH 15/76] Add parametric keys test --- .../test/structured/TestParametricKeys.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java new file mode 100644 index 0000000..65d9ad8 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java @@ -0,0 +1,77 @@ +package dev.lukebemish.codecextras.test.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Key2; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +public class TestParametricKeys { + private record WithType(String string) implements App { + public static class Mu implements K1 {} + + private static WithType unbox(App box) { + return (WithType) box; + } + } + + private record Prefix(String string) implements App { + public static class Mu implements K1 {} + + private static Prefix unbox(App box) { + return (Prefix) box; + } + } + + private static Codec> withTypeCodec(Prefix prefix) { + return Codec.STRING.comapFlatMap( + s -> s.startsWith(prefix.string()) ? + DataResult.success(new WithType<>(s.substring(prefix.string().length()))) : + DataResult.error(() -> "Provided string \""+s+"\" does not start with prefix \""+prefix.string()+"\""), + w -> prefix.string()+w.string() + ); + } + + private static final Key2 WITH_TYPE = Key2.create("with_type"); + + private static final Structure> STRUCTURE = Structure.parametricallyKeyed( + WITH_TYPE, + new Prefix<>("prefix:"), + WithType::unbox + ); + + private static final Codec> CODEC = new CodecInterpreter( + Keys.builder().build(), + Keys2., K1, K1>builder() + .add(WITH_TYPE, new ParametricKeyedValue<>(new ParametricKeyedValue.Converter<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(withTypeCodec(Prefix.unbox(parameter)).xmap(Function.identity(), WithType::unbox)); + } + })) + .build() + ).interpret(STRUCTURE).getOrThrow(); + + private final String json = "\"prefix:123\""; + + private final WithType data = new WithType<>("123"); + + @Test + void testEncodingCodec() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, data, json, CODEC); + } + + @Test + void testDecodingCodec() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, data, CODEC); + } +} From 3390fbb9e4d2cad7bff9ae103a9b1469c5e3bdd1 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 22 Jul 2024 22:49:17 -0500 Subject: [PATCH 16/76] Fix ordering of keys in map codec interpreter's nested codec interpreter --- .../structured/MapCodecInterpreter.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 4ed8d7b..5d5da66 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -20,24 +20,24 @@ public MapCodecInterpreter( Keys2, K1, K1> parametricCodecKeys ) { super(keys, parametricKeys); - this.codecInterpreter = new CodecInterpreter(codecKeys.join(keys.map(new Keys.Converter<>() { + this.codecInterpreter = new CodecInterpreter(keys.map(new Keys.Converter<>() { @Override public App convert(App app) { return new CodecInterpreter.Holder<>(unbox(app).codec()); } - })), parametricCodecKeys.join(parametricKeys.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { + }).join(codecKeys), parametricKeys.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { @Override public App2, A, B> convert(App2, A, B> input) { var unboxed = ParametricKeyedValue.unbox(input); - return new ParametricKeyedValue<>(new ParametricKeyedValue.Converter() { - @Override - public App> convert(App parameter) { - var mapCodec = unbox(unboxed.converter().convert(parameter)); - return new CodecInterpreter.Holder<>(mapCodec.codec()); - } - }); + return new ParametricKeyedValue<>(new ParametricKeyedValue.Converter<>() { + @Override + public App> convert(App parameter) { + var mapCodec = unbox(unboxed.converter().convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }); } - }))); + }).join(parametricCodecKeys)); } public MapCodecInterpreter() { From 2d1ffa210ba92dcf3aa7fd161247ef53448f6dc0 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 22 Jul 2024 22:52:54 -0500 Subject: [PATCH 17/76] Simplify ParametricKeyedValue --- .../structured/KeyStoringInterpreter.java | 2 +- .../structured/MapCodecInterpreter.java | 14 +++++++------- .../structured/ParametricKeyedValue.java | 10 ++++------ .../test/structured/TestParametricKeys.java | 4 ++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java index 3828ed2..7865e6a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java @@ -22,7 +22,7 @@ public DataResult> keyed(Key key) { public DataResult>> parametricallyKeyed(Key2 key, App parameter) { return parametricKeys.get(key) .map(ParametricKeyedValue::unbox) - .map(val -> val.converter().convert(parameter)) + .map(val -> val.convert(parameter)) .map(DataResult::success) .orElse(DataResult.error(() -> "Unknown key "+key.name())); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 5d5da66..e567d48 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -29,13 +29,13 @@ public App convert(App app) { @Override public App2, A, B> convert(App2, A, B> input) { var unboxed = ParametricKeyedValue.unbox(input); - return new ParametricKeyedValue<>(new ParametricKeyedValue.Converter<>() { - @Override - public App> convert(App parameter) { - var mapCodec = unbox(unboxed.converter().convert(parameter)); - return new CodecInterpreter.Holder<>(mapCodec.codec()); - } - }); + return new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var mapCodec = unbox(unboxed.convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }; } }).join(parametricCodecKeys)); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java index c20e18c..0bdc345 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java @@ -5,14 +5,12 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.kinds.K2; -public record ParametricKeyedValue(Converter converter) implements App2, MuP, MuO> { - public static class Mu implements K2 {} +public interface ParametricKeyedValue extends App2, MuP, MuO> { + class Mu implements K2 {} - public interface Converter { - App> convert(App parameter); - } + App> convert(App parameter); - public static ParametricKeyedValue unbox(App2, MuP, MuO> boxed) { + static ParametricKeyedValue unbox(App2, MuP, MuO> boxed) { return (ParametricKeyedValue) boxed; } } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java index 65d9ad8..8eaa8fa 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java @@ -52,12 +52,12 @@ private static Codec> withTypeCodec(Prefix prefix) { private static final Codec> CODEC = new CodecInterpreter( Keys.builder().build(), Keys2., K1, K1>builder() - .add(WITH_TYPE, new ParametricKeyedValue<>(new ParametricKeyedValue.Converter<>() { + .add(WITH_TYPE, new ParametricKeyedValue<>() { @Override public App> convert(App parameter) { return new CodecInterpreter.Holder<>(withTypeCodec(Prefix.unbox(parameter)).xmap(Function.identity(), WithType::unbox)); } - })) + }) .build() ).interpret(STRUCTURE).getOrThrow(); From 2497c743e0100da0efdce80231e1aa66b807f2a6 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 22 Jul 2024 22:58:05 -0500 Subject: [PATCH 18/76] Make accidental instantiation of various Mu impossible --- .../lukebemish/codecextras/structured/CodecInterpreter.java | 2 +- .../codecextras/structured/JsonSchemaInterpreter.java | 2 +- .../codecextras/structured/MapCodecInterpreter.java | 2 +- .../codecextras/structured/ParametricKeyedValue.java | 2 +- src/main/java/dev/lukebemish/codecextras/types/Identity.java | 2 +- .../codecextras/test/structured/TestParametricKeys.java | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 778d82f..261071b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -65,7 +65,7 @@ public DataResult> interpret(Structure structure) { } public record Holder(Codec codec) implements App { - public static final class Mu implements K1 {} + public static final class Mu implements K1 { private Mu() {} } static Holder unbox(App box) { return (Holder) box; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index a37108d..3a5b1c1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -96,7 +96,7 @@ public DataResult interpret(Structure structure) { } public record Holder(JsonObject jsonObject) implements App { - public static final class Mu implements K1 {} + public static final class Mu implements K1 { private Mu() {} } static Holder unbox(App box) { return (Holder) box; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index e567d48..d558717 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -84,7 +84,7 @@ public DataResult> interpret(Structure structure) { } public record Holder(MapCodec mapCodec) implements App { - public static final class Mu implements K1 {} + public static final class Mu implements K1 { private Mu() {} } static Holder unbox(App box) { return (Holder) box; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java index 0bdc345..47103d7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java @@ -6,7 +6,7 @@ import com.mojang.datafixers.kinds.K2; public interface ParametricKeyedValue extends App2, MuP, MuO> { - class Mu implements K2 {} + final class Mu implements K2 { private Mu() {} } App> convert(App parameter); diff --git a/src/main/java/dev/lukebemish/codecextras/types/Identity.java b/src/main/java/dev/lukebemish/codecextras/types/Identity.java index 527768e..ab860b0 100644 --- a/src/main/java/dev/lukebemish/codecextras/types/Identity.java +++ b/src/main/java/dev/lukebemish/codecextras/types/Identity.java @@ -4,7 +4,7 @@ import com.mojang.datafixers.kinds.K1; public record Identity(T value) implements App { - public static final class Mu implements K1 {} + public static final class Mu implements K1 { private Mu() {} } public static Identity unbox(App input) { return (Identity) input; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java index 8eaa8fa..03f79f1 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java @@ -17,7 +17,7 @@ public class TestParametricKeys { private record WithType(String string) implements App { - public static class Mu implements K1 {} + public static class Mu implements K1 { private Mu() {} } private static WithType unbox(App box) { return (WithType) box; @@ -25,7 +25,7 @@ private static WithType unbox(App box) { } private record Prefix(String string) implements App { - public static class Mu implements K1 {} + public static class Mu implements K1 { private Mu() {} } private static Prefix unbox(App box) { return (Prefix) box; From 3f8b1e5468fc14b88295860cf096472ac66bc8b7 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 10:08:19 -0500 Subject: [PATCH 19/76] Built-in MC structures --- .../structured/CodecInterpreter.java | 8 ++ .../codecextras/structured/Interpreter.java | 5 + .../structured/JsonSchemaInterpreter.java | 8 ++ .../structured/MapCodecInterpreter.java | 8 ++ .../codecextras/structured/Structure.java | 12 ++ .../lukebemish/codecextras/types/AppMu.java | 12 ++ .../structured/MinecraftStructures.java | 121 ++++++++++++++++++ .../structured/StreamCodecInterpreter.java | 2 +- .../stream => }/structured/package-info.java | 2 +- 9 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/types/AppMu.java create mode 100644 src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java rename src/stream/java/dev/lukebemish/codecextras/stream/{mutable/dev/lukebemish/codecextras/stream => }/structured/StreamCodecInterpreter.java (98%) rename src/stream/java/dev/lukebemish/codecextras/stream/{mutable/dev/lukebemish/codecextras/stream => }/structured/package-info.java (56%) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 261071b..f9c03e7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -8,6 +8,7 @@ import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Optional; import java.util.function.Function; public class CodecInterpreter extends KeyStoringInterpreter { @@ -56,6 +57,13 @@ public DataResult> annotate(App input, Keys< return DataResult.success(input); } + public static final Key KEY = Key.create("CodecInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } + public static Codec unbox(App box) { return Holder.unbox(box).codec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 9df78fc..a9f62c3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -6,6 +6,7 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Optional; import java.util.function.Function; public interface Interpreter { @@ -19,6 +20,10 @@ public interface Interpreter { DataResult> annotate(App input, Keys annotations); + default Optional> key() { + return Optional.empty(); + } + Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); Key BYTE = Key.create("BYTE"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 3a5b1c1..462f479 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -7,6 +7,7 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -95,6 +96,13 @@ public DataResult interpret(Structure structure) { return structure.interpret(this).map(JsonSchemaInterpreter::unbox); } + public static final Key KEY = Key.create("JsonSchemaInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } + public record Holder(JsonObject jsonObject) implements App { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index d558717..9a525d3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -8,6 +8,7 @@ import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Optional; import java.util.function.Function; public class MapCodecInterpreter extends KeyStoringInterpreter { @@ -90,4 +91,11 @@ static Holder unbox(App box) { return (Holder) box; } } + + public static final Key KEY = Key.create("MapCodecInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index e82d0af..8c4a73e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.AppMu; import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Optional; @@ -101,6 +102,17 @@ public DataResult> interpret(Interpreter interpre }; } + static Structure keyed(Key key, Keys keys) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.key().flatMap(k -> keys.get(k).>map(AppMu::unbox).map(AppMu::value)) + .map(DataResult::success) + .orElseGet(() -> interpreter.keyed(key)); + } + }; + } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer) { return new Structure<>() { @Override diff --git a/src/main/java/dev/lukebemish/codecextras/types/AppMu.java b/src/main/java/dev/lukebemish/codecextras/types/AppMu.java new file mode 100644 index 0000000..1460f4d --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/AppMu.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +public record AppMu(App value) implements App { + public static final class Mu implements K1 { private Mu() {} } + + public static AppMu unbox(App box) { + return (AppMu) box; + } +} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java new file mode 100644 index 0000000..b92938e --- /dev/null +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -0,0 +1,121 @@ +package dev.lukebemish.codecextras.stream.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Key2; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.MapCodecInterpreter; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.AppMu; +import net.minecraft.core.Registry; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; + +public final class MinecraftStructures { + private MinecraftStructures() {} + + public static final Keys MAP_CODEC_KEYS = Keys.builder() + .build(); + + public static final Keys2, K1, K1> MAP_CODEC_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .build(); + + public static final Keys CODEC_KEYS = MAP_CODEC_KEYS.map(new Keys.Converter<>() { + @Override + public App convert(App app) { + return new CodecInterpreter.Holder<>(MapCodecInterpreter.unbox(app).codec()); + } + }).join(Keys.builder() + .add(Types.RESOURCE_LOCATION, new CodecInterpreter.Holder<>(ResourceLocation.CODEC)) + .build() + ); + + public static final Keys2, K1, K1> CODEC_PARAMETRIC_KEYS = MAP_CODEC_PARAMETRIC_KEYS.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { + @Override + public App2, A, B> convert(App2, A, B> input) { + var unboxed = ParametricKeyedValue.unbox(input); + return new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var mapCodec = MapCodecInterpreter.unbox(unboxed.convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }; + } + }).join(Keys2., K1, K1>builder() + .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(ResourceKey.codec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value())); + } + }) + .build() + ); + + public static final Keys, Object> REGISTRY_STREAM_KEYS = Keys., Object>builder() + .add(Types.RESOURCE_LOCATION, new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast())) + .build(); + + public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() + .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App, App> convert(App parameter) { + return new StreamCodecInterpreter.Holder<>( + ResourceKey.streamCodec(Types.RegistryKeyHolder.unbox(parameter).value()).>map(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value()).cast() + ); + } + }) + .build(); + + public static final Keys JSON_SCHEMA_KEYS = Keys.builder() + .build(); + + public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .build(); + + public static final class Types { + private Types() {} + + public static final Key RESOURCE_LOCATION = Key.create("resource_location"); + + public record ResourceKeyHolder(ResourceKey value) implements App { + public static final class Mu implements K1 { private Mu() {} } + + public static ResourceKeyHolder unbox(App box) { + return (ResourceKeyHolder) box; + } + } + + public record RegistryKeyHolder(ResourceKey> value) implements App { + public static final class Mu implements K1 { private Mu() {} } + + public static RegistryKeyHolder unbox(App box) { + return (RegistryKeyHolder) box; + } + } + + public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); + } + + public static final class Structures { + private Structures() {} + + public static final Structure RESOURCE_LOCATION = Structure.keyed( + Types.RESOURCE_LOCATION, Keys.builder() + .add(CodecInterpreter.KEY, new AppMu<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) + .build() + ); + + public static Structure> resourceKey(ResourceKey> registry) { + return Structure.parametricallyKeyed(Types.RESOURCE_KEY, new Types.RegistryKeyHolder<>(registry), Types.ResourceKeyHolder::unbox) + .xmap(Types.ResourceKeyHolder::value, Types.ResourceKeyHolder::new); + } + } +} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java similarity index 98% rename from src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java rename to src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 1a4ad09..d50ba46 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -1,4 +1,4 @@ -package dev.lukebemish.codecextras.stream.mutable.dev.lukebemish.codecextras.stream.structured; +package dev.lukebemish.codecextras.stream.structured; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/package-info.java similarity index 56% rename from src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java rename to src/stream/java/dev/lukebemish/codecextras/stream/structured/package-info.java index 57a5da3..1cc6bcd 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/dev/lukebemish/codecextras/stream/structured/package-info.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/package-info.java @@ -1,6 +1,6 @@ @NullMarked @ApiStatus.Experimental -package dev.lukebemish.codecextras.stream.mutable.dev.lukebemish.codecextras.stream.structured; +package dev.lukebemish.codecextras.stream.structured; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.NullMarked; From 6a6ad1a4bdf3b1bb0d7f4e1954eebc420c4dffb8 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 15:20:51 -0500 Subject: [PATCH 20/76] Fix type inference --- src/main/java/dev/lukebemish/codecextras/types/AppMu.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/types/AppMu.java b/src/main/java/dev/lukebemish/codecextras/types/AppMu.java index 1460f4d..b66da73 100644 --- a/src/main/java/dev/lukebemish/codecextras/types/AppMu.java +++ b/src/main/java/dev/lukebemish/codecextras/types/AppMu.java @@ -3,10 +3,10 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; -public record AppMu(App value) implements App { +public record AppMu(App value) implements App { public static final class Mu implements K1 { private Mu() {} } - public static AppMu unbox(App box) { - return (AppMu) box; + public static AppMu unbox(App box) { + return (AppMu) box; } } From 99671a04fa7c95b5e3175f2a8f8721fbf9a48d51 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 15:39:18 -0500 Subject: [PATCH 21/76] Refactor some minor things --- .../lukebemish/codecextras/structured/Structure.java | 6 +++--- .../java/dev/lukebemish/codecextras/types/AppMu.java | 12 ------------ .../dev/lukebemish/codecextras/types/Raised.java | 12 ++++++++++++ .../stream/structured/MinecraftStructures.java | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 src/main/java/dev/lukebemish/codecextras/types/AppMu.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/Raised.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 8c4a73e..213cd9e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,8 +4,8 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; -import dev.lukebemish.codecextras.types.AppMu; import dev.lukebemish.codecextras.types.Identity; +import dev.lukebemish.codecextras.types.Raised; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -102,11 +102,11 @@ public DataResult> interpret(Interpreter interpre }; } - static Structure keyed(Key key, Keys keys) { + static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.key().flatMap(k -> keys.get(k).>map(AppMu::unbox).map(AppMu::value)) + return interpreter.key().flatMap(k -> keys.get(k).>map(Raised::unbox).map(Raised::value)) .map(DataResult::success) .orElseGet(() -> interpreter.keyed(key)); } diff --git a/src/main/java/dev/lukebemish/codecextras/types/AppMu.java b/src/main/java/dev/lukebemish/codecextras/types/AppMu.java deleted file mode 100644 index b66da73..0000000 --- a/src/main/java/dev/lukebemish/codecextras/types/AppMu.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.lukebemish.codecextras.types; - -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.K1; - -public record AppMu(App value) implements App { - public static final class Mu implements K1 { private Mu() {} } - - public static AppMu unbox(App box) { - return (AppMu) box; - } -} diff --git a/src/main/java/dev/lukebemish/codecextras/types/Raised.java b/src/main/java/dev/lukebemish/codecextras/types/Raised.java new file mode 100644 index 0000000..4097a92 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/Raised.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +public record Raised(App value) implements App, F> { + public static final class Mu implements K1 { private Mu() {} } + + public static Raised unbox(App, M> box) { + return (Raised) box; + } +} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index b92938e..461d5dd 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -12,7 +12,7 @@ import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.types.AppMu; +import dev.lukebemish.codecextras.types.Raised; import net.minecraft.core.Registry; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; @@ -108,8 +108,8 @@ public static final class Structures { private Structures() {} public static final Structure RESOURCE_LOCATION = Structure.keyed( - Types.RESOURCE_LOCATION, Keys.builder() - .add(CodecInterpreter.KEY, new AppMu<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) + Types.RESOURCE_LOCATION, Keys., K1>builder() + .add(CodecInterpreter.KEY, new Raised<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) .build() ); From a5062152e68bf220fc3744a954f64f5a1b9d41e1 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 15:45:27 -0500 Subject: [PATCH 22/76] Structure-specific parametric keys --- .../codecextras/structured/Structure.java | 13 +++++++++++++ .../stream/structured/MinecraftStructures.java | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 213cd9e..5f68797 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -124,6 +124,19 @@ public DataResult> interpret(Interpreter interpre }; } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.key().flatMap(k -> keys.get(k).>map(Raised::unbox).map(Raised::value)) + .map(DataResult::success) + .orElseGet(() -> interpreter.parametricallyKeyed(key, parameter).flatMap(app -> + interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) + )); + } + }; + } + static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index 461d5dd..042317c 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -114,8 +114,19 @@ private Structures() {} ); public static Structure> resourceKey(ResourceKey> registry) { - return Structure.parametricallyKeyed(Types.RESOURCE_KEY, new Types.RegistryKeyHolder<>(registry), Types.ResourceKeyHolder::unbox) - .xmap(Types.ResourceKeyHolder::value, Types.ResourceKeyHolder::new); + return Structure.parametricallyKeyed( + Types.RESOURCE_KEY, + new Types.RegistryKeyHolder<>(registry), + Types.ResourceKeyHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Raised<>(new CodecInterpreter.Holder<>( + ResourceKey.codec(registry).xmap(Types.ResourceKeyHolder::new, Types.ResourceKeyHolder::value) + )) + ) + .build() + ).xmap(Types.ResourceKeyHolder::value, Types.ResourceKeyHolder::new); } } } From 6e3bc3eab40ef2ae133104e9a12b2d5167f74d47 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 15:50:58 -0500 Subject: [PATCH 23/76] Rename Raised to Flip --- .../lukebemish/codecextras/structured/Structure.java | 10 +++++----- .../java/dev/lukebemish/codecextras/types/Flip.java | 12 ++++++++++++ .../dev/lukebemish/codecextras/types/Raised.java | 12 ------------ .../stream/structured/MinecraftStructures.java | 10 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/types/Flip.java delete mode 100644 src/main/java/dev/lukebemish/codecextras/types/Raised.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 5f68797..b863cc3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -4,8 +4,8 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; -import dev.lukebemish.codecextras.types.Raised; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -102,11 +102,11 @@ public DataResult> interpret(Interpreter interpre }; } - static Structure keyed(Key key, Keys, K1> keys) { + static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.key().flatMap(k -> keys.get(k).>map(Raised::unbox).map(Raised::value)) + return interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) .map(DataResult::success) .orElseGet(() -> interpreter.keyed(key)); } @@ -124,11 +124,11 @@ public DataResult> interpret(Interpreter interpre }; } - static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys) { + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys) { return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.key().flatMap(k -> keys.get(k).>map(Raised::unbox).map(Raised::value)) + return interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) .map(DataResult::success) .orElseGet(() -> interpreter.parametricallyKeyed(key, parameter).flatMap(app -> interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) diff --git a/src/main/java/dev/lukebemish/codecextras/types/Flip.java b/src/main/java/dev/lukebemish/codecextras/types/Flip.java new file mode 100644 index 0000000..3ab4daa --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/Flip.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +public record Flip(App value) implements App, F> { + public static final class Mu implements K1 { private Mu() {} } + + public static Flip unbox(App, M> box) { + return (Flip) box; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/Raised.java b/src/main/java/dev/lukebemish/codecextras/types/Raised.java deleted file mode 100644 index 4097a92..0000000 --- a/src/main/java/dev/lukebemish/codecextras/types/Raised.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.lukebemish.codecextras.types; - -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.K1; - -public record Raised(App value) implements App, F> { - public static final class Mu implements K1 { private Mu() {} } - - public static Raised unbox(App, M> box) { - return (Raised) box; - } -} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index 042317c..4a70be1 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -12,7 +12,7 @@ import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.types.Raised; +import dev.lukebemish.codecextras.types.Flip; import net.minecraft.core.Registry; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; @@ -108,8 +108,8 @@ public static final class Structures { private Structures() {} public static final Structure RESOURCE_LOCATION = Structure.keyed( - Types.RESOURCE_LOCATION, Keys., K1>builder() - .add(CodecInterpreter.KEY, new Raised<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) + Types.RESOURCE_LOCATION, Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) .build() ); @@ -118,10 +118,10 @@ public static Structure> resourceKey(ResourceKey(registry), Types.ResourceKeyHolder::unbox, - Keys.>, K1>builder() + Keys.>, K1>builder() .add( CodecInterpreter.KEY, - new Raised<>(new CodecInterpreter.Holder<>( + new Flip<>(new CodecInterpreter.Holder<>( ResourceKey.codec(registry).xmap(Types.ResourceKeyHolder::new, Types.ResourceKeyHolder::value) )) ) From 99f676e5e5f46e4be4debcbdea598923a56a4c20 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 16:37:08 -0500 Subject: [PATCH 24/76] Implement dispatch for everything except JSON schema --- .../structured/CodecAndMapInterpreters.java | 64 +++++++++++++++++ .../structured/CodecInterpreter.java | 40 +++++++++-- .../codecextras/structured/Interpreter.java | 3 + .../structured/JsonSchemaInterpreter.java | 7 ++ .../structured/MapCodecInterpreter.java | 68 ++++++++++--------- .../codecextras/structured/Structure.java | 13 ++++ .../structured/StreamCodecInterpreter.java | 20 ++++++ .../test/structured/TestParametricKeys.java | 7 +- .../test/structured/TestStructured.java | 2 +- 9 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/CodecAndMapInterpreters.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecAndMapInterpreters.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecAndMapInterpreters.java new file mode 100644 index 0000000..1b0f647 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecAndMapInterpreters.java @@ -0,0 +1,64 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; + +class CodecAndMapInterpreters { + private final CodecInterpreter codecInterpreter; + private final MapCodecInterpreter mapCodecInterpreter; + + CodecAndMapInterpreters( + Keys codecKeys, + Keys mapCodecKeys, + Keys2, K1, K1> parametricCodecKeys, + Keys2, K1, K1> parametricMapCodecKeys + ) { + this.mapCodecInterpreter = new MapCodecInterpreter(mapCodecKeys, parametricMapCodecKeys) { + @Override + protected CodecInterpreter codecInterpreter() { + return CodecAndMapInterpreters.this.codecInterpreter; + } + }; + this.codecInterpreter = new CodecInterpreter(mapCodecKeys.map(new Keys.Converter<>() { + @Override + public App convert(App app) { + return new CodecInterpreter.Holder<>(MapCodecInterpreter.unbox(app).codec()); + } + }).join(codecKeys), parametricMapCodecKeys.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { + @Override + public App2, A, B> convert(App2, A, B> input) { + var unboxed = ParametricKeyedValue.unbox(input); + return new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var mapCodec = MapCodecInterpreter.unbox(unboxed.convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }; + } + }).join(parametricCodecKeys)) { + @Override + protected MapCodecInterpreter mapCodecInterpreter() { + return CodecAndMapInterpreters.this.mapCodecInterpreter; + } + }; + } + + CodecAndMapInterpreters() { + this( + Keys.builder().build(), + Keys.builder().build(), + Keys2., K1, K1>builder().build(), + Keys2., K1, K1>builder().build() + ); + } + + public CodecInterpreter codecInterpreter() { + return codecInterpreter; + } + + public MapCodecInterpreter mapCodecInterpreter() { + return mapCodecInterpreter; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index f9c03e7..5454222 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -5,13 +5,16 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; -public class CodecInterpreter extends KeyStoringInterpreter { +public abstract class CodecInterpreter extends KeyStoringInterpreter { public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) @@ -27,13 +30,21 @@ public CodecInterpreter(Keys keys, Keys2builder().build(), - Keys2., K1, K1>builder().build() - ); + public static CodecInterpreter create( + Keys codecKeys, + Keys mapCodecKeys, + Keys2, K1, K1> parametricCodecKeys, + Keys2, K1, K1> parametricMapCodecKeys + ) { + return new CodecAndMapInterpreters(codecKeys, mapCodecKeys, parametricCodecKeys, parametricMapCodecKeys).codecInterpreter(); } + public static CodecInterpreter create() { + return new CodecAndMapInterpreters().codecInterpreter(); + } + + protected abstract MapCodecInterpreter mapCodecInterpreter(); + @Override public DataResult>> list(App single) { return DataResult.success(new Holder<>(CommentFirstListCodec.of(Holder.unbox(single).codec))); @@ -57,6 +68,23 @@ public DataResult> annotate(App input, Keys< return DataResult.success(input); } + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + return keyStructure.interpret(this).flatMap(keyCodecApp -> { + var keyCodec = unbox(keyCodecApp); + // Object here as it's the furthest super A and we have only ? super A + Map> codecMap = new HashMap<>(); + for (var entry : structures.entrySet()) { + var result = entry.getValue().interpret(mapCodecInterpreter()); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + codecMap.put(entry.getKey(), MapCodecInterpreter.unbox(result.result().orElseThrow())); + } + return DataResult.success(new Holder<>(keyCodec.dispatch(key, function, codecMap::get))); + }); + } + public static final Key KEY = Key.create("CodecInterpreter"); @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index a9f62c3..95fb6d7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -6,6 +6,7 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -20,6 +21,8 @@ public interface Interpreter { DataResult> annotate(App input, Keys annotations); + DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures); + default Optional> key() { return Optional.empty(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 462f479..cc2d1e5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -7,6 +7,7 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -88,6 +89,12 @@ public DataResult> annotate(App input, Keys< return DataResult.success(new Holder<>(schema)); } + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + // TODO: implement + return DataResult.error(() -> "Not yet implemented!"); + } + public static JsonObject unbox(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 9a525d3..f97e1cf 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -1,55 +1,40 @@ package dev.lukebemish.codecextras.structured; import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; -public class MapCodecInterpreter extends KeyStoringInterpreter { - private final CodecInterpreter codecInterpreter; - +public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( Keys keys, - Keys codecKeys, - Keys2, K1, K1> parametricKeys, - Keys2, K1, K1> parametricCodecKeys + Keys2, K1, K1> parametricKeys ) { super(keys, parametricKeys); - this.codecInterpreter = new CodecInterpreter(keys.map(new Keys.Converter<>() { - @Override - public App convert(App app) { - return new CodecInterpreter.Holder<>(unbox(app).codec()); - } - }).join(codecKeys), parametricKeys.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { - @Override - public App2, A, B> convert(App2, A, B> input) { - var unboxed = ParametricKeyedValue.unbox(input); - return new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - var mapCodec = unbox(unboxed.convert(parameter)); - return new CodecInterpreter.Holder<>(mapCodec.codec()); - } - }; - } - }).join(parametricCodecKeys)); } - public MapCodecInterpreter() { - this( - Keys.builder().build(), - Keys.builder().build(), - Keys2., K1, K1>builder().build(), - Keys2., K1, K1>builder().build() - ); + public static MapCodecInterpreter create( + Keys codecKeys, + Keys mapCodecKeys, + Keys2, K1, K1> parametricCodecKeys, + Keys2, K1, K1> parametricMapCodecKeys + ) { + return new CodecAndMapInterpreters(codecKeys, mapCodecKeys, parametricCodecKeys, parametricMapCodecKeys).mapCodecInterpreter(); } + public static MapCodecInterpreter create() { + return new CodecAndMapInterpreters().mapCodecInterpreter(); + } + + protected abstract CodecInterpreter codecInterpreter(); + @Override public DataResult>> list(App single) { return DataResult.error(() -> "Cannot make a MapCodec for a list"); @@ -57,7 +42,7 @@ public DataResult>> list(App single) { @Override public DataResult> record(List> fields, Function creator) { - return StructuredMapCodec.of(fields, creator, codecInterpreter, CodecInterpreter::unbox) + return StructuredMapCodec.of(fields, creator, codecInterpreter(), CodecInterpreter::unbox) .map(Holder::new); } @@ -84,6 +69,23 @@ public DataResult> interpret(Structure structure) { return structure.interpret(this).map(MapCodecInterpreter::unbox); } + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + return keyStructure.interpret(codecInterpreter()).flatMap(keyCodecApp -> { + var keyCodec = CodecInterpreter.unbox(keyCodecApp); + // Object here as it's the furthest super A and we have only ? super A + Map> codecMap = new HashMap<>(); + for (var entry : structures.entrySet()) { + var result = entry.getValue().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + codecMap.put(entry.getKey(), MapCodecInterpreter.unbox(result.result().orElseThrow())); + } + return DataResult.success(new MapCodecInterpreter.Holder<>(keyCodec.dispatchMap(key, function, codecMap::get))); + }); + } + public record Holder(MapCodec mapCodec) implements App { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index b863cc3..6a4725d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; @@ -7,6 +8,7 @@ import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -141,6 +143,17 @@ static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } + default Structure dispatch(String key, Function function, Supplier>> structures) { + var structureSupplier = Suppliers.memoize(structures::get); + var outer = this; + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.dispatch(key, outer, function, structureSupplier.get()); + } + }; + } + Structure UNIT = keyed(Interpreter.UNIT); Structure BOOL = keyed(Interpreter.BOOL); Structure BYTE = keyed(Interpreter.BYTE); diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index d50ba46..5e63787 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -14,7 +14,9 @@ import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Function; import net.minecraft.network.codec.ByteBufCodecs; @@ -91,6 +93,24 @@ public DataResult, A>> annotate(App, A> input, return DataResult.success(input); } + @Override + public DataResult, E>> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + return keyStructure.interpret(this).flatMap(keyCodecApp -> { + var keyStreamCodec = unbox(keyCodecApp); + Map> codecMap = new HashMap<>(); + for (var entry : structures.entrySet()) { + var result = entry.getValue().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + codecMap.put(entry.getKey(), StreamCodecInterpreter.unbox(result.result().orElseThrow())); + } + return DataResult.success(new Holder<>( + keyStreamCodec.dispatch(function, codecMap::get) + )); + }); + } + private static void encodeSingleField(B buf, Field field, A data) { var missingBehaviour = field.missingBehavior(); if (missingBehaviour.isEmpty()) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java index 03f79f1..fcf0ab2 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java @@ -9,6 +9,7 @@ import dev.lukebemish.codecextras.structured.Key2; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.test.CodecAssertions; @@ -49,8 +50,9 @@ private static Codec> withTypeCodec(Prefix prefix) { WithType::unbox ); - private static final Codec> CODEC = new CodecInterpreter( + private static final Codec> CODEC = CodecInterpreter.create( Keys.builder().build(), + Keys.builder().build(), Keys2., K1, K1>builder() .add(WITH_TYPE, new ParametricKeyedValue<>() { @Override @@ -58,7 +60,8 @@ public App> convert(App(withTypeCodec(Prefix.unbox(parameter)).xmap(Function.identity(), WithType::unbox)); } }) - .build() + .build(), + Keys2., K1, K1>builder().build() ).interpret(STRUCTURE).getOrThrow(); private final String json = "\"prefix:123\""; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 90c4f5a..b7a61f7 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -21,7 +21,7 @@ private record TestRecord(int a, String b, List c, Optional d) return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); }); - private static final Codec CODEC = new CodecInterpreter().interpret(STRUCTURE).getOrThrow(); + private static final Codec CODEC = CodecInterpreter.create().interpret(STRUCTURE).getOrThrow(); } private final String json = """ From c8aea64dc44c737e8594a7e014cf532edc83ebb7 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 16:39:08 -0500 Subject: [PATCH 25/76] Implement lazy structure --- .editorconfig | 2 +- .../codecextras/structured/Structure.java | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.editorconfig b/.editorconfig index b044983..dd3ccd8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ insert_final_newline = true charset = utf-8 indent_size = 4 trim_trailing_whitespace = true -indent_style = tab +indent_style = space [*.{json,json5,mcmeta}] indent_size = 2 diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6a4725d..6c53001 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -95,6 +95,26 @@ default Structure xmap(Function deserializer, Function serial return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); } + default Structure dispatch(String key, Function function, Supplier>> structures) { + var structureSupplier = Suppliers.memoize(structures::get); + var outer = this; + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.dispatch(key, outer, function, structureSupplier.get()); + } + }; + } + + static Structure lazy(Supplier> supplier) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return supplier.get().interpret(interpreter); + } + }; + } + static Structure keyed(Key key) { return new Structure<>() { @Override @@ -143,17 +163,6 @@ static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } - default Structure dispatch(String key, Function function, Supplier>> structures) { - var structureSupplier = Suppliers.memoize(structures::get); - var outer = this; - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return interpreter.dispatch(key, outer, function, structureSupplier.get()); - } - }; - } - Structure UNIT = keyed(Interpreter.UNIT); Structure BOOL = keyed(Interpreter.BOOL); Structure BYTE = keyed(Interpreter.BYTE); From c3b31cc617a157d5c3b061dcdda3593a01a3a1bf Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 16:43:27 -0500 Subject: [PATCH 26/76] Avoid needless memoized supplier --- .../java/dev/lukebemish/codecextras/structured/Structure.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6c53001..b7b3cfb 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -1,6 +1,5 @@ package dev.lukebemish.codecextras.structured; -import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; @@ -96,12 +95,11 @@ default Structure xmap(Function deserializer, Function serial } default Structure dispatch(String key, Function function, Supplier>> structures) { - var structureSupplier = Suppliers.memoize(structures::get); var outer = this; return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.dispatch(key, outer, function, structureSupplier.get()); + return interpreter.dispatch(key, outer, function, structures.get()); } }; } From e512f7de22ee96836a3d467296c76c07c1bfa0df Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 23:54:29 -0500 Subject: [PATCH 27/76] Easier creation of interpreters --- .../structured/CodecInterpreter.java | 21 ++++++- .../structured/JsonSchemaInterpreter.java | 10 +++- .../structured/KeyStoringInterpreter.java | 12 +++- .../structured/MapCodecInterpreter.java | 21 ++++++- .../structured/ParametricKeyedValue.java | 14 +++++ .../codecextras/types/Identity.java | 24 ++++++++ .../structured/MinecraftStructures.java | 59 ++++++++++++++++++- .../structured/StreamCodecInterpreter.java | 14 +++-- 8 files changed, 164 insertions(+), 11 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 5454222..486652a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -14,7 +14,7 @@ import java.util.Optional; import java.util.function.Function; -public abstract class CodecInterpreter extends KeyStoringInterpreter { +public abstract class CodecInterpreter extends KeyStoringInterpreter { public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(Codec.unit(Unit.INSTANCE))) @@ -85,6 +85,25 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public CodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { + return new CodecAndMapInterpreters(keys().join(keys), mapCodecInterpreter().keys(), parametricKeys().join(parametricKeys), mapCodecInterpreter().parametricKeys()).codecInterpreter(); + } + + public CodecInterpreter with( + Keys codecKeys, + Keys mapCodecKeys, + Keys2, K1, K1> parametricCodecKeys, + Keys2, K1, K1> parametricMapCodecKeys + ) { + return new CodecAndMapInterpreters( + keys().join(codecKeys), + mapCodecInterpreter().keys().join(mapCodecKeys), + parametricKeys().join(parametricCodecKeys), + mapCodecInterpreter().parametricKeys().join(parametricMapCodecKeys) + ).codecInterpreter(); + } + public static final Key KEY = Key.create("CodecInterpreter"); @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index cc2d1e5..986ab47 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -13,7 +13,7 @@ import java.util.function.Supplier; import org.jspecify.annotations.Nullable; -public class JsonSchemaInterpreter extends KeyStoringInterpreter { +public class JsonSchemaInterpreter extends KeyStoringInterpreter { public JsonSchemaInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { super(keys.join(Keys.builder() .add(Interpreter.UNIT, new Holder<>(OBJECT)) @@ -29,6 +29,14 @@ public JsonSchemaInterpreter(Keys keys, Keys2 keys, Keys2, K1, K1> parametricKeys) { + return new JsonSchemaInterpreter( + keys().join(keys), + parametricKeys().join(parametricKeys) + ); + } + public JsonSchemaInterpreter() { this( Keys.builder().build(), diff --git a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java index 7865e6a..993c75c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/KeyStoringInterpreter.java @@ -4,7 +4,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; -public abstract class KeyStoringInterpreter implements Interpreter { +public abstract class KeyStoringInterpreter> implements Interpreter { private final Keys keys; private final Keys2, K1, K1> parametricKeys; @@ -26,4 +26,14 @@ public DataResult>> para .map(DataResult::success) .orElse(DataResult.error(() -> "Unknown key "+key.name())); } + + protected Keys keys() { + return keys; + } + + protected Keys2, K1, K1> parametricKeys() { + return parametricKeys; + } + + public abstract SELF with(Keys keys, Keys2, K1, K1> parametricKeys); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index f97e1cf..ef91639 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -12,7 +12,7 @@ import java.util.Optional; import java.util.function.Function; -public abstract class MapCodecInterpreter extends KeyStoringInterpreter { +public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( Keys keys, Keys2, K1, K1> parametricKeys @@ -86,6 +86,25 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public MapCodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { + return new CodecAndMapInterpreters(codecInterpreter().keys(), keys().join(keys), codecInterpreter().parametricKeys(), parametricKeys().join(parametricKeys)).mapCodecInterpreter(); + } + + public MapCodecInterpreter with( + Keys codecKeys, + Keys mapCodecKeys, + Keys2, K1, K1> parametricCodecKeys, + Keys2, K1, K1> parametricMapCodecKeys + ) { + return new CodecAndMapInterpreters( + codecInterpreter().keys().join(codecKeys), + keys().join(mapCodecKeys), + codecInterpreter().parametricKeys().join(parametricCodecKeys), + parametricKeys().join(parametricMapCodecKeys) + ).mapCodecInterpreter(); + } + public record Holder(MapCodec mapCodec) implements App { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java index 47103d7..22c2498 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/ParametricKeyedValue.java @@ -10,6 +10,20 @@ final class Mu implements K2 { private Mu() {} } App> convert(App parameter); + default ParametricKeyedValue map(Converter converter) { + var outer = this; + return new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return converter.convert(outer.convert(parameter)); + } + }; + } + + interface Converter { + App> convert(App> app); + } + static ParametricKeyedValue unbox(App2, MuP, MuO> boxed) { return (ParametricKeyedValue) boxed; } diff --git a/src/main/java/dev/lukebemish/codecextras/types/Identity.java b/src/main/java/dev/lukebemish/codecextras/types/Identity.java index ab860b0..8eb0e48 100644 --- a/src/main/java/dev/lukebemish/codecextras/types/Identity.java +++ b/src/main/java/dev/lukebemish/codecextras/types/Identity.java @@ -1,7 +1,9 @@ package dev.lukebemish.codecextras.types; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Applicative; import com.mojang.datafixers.kinds.K1; +import java.util.function.Function; public record Identity(T value) implements App { public static final class Mu implements K1 { private Mu() {} } @@ -9,4 +11,26 @@ public static final class Mu implements K1 { private Mu() {} } public static Identity unbox(App input) { return (Identity) input; } + + enum Instance implements Applicative { + INSTANCE; + + public static final class Mu implements Applicative.Mu { private Mu() {} } + + @Override + public App point(A a) { + return new Identity<>(a); + } + + @Override + public Function, App> lift1(App> function) { + var f = unbox(function).value(); + return app -> new Identity<>(f.apply(unbox(app).value())); + } + + @Override + public App map(Function func, App ts) { + return new Identity<>(func.apply(unbox(ts).value())); + } + } } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index 4a70be1..5037a0b 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -14,6 +14,7 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Flip; import net.minecraft.core.Registry; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -59,14 +60,28 @@ public App> c .build() ); - public static final Keys, Object> REGISTRY_STREAM_KEYS = Keys., Object>builder() + public static final CodecInterpreter CODEC_INTERPRETER = CodecInterpreter.create().with( + CODEC_KEYS, + MAP_CODEC_KEYS, + CODEC_PARAMETRIC_KEYS, + MAP_CODEC_PARAMETRIC_KEYS + ); + + public static final MapCodecInterpreter MAP_CODEC_INTERPRETER = MapCodecInterpreter.create().with( + CODEC_KEYS, + MAP_CODEC_KEYS, + CODEC_PARAMETRIC_KEYS, + MAP_CODEC_PARAMETRIC_KEYS + ); + + public static final Keys, Object> FRIENDLY_STREAM_KEYS = Keys., Object>builder() .add(Types.RESOURCE_LOCATION, new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast())) .build(); - public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() + public static final Keys2>, K1, K1> FRIENDLY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { @Override - public App, App> convert(App parameter) { + public App, App> convert(App parameter) { return new StreamCodecInterpreter.Holder<>( ResourceKey.streamCodec(Types.RegistryKeyHolder.unbox(parameter).value()).>map(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value()).cast() ); @@ -74,12 +89,50 @@ public App, App FRIENDLY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + FRIENDLY_STREAM_KEYS, + FRIENDLY_STREAM_PARAMETRIC_KEYS + ); + + public static final Keys, Object> REGISTRY_STREAM_KEYS = FRIENDLY_STREAM_KEYS.map(new Keys.Converter, StreamCodecInterpreter.Holder.Mu, Object>() { + @Override + public App, A> convert(App, A> input) { + return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(input).cast()); + } + }).join(Keys., Object>builder() + .build() + ); + + public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = FRIENDLY_STREAM_PARAMETRIC_KEYS.map(new Keys2.Converter>, ParametricKeyedValue.Mu>, K1, K1>() { + @Override + public App2>, A, B> convert(App2>, A, B> input) { + return new ParametricKeyedValue<>() { + @Override + public App, App> convert(App parameter) { + return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(ParametricKeyedValue.unbox(input).convert(parameter)).cast()); + } + }; + } + }).join(Keys2.>, K1, K1>builder() + .build() + ); + + public static final StreamCodecInterpreter REGISTRY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + REGISTRY_STREAM_KEYS, + REGISTRY_STREAM_PARAMETRIC_KEYS + ); + public static final Keys JSON_SCHEMA_KEYS = Keys.builder() .build(); public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() .build(); + public static final JsonSchemaInterpreter JSON_SCHEMA_INTERPRETER = new JsonSchemaInterpreter( + JSON_SCHEMA_KEYS, + JSON_SCHEMA_PARAMETRIC_KEYS + ); + public static final class Types { private Types() {} diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 5e63787..61e055f 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -13,17 +13,18 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; -public class StreamCodecInterpreter extends KeyStoringInterpreter> { +public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { super(keys.join(Keys., Object>builder() .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) @@ -46,6 +47,11 @@ public StreamCodecInterpreter() { ); } + @Override + public StreamCodecInterpreter with(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { + return new StreamCodecInterpreter<>(keys().join(keys), parametricKeys().join(parametricKeys)); + } + @Override public DataResult, List>> list(App, A> single) { return DataResult.success(new Holder<>(StreamCodecInterpreter.list(unbox(single)))); From 99befe2a2479ca0867919ea703c264ccbdbfc365 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 23 Jul 2024 23:54:47 -0500 Subject: [PATCH 28/76] Fix formatting --- .../stream/structured/StreamCodecInterpreter.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 61e055f..3895593 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -13,16 +13,15 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { From bb23f3e838ba0acc23b0ea8890726a5809737dbf Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 24 Jul 2024 00:21:08 -0500 Subject: [PATCH 29/76] Lazy structures and registry dispatch structure --- .../structured/CodecInterpreter.java | 19 +++++++++++++ .../codecextras/structured/Interpreter.java | 2 ++ .../structured/JsonSchemaInterpreter.java | 5 ++++ .../structured/MapCodecInterpreter.java | 27 +++++++++++++++++++ .../codecextras/structured/Structure.java | 19 +++++++++++++ .../structured/MinecraftStructures.java | 18 +++++++++++++ .../structured/StreamCodecInterpreter.java | 10 +++++++ 7 files changed, 100 insertions(+) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 486652a..5767694 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -1,10 +1,13 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; @@ -68,6 +71,22 @@ public DataResult> annotate(App input, Keys< return DataResult.success(input); } + @Override + public DataResult> lazy(Structure structure) { + var supplier = Suppliers.memoize(() -> structure.interpret(this)); + return DataResult.success(new Holder<>(new Codec() { + @Override + public DataResult> decode(DynamicOps ops, T input) { + return supplier.get().map(CodecInterpreter::unbox).flatMap(c -> c.decode(ops, input)); + } + + @Override + public DataResult encode(A input, DynamicOps ops, T prefix) { + return supplier.get().map(CodecInterpreter::unbox).flatMap(c -> c.encode(input, ops, prefix)); + } + })); + } + @Override public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 95fb6d7..6e2c31d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -23,6 +23,8 @@ public interface Interpreter { DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures); + DataResult> lazy(Structure structure); + default Optional> key() { return Optional.empty(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 986ab47..1beeb92 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -103,6 +103,11 @@ public DataResult> dispatch(String key, Structure ke return DataResult.error(() -> "Not yet implemented!"); } + @Override + public DataResult> lazy(Structure structure) { + return structure.interpret(this); + } + public static JsonObject unbox(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index ef91639..fdc0bc3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -1,9 +1,13 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.HashMap; @@ -11,6 +15,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Stream; public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( @@ -69,6 +74,28 @@ public DataResult> interpret(Structure structure) { return structure.interpret(this).map(MapCodecInterpreter::unbox); } + @Override + public DataResult> lazy(Structure structure) { + var supplier = Suppliers.memoize(() -> structure.interpret(this)); + return DataResult.success(new Holder<>(new MapCodec() { + @Override + public Stream keys(DynamicOps ops) { + return supplier.get().map(MapCodecInterpreter::unbox).result().map(c -> c.keys(ops)).orElse(Stream.of()); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + return supplier.get().map(MapCodecInterpreter::unbox).flatMap(c -> c.decode(ops, input)); + } + + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + var codec = supplier.get().map(MapCodecInterpreter::unbox); + return codec.result().map(c -> c.encode(input, ops, prefix)).orElse(prefix.withErrorsFrom(codec)); + } + })); + } + @Override public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { return keyStructure.interpret(codecInterpreter()).flatMap(keyCodecApp -> { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index b7b3cfb..6e2d1c0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -104,6 +104,25 @@ public DataResult> interpret(Interpreter interpre }; } + default Structure lazy() { + var outer = this; + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.lazy(outer); + } + }; + } + + static Structure lazyInitialized(Supplier> supplier) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return supplier.get().interpret(interpreter); + } + }; + } + static Structure lazy(Supplier> supplier) { return new Structure<>() { @Override diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index 5037a0b..578cfcb 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -3,7 +3,9 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Interpreter; import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; @@ -13,6 +15,10 @@ import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Flip; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -181,5 +187,17 @@ public static Structure> resourceKey(ResourceKey Structure registryDispatch(String keyField, Function>> structureFunction, Registry> registry) { + var keyStructure = resourceKey(registry.key()); + return new Structure() { + @Override + public DataResult> interpret(Interpreter interpreter) { + Map>, Structure> map = new IdentityHashMap<>(); + registry.registryKeySet().forEach(key -> map.put(key, Objects.requireNonNull(registry.get(key)))); + return interpreter.dispatch(keyField, keyStructure, structureFunction, map); + } + }.lazy(); + } } } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 3895593..e2ae938 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.stream.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; @@ -116,6 +117,15 @@ public DataResult, E>> dispatch(String key, Structure }); } + @Override + public DataResult, A>> lazy(Structure structure) { + var supplier = Suppliers.memoize(() -> structure.interpret(this)); + return DataResult.success(new Holder<>(StreamCodec.of( + (buf, data) -> unbox(supplier.get().getOrThrow()).encode(buf, data), + buf -> unbox(supplier.get().getOrThrow()).decode(buf) + ))); + } + private static void encodeSingleField(B buf, Field field, A data) { var missingBehaviour = field.missingBehavior(); if (missingBehaviour.isEmpty()) { From a68d4365f8050dcf0c2a7072c69e4a57057e2b74 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 24 Jul 2024 18:18:05 -0500 Subject: [PATCH 30/76] Make approach to laziness of dispatch structures a bit more sensible --- .../structured/CodecInterpreter.java | 42 ++++++--------- .../codecextras/structured/Interpreter.java | 6 +-- .../structured/JsonSchemaInterpreter.java | 9 +--- .../structured/MapCodecInterpreter.java | 51 ++++++------------- .../codecextras/structured/Structure.java | 16 ++---- .../structured/MinecraftStructures.java | 15 +----- .../structured/StreamCodecInterpreter.java | 32 +++++------- 7 files changed, 54 insertions(+), 117 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 5767694..f4d92c2 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -3,11 +3,9 @@ import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; -import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; @@ -15,7 +13,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; public abstract class CodecInterpreter extends KeyStoringInterpreter { public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { @@ -72,35 +72,23 @@ public DataResult> annotate(App input, Keys< } @Override - public DataResult> lazy(Structure structure) { - var supplier = Suppliers.memoize(() -> structure.interpret(this)); - return DataResult.success(new Holder<>(new Codec() { - @Override - public DataResult> decode(DynamicOps ops, T input) { - return supplier.get().map(CodecInterpreter::unbox).flatMap(c -> c.decode(ops, input)); - } - - @Override - public DataResult encode(A input, DynamicOps ops, T prefix) { - return supplier.get().map(CodecInterpreter::unbox).flatMap(c -> c.encode(input, ops, prefix)); - } - })); - } - - @Override - public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyCodec = unbox(keyCodecApp); // Object here as it's the furthest super A and we have only ? super A - Map> codecMap = new HashMap<>(); - for (var entry : structures.entrySet()) { - var result = entry.getValue().interpret(mapCodecInterpreter()); - if (result.error().isPresent()) { - return DataResult.error(result.error().get().messageSupplier()); + Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { + Map>> codecMap = new HashMap<>(); + for (var entryKey : keys) { + var result = structures.apply(entryKey).interpret(mapCodecInterpreter()); + if (result.error().isPresent()) { + codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); + } else { + codecMap.put(entryKey, DataResult.success(MapCodecInterpreter.unbox(result.result().orElseThrow()))); + } } - codecMap.put(entry.getKey(), MapCodecInterpreter.unbox(result.result().orElseThrow())); - } - return DataResult.success(new Holder<>(keyCodec.dispatch(key, function, codecMap::get))); + return codecMap; + }); + return DataResult.success(new Holder<>(keyCodec.partialDispatch(key, function, k -> codecMapSupplier.get().get(k)))); }); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 6e2c31d..470cb48 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -6,8 +6,8 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; public interface Interpreter { @@ -21,9 +21,7 @@ public interface Interpreter { DataResult> annotate(App input, Keys annotations); - DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures); - - DataResult> lazy(Structure structure); + DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures); default Optional> key() { return Optional.empty(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java index 1beeb92..ec9d31c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java @@ -7,8 +7,8 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -98,16 +98,11 @@ public DataResult> annotate(App input, Keys< } @Override - public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { // TODO: implement return DataResult.error(() -> "Not yet implemented!"); } - @Override - public DataResult> lazy(Structure structure) { - return structure.interpret(this); - } - public static JsonObject unbox(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index fdc0bc3..cfd9e56 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -4,18 +4,17 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; -import com.mojang.serialization.MapLike; -import com.mojang.serialization.RecordBuilder; +import com.mojang.serialization.codecs.KeyDispatchCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; -import java.util.stream.Stream; +import java.util.function.Supplier; public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( @@ -75,41 +74,23 @@ public DataResult> interpret(Structure structure) { } @Override - public DataResult> lazy(Structure structure) { - var supplier = Suppliers.memoize(() -> structure.interpret(this)); - return DataResult.success(new Holder<>(new MapCodec() { - @Override - public Stream keys(DynamicOps ops) { - return supplier.get().map(MapCodecInterpreter::unbox).result().map(c -> c.keys(ops)).orElse(Stream.of()); - } - - @Override - public DataResult decode(DynamicOps ops, MapLike input) { - return supplier.get().map(MapCodecInterpreter::unbox).flatMap(c -> c.decode(ops, input)); - } - - @Override - public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { - var codec = supplier.get().map(MapCodecInterpreter::unbox); - return codec.result().map(c -> c.encode(input, ops, prefix)).orElse(prefix.withErrorsFrom(codec)); - } - })); - } - - @Override - public DataResult> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { return keyStructure.interpret(codecInterpreter()).flatMap(keyCodecApp -> { var keyCodec = CodecInterpreter.unbox(keyCodecApp); // Object here as it's the furthest super A and we have only ? super A - Map> codecMap = new HashMap<>(); - for (var entry : structures.entrySet()) { - var result = entry.getValue().interpret(this); - if (result.error().isPresent()) { - return DataResult.error(result.error().get().messageSupplier()); + Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { + Map>> codecMap = new HashMap<>(); + for (var entryKey : keys) { + var result = structures.apply(entryKey).interpret(this); + if (result.error().isPresent()) { + codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); + } else { + codecMap.put(entryKey, DataResult.success(MapCodecInterpreter.unbox(result.result().orElseThrow()))); + } } - codecMap.put(entry.getKey(), MapCodecInterpreter.unbox(result.result().orElseThrow())); - } - return DataResult.success(new MapCodecInterpreter.Holder<>(keyCodec.dispatchMap(key, function, codecMap::get))); + return codecMap; + }); + return DataResult.success(new MapCodecInterpreter.Holder<>(new KeyDispatchCodec<>(key, keyCodec, function, k -> codecMapSupplier.get().get(k)))); }); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6e2d1c0..73fca74 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -7,8 +7,8 @@ import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -94,22 +94,12 @@ default Structure xmap(Function deserializer, Function serial return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); } - default Structure dispatch(String key, Function function, Supplier>> structures) { + default Structure dispatch(String key, Function> function, Supplier> keys, Function> structures) { var outer = this; return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.dispatch(key, outer, function, structures.get()); - } - }; - } - - default Structure lazy() { - var outer = this; - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return interpreter.lazy(outer); + return interpreter.dispatch(key, outer, function, keys.get(), structures); } }; } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java index 578cfcb..3225a24 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java @@ -5,7 +5,6 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.Interpreter; import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; @@ -15,9 +14,6 @@ import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Flip; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; import java.util.function.Function; import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; @@ -188,16 +184,9 @@ public static Structure> resourceKey(ResourceKey Structure registryDispatch(String keyField, Function>> structureFunction, Registry> registry) { + public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { var keyStructure = resourceKey(registry.key()); - return new Structure() { - @Override - public DataResult> interpret(Interpreter interpreter) { - Map>, Structure> map = new IdentityHashMap<>(); - registry.registryKeySet().forEach(key -> map.put(key, Objects.requireNonNull(registry.get(key)))); - return interpreter.dispatch(keyField, keyStructure, structureFunction, map); - } - }.lazy(); + return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, registry::get); } } } diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index e2ae938..1893650 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -19,7 +19,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import org.jspecify.annotations.Nullable; @@ -100,32 +102,26 @@ public DataResult, A>> annotate(App, A> input, } @Override - public DataResult, E>> dispatch(String key, Structure keyStructure, Function function, Map> structures) { + public DataResult, E>> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyStreamCodec = unbox(keyCodecApp); - Map> codecMap = new HashMap<>(); - for (var entry : structures.entrySet()) { - var result = entry.getValue().interpret(this); - if (result.error().isPresent()) { - return DataResult.error(result.error().get().messageSupplier()); + Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { + Map>> codecMap = new HashMap<>(); + for (var entryKey : keys) { + var result = structures.apply(entryKey).interpret(this); + if (result.error().isPresent()) { + codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); + } + codecMap.put(entryKey, DataResult.success(StreamCodecInterpreter.unbox(result.result().orElseThrow()))); } - codecMap.put(entry.getKey(), StreamCodecInterpreter.unbox(result.result().orElseThrow())); - } + return codecMap; + }); return DataResult.success(new Holder<>( - keyStreamCodec.dispatch(function, codecMap::get) + keyStreamCodec.dispatch(function.andThen(DataResult::getOrThrow), k -> codecMapSupplier.get().get(k).getOrThrow()) )); }); } - @Override - public DataResult, A>> lazy(Structure structure) { - var supplier = Suppliers.memoize(() -> structure.interpret(this)); - return DataResult.success(new Holder<>(StreamCodec.of( - (buf, data) -> unbox(supplier.get().getOrThrow()).encode(buf, data), - buf -> unbox(supplier.get().getOrThrow()).decode(buf) - ))); - } - private static void encodeSingleField(B buf, Field field, A data) { var missingBehaviour = field.missingBehavior(); if (missingBehaviour.isEmpty()) { From 9594d2f94adf3da9fc59866b4107607765331ca2 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 31 Jul 2024 11:25:19 -0500 Subject: [PATCH 31/76] Some more basic types for conversion between K1 and K2 --- .../codecextras/types/ConstantFirst.java | 14 ++++++++++++++ .../codecextras/types/ConstantSecond.java | 14 ++++++++++++++ .../dev/lukebemish/codecextras/types/First.java | 14 ++++++++++++++ .../dev/lukebemish/codecextras/types/Second.java | 14 ++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/dev/lukebemish/codecextras/types/ConstantFirst.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/ConstantSecond.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/First.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/Second.java diff --git a/src/main/java/dev/lukebemish/codecextras/types/ConstantFirst.java b/src/main/java/dev/lukebemish/codecextras/types/ConstantFirst.java new file mode 100644 index 0000000..e67f413 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/ConstantFirst.java @@ -0,0 +1,14 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.kinds.K2; + +public record ConstantFirst(App value) implements App2, A, B> { + public static final class Mu implements K2 { private Mu() {} } + + public static ConstantFirst unbox(App2, A, B> box) { + return (ConstantFirst) box; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/ConstantSecond.java b/src/main/java/dev/lukebemish/codecextras/types/ConstantSecond.java new file mode 100644 index 0000000..f5883cb --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/ConstantSecond.java @@ -0,0 +1,14 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.kinds.K2; + +public record ConstantSecond(App value) implements App2, A, B> { + public static final class Mu implements K2 { private Mu() {} } + + public static ConstantSecond unbox(App2, A, B> box) { + return (ConstantSecond) box; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/First.java b/src/main/java/dev/lukebemish/codecextras/types/First.java new file mode 100644 index 0000000..77a78a8 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/First.java @@ -0,0 +1,14 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.kinds.K2; + +public record First(App2 value) implements App, A> { + public static final class Mu implements K1 { private Mu() {} } + + public static First unbox(App, A> box) { + return (First) box; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/Second.java b/src/main/java/dev/lukebemish/codecextras/types/Second.java new file mode 100644 index 0000000..a01e077 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/Second.java @@ -0,0 +1,14 @@ +package dev.lukebemish.codecextras.types; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.kinds.K2; + +public record Second(App2 value) implements App, B> { + public static final class Mu implements K1 { private Mu() {} } + + public static Second unbox(App, B> box) { + return (Second) box; + } +} From ae3d85dd476e9254304e1d8991bcdd6ffadf6ccf Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 01:31:10 -0500 Subject: [PATCH 32/76] Dispatch changes and references in schemas, plus prepare for 1.21.2 --- build.gradle | 83 +++-- gradle/libs.versions.toml | 7 + settings.gradle | 6 +- .../structured/CodecInterpreter.java | 8 +- .../codecextras/structured/Interpreter.java | 2 +- .../structured/JsonSchemaInterpreter.java | 152 --------- .../structured/MapCodecInterpreter.java | 14 +- .../codecextras/structured/Structure.java | 10 +- .../schema/JsonSchemaInterpreter.java | 294 ++++++++++++++++++ .../structured/schema/SchemaAnnotations.java | 13 + .../structured/schema/package-info.java | 6 + .../structured/MinecraftStructures.java | 29 +- .../minecraft/structured/package-info.java | 6 + .../stream/AsymmetricalStreamCodecs.java | 0 .../codecextras/stream/StreamCodecExtras.java | 0 .../stream/mutable/StreamDataElementType.java | 0 .../stream/mutable/package-info.java | 0 .../codecextras/stream/package-info.java | 0 .../structured/StreamCodecInterpreter.java | 11 +- .../stream/structured/package-info.java | 0 .../resources/fabric.mod.json | 6 + .../resources/fabric.mod.json | 6 - .../test/structured/TestDispatch.java | 178 +++++++++++ .../test/structured/TestStructured.java | 4 +- version.properties | 2 +- 25 files changed, 614 insertions(+), 223 deletions(-) create mode 100644 gradle/libs.versions.toml delete mode 100644 src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/schema/SchemaAnnotations.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/schema/package-info.java rename src/{stream/java/dev/lukebemish/codecextras/stream => minecraft/java/dev/lukebemish/codecextras/minecraft}/structured/MinecraftStructures.java (90%) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/package-info.java rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/AsymmetricalStreamCodecs.java (100%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/StreamCodecExtras.java (100%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/mutable/StreamDataElementType.java (100%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/mutable/package-info.java (100%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/package-info.java (100%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java (98%) rename src/{stream => minecraft}/java/dev/lukebemish/codecextras/stream/structured/package-info.java (100%) create mode 100644 src/minecraftIntermediary/resources/fabric.mod.json delete mode 100644 src/streamIntermediary/resources/fabric.mod.json create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java diff --git a/build.gradle b/build.gradle index 2f384ff..3ad5abe 100644 --- a/build.gradle +++ b/build.gradle @@ -96,8 +96,8 @@ managedVersioning.apply() println "Building: $version" sourceSets { - stream {} - streamIntermediary {} + minecraft {} + minecraftIntermediary {} jmh {} } @@ -105,15 +105,21 @@ java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) withSourcesJar() withJavadocJar() - registerFeature("stream") { - usingSourceSet sourceSets.stream + registerFeature("minecraft") { + usingSourceSet sourceSets.minecraft withSourcesJar() withJavadocJar() + capability(project.group as String, "$project.name-minecraft", project.version as String) + // Old name + capability(project.group as String, "$project.name-stream", project.version as String) } - registerFeature("streamIntermediary") { - usingSourceSet sourceSets.streamIntermediary - capability(project.group as String, "$project.name-stream", project.version as String) - capability(project.group as String, "$project.name-stream-intermediary", project.version as String) + registerFeature("minecraftIntermediary") { + usingSourceSet sourceSets.minecraftIntermediary + capability(project.group as String, "$project.name-minecraft", project.version as String) + capability(project.group as String, "$project.name-minecraft-intermediary", project.version as String) + // Old name + capability(project.group as String, "$project.name-stream", project.version as String) + capability(project.group as String, "$project.name-stream-intermediary", project.version as String) } } @@ -155,24 +161,43 @@ dependencies { annotationProcessor 'dev.lukebemish.autoextension:autoextension:0.1.1' compileOnly 'dev.lukebemish.autoextension:autoextension:0.1.1' - streamApi project(':') - streamCompileOnly cLibs.bundles.compileonly - streamAnnotationProcessor cLibs.bundles.annotationprocessor - streamIntermediaryApi project(':') - testImplementation sourceSets.stream.output + minecraftApi project(':') + minecraftCompileOnly cLibs.bundles.compileonly + minecraftAnnotationProcessor cLibs.bundles.annotationprocessor + minecraftIntermediaryApi project(':') + testImplementation sourceSets.minecraft.output } -['streamJar', 'streamIntermediaryJar', 'jar'].each { - tasks.named(it, Jar) { - manifest { - attributes( - 'Specification-Version' : project.version, - 'Implementation-Version' : project.version, - 'Implementation-Commit-Time': managedVersioning.timestamp.get(), - 'Implementation-Commit' : managedVersioning.hash.get() - ) - } - } +['minecraftJar', 'minecraftIntermediaryJar', 'jar'].each { + tasks.named(it, Jar) { + manifest { + attributes( + 'Specification-Version' : project.version, + 'Implementation-Version' : project.version, + 'Implementation-Commit-Time': managedVersioning.timestamp.get(), + 'Implementation-Commit' : managedVersioning.hash.get() + ) + } + } +} + +tasks.named('jar', Jar) { + manifest { + attributes( + 'Automatic-Module-Name': project.group + '.' + project.name + ) + } +} + +['minecraftJar', 'minecraftIntermediaryJar'].each { + tasks.named(it, Jar) { + manifest { + attributes( + 'Automatic-Module-Name' : project.group + '.' + project.name + '.minecraft', + 'Implementation-Minecraft-Version' : libs.versions.minecraft.get() + ) + } + } } tasks.register('jmh', JavaExec) { @@ -196,16 +221,16 @@ tasks.register('jmhResults', FormatJmhOutput) { formattedResults.set project.file('build/reports/jmh/results.md') } -streamJar { +minecraftJar { manifest.attributes('FMLModType': 'GAMELIBRARY') } -streamIntermediaryJar { +minecraftIntermediaryJar { manifest.attributes('FMLModType': 'GAMELIBRARY') } -tasks.named('remapStreamIntermediaryJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> - task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-stream-intermediary.jar") +tasks.named('remapMinecraftIntermediaryJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> + task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-intermediary.jar") } jar { @@ -222,7 +247,7 @@ tasks.compileJava { } } -processStreamIntermediaryResources { +processMinecraftIntermediaryResources { inputs.property "version", project.version.toString() filesMatching("fabric.mod.json") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..ee0c4e4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] + +minecraft = "24w35a" + +[libraries] + +minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } diff --git a/settings.gradle b/settings.gradle index 6ebe899..dc99af9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,11 +28,11 @@ plugins { multisource.of(':') { configureEach { - minecraft.add 'com.mojang:minecraft:1.20.6' + minecraft.add project.libs.minecraft mappings.add loom.officialMojangMappings() } - common('stream', []) {} - fabric('streamIntermediary', ['stream']) {} + common('minecraft', []) {} + fabric('minecraftIntermediary', ['minecraft']) {} repositories { it.removeIf { it.name == 'Forge' } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index f4d92c2..9830e64 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -66,9 +66,9 @@ public DataResult> flatXmap(App input, Fu } @Override - public DataResult> annotate(App input, Keys annotations) { + public DataResult> annotate(Structure original, Keys annotations) { // No annotations handled here - return DataResult.success(input); + return original.interpret(this); } @Override @@ -76,8 +76,8 @@ public DataResult> dispatch(String key, Structure ke return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyCodec = unbox(keyCodecApp); // Object here as it's the furthest super A and we have only ? super A - Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { - Map>> codecMap = new HashMap<>(); + Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { + Map>> codecMap = new HashMap<>(); for (var entryKey : keys) { var result = structures.apply(entryKey).interpret(mapCodecInterpreter()); if (result.error().isPresent()) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 470cb48..76bcd93 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -19,7 +19,7 @@ public interface Interpreter { DataResult> flatXmap(App input, Function> deserializer, Function> serializer); - DataResult> annotate(App input, Keys annotations); + DataResult> annotate(Structure original, Keys annotations); DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java deleted file mode 100644 index ec9d31c..0000000 --- a/src/main/java/dev/lukebemish/codecextras/structured/JsonSchemaInterpreter.java +++ /dev/null @@ -1,152 +0,0 @@ -package dev.lukebemish.codecextras.structured; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.K1; -import com.mojang.serialization.DataResult; -import dev.lukebemish.codecextras.types.Identity; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; - -public class JsonSchemaInterpreter extends KeyStoringInterpreter { - public JsonSchemaInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { - super(keys.join(Keys.builder() - .add(Interpreter.UNIT, new Holder<>(OBJECT)) - .add(Interpreter.BOOL, new Holder<>(BOOLEAN)) - .add(Interpreter.BYTE, new Holder<>(INTEGER)) - .add(Interpreter.SHORT, new Holder<>(INTEGER)) - .add(Interpreter.INT, new Holder<>(INTEGER)) - .add(Interpreter.LONG, new Holder<>(INTEGER)) - .add(Interpreter.FLOAT, new Holder<>(NUMBER)) - .add(Interpreter.DOUBLE, new Holder<>(NUMBER)) - .add(Interpreter.STRING, new Holder<>(STRING)) - .build() - ), parametricKeys); - } - - @Override - public JsonSchemaInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { - return new JsonSchemaInterpreter( - keys().join(keys), - parametricKeys().join(parametricKeys) - ); - } - - public JsonSchemaInterpreter() { - this( - Keys.builder().build(), - Keys2., K1, K1>builder().build() - ); - } - - @Override - public DataResult>> list(App single) { - var object = copy(ARRAY); - object.add("items", unbox(single)); - return DataResult.success(new Holder<>(object)); - } - - @Override - public DataResult> record(List> fields, Function creator) { - var object = copy(OBJECT); - var properties = new JsonObject(); - var required = new JsonArray(); - for (RecordStructure.Field field : fields) { - Supplier error = singleField(field, properties, required); - if (error != null) { - return DataResult.error(error); - } - } - object.add("properties", properties); - object.add("required", required); - return DataResult.success(new Holder<>(object)); - } - - private @Nullable Supplier singleField(RecordStructure.Field field, JsonObject properties, JsonArray required) { - var partialResolt = field.structure().interpret(this); - if (partialResolt.isError()) { - return partialResolt.error().orElseThrow().messageSupplier(); - } - var fieldObject = copy(unbox(partialResolt.result().orElseThrow())); - - field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); - - properties.add(field.name(), fieldObject); - return null; - } - - @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - return DataResult.success(new Holder<>(unbox(input))); - } - - @Override - public DataResult> annotate(App input, Keys annotations) { - var schema = copy(unbox(input)); - Annotation.get(annotations, Annotation.DESCRIPTION).or(() -> Annotation.get(annotations, Annotation.COMMENT)).ifPresent(comment -> { - schema.addProperty("description", comment); - }); - Annotation.get(annotations, Annotation.TITLE).ifPresent(comment -> { - schema.addProperty("title", comment); - }); - return DataResult.success(new Holder<>(schema)); - } - - @Override - public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { - // TODO: implement - return DataResult.error(() -> "Not yet implemented!"); - } - - public static JsonObject unbox(App box) { - return Holder.unbox(box).jsonObject; - } - - public DataResult interpret(Structure structure) { - return structure.interpret(this).map(JsonSchemaInterpreter::unbox); - } - - public static final Key KEY = Key.create("JsonSchemaInterpreter"); - - @Override - public Optional> key() { - return Optional.of(KEY); - } - - public record Holder(JsonObject jsonObject) implements App { - public static final class Mu implements K1 { private Mu() {} } - - static Holder unbox(App box) { - return (Holder) box; - } - } - - private JsonObject copy(JsonObject object) { - JsonObject copy = new JsonObject(); - for (String key : object.keySet()) { - copy.add(key, object.get(key)); - } - return copy; - } - - private static final JsonObject OBJECT = new JsonObject(); - private static final JsonObject NUMBER = new JsonObject(); - private static final JsonObject STRING = new JsonObject(); - private static final JsonObject BOOLEAN = new JsonObject(); - private static final JsonObject INTEGER = new JsonObject(); - private static final JsonObject ARRAY = new JsonObject(); - - static { - OBJECT.addProperty("type", "object"); - NUMBER.addProperty("type", "number"); - STRING.addProperty("type", "string"); - BOOLEAN.addProperty("type", "boolean"); - INTEGER.addProperty("type", "integer"); - ARRAY.addProperty("type", "array"); - } -} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index cfd9e56..c951ff1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -57,12 +57,14 @@ public DataResult> flatXmap(App input, Fu } @Override - public DataResult> annotate(App input, Keys annotations) { - var mapCodec = new Object() { - MapCodec m = unbox(input); - }; - mapCodec.m = Annotation.get(annotations, Annotation.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); - return DataResult.success(new Holder<>(mapCodec.m)); + public DataResult> annotate(Structure original, Keys annotations) { + return original.interpret(this).map(input -> { + var mapCodec = new Object() { + MapCodec m = unbox(input); + }; + mapCodec.m = Annotation.get(annotations, Annotation.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); + return new Holder<>(mapCodec.m); + }); } public static MapCodec unbox(App box) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 73fca74..0985805 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -6,12 +6,13 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; +import org.jspecify.annotations.Nullable; + import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; public interface Structure { DataResult> interpret(Interpreter interpreter); @@ -41,12 +42,11 @@ final class AnnotatedDelegatingStructure implements Structure { @Override public DataResult> interpret(Interpreter interpreter) { - var result = interpretNoAnnotations(interpreter); - return result.flatMap(r -> interpreter.annotate(r, annotations)); + return interpreter.annotate(original(), annotations); } - private DataResult> interpretNoAnnotations(Interpreter interpreter) { - return delegate != null ? delegate.interpretNoAnnotations(interpreter) : outer.interpret(interpreter); + private Structure original() { + return delegate != null ? delegate.original() : outer; } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java new file mode 100644 index 0000000..6e4311d --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -0,0 +1,294 @@ +package dev.lukebemish.codecextras.structured.schema; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.Annotation; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.RecordStructure; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.Identity; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +public class JsonSchemaInterpreter extends KeyStoringInterpreter { + private final CodecInterpreter codecInterpreter; + private final DynamicOps ops; + + public JsonSchemaInterpreter( + Keys keys, + Keys2, K1, K1> parametricKeys, + CodecInterpreter codecInterpreter, + DynamicOps ops + ) { + super(keys.join(Keys.builder() + .add(Interpreter.UNIT, new Holder<>(OBJECT.get())) + .add(Interpreter.BOOL, new Holder<>(BOOLEAN.get())) + .add(Interpreter.BYTE, new Holder<>(INTEGER.get())) + .add(Interpreter.SHORT, new Holder<>(INTEGER.get())) + .add(Interpreter.INT, new Holder<>(INTEGER.get())) + .add(Interpreter.LONG, new Holder<>(INTEGER.get())) + .add(Interpreter.FLOAT, new Holder<>(NUMBER.get())) + .add(Interpreter.DOUBLE, new Holder<>(NUMBER.get())) + .add(Interpreter.STRING, new Holder<>(STRING.get())) + .build() + ), parametricKeys); + this.codecInterpreter = codecInterpreter; + this.ops = ops; + } + + @Override + public JsonSchemaInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { + return new JsonSchemaInterpreter( + keys().join(keys), + parametricKeys().join(parametricKeys), + this.codecInterpreter, this.ops + ); + } + + public JsonSchemaInterpreter() { + this( + Keys.builder().build(), + Keys2., K1, K1>builder().build(), + CodecInterpreter.create(), + JsonOps.INSTANCE + ); + } + + @Override + public DataResult>> list(App single) { + var object = ARRAY.get(); + object.add("items", schemaValue(single)); + return DataResult.success(new Holder<>(object, definitions(single))); + } + + @Override + public DataResult> record(List> fields, Function creator) { + var object = OBJECT.get(); + var properties = new JsonObject(); + var required = new JsonArray(); + Map> definitions = new HashMap<>(); + for (RecordStructure.Field field : fields) { + Supplier error = singleField(field, properties, required, definitions); + if (error != null) { + return DataResult.error(error); + } + } + object.add("properties", properties); + object.add("required", required); + return DataResult.success(new Holder<>(object, definitions)); + } + + private @Nullable Supplier singleField(RecordStructure.Field field, JsonObject properties, JsonArray required, Map> definitions) { + var partialResolt = field.structure().interpret(this); + if (partialResolt.isError()) { + return partialResolt.error().orElseThrow().messageSupplier(); + } + var fieldObject = copy(schemaValue(partialResolt.result().orElseThrow())); + definitions.putAll(definitions(partialResolt.result().orElseThrow())); + + field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); + + properties.add(field.name(), fieldObject); + return null; + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + return DataResult.success(new Holder<>(schemaValue(input), definitions(input))); + } + + @Override + public DataResult> annotate(Structure input, Keys annotations) { + JsonObject schema; + Map> definitions; + var refName = annotations.get(SchemaAnnotations.REUSE_KEY); + if (refName.isPresent()) { + schema = new JsonObject(); + var ref = Identity.unbox(refName.get()).value(); + schema.addProperty("$ref", "#/$defs/"+ref); + definitions = new HashMap<>(); + definitions.put(ref, input); + } else { + var result = input.interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + schema = schemaValue(result.result().orElseThrow()); + definitions = new HashMap<>(definitions(result.result().orElseThrow())); + } + + Annotation.get(annotations, Annotation.DESCRIPTION).or(() -> Annotation.get(annotations, Annotation.COMMENT)).ifPresent(comment -> { + schema.addProperty("description", comment); + }); + Annotation.get(annotations, Annotation.TITLE).ifPresent(comment -> { + schema.addProperty("title", comment); + }); + return DataResult.success(new Holder<>(schema, definitions)); + } + + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + return keyStructure.interpret(this).flatMap(keySchemaApp -> { + var definitions = new HashMap<>(definitions(keySchemaApp)); + var keySchema = schemaValue(keySchemaApp); + JsonObject out = new JsonObject(); + JsonObject properties = new JsonObject(); + JsonArray required = new JsonArray(); + + required.add(key); + properties.add(key, keySchema); + + JsonArray allOf = new JsonArray(); + var keyCodecResult = codecInterpreter.interpret(keyStructure); + if (keyCodecResult.error().isPresent()) { + return DataResult.error(keyCodecResult.error().get().messageSupplier()); + } + var keyCodec = keyCodecResult.result().orElseThrow(); + for (A entryKey : keys) { + var result = structures.apply(entryKey).interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + var entrySchema = schemaValue(result.result().orElseThrow()); + definitions.putAll(definitions(result.result().orElseThrow())); + var entryValueResult = keyCodec.encodeStart(JsonOps.INSTANCE, entryKey); + if (entryValueResult.error().isPresent()) { + return DataResult.error(entryValueResult.error().get().messageSupplier()); + } + var entryValue = entryValueResult.result().orElseThrow(); + var ifObj = new JsonObject(); + var ifProperties = new JsonObject(); + var keyProperty = new JsonObject(); + keyProperty.add("const", entryValue); + ifProperties.add(key, keyProperty); + ifObj.add("properties", ifProperties); + var obj = new JsonObject(); + obj.add("if", ifObj); + obj.add("then", entrySchema); + allOf.add(obj); + } + + out.add("properties", properties); + out.add("required", required); + out.add("allOf", allOf); + return DataResult.success(new Holder<>(out, definitions)); + }); + } + + private static JsonObject schemaValue(App box) { + return Holder.unbox(box).jsonObject; + } + + private static Map> definitions(App box) { + return Holder.unbox(box).definition; + } + + public DataResult partialInterpret(Structure structure) { + return structure.interpret(this).map(JsonSchemaInterpreter::schemaValue); + } + + public DataResult rootSchema(Structure structure) { + return structure.interpret(this).flatMap(holder -> { + var object = copy(schemaValue(holder)); + var definitions = definitions(holder); + var defsObject = new JsonObject(); + while (!definitions.isEmpty()) { + var newDefs = new HashMap>(); + for (Map.Entry> entry : definitions.entrySet()) { + if (defsObject.has(entry.getKey())) { + continue; + } + var result = entry.getValue().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + var schema = schemaValue(result.result().orElseThrow()); + defsObject.add(entry.getKey(), schema); + newDefs.putAll(definitions(result.result().orElseThrow())); + } + definitions = newDefs; + } + if (!defsObject.isEmpty()) { + object.add("$defs", defsObject); + } + return DataResult.success(object); + }); + } + + public static final Key KEY = Key.create("JsonSchemaInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } + + public record Holder(JsonObject jsonObject, Map> definition) implements App { + public Holder(JsonObject object) { + this(object, Map.of()); + } + + public static final class Mu implements K1 { private Mu() {} } + + static Holder unbox(App box) { + return (Holder) box; + } + } + + private JsonObject copy(JsonObject object) { + JsonObject copy = new JsonObject(); + for (String key : object.keySet()) { + copy.add(key, object.get(key)); + } + return copy; + } + + public static final Supplier OBJECT = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "object"); + return object; + }; + public static final Supplier NUMBER = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "number"); + return object; + }; + public static final Supplier STRING = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "string"); + return object; + }; + public static final Supplier BOOLEAN = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "boolean"); + return object; + }; + public static final Supplier INTEGER = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "integer"); + return object; + }; + public static final Supplier ARRAY = () -> { + JsonObject object = new JsonObject(); + object.addProperty("type", "array"); + return object; + }; +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/SchemaAnnotations.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/SchemaAnnotations.java new file mode 100644 index 0000000..95b7fb6 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/SchemaAnnotations.java @@ -0,0 +1,13 @@ +package dev.lukebemish.codecextras.structured.schema; + +import dev.lukebemish.codecextras.structured.Key; + +public final class SchemaAnnotations { + private SchemaAnnotations() {} + + /** + * The key used with {@code $ref} and {@code $defs} to reuse this schema throughout a root schema. + * Only one schema in a nested structure should have this key. + */ + public static final Key REUSE_KEY = Key.create("reuseKey"); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/package-info.java new file mode 100644 index 0000000..f259ed8 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.structured.schema; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java similarity index 90% rename from src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java rename to src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 3225a24..44419f4 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -1,11 +1,12 @@ -package dev.lukebemish.codecextras.stream.structured; +package dev.lukebemish.codecextras.minecraft.structured; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; import dev.lukebemish.codecextras.structured.Keys; @@ -13,14 +14,16 @@ import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; -import java.util.function.Function; import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import java.util.function.Function; + public final class MinecraftStructures { private MinecraftStructures() {} @@ -125,16 +128,18 @@ public App, App JSON_SCHEMA_KEYS = Keys.builder() + .add(Types.RESOURCE_LOCATION, new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get())) .build(); public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) .build(); - public static final JsonSchemaInterpreter JSON_SCHEMA_INTERPRETER = new JsonSchemaInterpreter( - JSON_SCHEMA_KEYS, - JSON_SCHEMA_PARAMETRIC_KEYS - ); - public static final class Types { private Types() {} @@ -186,7 +191,13 @@ public static Structure> resourceKey(ResourceKey Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { var keyStructure = resourceKey(registry.key()); - return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, registry::get); + return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> + registry.getValueOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry())+"::"+toDefsKey(k.location())) + ); + } + + private static String toDefsKey(ResourceLocation location) { + return location.getNamespace().replace('/', '.') + ":" + location.getPath().replace('/', '.'); } } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/package-info.java new file mode 100644 index 0000000..183b900 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.minecraft.structured; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/AsymmetricalStreamCodecs.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/AsymmetricalStreamCodecs.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/AsymmetricalStreamCodecs.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/AsymmetricalStreamCodecs.java diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/StreamCodecExtras.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/StreamCodecExtras.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/StreamCodecExtras.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/StreamCodecExtras.java diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/StreamDataElementType.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/mutable/StreamDataElementType.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/mutable/StreamDataElementType.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/mutable/StreamDataElementType.java diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/mutable/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/mutable/package-info.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/mutable/package-info.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/mutable/package-info.java diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/package-info.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/package-info.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/package-info.java diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java similarity index 98% rename from src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 1893650..6c96cda 100644 --- a/src/stream/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -14,6 +14,10 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -22,9 +26,6 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { @@ -96,9 +97,9 @@ public DataResult, Y>> flatXmap(App, X> inp } @Override - public DataResult, A>> annotate(App, A> input, Keys annotations) { + public DataResult, A>> annotate(Structure original, Keys annotations) { // No annotations handled here - return DataResult.success(input); + return original.interpret(this); } @Override diff --git a/src/stream/java/dev/lukebemish/codecextras/stream/structured/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/package-info.java similarity index 100% rename from src/stream/java/dev/lukebemish/codecextras/stream/structured/package-info.java rename to src/minecraft/java/dev/lukebemish/codecextras/stream/structured/package-info.java diff --git a/src/minecraftIntermediary/resources/fabric.mod.json b/src/minecraftIntermediary/resources/fabric.mod.json new file mode 100644 index 0000000..713d387 --- /dev/null +++ b/src/minecraftIntermediary/resources/fabric.mod.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "id": "dev_lukebemish_codecextras-minecraft", + "version": "${version}", + "name": "CodecExtras - Minecraft-specific adapters" +} diff --git a/src/streamIntermediary/resources/fabric.mod.json b/src/streamIntermediary/resources/fabric.mod.json deleted file mode 100644 index 30f1d83..0000000 --- a/src/streamIntermediary/resources/fabric.mod.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaVersion": 1, - "id": "dev_lukebemish_codecextras-stream", - "version": "${version}", - "name": "CodecExtras - StreamCodecs" -} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java new file mode 100644 index 0000000..68e60de --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java @@ -0,0 +1,178 @@ +package dev.lukebemish.codecextras.test.structured; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; +import dev.lukebemish.codecextras.test.CodecAssertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TestDispatch { + private interface Dispatches { + Map> MAP = new HashMap<>(); + Structure STRUCTURE = Structure.STRING.dispatch( + "type", + d -> DataResult.success(d.key()), + MAP::keySet, + MAP::get + ).annotate(SchemaAnnotations.REUSE_KEY, "dispatches"); + String key(); + } + + private record Abc(int a, String b, float c) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var a = i.add("a", Structure.INT, Abc::a); + var b = i.add("b", Structure.STRING, Abc::b); + var c = i.add("c", Structure.FLOAT, Abc::c); + return container -> new Abc(a.apply(container), b.apply(container), c.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "abc"); + + @Override + public String key() { + return "abc"; + } + } + + private record Xyz(String x, int y, float z) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var x = i.add("x", Structure.STRING, Xyz::x); + var y = i.add("y", Structure.INT, Xyz::y); + var z = i.add("z", Structure.FLOAT, Xyz::z); + return container -> new Xyz(x.apply(container), y.apply(container), z.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "xyz"); + + @Override + public String key() { + return "xyz"; + } + } + + static { + Dispatches.MAP.put("abc", Abc.STRUCTURE); + Dispatches.MAP.put("xyz", Xyz.STRUCTURE); + } + + private static final Structure> LIST_STRUCTURE = Dispatches.STRUCTURE.listOf(); + private static final Codec> CODEC = CodecInterpreter.create().interpret(LIST_STRUCTURE).getOrThrow(); + + private final String json = """ + [ + { + "type": "abc", + "a": 1, + "b": "test", + "c": 1.0 + }, + { + "type": "xyz", + "x": "test", + "y": 1, + "z": 1.0 + } + ]"""; + + private final String schema = """ + { + "$ref": "#/$defs/dispatches", + "$defs": { + "dispatches": { + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "abc" + } + } + }, + "then": { + "$ref": "#/$defs/abc" + } + }, + { + "if": { + "properties": { + "type": { + "const": "xyz" + } + } + }, + "then": { + "$ref": "#/$defs/xyz" + } + } + ] + }, + "xyz": { + "type": "object", + "properties": { + "x": { + "type": "string" + }, + "y": { + "type": "integer" + }, + "z": { + "type": "number" + } + }, + "required": [ + "x", + "y", + "z" + ] + }, + "abc": { + "type": "object", + "properties": { + "a": { + "type": "integer" + }, + "b": { + "type": "string" + }, + "c": { + "type": "number" + } + }, + "required": [ + "a", + "b", + "c" + ] + } + } + }"""; + + private final List list = List.of(new Abc(1, "test", 1.0f), new Xyz("test", 1, 1.0f)); + + @Test + void testDecoding() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, list, CODEC); + } + + @Test + void testEncoding() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, list, json, CODEC); + } + + @Test + void testJsonSchema() { + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().rootSchema(Dispatches.STRUCTURE).getOrThrow().toString()); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index b7a61f7..8063708 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -4,7 +4,7 @@ import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.JsonSchemaInterpreter; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.test.CodecAssertions; import java.util.List; @@ -69,6 +69,6 @@ void testEncodingCodec() { @Test void testJsonSchema() { - CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(TestRecord.STRUCTURE).getOrThrow().toString()); + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().rootSchema(TestRecord.STRUCTURE).getOrThrow().toString()); } } diff --git a/version.properties b/version.properties index 7ad95fa..4950f0d 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -version=2.3.0 +version=3.0.0 From 44d70630d35a0eff9ac911a7e782c175451fdaf2 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 01:40:04 -0500 Subject: [PATCH 33/76] Default values in schemas --- .../structured/RecordStructure.java | 2 +- .../schema/JsonSchemaInterpreter.java | 23 ++++++++++++++++++- .../test/structured/TestStructured.java | 16 +++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index fbf522d..462e198 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -117,7 +117,7 @@ public Key> addOptional(String name, Structure structure, Fun public Key addOptional(String name, Structure structure, Function getter, Supplier defaultValue) { var key = new Key(count); count++; - fields.add(new FieldImpl<>(name, structure, getter, Optional.of(new MissingBehaviorImpl<>(defaultValue, t -> !t.equals(defaultValue))), key)); + fields.add(new FieldImpl<>(name, structure, getter, Optional.of(new MissingBehaviorImpl<>(defaultValue, t -> !t.equals(defaultValue.get()))), key)); fieldNames.add(name); return key; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 6e4311d..626c12d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -105,7 +105,28 @@ public DataResult> record(List var fieldObject = copy(schemaValue(partialResolt.result().orElseThrow())); definitions.putAll(definitions(partialResolt.result().orElseThrow())); - field.missingBehavior().ifPresentOrElse(missingBehavior -> {}, () -> required.add(field.name())); + var error = new Object() { + @Nullable Supplier value = null; + }; + + field.missingBehavior().ifPresentOrElse(missingBehavior -> { + var codec = codecInterpreter.interpret(field.structure()); + if (codec.error().isPresent()) { + error.value = codec.error().get().messageSupplier(); + return; + } + var defaultValue = missingBehavior.missing().get(); + var defaultValueResult = codec.result().orElseThrow().encodeStart(ops, defaultValue); + if (defaultValueResult.error().isPresent()) { + // If it cannot serialize the default value, we just don't report it -- it could be something like an Optional where the default value does not exist. + return; + } + fieldObject.add("default", defaultValueResult.result().orElseThrow()); + }, () -> required.add(field.name())); + + if (error.value != null) { + return error.value; + } properties.add(field.name(), fieldObject); return null; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 8063708..ab4c8f7 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -4,21 +4,23 @@ import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.test.CodecAssertions; +import org.junit.jupiter.api.Test; + import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.Test; class TestStructured { - private record TestRecord(int a, String b, List c, Optional d) { + private record TestRecord(int a, String b, List c, Optional d, String e) { private static final Structure STRUCTURE = Structure.record(i -> { var a = i.add("a", Structure.INT.annotate(Annotation.COMMENT, "Field A"), TestRecord::a); var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); - return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container)); + var e = i.addOptional("e", Structure.STRING, TestRecord::e, () -> "default"); + return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container)); }); private static final Codec CODEC = CodecInterpreter.create().interpret(STRUCTURE).getOrThrow(); @@ -50,12 +52,16 @@ private record TestRecord(int a, String b, List c, Optional d) }, "d": { "type": "string" + }, + "e": { + "type": "string", + "default": "default" } }, "required": ["a", "b", "c"] }"""; - private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty()); + private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty(), "default"); @Test void testDecodingCodec() { From e3236e897fa98c332df39e112e1e60556a7f0a3a Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 15:10:15 -0500 Subject: [PATCH 34/76] Apply formatting --- .../dev/lukebemish/codecextras/structured/Structure.java | 3 +-- .../structured/schema/JsonSchemaInterpreter.java | 3 +-- .../minecraft/structured/MinecraftStructures.java | 5 ++--- .../stream/structured/StreamCodecInterpreter.java | 7 +++---- .../codecextras/test/structured/TestDispatch.java | 5 ++--- .../codecextras/test/structured/TestStructured.java | 3 +-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 0985805..f807d3e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -6,13 +6,12 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; -import org.jspecify.annotations.Nullable; - import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; public interface Structure { DataResult> interpret(Interpreter interpreter); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 626c12d..4e3d868 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -19,8 +19,6 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; -import org.jspecify.annotations.Nullable; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,6 +26,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { private final CodecInterpreter codecInterpreter; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 44419f4..fe4c8d2 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -6,7 +6,6 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; import dev.lukebemish.codecextras.structured.Keys; @@ -14,16 +13,16 @@ import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; +import java.util.function.Function; import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import java.util.function.Function; - public final class MinecraftStructures { private MinecraftStructures() {} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 6c96cda..7fd2eac 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -14,10 +14,6 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,6 +22,9 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java index 68e60de..18f6330 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java @@ -4,15 +4,14 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.test.CodecAssertions; -import org.junit.jupiter.api.Test; - import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Test; public class TestDispatch { private interface Dispatches { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index ab4c8f7..b66913e 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -7,10 +7,9 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.test.CodecAssertions; -import org.junit.jupiter.api.Test; - import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.Test; class TestStructured { private record TestRecord(int a, String b, List c, Optional d, String e) { From 74b2475e2c0415249fa028d2336d382dc937edf4 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 15:39:36 -0500 Subject: [PATCH 35/76] More vanilla structure types --- .../structured/MinecraftStructures.java | 94 ++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index fe4c8d2..1a95d0f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -17,11 +17,14 @@ import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; import java.util.function.Function; +import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; +import net.minecraft.core.RegistryCodecs; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; public final class MinecraftStructures { private MinecraftStructures() {} @@ -61,6 +64,24 @@ public App> c return new CodecInterpreter.Holder<>(ResourceKey.codec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value())); } }) + .add(Types.TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(TagKey.codec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.TagKeyHolder::new, a -> Types.TagKeyHolder.unbox(a).value())); + } + }) + .add(Types.HASHED_TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(TagKey.hashedCodec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.TagKeyHolder::new, a -> Types.TagKeyHolder.unbox(a).value())); + } + }) + .add(Types.HOMOGENOUS_LIST_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(RegistryCodecs.homogeneousList(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.HolderSetHolder::new, a -> Types.HolderSetHolder.unbox(a).value())); + } + }) .build() ); @@ -130,6 +151,7 @@ public App, App(JsonSchemaInterpreter.STRING.get())) .build(); + // TODO: Add regex fo schemas public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { @Override @@ -137,6 +159,18 @@ public App(JsonSchemaInterpreter.STRING.get()); } }) + .add(Types.TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) + .add(Types.HASHED_TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) .build(); public static final class Types { @@ -152,6 +186,22 @@ public static ResourceKeyHolder unbox(App box) { } } + public record TagKeyHolder(TagKey value) implements App { + public static final class Mu implements K1 { private Mu() {} } + + public static TagKeyHolder unbox(App box) { + return (TagKeyHolder) box; + } + } + + public record HolderSetHolder(HolderSet value) implements App { + public static final class Mu implements K1 { private Mu() {} } + + public static HolderSetHolder unbox(App box) { + return (HolderSetHolder) box; + } + } + public record RegistryKeyHolder(ResourceKey> value) implements App { public static final class Mu implements K1 { private Mu() {} } @@ -161,6 +211,12 @@ public static RegistryKeyHolder unbox(App box) { } public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); + + public static final Key2 TAG_KEY = Key2.create("tag_key"); + + public static final Key2 HASHED_TAG_KEY = Key2.create("#tag_key"); + + public static final Key2 HOMOGENOUS_LIST_KEY = Key2.create("homogenous_list"); } public static final class Structures { @@ -175,8 +231,8 @@ private Structures() {} public static Structure> resourceKey(ResourceKey> registry) { return Structure.parametricallyKeyed( Types.RESOURCE_KEY, - new Types.RegistryKeyHolder<>(registry), - Types.ResourceKeyHolder::unbox, + new Types.RegistryKeyHolder<>(registry), + Types.ResourceKeyHolder::unbox, Keys.>, K1>builder() .add( CodecInterpreter.KEY, @@ -185,7 +241,39 @@ public static Structure> resourceKey(ResourceKey Structure> homogenousList(ResourceKey> registry) { + return Structure.parametricallyKeyed( + Types.HOMOGENOUS_LIST_KEY, + new Types.RegistryKeyHolder<>(registry), + Types.HolderSetHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + RegistryCodecs.homogeneousList(registry).xmap(Types.HolderSetHolder::new, Types.HolderSetHolder::value) + )) + ) + .build() + ).xmap(Types.HolderSetHolder::value, Types.HolderSetHolder::new); + } + + public static Structure> tagKey(ResourceKey> registry, boolean hashPrefix) { + return Structure.parametricallyKeyed( + hashPrefix ? Types.HASHED_TAG_KEY : Types.TAG_KEY, + new Types.RegistryKeyHolder<>(registry), + Types.TagKeyHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + (hashPrefix ? TagKey.hashedCodec(registry) : TagKey.codec(registry)).xmap(Types.TagKeyHolder::new, Types.TagKeyHolder::value) + )) + ) + .build() + ).xmap(Types.TagKeyHolder::value, Types.TagKeyHolder::new); } public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { From 3fc8b72464f32d606457119eb793120d7675a927 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 20:03:30 -0500 Subject: [PATCH 36/76] Initial config screen interpreter work --- .gitignore | 2 + build.gradle | 13 ++ gradle/libs.versions.toml | 4 +- settings.gradle | 4 + .../structured/MinecraftStructures.java | 2 +- .../structured/config/ComponentInfo.java | 36 ++++ .../structured/config/ConfigAnnotations.java | 10 + .../structured/config/OptionsEntry.java | 42 +++++ .../config/OptionsEntryInterpreter.java | 176 ++++++++++++++++++ .../structured/config/RecordConfigScreen.java | 78 ++++++++ .../structured/config/RecordEntry.java | 6 + .../structured/config/ScreenFactory.java | 10 + .../structured/config/WidgetFactory.java | 10 + .../structured/config/package-info.java | 6 + src/minecraft/resources/META-INF/MANIFEST.MF | 1 + .../test/neoforge/CodecExtrasTest.java | 35 ++++ .../resources/META-INF/neoforge.mods.toml | 9 + testFabric/build.gradle | 22 +++ testNeoforge/build.gradle | 33 ++++ 19 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/package-info.java create mode 100644 src/minecraft/resources/META-INF/MANIFEST.MF create mode 100644 src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java create mode 100644 src/testNeoforge/resources/META-INF/neoforge.mods.toml create mode 100644 testFabric/build.gradle create mode 100644 testNeoforge/build.gradle diff --git a/.gitignore b/.gitignore index 43654d1..e23bd4c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ build # other eclipse +run +runs diff --git a/build.gradle b/build.gradle index 3ad5abe..8e4c415 100644 --- a/build.gradle +++ b/build.gradle @@ -101,6 +101,16 @@ sourceSets { jmh {} } +configurations { + testNeoforgeRuntimeClasspath.extendsFrom minecraftRuntimeClasspath + testFabricRuntimeClasspath.extendsFrom minecraftIntermediaryRuntimeClasspath + testFabricToRemapRuntimeClasspath.extendsFrom minecraftIntermediaryToRemapRuntimeClasspath + + testNeoforgeCompileClasspath.extendsFrom minecraftCompileClasspath + testFabricCompileClasspath.extendsFrom minecraftIntermediaryCompileClasspath + testFabricToRemapCompileClasspath.extendsFrom minecraftIntermediaryToRemapCompileClasspath +} + java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) withSourcesJar() @@ -166,6 +176,9 @@ dependencies { minecraftAnnotationProcessor cLibs.bundles.annotationprocessor minecraftIntermediaryApi project(':') testImplementation sourceSets.minecraft.output + + testNeoforgeCompileOnly sourceSets.minecraft.output + testFabricCompileOnly sourceSets.minecraftIntermediary.output } ['minecraftJar', 'minecraftIntermediaryJar', 'jar'].each { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee0c4e4..9a4ac91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,9 @@ [versions] -minecraft = "24w35a" +minecraft = "1.21.1" +neoforge = "21.1.31" [libraries] minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } +neoforge = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge" } diff --git a/settings.gradle b/settings.gradle index dc99af9..fc5cc26 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,6 +33,10 @@ multisource.of(':') { } common('minecraft', []) {} fabric('minecraftIntermediary', ['minecraft']) {} + neoforge('testNeoforge', []) { + neoForge.add project.libs.neoforge + } + fabric('testFabric', []) {} repositories { it.removeIf { it.name == 'Forge' } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 1a95d0f..dcbd1db 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -279,7 +279,7 @@ public static Structure> tagKey(ResourceKey> public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { var keyStructure = resourceKey(registry.key()); return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> - registry.getValueOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry())+"::"+toDefsKey(k.location())) + registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry())+"::"+toDefsKey(k.location())) ); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java new file mode 100644 index 0000000..e8b6a51 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java @@ -0,0 +1,36 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import java.util.Optional; +import net.minecraft.network.chat.Component; + +public record ComponentInfo(Optional maybeTitle, Optional maybeDescription) { + private static final ComponentInfo EMPTY = new ComponentInfo(Optional.empty(), Optional.empty()); + + public static ComponentInfo empty() { + return EMPTY; + } + + public Component title() { + return maybeTitle.orElseGet(Component::empty); + } + + public Component description() { + return maybeDescription.orElseGet(Component::empty); + } + + public ComponentInfo fallbackTitle(Component fallback) { + if (maybeTitle.isPresent()) { + return this; + } else { + return new ComponentInfo(Optional.of(fallback), maybeDescription); + } + } + + public ComponentInfo fallbackDescription(Component fallback) { + if (maybeDescription.isPresent()) { + return this; + } else { + return new ComponentInfo(maybeTitle, Optional.of(fallback)); + } + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java new file mode 100644 index 0000000..de552bc --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java @@ -0,0 +1,10 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import dev.lukebemish.codecextras.structured.Key; +import net.minecraft.network.chat.Component; + +public final class ConfigAnnotations { + private ConfigAnnotations() {} + + public Key TITLE = Key.create("title"); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java new file mode 100644 index 0000000..af90fcc --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java @@ -0,0 +1,42 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import java.util.function.UnaryOperator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.options.OptionsSubScreen; + +public record OptionsEntry(WidgetFactory widget, ScreenFactory screen, ComponentInfo componentInfo) implements App { + public static final class Mu implements K1 { private Mu() {} } + + private static final int FULL_WIDTH = 310; + + public static OptionsEntry unbox(App app) { + return (OptionsEntry) app; + } + + public static OptionsEntry single(WidgetFactory first, ComponentInfo componentInfo) { + return new OptionsEntry<>(first, (parent, ops, original, update, onClose, info) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, info.title()) { + private JsonElement value = original; + + @Override + protected void addOptions() { + this.list.addSmall(first.create(this, FULL_WIDTH, value, newValue -> { + value = newValue; + update.accept(newValue); + }, componentInfo), null); + } + + @Override + public void onClose() { + onClose.accept(value); + super.onClose(); + } + }, componentInfo); + } + + OptionsEntry withComponentInfo(UnaryOperator function) { + return new OptionsEntry<>(this.widget, this.screen, function.apply(this.componentInfo)); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java new file mode 100644 index 0000000..e0106b7 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java @@ -0,0 +1,176 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonPrimitive; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.RecordStructure; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.Identity; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +public class OptionsEntryInterpreter extends KeyStoringInterpreter { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final CodecInterpreter codecInterpreter; + private final DynamicOps dynamicOps; + + public OptionsEntryInterpreter( + Keys keys, + Keys2, K1, K1> parametricKeys, + CodecInterpreter codecInterpreter, + DynamicOps dynamicOps + ) { + super( + keys.join(Keys.builder() + .add(Interpreter.INT, OptionsEntry.single( + (parent, width, original, update, info) -> new EditBox(Minecraft.getInstance().font, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, info.title()) { + { + if (!original.isJsonPrimitive()) { + if (!original.isJsonNull()) { + LOGGER.warn("{} is not an integer", original); + } + } else { + try { + var intValue = original.getAsJsonPrimitive().getAsInt(); + this.setValue(intValue+""); + } catch (NumberFormatException ignored) { + LOGGER.warn("{} is not an integer", original); + } + } + this.setResponder(string -> { + if (string.isEmpty()) { + update.accept(JsonNull.INSTANCE); + return; + } + try { + var intValue = Integer.parseInt(string); + update.accept(new JsonPrimitive(intValue)); + } catch (NumberFormatException ignored) { + LOGGER.warn("{} is not an integer", original); + } + }); + } + + @Override + public void insertText(String string) { + super.insertText(string.replaceAll("[^0-9\\-]+", "")); + } + }, + ComponentInfo.empty() + )) + .build()), + parametricKeys.join(Keys2., K1, K1>builder() + .build()) + ); + this.codecInterpreter = codecInterpreter; + this.dynamicOps = dynamicOps; + } + + public OptionsEntryInterpreter( + CodecInterpreter codecInterpreter, + DynamicOps dynamicOps + ) { + this( + Keys.builder().build(), + Keys2., K1, K1>builder().build(), + codecInterpreter, + dynamicOps + ); + } + + @Override + public OptionsEntryInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { + return new OptionsEntryInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter, this.dynamicOps); + } + + @Override + public DataResult>> list(App single) { + return DataResult.error(() -> "Not yet implemented"); + } + + @Override + public DataResult> record(List> fields, Function creator) { + List> entries = new ArrayList<>(); + List> errors = new ArrayList<>(); + for (var field : fields) { + handleEntry(field, entries, errors); + } + if (!errors.isEmpty()) { + return DataResult.error(() -> "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); + } + ScreenFactory factory = (parent, ops, original, update, onClose, componentInfo) -> + new RecordConfigScreen(parent, componentInfo.title(), entries, ops, original, update, onClose); + return DataResult.success(new OptionsEntry<>( + (parent, width, original, update, componentInfo) -> Button.builder( + Component.translatable("codecextras.config.configurerecord"), + b -> { + Minecraft.getInstance().setScreen(factory.open( + parent, dynamicOps, original, update, jsonElement -> {}, componentInfo + )); + } + ).width(Button.DEFAULT_WIDTH).build(), + factory, + ComponentInfo.empty() + )); + } + + private void handleEntry(RecordStructure.Field field, List> entries, List> errors) { + var shouldEncode = field.missingBehavior().map(RecordStructure.Field.MissingBehavior::predicate).orElse(t -> true); + var codecResult = codecInterpreter.interpret(field.structure()); + if (codecResult.isError()) { + errors.add(codecResult.error().orElseThrow().messageSupplier()); + return; + } + var optionEntryResult = field.structure().interpret(this).map(OptionsEntry::unbox); + if (optionEntryResult.isError()) { + errors.add(optionEntryResult.error().orElseThrow().messageSupplier()); + return; + } + entries.add(new RecordEntry<>( + field.name(), + optionEntryResult.getOrThrow().withComponentInfo(info -> info.fallbackTitle(Component.literal(field.name()))), + shouldEncode, + codecResult.getOrThrow() + )); + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + return DataResult.error(() -> "Not yet implemented"); + } + + @Override + public DataResult> annotate(Structure original, Keys annotations) { + return DataResult.error(() -> "Not yet implemented"); + } + + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + return DataResult.error(() -> "Not yet implemented"); + } + + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(OptionsEntry::unbox); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java new file mode 100644 index 0000000..fbbac96 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java @@ -0,0 +1,78 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DynamicOps; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.options.OptionsSubScreen; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +class RecordConfigScreen extends OptionsSubScreen { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final List> entries; + private final JsonObject jsonValue; + private final Consumer update; + private final Consumer onClose; + private final DynamicOps ops; + + public RecordConfigScreen(Screen screen, Component component, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update, Consumer onClose) { + super(screen, Minecraft.getInstance().options, component); + this.entries = entries; + if (jsonValue.isJsonObject()) { + this.jsonValue = jsonValue.getAsJsonObject(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON object", jsonValue); + } + this.jsonValue = new JsonObject(); + } + this.update = update; + this.onClose = onClose; + this.ops = ops; + } + + @Override + public void onClose() { + this.update.accept(jsonValue); + this.onClose.accept(jsonValue); + super.onClose(); + } + + @Override + protected void addOptions() { + for (var entry: this.entries) { + JsonElement specificValue = this.jsonValue.has(entry.key()) ? this.jsonValue.get(entry.key()) : JsonNull.INSTANCE; + this.list.addSmall( + new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().componentInfo().title(), font).alignLeft(), + entry.entry().widget().create(this, Button.DEFAULT_WIDTH, specificValue, newValue -> { + if (shouldUpdate(newValue, specificValue, entry)) { + this.jsonValue.add(entry.key(), newValue); + } else { + this.jsonValue.remove(entry.key()); + } + }, entry.entry().componentInfo()) + ); + } + } + + boolean shouldUpdate(JsonElement newValue, JsonElement oldValue, RecordEntry entry) { + if (newValue.isJsonNull() || newValue.equals(oldValue)) { + return false; + } + var decoded = entry.codec().parse(this.ops, newValue); + if (decoded.isError()) { + LOGGER.warn("Could not encode new value {}", newValue); + return false; + } + return entry.shouldEncode().test(decoded.result().orElseThrow()); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java new file mode 100644 index 0000000..c3cd30e --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java @@ -0,0 +1,6 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.mojang.serialization.Codec; +import java.util.function.Predicate; + +record RecordEntry(String key, OptionsEntry entry, Predicate shouldEncode, Codec codec) {} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java new file mode 100644 index 0000000..6c1c990 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java @@ -0,0 +1,10 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; +import net.minecraft.client.gui.screens.Screen; + +public interface ScreenFactory { + Screen open(Screen parent, DynamicOps ops, JsonElement original, Consumer update, Consumer onClose, ComponentInfo componentInfo); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java new file mode 100644 index 0000000..d0ea98b --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java @@ -0,0 +1,10 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; + +public interface WidgetFactory { + AbstractWidget create(Screen parent, int width, JsonElement original, Consumer update, ComponentInfo componentInfo); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/package-info.java new file mode 100644 index 0000000..4dfe823 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.minecraft.structured.config; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/minecraft/resources/META-INF/MANIFEST.MF b/src/minecraft/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..b325e5e --- /dev/null +++ b/src/minecraft/resources/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +FMLModType: GAMELIBRARY diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java new file mode 100644 index 0000000..a273862 --- /dev/null +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -0,0 +1,35 @@ +package dev.lukebemish.codecextras.test.neoforge; + +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.minecraft.structured.config.OptionsEntry; +import dev.lukebemish.codecextras.minecraft.structured.config.OptionsEntryInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; + +@Mod("codecextras_testmod") +public class CodecExtrasTest { + private record TestRecord(int a, int b, int c) { + private static final Structure STRUCTURE = Structure.record(builder -> { + var a = builder.add("a", Structure.INT, TestRecord::a); + var b = builder.add("b", Structure.INT, TestRecord::b); + var c = builder.add("c", Structure.INT, TestRecord::c); + return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container)); + }); + } + + public CodecExtrasTest(ModContainer modContainer) { + OptionsEntry entry = new OptionsEntryInterpreter( + MinecraftStructures.CODEC_INTERPRETER, + JsonOps.INSTANCE + ).interpret(TestRecord.STRUCTURE).getOrThrow(); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (modContainer1, arg) -> { + return entry.screen().open(arg, JsonOps.INSTANCE, new JsonObject(), jsonElement -> {}, jsonElement -> { + System.out.println("New JSON: "+jsonElement); + }, entry.componentInfo()); + }); + } +} diff --git a/src/testNeoforge/resources/META-INF/neoforge.mods.toml b/src/testNeoforge/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..068714f --- /dev/null +++ b/src/testNeoforge/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,9 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="LGPL-3.0-or-later" + +[[mods]] +modId="codecextras_testmod" +version="1.0.0" +displayName="CodecExtras Test Mod" +description="A test mod for CodecExtras" diff --git a/testFabric/build.gradle b/testFabric/build.gradle new file mode 100644 index 0000000..30b2273 --- /dev/null +++ b/testFabric/build.gradle @@ -0,0 +1,22 @@ +configurations { + normalRuntimeModClasses + runsImplementation.extendsFrom normalRuntimeModClasses +} + +loom { + runs { + configureEach { + runDir "runs/${it.name}" + } + } + + mods.register("codecextras") { + configuration configurations.normalRuntimeModClasses + } +} + +dependencies { + normalRuntimeModClasses(project(path: ':', configuration: 'minecraftIntermediaryRuntimeModClasses')) { + transitive = false + } +} diff --git a/testNeoforge/build.gradle b/testNeoforge/build.gradle new file mode 100644 index 0000000..3884508 --- /dev/null +++ b/testNeoforge/build.gradle @@ -0,0 +1,33 @@ +configurations { + normalRuntimeModClasses + runsImplementation.extendsFrom normalRuntimeModClasses + normalModClassesSource +} + +loom { + runs { + configureEach { + runDir "runs/${it.name}" + } + } + + mods.register("codecextras") { + configuration configurations.normalRuntimeModClasses + } +} + +tasks.register('copyNormalClasses', Copy) { + from configurations.normalModClassesSource + dependsOn configurations.normalModClassesSource + into layout.buildDirectory.dir('minecraftCodecExtras') +} + +dependencies { + forgeRuntimeLibrary project(path: ':', configuration: 'runtimeElements') + normalModClassesSource(project(path: ':', configuration: 'minecraftRuntimeModClasses')) { + transitive = false + } + normalRuntimeModClasses(files(layout.buildDirectory.dir('minecraftCodecExtras')).tap { + builtBy tasks.copyNormalClasses + }) +} From ae4e5147bd838526abc219cfe740136792a6fd54 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 20:12:01 -0500 Subject: [PATCH 37/76] Some refactors --- .../minecraft/structured/config/OptionsEntry.java | 12 +++++++++--- .../structured/config/OptionsEntryInterpreter.java | 6 +++--- .../structured/config/RecordConfigScreen.java | 5 +---- .../minecraft/structured/config/ScreenFactory.java | 2 +- .../codecextras/test/neoforge/CodecExtrasTest.java | 8 +++----- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java index af90fcc..2d149cd 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java @@ -3,8 +3,11 @@ import com.google.gson.JsonElement; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; import java.util.function.UnaryOperator; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.options.OptionsSubScreen; public record OptionsEntry(WidgetFactory widget, ScreenFactory screen, ComponentInfo componentInfo) implements App { @@ -17,14 +20,13 @@ public static OptionsEntry unbox(App app) { } public static OptionsEntry single(WidgetFactory first, ComponentInfo componentInfo) { - return new OptionsEntry<>(first, (parent, ops, original, update, onClose, info) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, info.title()) { + return new OptionsEntry<>(first, (parent, ops, original, onClose, info) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, info.title()) { private JsonElement value = original; @Override protected void addOptions() { this.list.addSmall(first.create(this, FULL_WIDTH, value, newValue -> { value = newValue; - update.accept(newValue); }, componentInfo), null); } @@ -36,7 +38,11 @@ public void onClose() { }, componentInfo); } - OptionsEntry withComponentInfo(UnaryOperator function) { + public OptionsEntry withComponentInfo(UnaryOperator function) { return new OptionsEntry<>(this.widget, this.screen, function.apply(this.componentInfo)); } + + public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, JsonElement initialData) { + return screen().open(parent, ops, initialData, onClose, componentInfo); + } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java index e0106b7..cdd2bed 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java @@ -119,14 +119,14 @@ public DataResult> record(List "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); } - ScreenFactory factory = (parent, ops, original, update, onClose, componentInfo) -> - new RecordConfigScreen(parent, componentInfo.title(), entries, ops, original, update, onClose); + ScreenFactory factory = (parent, ops, original, onClose, componentInfo) -> + new RecordConfigScreen(parent, componentInfo.title(), entries, ops, original, onClose); return DataResult.success(new OptionsEntry<>( (parent, width, original, update, componentInfo) -> Button.builder( Component.translatable("codecextras.config.configurerecord"), b -> { Minecraft.getInstance().setScreen(factory.open( - parent, dynamicOps, original, update, jsonElement -> {}, componentInfo + parent, dynamicOps, original, update, componentInfo )); } ).width(Button.DEFAULT_WIDTH).build(), diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java index fbbac96..1373257 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java @@ -21,10 +21,9 @@ class RecordConfigScreen extends OptionsSubScreen { private final List> entries; private final JsonObject jsonValue; private final Consumer update; - private final Consumer onClose; private final DynamicOps ops; - public RecordConfigScreen(Screen screen, Component component, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update, Consumer onClose) { + public RecordConfigScreen(Screen screen, Component component, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { super(screen, Minecraft.getInstance().options, component); this.entries = entries; if (jsonValue.isJsonObject()) { @@ -36,14 +35,12 @@ public RecordConfigScreen(Screen screen, Component component, List ops, JsonElement original, Consumer update, Consumer onClose, ComponentInfo componentInfo); + Screen open(Screen parent, DynamicOps ops, JsonElement original, Consumer onClose, ComponentInfo componentInfo); } diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index a273862..4ce2fb6 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -26,10 +26,8 @@ public CodecExtrasTest(ModContainer modContainer) { MinecraftStructures.CODEC_INTERPRETER, JsonOps.INSTANCE ).interpret(TestRecord.STRUCTURE).getOrThrow(); - modContainer.registerExtensionPoint(IConfigScreenFactory.class, (modContainer1, arg) -> { - return entry.screen().open(arg, JsonOps.INSTANCE, new JsonObject(), jsonElement -> {}, jsonElement -> { - System.out.println("New JSON: "+jsonElement); - }, entry.componentInfo()); - }); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> entry.rootScreen(parent, jsonElement -> { + System.out.println("New JSON: "+jsonElement); + }, JsonOps.INSTANCE, new JsonObject())); } } From 0ac8ada0ccbc9024667275d0e7cecf65bea7ea4c Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 29 Aug 2024 23:48:18 -0500 Subject: [PATCH 38/76] More work on config screen interpretation --- .../schema/JsonSchemaInterpreter.java | 4 +- .../structured/config/ComponentInfo.java | 8 + .../structured/config/ConfigAnnotations.java | 3 +- .../structured/config/ConfigScreenEntry.java | 64 +++++++ .../config/ConfigScreenInterpreter.java | 164 ++++++++++++++++ .../structured/config/EntryCreationInfo.java | 15 ++ .../structured/config/OptionsEntry.java | 48 ----- .../config/OptionsEntryInterpreter.java | 176 ------------------ .../structured/config/RecordConfigScreen.java | 30 +-- .../structured/config/RecordEntry.java | 2 +- .../structured/config/ScreenFactory.java | 4 +- .../structured/config/VerifyingEditBox.java | 65 +++++++ .../structured/config/WidgetFactory.java | 5 +- .../test/neoforge/CodecExtrasTest.java | 10 +- 14 files changed, 349 insertions(+), 249 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 4e3d868..098c1b5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -140,10 +140,10 @@ public DataResult> flatXmap(App input, Fu public DataResult> annotate(Structure input, Keys annotations) { JsonObject schema; Map> definitions; - var refName = annotations.get(SchemaAnnotations.REUSE_KEY); + var refName = Annotation.get(annotations, SchemaAnnotations.REUSE_KEY); if (refName.isPresent()) { schema = new JsonObject(); - var ref = Identity.unbox(refName.get()).value(); + var ref = refName.get(); schema.addProperty("$ref", "#/$defs/"+ref); definitions = new HashMap<>(); definitions.put(ref, input); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java index e8b6a51..6444403 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ComponentInfo.java @@ -33,4 +33,12 @@ public ComponentInfo fallbackDescription(Component fallback) { return new ComponentInfo(maybeTitle, Optional.of(fallback)); } } + + public ComponentInfo withTitle(Component title) { + return new ComponentInfo(Optional.of(title), maybeDescription); + } + + public ComponentInfo withDescription(Component description) { + return new ComponentInfo(maybeTitle, Optional.of(description)); + } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java index de552bc..c851d41 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java @@ -6,5 +6,6 @@ public final class ConfigAnnotations { private ConfigAnnotations() {} - public Key TITLE = Key.create("title"); + public static Key TITLE = Key.create("title"); + public static Key DESCRIPTOIN = Key.create("description"); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java new file mode 100644 index 0000000..2b033d2 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -0,0 +1,64 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.options.OptionsSubScreen; + +public record ConfigScreenEntry(WidgetFactory widget, ScreenFactory screen, EntryCreationInfo entryCreationInfo) implements App { + + public static final class Mu implements K1 { private Mu() {} } + + private static final int FULL_WIDTH = 310; + + public static ConfigScreenEntry unbox(App app) { + return (ConfigScreenEntry) app; + } + + public static ConfigScreenEntry single(WidgetFactory first, EntryCreationInfo entryCreationInfo) { + return new ConfigScreenEntry<>(first, (parent, ops, original, onClose, creationInfo) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, creationInfo.componentInfo().title()) { + private JsonElement value = original; + + @Override + protected void addOptions() { + this.list.addSmall(first.create(this, FULL_WIDTH, ops, value, newValue -> { + value = newValue; + }, creationInfo), null); + } + + @Override + public void onClose() { + onClose.accept(value); + super.onClose(); + } + }, entryCreationInfo); + } + + public ConfigScreenEntry withComponentInfo(UnaryOperator function) { + return new ConfigScreenEntry<>(this.widget, this.screen, this.entryCreationInfo.withComponentInfo(function)); + } + + public ConfigScreenEntry withEntryCreationInfo(Function, EntryCreationInfo> function, Function, EntryCreationInfo> reverse) { + return new ConfigScreenEntry<>( + (parent, width, ops, original, update, entry) -> { + var entryCreationInfo = reverse.apply(entry); + return this.widget.create(parent, width, ops, original, update, entryCreationInfo); + }, + (parent, ops, original, onClose, entry) -> { + var entryCreationInfo = reverse.apply(entry); + return this.screen.open(parent, ops, original, onClose, entryCreationInfo); + }, + function.apply(this.entryCreationInfo) + ); + } + + public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, JsonElement initialData) { + return screen().open(parent, ops, initialData, onClose, this.entryCreationInfo()); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java new file mode 100644 index 0000000..8423eeb --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -0,0 +1,164 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.structured.Annotation; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.RecordStructure; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.Identity; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +public class ConfigScreenInterpreter extends KeyStoringInterpreter { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final CodecInterpreter codecInterpreter; + + public ConfigScreenInterpreter( + Keys keys, + Keys2, K1, K1> parametricKeys, + CodecInterpreter codecInterpreter + ) { + super( + keys.join(Keys.builder() + .add(Interpreter.INT, ConfigScreenEntry.single( + VerifyingEditBox.of(string -> { + try { + return DataResult.success(Integer.parseInt(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not an integer: "+string); + } + }, integer -> DataResult.success(integer+""), string -> string.matches("-?[0-9]*"), true), + new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + )) + .build()), + parametricKeys.join(Keys2., K1, K1>builder() + .build()) + ); + this.codecInterpreter = codecInterpreter; + } + + public ConfigScreenInterpreter( + CodecInterpreter codecInterpreter + ) { + this( + Keys.builder().build(), + Keys2., K1, K1>builder().build(), + codecInterpreter + ); + } + + @Override + public ConfigScreenInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { + return new ConfigScreenInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter); + } + + @Override + public DataResult>> list(App single) { + return DataResult.error(() -> "Not yet implemented"); + } + + @Override + public DataResult> record(List> fields, Function creator) { + List> entries = new ArrayList<>(); + List> errors = new ArrayList<>(); + for (var field : fields) { + handleEntry(field, entries, errors); + } + if (!errors.isEmpty()) { + return DataResult.error(() -> "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); + } + ScreenFactory factory = (parent, ops, original, onClose, creationInfo) -> + new RecordConfigScreen<>(parent, creationInfo, entries, ops, original, onClose); + var codecResult = codecInterpreter.record(fields, creator); + if (codecResult.isError()) { + return DataResult.error(() -> "Error creating record codec: "+codecResult.error().orElseThrow().messageSupplier()); + } + return DataResult.success(new ConfigScreenEntry<>( + (parent, width, ops, original, update, creationInfo) -> Button.builder( + Component.translatable("codecextras.config.configurerecord"), + b -> { + Minecraft.getInstance().setScreen(factory.open( + parent, ops, original, update, creationInfo + )); + } + ).width(Button.DEFAULT_WIDTH).build(), + factory, + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + )); + } + + private void handleEntry(RecordStructure.Field field, List> entries, List> errors) { + var shouldEncode = field.missingBehavior().map(RecordStructure.Field.MissingBehavior::predicate).orElse(t -> true); + var codecResult = codecInterpreter.interpret(field.structure()); + if (codecResult.isError()) { + errors.add(codecResult.error().orElseThrow().messageSupplier()); + return; + } + var optionEntryResult = field.structure().interpret(this).map(ConfigScreenEntry::unbox); + if (optionEntryResult.isError()) { + errors.add(optionEntryResult.error().orElseThrow().messageSupplier()); + return; + } + entries.add(new RecordEntry<>( + field.name(), + optionEntryResult.getOrThrow().withComponentInfo(info -> info.fallbackTitle(Component.literal(field.name()))), + shouldEncode, + codecResult.getOrThrow() + )); + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + var original = ConfigScreenEntry.unbox(input); + return DataResult.success(original.withEntryCreationInfo( + info -> info.withCodec(codec -> codec.flatXmap(deserializer, serializer)), + info -> info.withCodec(codec -> codec.flatXmap(serializer, deserializer)) + )); + } + + @Override + public DataResult> annotate(Structure original, Keys annotations) { + var result = original.interpret(this); + return result.map(app -> { + var entry = ConfigScreenEntry.unbox(app); + var withTitle = Annotation + .get(annotations, ConfigAnnotations.TITLE) + .or(() -> Annotation.get(annotations, Annotation.TITLE).map(Component::literal)) + .map(title -> entry.withComponentInfo(info -> info.withTitle(title))) + .orElse(entry); + return Annotation + .get(annotations, ConfigAnnotations.DESCRIPTOIN) + .or(() -> Annotation.get(annotations, Annotation.DESCRIPTION).map(Component::literal)) + .or(() -> Annotation.get(annotations, Annotation.COMMENT).map(Component::literal)) + .map(description -> withTitle.withComponentInfo(info -> info.withDescription(description))) + .orElse(withTitle); + }); + } + + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + return DataResult.error(() -> "Not yet implemented"); + } + + public DataResult> interpret(Structure structure) { + return structure.interpret(this).map(ConfigScreenEntry::unbox); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java new file mode 100644 index 0000000..3a70a72 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java @@ -0,0 +1,15 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.mojang.serialization.Codec; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +public record EntryCreationInfo(Codec codec, ComponentInfo componentInfo) { + public EntryCreationInfo withComponentInfo(UnaryOperator function) { + return new EntryCreationInfo<>(this.codec, function.apply(this.componentInfo)); + } + + public EntryCreationInfo withCodec(Function, Codec> function) { + return new EntryCreationInfo<>(function.apply(this.codec), this.componentInfo); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java deleted file mode 100644 index 2d149cd..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntry.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.google.gson.JsonElement; -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.K1; -import com.mojang.serialization.DynamicOps; -import java.util.function.Consumer; -import java.util.function.UnaryOperator; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.options.OptionsSubScreen; - -public record OptionsEntry(WidgetFactory widget, ScreenFactory screen, ComponentInfo componentInfo) implements App { - public static final class Mu implements K1 { private Mu() {} } - - private static final int FULL_WIDTH = 310; - - public static OptionsEntry unbox(App app) { - return (OptionsEntry) app; - } - - public static OptionsEntry single(WidgetFactory first, ComponentInfo componentInfo) { - return new OptionsEntry<>(first, (parent, ops, original, onClose, info) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, info.title()) { - private JsonElement value = original; - - @Override - protected void addOptions() { - this.list.addSmall(first.create(this, FULL_WIDTH, value, newValue -> { - value = newValue; - }, componentInfo), null); - } - - @Override - public void onClose() { - onClose.accept(value); - super.onClose(); - } - }, componentInfo); - } - - public OptionsEntry withComponentInfo(UnaryOperator function) { - return new OptionsEntry<>(this.widget, this.screen, function.apply(this.componentInfo)); - } - - public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, JsonElement initialData) { - return screen().open(parent, ops, initialData, onClose, componentInfo); - } -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java deleted file mode 100644 index cdd2bed..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/OptionsEntryInterpreter.java +++ /dev/null @@ -1,176 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonPrimitive; -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.K1; -import com.mojang.logging.LogUtils; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; -import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.Interpreter; -import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; -import dev.lukebemish.codecextras.structured.Keys; -import dev.lukebemish.codecextras.structured.Keys2; -import dev.lukebemish.codecextras.structured.ParametricKeyedValue; -import dev.lukebemish.codecextras.structured.RecordStructure; -import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.types.Identity; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.network.chat.Component; -import org.slf4j.Logger; - -public class OptionsEntryInterpreter extends KeyStoringInterpreter { - private static final Logger LOGGER = LogUtils.getLogger(); - - private final CodecInterpreter codecInterpreter; - private final DynamicOps dynamicOps; - - public OptionsEntryInterpreter( - Keys keys, - Keys2, K1, K1> parametricKeys, - CodecInterpreter codecInterpreter, - DynamicOps dynamicOps - ) { - super( - keys.join(Keys.builder() - .add(Interpreter.INT, OptionsEntry.single( - (parent, width, original, update, info) -> new EditBox(Minecraft.getInstance().font, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, info.title()) { - { - if (!original.isJsonPrimitive()) { - if (!original.isJsonNull()) { - LOGGER.warn("{} is not an integer", original); - } - } else { - try { - var intValue = original.getAsJsonPrimitive().getAsInt(); - this.setValue(intValue+""); - } catch (NumberFormatException ignored) { - LOGGER.warn("{} is not an integer", original); - } - } - this.setResponder(string -> { - if (string.isEmpty()) { - update.accept(JsonNull.INSTANCE); - return; - } - try { - var intValue = Integer.parseInt(string); - update.accept(new JsonPrimitive(intValue)); - } catch (NumberFormatException ignored) { - LOGGER.warn("{} is not an integer", original); - } - }); - } - - @Override - public void insertText(String string) { - super.insertText(string.replaceAll("[^0-9\\-]+", "")); - } - }, - ComponentInfo.empty() - )) - .build()), - parametricKeys.join(Keys2., K1, K1>builder() - .build()) - ); - this.codecInterpreter = codecInterpreter; - this.dynamicOps = dynamicOps; - } - - public OptionsEntryInterpreter( - CodecInterpreter codecInterpreter, - DynamicOps dynamicOps - ) { - this( - Keys.builder().build(), - Keys2., K1, K1>builder().build(), - codecInterpreter, - dynamicOps - ); - } - - @Override - public OptionsEntryInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { - return new OptionsEntryInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter, this.dynamicOps); - } - - @Override - public DataResult>> list(App single) { - return DataResult.error(() -> "Not yet implemented"); - } - - @Override - public DataResult> record(List> fields, Function creator) { - List> entries = new ArrayList<>(); - List> errors = new ArrayList<>(); - for (var field : fields) { - handleEntry(field, entries, errors); - } - if (!errors.isEmpty()) { - return DataResult.error(() -> "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); - } - ScreenFactory factory = (parent, ops, original, onClose, componentInfo) -> - new RecordConfigScreen(parent, componentInfo.title(), entries, ops, original, onClose); - return DataResult.success(new OptionsEntry<>( - (parent, width, original, update, componentInfo) -> Button.builder( - Component.translatable("codecextras.config.configurerecord"), - b -> { - Minecraft.getInstance().setScreen(factory.open( - parent, dynamicOps, original, update, componentInfo - )); - } - ).width(Button.DEFAULT_WIDTH).build(), - factory, - ComponentInfo.empty() - )); - } - - private void handleEntry(RecordStructure.Field field, List> entries, List> errors) { - var shouldEncode = field.missingBehavior().map(RecordStructure.Field.MissingBehavior::predicate).orElse(t -> true); - var codecResult = codecInterpreter.interpret(field.structure()); - if (codecResult.isError()) { - errors.add(codecResult.error().orElseThrow().messageSupplier()); - return; - } - var optionEntryResult = field.structure().interpret(this).map(OptionsEntry::unbox); - if (optionEntryResult.isError()) { - errors.add(optionEntryResult.error().orElseThrow().messageSupplier()); - return; - } - entries.add(new RecordEntry<>( - field.name(), - optionEntryResult.getOrThrow().withComponentInfo(info -> info.fallbackTitle(Component.literal(field.name()))), - shouldEncode, - codecResult.getOrThrow() - )); - } - - @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { - return DataResult.error(() -> "Not yet implemented"); - } - - @Override - public DataResult> annotate(Structure original, Keys annotations) { - return DataResult.error(() -> "Not yet implemented"); - } - - @Override - public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { - return DataResult.error(() -> "Not yet implemented"); - } - - public DataResult> interpret(Structure structure) { - return structure.interpret(this).map(OptionsEntry::unbox); - } -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java index 1373257..e6b0e7a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java @@ -8,23 +8,25 @@ import java.util.List; import java.util.function.Consumer; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.options.OptionsSubScreen; -import net.minecraft.network.chat.Component; import org.slf4j.Logger; -class RecordConfigScreen extends OptionsSubScreen { +class RecordConfigScreen extends OptionsSubScreen { private static final Logger LOGGER = LogUtils.getLogger(); private final List> entries; private final JsonObject jsonValue; private final Consumer update; private final DynamicOps ops; + private final EntryCreationInfo creationInfo; - public RecordConfigScreen(Screen screen, Component component, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { - super(screen, Minecraft.getInstance().options, component); + public RecordConfigScreen(Screen screen, EntryCreationInfo creationInfo, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { + super(screen, Minecraft.getInstance().options, creationInfo.componentInfo().title()); + this.creationInfo = creationInfo; this.entries = entries; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); @@ -49,18 +51,22 @@ protected void addOptions() { for (var entry: this.entries) { JsonElement specificValue = this.jsonValue.has(entry.key()) ? this.jsonValue.get(entry.key()) : JsonNull.INSTANCE; this.list.addSmall( - new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().componentInfo().title(), font).alignLeft(), - entry.entry().widget().create(this, Button.DEFAULT_WIDTH, specificValue, newValue -> { - if (shouldUpdate(newValue, specificValue, entry)) { - this.jsonValue.add(entry.key(), newValue); - } else { - this.jsonValue.remove(entry.key()); - } - }, entry.entry().componentInfo()) + new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), font).alignLeft(), + createEntryWidget(entry, specificValue) ); } } + private AbstractWidget createEntryWidget(RecordEntry entry, JsonElement specificValue) { + return entry.entry().widget().create(this, Button.DEFAULT_WIDTH, ops, specificValue, newValue -> { + if (shouldUpdate(newValue, specificValue, entry)) { + this.jsonValue.add(entry.key(), newValue); + } else { + this.jsonValue.remove(entry.key()); + } + }, entry.entry().entryCreationInfo()); + } + boolean shouldUpdate(JsonElement newValue, JsonElement oldValue, RecordEntry entry) { if (newValue.isJsonNull() || newValue.equals(oldValue)) { return false; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java index c3cd30e..4c1b957 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java @@ -3,4 +3,4 @@ import com.mojang.serialization.Codec; import java.util.function.Predicate; -record RecordEntry(String key, OptionsEntry entry, Predicate shouldEncode, Codec codec) {} +record RecordEntry(String key, ConfigScreenEntry entry, Predicate shouldEncode, Codec codec) {} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java index cb5cc17..025bd9b 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java @@ -5,6 +5,6 @@ import java.util.function.Consumer; import net.minecraft.client.gui.screens.Screen; -public interface ScreenFactory { - Screen open(Screen parent, DynamicOps ops, JsonElement original, Consumer onClose, ComponentInfo componentInfo); +public interface ScreenFactory { + Screen open(Screen parent, DynamicOps ops, JsonElement original, Consumer onClose, EntryCreationInfo entry); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java new file mode 100644 index 0000000..cffd29f --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java @@ -0,0 +1,65 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import org.slf4j.Logger; + +public class VerifyingEditBox extends EditBox { + private static final Logger LOGGER = LogUtils.getLogger(); + + private VerifyingEditBox(int width, EntryCreationInfo creationInfo, DynamicOps ops, Consumer update, JsonElement original, Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { + super(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().title()); + + if (!original.isJsonPrimitive()) { + if (!original.isJsonNull()) { + LOGGER.warn("`{}` is not primitive, as required for an edit box", original); + } + } else { + var decoded = creationInfo.codec().parse(ops, original); + if (decoded.isError()) { + LOGGER.warn("Failed to decode `{}`: {}", original, decoded.error().orElseThrow().message()); + } else { + var decodedValue = decoded.getOrThrow(); + var stringResult = fromData.apply(decodedValue); + if (stringResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as string: {}", decodedValue, stringResult.error().get().message()); + } else { + this.setValue(stringResult.getOrThrow()); + } + } + } + + this.setFilter(filter); + + this.setResponder(string -> { + if (emptyIsMissing && string.isEmpty()) { + update.accept(JsonNull.INSTANCE); + return; + } + var dataResult = toData.apply(string); + if (dataResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as data: {}", string, dataResult.error().get().message()); + } else { + var jsonResult = creationInfo.codec().encode(dataResult.getOrThrow(), ops, original); + if (jsonResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as json: {}", dataResult.getOrThrow(), jsonResult.error().get().message()); + } else { + update.accept(jsonResult.getOrThrow()); + } + } + }); + } + + public static WidgetFactory of(Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { + return (parent, width, ops, original, update, creationInfo) -> new VerifyingEditBox<>(width, creationInfo, ops, update, original, toData, fromData, filter, emptyIsMissing); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java index d0ea98b..d1c8d31 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java @@ -1,10 +1,11 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.screens.Screen; -public interface WidgetFactory { - AbstractWidget create(Screen parent, int width, JsonElement original, Consumer update, ComponentInfo componentInfo); +public interface WidgetFactory { + AbstractWidget create(Screen parent, int width, DynamicOps ops, JsonElement original, Consumer update, EntryCreationInfo entry); } diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index 4ce2fb6..f9f8541 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -3,8 +3,8 @@ import com.google.gson.JsonObject; import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; -import dev.lukebemish.codecextras.minecraft.structured.config.OptionsEntry; -import dev.lukebemish.codecextras.minecraft.structured.config.OptionsEntryInterpreter; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.structured.Structure; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; @@ -22,10 +22,10 @@ private record TestRecord(int a, int b, int c) { } public CodecExtrasTest(ModContainer modContainer) { - OptionsEntry entry = new OptionsEntryInterpreter( - MinecraftStructures.CODEC_INTERPRETER, - JsonOps.INSTANCE + ConfigScreenEntry entry = new ConfigScreenInterpreter( + MinecraftStructures.CODEC_INTERPRETER ).interpret(TestRecord.STRUCTURE).getOrThrow(); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> entry.rootScreen(parent, jsonElement -> { System.out.println("New JSON: "+jsonElement); }, JsonOps.INSTANCE, new JsonObject())); From 32de9d6cb919420e2449779913d47c192e751257 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sat, 31 Aug 2024 18:17:51 -0500 Subject: [PATCH 39/76] Missing/optional values, primitives, lists, and tooltips for config screens --- build.gradle | 53 +++-- .../codecextras/config/ConfigType.java | 21 +- .../lukebemish/codecextras/config/OpsIo.java | 2 +- src/main/resources/META-INF/MANIFEST.MF | 2 + .../structured/config/ConfigScreenEntry.java | 47 +++-- .../config/ConfigScreenInterpreter.java | 146 +++++++++++--- .../structured/config/EntryCreationInfo.java | 5 +- .../structured/config/EntryListScreen.java | 149 ++++++++++++++ .../structured/config/LayoutFactory.java | 11 ++ .../structured/config/ListConfigScreen.java | 104 ++++++++++ .../structured/config/RecordConfigScreen.java | 63 +++--- .../structured/config/RecordEntry.java | 5 +- .../structured/config/VerifyingEditBox.java | 65 ------- .../structured/config/WidgetFactory.java | 11 -- .../minecraft/structured/config/Widgets.java | 182 ++++++++++++++++++ src/minecraft/resources/META-INF/MANIFEST.MF | 1 - .../resources/META-INF/neoforge.mods.toml | 8 + .../codecextras_minecraft/lang/en_us.json | 14 ++ src/minecraft/resources/fabric.mod.json | 6 + .../resources/fabric.mod.json | 6 - .../test/neoforge/CodecExtrasTest.java | 48 ++++- .../test/neoforge/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 2 +- testNeoforge/build.gradle | 22 +-- 24 files changed, 769 insertions(+), 208 deletions(-) create mode 100644 src/main/resources/META-INF/MANIFEST.MF create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java delete mode 100644 src/minecraft/resources/META-INF/MANIFEST.MF create mode 100644 src/minecraft/resources/META-INF/neoforge.mods.toml create mode 100644 src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json create mode 100644 src/minecraft/resources/fabric.mod.json delete mode 100644 src/minecraftIntermediary/resources/fabric.mod.json create mode 100644 src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/package-info.java diff --git a/build.gradle b/build.gradle index 8e4c415..1dc4987 100644 --- a/build.gradle +++ b/build.gradle @@ -109,6 +109,22 @@ configurations { testNeoforgeCompileClasspath.extendsFrom minecraftCompileClasspath testFabricCompileClasspath.extendsFrom minecraftIntermediaryCompileClasspath testFabricToRemapCompileClasspath.extendsFrom minecraftIntermediaryToRemapCompileClasspath + + runtimeModClasses { + canBeConsumed = true + canBeResolved = false + } +} + +artifacts { + sourceSets.main.output.classesDirs.each { file -> + add(configurations.runtimeModClasses.name, file) { + builtBy tasks.classes + } + } + add(configurations.runtimeModClasses.name, sourceSets.main.output.resourcesDir) { + builtBy tasks.processResources + } } java { @@ -142,7 +158,7 @@ repositories { } dependencies { - api 'com.mojang:datafixerupper:7.0.14' + api 'com.mojang:datafixerupper:8.0.16' api 'org.slf4j:slf4j-api:2.0.1' jmhCompileOnly cLibs.bundles.compileonly @@ -197,7 +213,8 @@ dependencies { tasks.named('jar', Jar) { manifest { attributes( - 'Automatic-Module-Name': project.group + '.' + project.name + 'Automatic-Module-Name': project.group + '.' + project.name, + 'FMLModType' : 'LIBRARY' ) } } @@ -234,22 +251,10 @@ tasks.register('jmhResults', FormatJmhOutput) { formattedResults.set project.file('build/reports/jmh/results.md') } -minecraftJar { - manifest.attributes('FMLModType': 'GAMELIBRARY') -} - -minecraftIntermediaryJar { - manifest.attributes('FMLModType': 'GAMELIBRARY') -} - tasks.named('remapMinecraftIntermediaryJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-intermediary.jar") } -jar { - manifest.attributes('FMLModType': 'LIBRARY') -} - tasks.compileJava { options.compilerArgs += [ '-Aautoextension.name=CodecExtras', @@ -260,20 +265,14 @@ tasks.compileJava { } } -processMinecraftIntermediaryResources { - inputs.property "version", project.version.toString() - - filesMatching("fabric.mod.json") { - expand "version": project.version.toString() - } -} - -processResources { - inputs.property "version", project.version.toString() +['processResources', 'processMinecraftResources', 'processMinecraftIntermediaryResources'].each { + tasks.named(it, ProcessResources) { + inputs.property "version", project.version.toString() - filesMatching("fabric.mod.json") { - expand "version": project.version.toString() - } + filesMatching(["fabric.mod.json", "META-INF/neoforge.mods.toml"]) { + expand "version": project.version.toString() + } + } } test { diff --git a/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java b/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java index 260ea6c..b737396 100644 --- a/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java +++ b/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -44,7 +45,7 @@ public ConfigType() { if (!touched[0]) { return null; } - return dataFixerBuilder.buildUnoptimized(); + return dataFixerBuilder.build().fixer(); }); } @@ -72,13 +73,24 @@ public ConfigHandle handle(Path location, OpsIo opsIo, Logger logger) } } return new ConfigHandle<>() { + private volatile @Nullable O loaded; + + @Override + public synchronized O load() { + var value = ConfigType.this.load(location, withLogging, logger); + this.loaded = value; + return value; + } + @Override - public O load() { - return ConfigType.this.load(location, withLogging, logger); + public O get() { + var value = this.loaded; + return Objects.requireNonNullElseGet(value, this::load); } @Override - public void save(O config) { + public synchronized void save(O config) { + this.loaded = config; ConfigType.this.save(location, withLogging, logger, config); } }; @@ -86,6 +98,7 @@ public void save(O config) { public interface ConfigHandle { O load(); + O get(); void save(O config); } diff --git a/src/main/java/dev/lukebemish/codecextras/config/OpsIo.java b/src/main/java/dev/lukebemish/codecextras/config/OpsIo.java index 7fd919c..8aac5c2 100644 --- a/src/main/java/dev/lukebemish/codecextras/config/OpsIo.java +++ b/src/main/java/dev/lukebemish/codecextras/config/OpsIo.java @@ -15,6 +15,6 @@ public interface OpsIo { void write(T value, OutputStream output) throws IOException; default OpsIo accompanied(Q token, Companion companion) { - return new SpecializedOpsIo(this, DelegatingOps.of(token, companion, ops())); + return new SpecializedOpsIo<>(this, DelegatingOps.of(token, companion, ops())); } } diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..72758bc --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +FMLModType: LIBRARY +Automatic-Module-Name: dev.lukebemish.codecextras diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 2b033d2..7ad4d77 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -1,17 +1,18 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.UnaryOperator; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.options.OptionsSubScreen; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public record ConfigScreenEntry(WidgetFactory widget, ScreenFactory screen, EntryCreationInfo entryCreationInfo) implements App { +public record ConfigScreenEntry(LayoutFactory widget, ScreenFactory screen, EntryCreationInfo entryCreationInfo) implements App { public static final class Mu implements K1 { private Mu() {} } @@ -21,21 +22,18 @@ public static ConfigScreenEntry unbox(App app) { return (ConfigScreenEntry) app; } - public static ConfigScreenEntry single(WidgetFactory first, EntryCreationInfo entryCreationInfo) { - return new ConfigScreenEntry<>(first, (parent, ops, original, onClose, creationInfo) -> new OptionsSubScreen(parent, Minecraft.getInstance().options, creationInfo.componentInfo().title()) { + public static ConfigScreenEntry single(LayoutFactory first, EntryCreationInfo entryCreationInfo) { + return new ConfigScreenEntry<>(first, (parent, ops, original, onClose, creationInfo) -> new EntryListScreen(parent, creationInfo.componentInfo().title()) { private JsonElement value = original; @Override - protected void addOptions() { - this.list.addSmall(first.create(this, FULL_WIDTH, ops, value, newValue -> { - value = newValue; - }, creationInfo), null); + protected void addEntries() { + this.list.addSingle(first.create(this, FULL_WIDTH, ops, value, newValue -> value = newValue, creationInfo, false)); } @Override - public void onClose() { + public void onExit() { onClose.accept(value); - super.onClose(); } }, entryCreationInfo); } @@ -46,9 +44,9 @@ public ConfigScreenEntry withComponentInfo(UnaryOperator funct public ConfigScreenEntry withEntryCreationInfo(Function, EntryCreationInfo> function, Function, EntryCreationInfo> reverse) { return new ConfigScreenEntry<>( - (parent, width, ops, original, update, entry) -> { + (parent, width, ops, original, update, entry, handleOptional) -> { var entryCreationInfo = reverse.apply(entry); - return this.widget.create(parent, width, ops, original, update, entryCreationInfo); + return this.widget.create(parent, width, ops, original, update, entryCreationInfo, handleOptional); }, (parent, ops, original, onClose, entry) -> { var entryCreationInfo = reverse.apply(entry); @@ -58,7 +56,26 @@ public ConfigScreenEntry withEntryCreationInfo(Function onClose, DynamicOps ops, JsonElement initialData) { - return screen().open(parent, ops, initialData, onClose, this.entryCreationInfo()); + public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, T initialData) { + return this.rootScreen(parent, onClose, ops, initialData, LoggerFactory.getLogger(ConfigScreenEntry.class)); + } + + public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, T initialData, Logger logger) { + var initial = entryCreationInfo.codec().encodeStart(ops, initialData); + JsonElement initialJson; + if (initial.error().isPresent()) { + logger.warn("Failed to encode `{}`: {}", initialData, initial.error().get().message()); + initialJson = JsonNull.INSTANCE; + } else { + initialJson = initial.getOrThrow(); + } + return screen().open(parent, ops, initialJson, json -> { + var decoded = entryCreationInfo.codec().parse(ops, json); + if (decoded.error().isPresent()) { + logger.warn("Failed to decode `{}`: {}", json, decoded.error().get().message()); + } else { + onClose.accept(decoded.getOrThrow()); + } + }, this.entryCreationInfo()); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 8423eeb..261d1be 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -1,8 +1,11 @@ package dev.lukebemish.codecextras.minecraft.structured.config; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; -import com.mojang.logging.LogUtils; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.structured.Annotation; @@ -24,11 +27,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.network.chat.Component; -import org.slf4j.Logger; public class ConfigScreenInterpreter extends KeyStoringInterpreter { - private static final Logger LOGGER = LogUtils.getLogger(); - private final CodecInterpreter codecInterpreter; public ConfigScreenInterpreter( @@ -38,16 +38,78 @@ public ConfigScreenInterpreter( ) { super( keys.join(Keys.builder() - .add(Interpreter.INT, ConfigScreenEntry.single( - VerifyingEditBox.of(string -> { - try { - return DataResult.success(Integer.parseInt(string)); - } catch (NumberFormatException e) { - return DataResult.error(() -> "Not an integer: "+string); - } - }, integer -> DataResult.success(integer+""), string -> string.matches("-?[0-9]*"), true), - new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) - )) + .add(Interpreter.INT, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Integer.parseInt(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not an integer: "+string); + } + }, integer -> DataResult.success(integer+""), string -> string.matches("-?[0-9]*"), true), + new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + )) + .add(Interpreter.BYTE, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Byte.parseByte(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a byte: "+string); + } + }, byteValue -> DataResult.success(byteValue+""), string -> string.matches("-?[0-9]*"), true), + new EntryCreationInfo<>(Codec.BYTE, ComponentInfo.empty()) + )) + .add(Interpreter.SHORT, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Short.parseShort(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a short: "+string); + } + }, shortValue -> DataResult.success(shortValue+""), string -> string.matches("-?[0-9]*"), true), + new EntryCreationInfo<>(Codec.SHORT, ComponentInfo.empty()) + )) + .add(Interpreter.LONG, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Long.parseLong(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a long: "+string); + } + }, longValue -> DataResult.success(longValue+""), string -> string.matches("-?[0-9]*"), true), + new EntryCreationInfo<>(Codec.LONG, ComponentInfo.empty()) + )) + .add(Interpreter.DOUBLE, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Double.parseDouble(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a double: "+string); + } + }, doubleValue -> DataResult.success(doubleValue+""), string -> string.matches("-?[0-9]*(\\.[0-9]*)?"), true), + new EntryCreationInfo<>(Codec.DOUBLE, ComponentInfo.empty()) + )) + .add(Interpreter.FLOAT, ConfigScreenEntry.single( + Widgets.text(string -> { + try { + return DataResult.success(Float.parseFloat(string)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a float: "+string); + } + }, floatValue -> DataResult.success(floatValue+""), string -> string.matches("-?[0-9]*(\\.[0-9]*)?"), true), + new EntryCreationInfo<>(Codec.FLOAT, ComponentInfo.empty()) + )) + .add(Interpreter.BOOL, ConfigScreenEntry.single( + Widgets.bool(false), + new EntryCreationInfo<>(Codec.BOOL, ComponentInfo.empty()) + )) + .add(Interpreter.UNIT, ConfigScreenEntry.single( + Widgets.bool(true), + new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), ComponentInfo.empty()) + )) + .add(Interpreter.STRING, ConfigScreenEntry.single( + Widgets.canHandleOptional(Widgets.text(DataResult::success, DataResult::success, false)), + new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) + )) .build()), parametricKeys.join(Keys2., K1, K1>builder() .build()) @@ -72,7 +134,30 @@ public ConfigScreenInterpreter with(Keys keys, Key @Override public DataResult>> list(App single) { - return DataResult.error(() -> "Not yet implemented"); + var unwrapped = ConfigScreenEntry.unbox(single); + var codecResult = codecInterpreter.list(new CodecInterpreter.Holder<>(unwrapped.entryCreationInfo().codec())).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(codecResult.error().orElseThrow().messageSupplier()); + } + ScreenFactory> factory = (parent, ops, original, onClose, creationInfo) -> + new ListConfigScreen<>(parent, creationInfo, unwrapped, ops, original, onClose); + return DataResult.success(new ConfigScreenEntry<>( + Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonArray(); + update.accept(original); + } + JsonElement finalOriginal = original; + return Button.builder( + Component.translatable("codecextras.config.configurelist"), + b -> Minecraft.getInstance().setScreen(factory.open( + parent, ops, finalOriginal, update, creationInfo + )) + ).width(width).build(); + }), + factory, + unwrapped.entryCreationInfo().withCodec(codecResult.getOrThrow()) + )); } @Override @@ -92,21 +177,25 @@ public DataResult> record(List "Error creating record codec: "+codecResult.error().orElseThrow().messageSupplier()); } return DataResult.success(new ConfigScreenEntry<>( - (parent, width, ops, original, update, creationInfo) -> Button.builder( + Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonObject(); + update.accept(original); + } + JsonElement finalOriginal = original; + return Button.builder( Component.translatable("codecextras.config.configurerecord"), - b -> { - Minecraft.getInstance().setScreen(factory.open( - parent, ops, original, update, creationInfo - )); - } - ).width(Button.DEFAULT_WIDTH).build(), + b -> Minecraft.getInstance().setScreen(factory.open( + parent, ops, finalOriginal, update, creationInfo + )) + ).width(width).build(); + }), factory, new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) )); } private void handleEntry(RecordStructure.Field field, List> entries, List> errors) { - var shouldEncode = field.missingBehavior().map(RecordStructure.Field.MissingBehavior::predicate).orElse(t -> true); var codecResult = codecInterpreter.interpret(field.structure()); if (codecResult.isError()) { errors.add(codecResult.error().orElseThrow().messageSupplier()); @@ -120,7 +209,7 @@ private void handleEntry(RecordStructure.Field field, List( field.name(), optionEntryResult.getOrThrow().withComponentInfo(info -> info.fallbackTitle(Component.literal(field.name()))), - shouldEncode, + field.missingBehavior(), codecResult.getOrThrow() )); } @@ -128,9 +217,14 @@ private void handleEntry(RecordStructure.Field field, List DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { var original = ConfigScreenEntry.unbox(input); + var codecOriginal = original.entryCreationInfo().codec(); + var codecMapped = codecInterpreter.flatXmap(new CodecInterpreter.Holder<>(codecOriginal), deserializer, serializer).map(CodecInterpreter::unbox); + if (codecMapped.error().isPresent()) { + return DataResult.error(codecMapped.error().get().messageSupplier()); + } return DataResult.success(original.withEntryCreationInfo( - info -> info.withCodec(codec -> codec.flatXmap(deserializer, serializer)), - info -> info.withCodec(codec -> codec.flatXmap(serializer, deserializer)) + info -> info.withCodec(codecMapped.getOrThrow()), + info -> info.withCodec(codecOriginal) )); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java index 3a70a72..b68c47f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java @@ -1,7 +1,6 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.mojang.serialization.Codec; -import java.util.function.Function; import java.util.function.UnaryOperator; public record EntryCreationInfo(Codec codec, ComponentInfo componentInfo) { @@ -9,7 +8,7 @@ public EntryCreationInfo withComponentInfo(UnaryOperator funct return new EntryCreationInfo<>(this.codec, function.apply(this.componentInfo)); } - public EntryCreationInfo withCodec(Function, Codec> function) { - return new EntryCreationInfo<>(function.apply(this.codec), this.componentInfo); + public EntryCreationInfo withCodec(Codec codec) { + return new EntryCreationInfo<>(codec, this.componentInfo); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java new file mode 100644 index 0000000..332f347 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java @@ -0,0 +1,149 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.Layout; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +abstract class EntryListScreen extends Screen { + protected final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + protected final Screen lastScreen; + protected @Nullable EntryList list; + + public EntryListScreen(Screen screen, Component title) { + super(title); + this.lastScreen = screen; + } + + protected void init() { + this.addTitle(); + this.addContents(); + this.addFooter(); + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + protected void addTitle() { + this.layout.addTitleHeader(this.title, this.font); + } + + protected void addContents() { + if (this.list == null) { + this.list = new EntryList(this.width); + } else { + this.list.clear(); + } + this.layout.addToContents(this.list); + this.addEntries(); + } + + protected void addFooter() { + this.layout.addToFooter(Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()).width(200).build()); + } + + protected void repositionElements() { + this.layout.arrangeElements(); + if (this.list != null) { + this.list.updateSize(this.width, this.layout); + } + } + + protected abstract void onExit(); + + public void onClose() { + this.onExit(); + this.minecraft.setScreen(this.lastScreen); + } + + protected abstract void addEntries(); + + protected final class EntryList extends ContainerObjectSelectionList { + public EntryList(int i) { + super(EntryListScreen.this.minecraft, i, EntryListScreen.this.layout.getContentHeight(), EntryListScreen.this.layout.getHeaderHeight(), 25); + } + + @Override + public int getRowWidth() { + return 310; + } + + public void addPair(LayoutElement left, LayoutElement right) { + var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH*2+EntryListScreen.Entry.SPACING, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + var leftLayout = new FrameLayout(Button.DEFAULT_WIDTH, 0); + leftLayout.addChild(left, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + layout.addChild(leftLayout, LayoutSettings.defaults().alignVerticallyMiddle()); + var rightLayout = new FrameLayout(Button.DEFAULT_WIDTH, 0); + rightLayout.addChild(right, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + layout.addChild(rightLayout, LayoutSettings.defaults().alignVerticallyMiddle()); + this.addEntry(new EntryListScreen.Entry(layout, EntryListScreen.this)); + } + + public void addSingle(LayoutElement layoutElement) { + var layout = new FrameLayout(Button.DEFAULT_WIDTH*2+EntryListScreen.Entry.SPACING, 0); + layout.addChild(layoutElement, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + this.addEntry(new EntryListScreen.Entry(layout, EntryListScreen.this)); + } + + @Override + public void updateSize(int i, HeaderAndFooterLayout headerAndFooterLayout) { + super.updateSize(i, headerAndFooterLayout); + for (var entry : this.children()) { + entry.layout.arrangeElements(); + } + } + + public void clear() { + super.clearEntries(); + } + } + + protected static final class Entry extends ContainerObjectSelectionList.Entry { + private final Layout layout; + private final List narratables; + private final List listeners; + private final Screen screen; + + public static final int SPACING = 10; + + private Entry(Layout layout, Screen screen) { + this.layout = layout; + List childWidgets = new ArrayList<>(); + layout.visitWidgets(childWidgets::add); + this.narratables = childWidgets; + this.listeners = childWidgets; + this.screen = screen; + } + + @Override + public List narratables() { + return this.narratables; + } + + @Override + public void render(GuiGraphics guiGraphics, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { + int q = this.screen.width / 2 - (Button.DEFAULT_WIDTH + SPACING / 2); + + layout.setPosition(q, j); + layout.visitWidgets((widget) -> widget.render(guiGraphics, n, o, f)); + } + + @Override + public List children() { + return this.listeners; + } + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java new file mode 100644 index 0000000..26545b3 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java @@ -0,0 +1,11 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.screens.Screen; + +public interface LayoutFactory { + LayoutElement create(Screen parent, int width, DynamicOps ops, JsonElement original, Consumer update, EntryCreationInfo creationInfo, boolean handleOptional); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java new file mode 100644 index 0000000..bedcd68 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java @@ -0,0 +1,104 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DynamicOps; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +public class ListConfigScreen extends EntryListScreen { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final ConfigScreenEntry entry; + private final JsonArray jsonValue; + private final Consumer update; + private final DynamicOps ops; + + @Override + protected void onExit() { + this.update.accept(jsonValue); + } + + @Override + protected void addEntries() { + var fullWidth = Button.DEFAULT_WIDTH*2+Entry.SPACING; + for (int i = 0; i < jsonValue.size(); i++) { + var index = i; + var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + // 5 gives us good spacing here + var remainingWidth = fullWidth - (Button.DEFAULT_HEIGHT + 5)*3; + layout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.remove"), b -> { + jsonValue.remove(index); + ListConfigScreen.this.rebuildWidgets(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.remove"))).build(), LayoutSettings.defaults().alignVerticallyMiddle()); + var upButton = Button.builder(Component.translatable("codecextras.config.list.icon.up"), b -> { + if (index == 0) { + return; + } + var oldAbove = jsonValue.get(index - 1); + var old = jsonValue.get(index); + jsonValue.set(index - 1, old); + jsonValue.set(index, oldAbove); + ListConfigScreen.this.rebuildWidgets(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.up"))).build(); + if (index == 0) { + upButton.active = false; + } + layout.addChild(upButton, LayoutSettings.defaults().alignVerticallyMiddle()); + var downButton = Button.builder(Component.translatable("codecextras.config.list.icon.down"), b -> { + if (index == jsonValue.size()-1) { + return; + } + var oldBelow = jsonValue.get(index + 1); + var old = jsonValue.get(index); + jsonValue.set(index + 1, old); + jsonValue.set(index, oldBelow); + ListConfigScreen.this.rebuildWidgets(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.down"))).build(); + if (index == jsonValue.size()-1) { + downButton.active = false; + } + layout.addChild(downButton, LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(entry.widget().create( + this, + remainingWidth, + ops, + jsonValue.get(index), + newValue -> this.jsonValue.set(index, newValue), entry.entryCreationInfo(), + false + ), LayoutSettings.defaults().alignVerticallyMiddle()); + this.list.addSingle(layout); + } + var addLayout = new FrameLayout(fullWidth, 0); + addLayout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.add"), b -> { + jsonValue.add(JsonNull.INSTANCE); + ListConfigScreen.this.rebuildWidgets(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.add"))).build(), LayoutSettings.defaults().alignHorizontallyLeft().alignVerticallyMiddle()); + this.list.addSingle(addLayout); + } + + public ListConfigScreen(Screen screen, EntryCreationInfo> creationInfo, ConfigScreenEntry entry, DynamicOps ops, JsonElement jsonValue, Consumer update) { + super(screen, creationInfo.componentInfo().title()); + this.entry = entry; + if (jsonValue.isJsonArray()) { + this.jsonValue = jsonValue.getAsJsonArray(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON array", jsonValue); + } + this.jsonValue = new JsonArray(); + } + this.update = update; + this.ops = ops; + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java index e6b0e7a..de50918 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java @@ -7,26 +7,23 @@ import com.mojang.serialization.DynamicOps; import java.util.List; import java.util.function.Consumer; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.options.OptionsSubScreen; import org.slf4j.Logger; -class RecordConfigScreen extends OptionsSubScreen { +class RecordConfigScreen extends EntryListScreen { private static final Logger LOGGER = LogUtils.getLogger(); private final List> entries; private final JsonObject jsonValue; private final Consumer update; private final DynamicOps ops; - private final EntryCreationInfo creationInfo; public RecordConfigScreen(Screen screen, EntryCreationInfo creationInfo, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { - super(screen, Minecraft.getInstance().options, creationInfo.componentInfo().title()); - this.creationInfo = creationInfo; + super(screen, creationInfo.componentInfo().title()); this.entries = entries; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); @@ -41,41 +38,57 @@ public RecordConfigScreen(Screen screen, EntryCreationInfo creationInfo, List } @Override - public void onClose() { + protected void onExit() { this.update.accept(jsonValue); - super.onClose(); } @Override - protected void addOptions() { + protected void addEntries() { for (var entry: this.entries) { JsonElement specificValue = this.jsonValue.has(entry.key()) ? this.jsonValue.get(entry.key()) : JsonNull.INSTANCE; - this.list.addSmall( - new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), font).alignLeft(), - createEntryWidget(entry, specificValue) - ); + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), font).alignLeft(); + var contents = createEntryWidget(entry, specificValue); + entry.entry().entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + label.setTooltip(tooltip); + }); + this.list.addPair(label, contents); } } - private AbstractWidget createEntryWidget(RecordEntry entry, JsonElement specificValue) { - return entry.entry().widget().create(this, Button.DEFAULT_WIDTH, ops, specificValue, newValue -> { - if (shouldUpdate(newValue, specificValue, entry)) { + private LayoutElement createEntryWidget(RecordEntry entry, JsonElement specificValue) { + // If this is missing, missing values are just not allowed + var defaultValue = entry.missingBehavior().map(behavior -> { + var value = behavior.missing().get(); + var encoded = entry.codec().encodeStart(ops, value); + if (encoded.error().isPresent()) { + // The default value is unencodeable, so we have to handle missing values in the widget + return JsonNull.INSTANCE; + } + return encoded.result().orElseThrow(); + }); + JsonElement specificValueWithDefault = specificValue.isJsonNull() && defaultValue.isPresent() ? defaultValue.get() : specificValue; + return entry.entry().widget().create(this, Button.DEFAULT_WIDTH, ops, specificValueWithDefault, newValue -> { + if (shouldUpdate(newValue, entry)) { this.jsonValue.add(entry.key(), newValue); } else { this.jsonValue.remove(entry.key()); } - }, entry.entry().entryCreationInfo()); + }, entry.entry().entryCreationInfo(), defaultValue.isPresent() && defaultValue.get().isJsonNull()); } - boolean shouldUpdate(JsonElement newValue, JsonElement oldValue, RecordEntry entry) { - if (newValue.isJsonNull() || newValue.equals(oldValue)) { + boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { + if (newValue.isJsonNull()) { return false; } - var decoded = entry.codec().parse(this.ops, newValue); - if (decoded.isError()) { - LOGGER.warn("Could not encode new value {}", newValue); - return false; + if (entry.missingBehavior().isPresent()) { + var decoded = entry.codec().parse(this.ops, newValue); + if (decoded.isError()) { + LOGGER.warn("Could not encode new value {}", newValue); + return false; + } + return entry.missingBehavior().get().predicate().test(decoded.result().orElseThrow()); } - return entry.shouldEncode().test(decoded.result().orElseThrow()); + return true; } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java index 4c1b957..1b7684c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordEntry.java @@ -1,6 +1,7 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.mojang.serialization.Codec; -import java.util.function.Predicate; +import dev.lukebemish.codecextras.structured.RecordStructure; +import java.util.Optional; -record RecordEntry(String key, ConfigScreenEntry entry, Predicate shouldEncode, Codec codec) {} +record RecordEntry(String key, ConfigScreenEntry entry, Optional> missingBehavior, Codec codec) {} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java deleted file mode 100644 index cffd29f..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VerifyingEditBox.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.mojang.logging.LogUtils; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; -import org.slf4j.Logger; - -public class VerifyingEditBox extends EditBox { - private static final Logger LOGGER = LogUtils.getLogger(); - - private VerifyingEditBox(int width, EntryCreationInfo creationInfo, DynamicOps ops, Consumer update, JsonElement original, Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { - super(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().title()); - - if (!original.isJsonPrimitive()) { - if (!original.isJsonNull()) { - LOGGER.warn("`{}` is not primitive, as required for an edit box", original); - } - } else { - var decoded = creationInfo.codec().parse(ops, original); - if (decoded.isError()) { - LOGGER.warn("Failed to decode `{}`: {}", original, decoded.error().orElseThrow().message()); - } else { - var decodedValue = decoded.getOrThrow(); - var stringResult = fromData.apply(decodedValue); - if (stringResult.error().isPresent()) { - LOGGER.warn("Failed to encode `{}` as string: {}", decodedValue, stringResult.error().get().message()); - } else { - this.setValue(stringResult.getOrThrow()); - } - } - } - - this.setFilter(filter); - - this.setResponder(string -> { - if (emptyIsMissing && string.isEmpty()) { - update.accept(JsonNull.INSTANCE); - return; - } - var dataResult = toData.apply(string); - if (dataResult.error().isPresent()) { - LOGGER.warn("Failed to encode `{}` as data: {}", string, dataResult.error().get().message()); - } else { - var jsonResult = creationInfo.codec().encode(dataResult.getOrThrow(), ops, original); - if (jsonResult.error().isPresent()) { - LOGGER.warn("Failed to encode `{}` as json: {}", dataResult.getOrThrow(), jsonResult.error().get().message()); - } else { - update.accept(jsonResult.getOrThrow()); - } - } - }); - } - - public static WidgetFactory of(Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { - return (parent, width, ops, original, update, creationInfo) -> new VerifyingEditBox<>(width, creationInfo, ops, update, original, toData, fromData, filter, emptyIsMissing); - } -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java deleted file mode 100644 index d1c8d31..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/WidgetFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; -import java.util.function.Consumer; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.screens.Screen; - -public interface WidgetFactory { - AbstractWidget create(Screen parent, int width, DynamicOps ops, JsonElement original, Consumer update, EntryCreationInfo entry); -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java new file mode 100644 index 0000000..4d962b9 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -0,0 +1,182 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonNull; +import com.google.gson.JsonPrimitive; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DataResult; +import java.util.function.Function; +import java.util.function.Predicate; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +public final class Widgets { + private static final Logger LOGGER = LogUtils.getLogger(); + + private Widgets() {} + + public static LayoutFactory text(Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { + return (parent, width, ops, original, update, creationInfo, handleOptional) -> { + var widget = new EditBox(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().title()); + creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + widget.setTooltip(tooltip); + }); + + widget.setFilter(filter); + + if (!handleOptional && original.isJsonNull()) { + original = new JsonPrimitive(""); + update.accept(original); + } + + var decoded = creationInfo.codec().parse(ops, original); + if (decoded.isError()) { + LOGGER.warn("Failed to decode `{}`: {}", original, decoded.error().orElseThrow().message()); + } else { + var decodedValue = decoded.getOrThrow(); + var stringResult = fromData.apply(decodedValue); + if (stringResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as string: {}", decodedValue, stringResult.error().get().message()); + } else { + widget.setValue(stringResult.getOrThrow()); + } + } + + widget.setResponder(string -> { + if (emptyIsMissing && string.isEmpty()) { + update.accept(JsonNull.INSTANCE); + return; + } + var dataResult = toData.apply(string); + if (dataResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as data: {}", string, dataResult.error().get().message()); + } else { + var jsonResult = creationInfo.codec().encodeStart(ops, dataResult.getOrThrow()); + if (jsonResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}` as json: {}", dataResult.getOrThrow(), jsonResult.error().get().message()); + } else { + update.accept(jsonResult.getOrThrow()); + } + } + }); + + return widget; + }; + } + + public static LayoutFactory text(Function> toData, Function> fromData, boolean emptyIsMissing) { + return text(toData, fromData, s -> true, emptyIsMissing); + } + + public static LayoutFactory canHandleOptional(LayoutFactory assumesNonOptional) { + return (parent, fullWidth, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional) { + return assumesNonOptional.create(parent, fullWidth, ops, original, update, creationInfo, false); + } + var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - Button.DEFAULT_SPACING; + var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + var object = new Object() { + private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, ops, original, update, creationInfo, false); + private final Button disabled = Button.builder(Component.translatable("codecextras.config.missing"), b -> {}) + .width(remainingWidth) + .build(); + boolean missing = original.isJsonNull(); + private final Checkbox lock = Checkbox.builder(Component.empty(), Minecraft.getInstance().font) + .maxWidth(Button.DEFAULT_HEIGHT) + .onValueChange((checkbox, b) -> { + missing = !b; + if (missing) { + update.accept(JsonNull.INSTANCE); + wrapped.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + var maxHeight = Math.max(disabled.getHeight(), wrapped.getHeight()); + disabled.setHeight(maxHeight); + disabled.visible = true; + } else { + wrapped.visitWidgets(w -> { + w.visible = true; + w.active = true; + }); + var maxHeight = Math.max(disabled.getHeight(), wrapped.getHeight()); + disabled.setHeight(maxHeight); + disabled.visible = false; + } + }) + .selected(!missing) + .build(); + + { + var maxHeight = Math.max(disabled.getHeight(), wrapped.getHeight()); + disabled.setHeight(maxHeight); + disabled.active = false; + disabled.visible = missing; + wrapped.visitWidgets(w -> { + w.visible = !missing; + w.active = !missing; + }); + + creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + lock.setTooltip(tooltip); + disabled.setTooltip(tooltip); + }); + } + }; + layout.addChild(object.lock, LayoutSettings.defaults().alignVerticallyMiddle()); + var right = new FrameLayout(); + right.addChild(object.disabled, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + right.addChild(object.wrapped, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + layout.addChild(right, LayoutSettings.defaults().alignVerticallyMiddle()); + return layout; + }; + } + + public static LayoutFactory bool(boolean falseIfMissing) { + LayoutFactory widget = (parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonPrimitive(false); + update.accept(original); + } + var w = Checkbox.builder(Component.empty(), Minecraft.getInstance().font) + .maxWidth(width) + .onValueChange((checkbox, b) -> { + if (falseIfMissing && !b) { + update.accept(JsonNull.INSTANCE); + } + update.accept(new JsonPrimitive(b)); + }) + .selected(original.isJsonPrimitive() && original.getAsJsonPrimitive().getAsBoolean()) + .build(); + creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + w.setTooltip(tooltip); + }); + w.setMessage(creationInfo.componentInfo().title()); + return w; + }; + if (!falseIfMissing) { + return canHandleOptional(widget); + } + return (parent, width, ops, original, update, entry, handleOptional) -> { + if (handleOptional) { + return widget.create(parent, width, ops, original, update, entry, true); + } + var button = Button.builder(Component.translatable("codecextras.config.unit"), b -> {}) + .width(width) + .build(); + button.active = false; + return button; + }; + } +} diff --git a/src/minecraft/resources/META-INF/MANIFEST.MF b/src/minecraft/resources/META-INF/MANIFEST.MF deleted file mode 100644 index b325e5e..0000000 --- a/src/minecraft/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1 +0,0 @@ -FMLModType: GAMELIBRARY diff --git a/src/minecraft/resources/META-INF/neoforge.mods.toml b/src/minecraft/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..5d6457e --- /dev/null +++ b/src/minecraft/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,8 @@ +modLoader="lowcodefml" +loaderVersion="[1,)" +license="LGPL-3.0-only" +[[mods]] +modId="codecextras_minecraft" +version="${version}" +displayName="CodecExtras - Minecraft Adapters" +description="" diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json new file mode 100644 index 0000000..014923f --- /dev/null +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -0,0 +1,14 @@ +{ + "codecextras.config.configurerecord": "Configure...", + "codecextras.config.configurelist": "Configure...", + "codecextras.config.missing": "Missing", + "codecextras.config.unit": "Not configurable", + "codecextras.config.list.icon.add": "+", + "codecextras.config.list.icon.up": "\u23f6", + "codecextras.config.list.icon.down": "\u23f7", + "codecextras.config.list.icon.remove": "\u274c", + "codecextras.config.list.add": "Add entry", + "codecextras.config.list.up": "Move up", + "codecextras.config.list.down": "Move down", + "codecextras.config.list.remove": "Remove entry" +} diff --git a/src/minecraft/resources/fabric.mod.json b/src/minecraft/resources/fabric.mod.json new file mode 100644 index 0000000..6fe53aa --- /dev/null +++ b/src/minecraft/resources/fabric.mod.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "id": "codecextras_minecraft", + "version": "${version}", + "name": "CodecExtras - Minecraft Adapters" +} diff --git a/src/minecraftIntermediary/resources/fabric.mod.json b/src/minecraftIntermediary/resources/fabric.mod.json deleted file mode 100644 index 713d387..0000000 --- a/src/minecraftIntermediary/resources/fabric.mod.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaVersion": 1, - "id": "dev_lukebemish_codecextras-minecraft", - "version": "${version}", - "name": "CodecExtras - Minecraft-specific adapters" -} diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index f9f8541..85412bd 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -1,33 +1,63 @@ package dev.lukebemish.codecextras.test.neoforge; -import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.config.ConfigType; +import dev.lukebemish.codecextras.config.GsonOpsIo; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; +import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.Structure; +import java.util.List; +import java.util.Optional; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; @Mod("codecextras_testmod") public class CodecExtrasTest { - private record TestRecord(int a, int b, int c) { + private record TestRecord(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings) { private static final Structure STRUCTURE = Structure.record(builder -> { - var a = builder.add("a", Structure.INT, TestRecord::a); - var b = builder.add("b", Structure.INT, TestRecord::b); - var c = builder.add("c", Structure.INT, TestRecord::c); - return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container)); + var a = builder.add("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestRecord::a); + var b = builder.add("b", Structure.FLOAT, TestRecord::b); + var c = builder.add("c", Structure.BOOL, TestRecord::c); + var d = builder.add("d", Structure.STRING, TestRecord::d); + var e = builder.addOptional("e", Structure.BOOL, TestRecord::e); + var f = builder.addOptional("f", Structure.STRING, TestRecord::f); + var g = builder.add("g", Structure.UNIT, TestRecord::g); + var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestRecord::strings, () -> List.of("test1", "test2")); + return container -> new TestRecord( + a.apply(container), b.apply(container), c.apply(container), + d.apply(container), e.apply(container), f.apply(container), + g.apply(container), strings.apply(container) + ); }); + + private static final Codec CODEC = MinecraftStructures.CODEC_INTERPRETER.interpret(STRUCTURE).getOrThrow(); } + private static final ConfigType.ConfigHandle CONFIG = new ConfigType() { + @Override + public Codec codec() { + return TestRecord.CODEC; + } + + @Override + public TestRecord defaultConfig() { + return new TestRecord(1, 2.0f, true, "test", Optional.empty(), Optional.empty(), Unit.INSTANCE, List.of("test1", "test2")); + } + }.handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); + public CodecExtrasTest(ModContainer modContainer) { ConfigScreenEntry entry = new ConfigScreenInterpreter( MinecraftStructures.CODEC_INTERPRETER ).interpret(TestRecord.STRUCTURE).getOrThrow(); - modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> entry.rootScreen(parent, jsonElement -> { - System.out.println("New JSON: "+jsonElement); - }, JsonOps.INSTANCE, new JsonObject())); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> + entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()) + ); } } diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/package-info.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/package-info.java new file mode 100644 index 0000000..18c61a7 --- /dev/null +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.neoforge; + +import org.jspecify.annotations.NullMarked; diff --git a/src/testNeoforge/resources/META-INF/neoforge.mods.toml b/src/testNeoforge/resources/META-INF/neoforge.mods.toml index 068714f..aa2b813 100644 --- a/src/testNeoforge/resources/META-INF/neoforge.mods.toml +++ b/src/testNeoforge/resources/META-INF/neoforge.mods.toml @@ -1,6 +1,6 @@ modLoader="javafml" loaderVersion="[1,)" -license="LGPL-3.0-or-later" +license="LGPL-3.0-only" [[mods]] modId="codecextras_testmod" diff --git a/testNeoforge/build.gradle b/testNeoforge/build.gradle index 3884508..06aaed3 100644 --- a/testNeoforge/build.gradle +++ b/testNeoforge/build.gradle @@ -1,7 +1,8 @@ configurations { normalRuntimeModClasses runsImplementation.extendsFrom normalRuntimeModClasses - normalModClassesSource + minecraftRuntimeModClasses + runsImplementation.extendsFrom minecraftRuntimeModClasses } loom { @@ -11,23 +12,20 @@ loom { } } + mods.register("codecextras_minecraft") { + configuration configurations.minecraftRuntimeModClasses + } + mods.register("codecextras") { configuration configurations.normalRuntimeModClasses } } -tasks.register('copyNormalClasses', Copy) { - from configurations.normalModClassesSource - dependsOn configurations.normalModClassesSource - into layout.buildDirectory.dir('minecraftCodecExtras') -} - dependencies { - forgeRuntimeLibrary project(path: ':', configuration: 'runtimeElements') - normalModClassesSource(project(path: ':', configuration: 'minecraftRuntimeModClasses')) { + normalRuntimeModClasses(project(path: ':', configuration: 'runtimeModClasses')) { + transitive = false + } + minecraftRuntimeModClasses(project(path: ':', configuration: 'minecraftRuntimeModClasses')) { transitive = false } - normalRuntimeModClasses(files(layout.buildDirectory.dir('minecraftCodecExtras')).tap { - builtBy tasks.copyNormalClasses - }) } From 9e2ae319cc51513559b1ca5504fcd5630530ba3a Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 01:53:52 -0500 Subject: [PATCH 40/76] Dispatch in config screens --- .../structured/config/ConfigScreenEntry.java | 27 +--- .../config/ConfigScreenInterpreter.java | 70 +++++++-- .../structured/config/DispatchPickScreen.java | 120 +++++++++++++++ .../config/DispatchScreenEntryProvider.java | 141 ++++++++++++++++++ .../structured/config/EntryListScreen.java | 28 ++-- ...reen.java => ListScreenEntryProvider.java} | 52 ++++--- ...en.java => RecordScreenEntryProvider.java} | 22 +-- .../structured/config/ScreenEntryFactory.java | 9 ++ .../structured/config/ScreenEntryList.java | 8 + .../config/ScreenEntryProvider.java | 12 ++ .../structured/config/ScreenFactory.java | 10 -- .../config/SingleScreenEntryProvider.java | 34 +++++ .../test/neoforge/CodecExtrasTest.java | 55 ++++++- 13 files changed, 493 insertions(+), 95 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java rename src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/{ListConfigScreen.java => ListScreenEntryProvider.java} (84%) rename src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/{RecordConfigScreen.java => RecordScreenEntryProvider.java} (82%) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryList.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 7ad4d77..8f943d3 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -12,34 +12,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public record ConfigScreenEntry(LayoutFactory widget, ScreenFactory screen, EntryCreationInfo entryCreationInfo) implements App { +public record ConfigScreenEntry(LayoutFactory widget, ScreenEntryFactory screenEntryProvider, EntryCreationInfo entryCreationInfo) implements App { public static final class Mu implements K1 { private Mu() {} } - private static final int FULL_WIDTH = 310; - public static ConfigScreenEntry unbox(App app) { return (ConfigScreenEntry) app; } public static ConfigScreenEntry single(LayoutFactory first, EntryCreationInfo entryCreationInfo) { - return new ConfigScreenEntry<>(first, (parent, ops, original, onClose, creationInfo) -> new EntryListScreen(parent, creationInfo.componentInfo().title()) { - private JsonElement value = original; - - @Override - protected void addEntries() { - this.list.addSingle(first.create(this, FULL_WIDTH, ops, value, newValue -> value = newValue, creationInfo, false)); - } - - @Override - public void onExit() { - onClose.accept(value); - } - }, entryCreationInfo); + return new ConfigScreenEntry<>(first, (ops, original, onClose, creationInfo) -> new SingleScreenEntryProvider<>(original, first, ops, creationInfo, onClose), entryCreationInfo); } public ConfigScreenEntry withComponentInfo(UnaryOperator function) { - return new ConfigScreenEntry<>(this.widget, this.screen, this.entryCreationInfo.withComponentInfo(function)); + return new ConfigScreenEntry<>(this.widget, this.screenEntryProvider, this.entryCreationInfo.withComponentInfo(function)); } public ConfigScreenEntry withEntryCreationInfo(Function, EntryCreationInfo> function, Function, EntryCreationInfo> reverse) { @@ -48,9 +34,9 @@ public ConfigScreenEntry withEntryCreationInfo(Function { + (ops, original, onClose, entry) -> { var entryCreationInfo = reverse.apply(entry); - return this.screen.open(parent, ops, original, onClose, entryCreationInfo); + return this.screenEntryProvider.open(ops, original, onClose, entryCreationInfo); }, function.apply(this.entryCreationInfo) ); @@ -69,7 +55,7 @@ public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps { + var provider = screenEntryProvider().open(ops, initialJson, json -> { var decoded = entryCreationInfo.codec().parse(ops, json); if (decoded.error().isPresent()) { logger.warn("Failed to decode `{}`: {}", json, decoded.error().get().message()); @@ -77,5 +63,6 @@ public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps DataResult>> list(App> factory = (parent, ops, original, onClose, creationInfo) -> - new ListConfigScreen<>(parent, creationInfo, unwrapped, ops, original, onClose); + ScreenEntryFactory> factory = (ops, original, onClose, creationInfo) -> + new ListScreenEntryProvider<>(unwrapped, ops, original, onClose); return DataResult.success(new ConfigScreenEntry<>( Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { @@ -150,9 +153,9 @@ public DataResult>> list(App Minecraft.getInstance().setScreen(factory.open( - parent, ops, finalOriginal, update, creationInfo - )) + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( + ops, finalOriginal, update, creationInfo + ), parent, creationInfo)) ).width(width).build(); }), factory, @@ -170,8 +173,8 @@ public DataResult> record(List "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); } - ScreenFactory factory = (parent, ops, original, onClose, creationInfo) -> - new RecordConfigScreen<>(parent, creationInfo, entries, ops, original, onClose); + ScreenEntryFactory factory = (ops, original, onClose, creationInfo) -> + new RecordScreenEntryProvider(entries, ops, original, onClose); var codecResult = codecInterpreter.record(fields, creator); if (codecResult.isError()) { return DataResult.error(() -> "Error creating record codec: "+codecResult.error().orElseThrow().messageSupplier()); @@ -185,9 +188,9 @@ public DataResult> record(List Minecraft.getInstance().setScreen(factory.open( - parent, ops, finalOriginal, update, creationInfo - )) + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( + ops, finalOriginal, update, creationInfo + ), parent, creationInfo)) ).width(width).build(); }), factory, @@ -231,8 +234,13 @@ public DataResult> flatXmap(App DataResult> annotate(Structure original, Keys annotations) { var result = original.interpret(this); + var codecResult = codecInterpreter.annotate(original, annotations).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(codecResult.error().orElseThrow().messageSupplier()); + } return result.map(app -> { - var entry = ConfigScreenEntry.unbox(app); + var originalCodec = ConfigScreenEntry.unbox(app).entryCreationInfo().codec(); + var entry = ConfigScreenEntry.unbox(app).withEntryCreationInfo(info -> info.withCodec(codecResult.getOrThrow()), info -> info.withCodec(originalCodec)); var withTitle = Annotation .get(annotations, ConfigAnnotations.TITLE) .or(() -> Annotation.get(annotations, Annotation.TITLE).map(Component::literal)) @@ -249,7 +257,45 @@ public DataResult> annotate(Structure origin @Override public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { - return DataResult.error(() -> "Not yet implemented"); + var keyResult = interpret(keyStructure).map(entry -> entry.withComponentInfo(info -> info.fallbackTitle(Component.literal(key)))); + if (keyResult.error().isPresent()) { + return DataResult.error(keyResult.error().get().messageSupplier()); + } + Supplier>>> entries = Suppliers.memoize(() -> { + Map>> map = new HashMap<>(); + for (var entryKey : keys) { + var result = structures.apply(entryKey).interpret(this); + if (result.error().isPresent()) { + map.put(entryKey, DataResult.error(result.error().get().messageSupplier())); + } else { + map.put(entryKey, DataResult.success(ConfigScreenEntry.unbox(result.getOrThrow()))); + } + } + return map; + }); + var codecResult = codecInterpreter.dispatch(key, keyStructure, function, keys, structures); + if (codecResult.isError()) { + return DataResult.error(() -> "Error creating dispatch codec: "+codecResult.error().orElseThrow().messageSupplier()); + } + ScreenEntryFactory factory = (ops, original, onClose, creationInfo) -> + new DispatchScreenEntryProvider<>(keyResult.getOrThrow().entryCreationInfo(), original, key, onClose, ops, entries.get()); + return DataResult.success(new ConfigScreenEntry<>( + Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonObject(); + update.accept(original); + } + JsonElement finalOriginal = original; + return Button.builder( + Component.translatable("codecextras.config.configurerecord"), + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( + ops, finalOriginal, update, creationInfo + ), parent, creationInfo)) + ).width(width).build(); + }), + factory, + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + )); } public DataResult> interpret(Structure structure) { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java new file mode 100644 index 0000000..9e63fc6 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java @@ -0,0 +1,120 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.ibm.icu.text.Collator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Consumer; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +class DispatchPickScreen extends Screen { + private final Screen lastScreen; + private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + private final Consumer<@Nullable String> onClose; + private @Nullable EntryList list; + private final List keys; + private @Nullable String selectedKey; + private final Button doneButton = Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()).width(200).build(); + + public DispatchPickScreen(Screen screen, Component title, List keys, @Nullable String selectedKey, Consumer<@Nullable String> onClose) { + super(title); + this.lastScreen = screen; + this.keys = keys; + this.selectedKey = selectedKey; + this.onClose = onClose; + } + + protected void init() { + this.addTitle(); + this.addContents(); + this.addFooter(); + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + this.lastScreen.resize(this.minecraft, this.width, this.height); + this.layout.arrangeElements(); + if (this.list != null) { + this.list.updateSize(this.width, this.layout); + } + } + + public void added() { + super.added(); + } + + protected void addTitle() { + this.layout.addTitleHeader(this.title, this.font); + } + + protected void addContents() { + this.list = new EntryList(); + this.list.setSelected(this.list.children().stream().filter((entry) -> Objects.equals(entry.key, this.selectedKey)).findFirst().orElse(null)); + this.layout.addToContents(this.list); + this.updateButtonValidity(); + } + + protected void addFooter() { + this.layout.addToFooter(this.doneButton); + } + + public void onClose() { + this.onClose.accept(this.selectedKey); + this.minecraft.setScreen(this.lastScreen); + } + + private final class EntryList extends ObjectSelectionList { + private EntryList() { + super(DispatchPickScreen.this.minecraft, DispatchPickScreen.this.width, DispatchPickScreen.this.layout.getContentHeight(), DispatchPickScreen.this.layout.getHeaderHeight(), 16); + Collator collator = Collator.getInstance(Locale.getDefault()); + DispatchPickScreen.this.keys.forEach(key -> { + this.addEntry(new Entry(key)); + }); + } + + public void setSelected(EntryList.@Nullable Entry entry) { + super.setSelected(entry); + if (entry != null) { + DispatchPickScreen.this.selectedKey = entry.key; + } + + DispatchPickScreen.this.updateButtonValidity(); + } + + private class Entry extends ObjectSelectionList.Entry { + final String key; + final Component name; + + public Entry(final String key) { + this.key = key; + this.name = Component.literal(key); + } + + public Component getNarration() { + return Component.translatable("narrator.select", this.name); + } + + public void render(GuiGraphics guiGraphics, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { + guiGraphics.drawString(DispatchPickScreen.this.font, this.name, k + 5, j + 2, 0xFFFFFF); + } + + public boolean mouseClicked(double d, double e, int i) { + DispatchPickScreen.EntryList.this.setSelected(this); + return super.mouseClicked(d, e, i); + } + } + } + + private void updateButtonValidity() { + this.doneButton.active = this.list.getSelected() != null; + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java new file mode 100644 index 0000000..f3d1aa4 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java @@ -0,0 +1,141 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +class DispatchScreenEntryProvider implements ScreenEntryProvider { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final EntryCreationInfo keyInfo; + private final String key; + private @Nullable String keyValue; + private JsonObject jsonValue; + private final Consumer update; + private final List keys; + private final Map keyValues; + private final Map> keyProviders; + private final DynamicOps ops; + + public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jsonValue, String key, Consumer update, DynamicOps ops, Map>> entries) { + this.keyInfo = keyInfo; + this.key = key; + if (jsonValue.isJsonObject()) { + this.jsonValue = jsonValue.getAsJsonObject(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON object", jsonValue); + } + this.jsonValue = new JsonObject(); + } + this.update = update; + this.ops = ops; + this.keys = new ArrayList<>(); + this.keyValues = new HashMap<>(); + this.keyProviders = new HashMap<>(); + for (var entry : entries.entrySet()) { + var keyResult = keyInfo.codec().encodeStart(ops, entry.getKey()); + if (keyResult.isError()) { + LOGGER.warn("Failed to encode key {}", entry.getKey()); + continue; + } + JsonElement keyElement = keyResult.getOrThrow(); + String keyAsString = stringify(keyElement); + keyValues.put(keyAsString, keyElement); + if (entry.getValue().isError()) { + LOGGER.warn("Failed to create screen entry for key {}: {}", entry.getKey(), entry.getValue().error().orElseThrow().message()); + } + keyProviders.put(keyAsString, entry.getValue().getOrThrow()); + this.keys.add(keyAsString); + } + this.keys.sort(Comparator.naturalOrder()); + if (this.jsonValue.has(key)) { + this.keyValue = stringify(this.jsonValue.get(key)); + } + } + + private String stringify(JsonElement keyElement) { + String keyAsString; + if (keyElement.isJsonPrimitive()) { + keyAsString = keyElement.getAsString(); + } else { + keyAsString = keyElement.toString(); + } + return keyAsString; + } + + @Override + public void onExit() { + if (nestedOnExit != null) { + nestedOnExit.run(); + } + update.accept(jsonValue); + } + + private @Nullable Runnable nestedOnExit; + + @Override + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyInfo.componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var contents = Button.builder(Component.literal(keyValue == null ? "" : keyValue), b -> { + Minecraft.getInstance().setScreen(new DispatchPickScreen(parent, keyInfo.componentInfo().title(), keys, keyValue, newKeyValue -> { + if (!Objects.equals(newKeyValue, keyValue)) { + keyValue = newKeyValue; + jsonValue = new JsonObject(); + if (keyValue != null) { + jsonValue.add(key, keyValues.get(keyValue)); + } else { + jsonValue.remove(key); + } + rebuild.run(); + } + })); + }).build(); + keyInfo.componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + label.setTooltip(tooltip); + contents.setTooltip(tooltip); + }); + list.addPair(label, contents); + if (keyValue != null) { + var provider = keyProviders.get(keyValue); + JsonObject valueCopy = new JsonObject(); + valueCopy.asMap().putAll(jsonValue.asMap()); + addEntry(provider, valueCopy, list, rebuild, parent); + } + } + + private void addEntry(ConfigScreenEntry provider, JsonObject valueCopy, ScreenEntryList list, Runnable rebuild, Screen parent) { + var entryProvider = provider.screenEntryProvider().open(ops, valueCopy, newValue -> { + if (newValue.isJsonObject()) { + for (var entry : newValue.getAsJsonObject().entrySet()) { + if (entry.getKey().equals(key)) { + continue; + } + jsonValue.add(entry.getKey(), entry.getValue()); + } + } else { + LOGGER.warn("Value {} was not a JSON object", newValue); + } + }, provider.entryCreationInfo()); + this.nestedOnExit = entryProvider::onExit; + entryProvider.addEntries(list, rebuild, parent); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java index 332f347..3e714bc 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java @@ -19,14 +19,16 @@ import net.minecraft.network.chat.Component; import org.jspecify.annotations.Nullable; -abstract class EntryListScreen extends Screen { - protected final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); - protected final Screen lastScreen; - protected @Nullable EntryList list; +class EntryListScreen extends Screen { + private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + private final Screen lastScreen; + private @Nullable EntryList list; + private final ScreenEntryProvider screenEntries; - public EntryListScreen(Screen screen, Component title) { + public EntryListScreen(Screen screen, Component title, ScreenEntryProvider screenEntries) { super(title); this.lastScreen = screen; + this.screenEntries = screenEntries; } protected void init() { @@ -62,16 +64,16 @@ protected void repositionElements() { } } - protected abstract void onExit(); - public void onClose() { - this.onExit(); + this.screenEntries.onExit(); this.minecraft.setScreen(this.lastScreen); } - protected abstract void addEntries(); + protected void addEntries() { + this.screenEntries.addEntries(this.list, this::rebuildWidgets, this); + } - protected final class EntryList extends ContainerObjectSelectionList { + final class EntryList extends ContainerObjectSelectionList implements ScreenEntryList { public EntryList(int i) { super(EntryListScreen.this.minecraft, i, EntryListScreen.this.layout.getContentHeight(), EntryListScreen.this.layout.getHeaderHeight(), 25); } @@ -81,6 +83,7 @@ public int getRowWidth() { return 310; } + @Override public void addPair(LayoutElement left, LayoutElement right) { var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH*2+EntryListScreen.Entry.SPACING, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var leftLayout = new FrameLayout(Button.DEFAULT_WIDTH, 0); @@ -92,6 +95,7 @@ public void addPair(LayoutElement left, LayoutElement right) { this.addEntry(new EntryListScreen.Entry(layout, EntryListScreen.this)); } + @Override public void addSingle(LayoutElement layoutElement) { var layout = new FrameLayout(Button.DEFAULT_WIDTH*2+EntryListScreen.Entry.SPACING, 0); layout.addChild(layoutElement, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); @@ -106,12 +110,12 @@ public void updateSize(int i, HeaderAndFooterLayout headerAndFooterLayout) { } } - public void clear() { + void clear() { super.clearEntries(); } } - protected static final class Entry extends ContainerObjectSelectionList.Entry { + static final class Entry extends ContainerObjectSelectionList.Entry { private final Layout layout; private final List narratables; private final List listeners; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java similarity index 84% rename from src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java rename to src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java index bedcd68..75a811c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListConfigScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java @@ -5,7 +5,6 @@ import com.google.gson.JsonNull; import com.mojang.logging.LogUtils; import com.mojang.serialization.DynamicOps; -import java.util.List; import java.util.function.Consumer; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; @@ -16,7 +15,7 @@ import net.minecraft.network.chat.Component; import org.slf4j.Logger; -public class ListConfigScreen extends EntryListScreen { +class ListScreenEntryProvider implements ScreenEntryProvider { private static final Logger LOGGER = LogUtils.getLogger(); private final ConfigScreenEntry entry; @@ -24,14 +23,28 @@ public class ListConfigScreen extends EntryListScreen { private final Consumer update; private final DynamicOps ops; + ListScreenEntryProvider(ConfigScreenEntry entry, DynamicOps ops, JsonElement jsonValue, Consumer update) { + this.entry = entry; + if (jsonValue.isJsonArray()) { + this.jsonValue = jsonValue.getAsJsonArray(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON array", jsonValue); + } + this.jsonValue = new JsonArray(); + } + this.update = update; + this.ops = ops; + } + @Override - protected void onExit() { + public void onExit() { this.update.accept(jsonValue); } @Override - protected void addEntries() { - var fullWidth = Button.DEFAULT_WIDTH*2+Entry.SPACING; + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + var fullWidth = Button.DEFAULT_WIDTH*2+ EntryListScreen.Entry.SPACING; for (int i = 0; i < jsonValue.size(); i++) { var index = i; var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); @@ -39,7 +52,7 @@ protected void addEntries() { var remainingWidth = fullWidth - (Button.DEFAULT_HEIGHT + 5)*3; layout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.remove"), b -> { jsonValue.remove(index); - ListConfigScreen.this.rebuildWidgets(); + rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.remove"))).build(), LayoutSettings.defaults().alignVerticallyMiddle()); var upButton = Button.builder(Component.translatable("codecextras.config.list.icon.up"), b -> { if (index == 0) { @@ -49,7 +62,7 @@ protected void addEntries() { var old = jsonValue.get(index); jsonValue.set(index - 1, old); jsonValue.set(index, oldAbove); - ListConfigScreen.this.rebuildWidgets(); + rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.up"))).build(); if (index == 0) { upButton.active = false; @@ -63,42 +76,27 @@ protected void addEntries() { var old = jsonValue.get(index); jsonValue.set(index + 1, old); jsonValue.set(index, oldBelow); - ListConfigScreen.this.rebuildWidgets(); + rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.down"))).build(); if (index == jsonValue.size()-1) { downButton.active = false; } layout.addChild(downButton, LayoutSettings.defaults().alignVerticallyMiddle()); layout.addChild(entry.widget().create( - this, + parent, remainingWidth, ops, jsonValue.get(index), newValue -> this.jsonValue.set(index, newValue), entry.entryCreationInfo(), false ), LayoutSettings.defaults().alignVerticallyMiddle()); - this.list.addSingle(layout); + list.addSingle(layout); } var addLayout = new FrameLayout(fullWidth, 0); addLayout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.add"), b -> { jsonValue.add(JsonNull.INSTANCE); - ListConfigScreen.this.rebuildWidgets(); + rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.add"))).build(), LayoutSettings.defaults().alignHorizontallyLeft().alignVerticallyMiddle()); - this.list.addSingle(addLayout); - } - - public ListConfigScreen(Screen screen, EntryCreationInfo> creationInfo, ConfigScreenEntry entry, DynamicOps ops, JsonElement jsonValue, Consumer update) { - super(screen, creationInfo.componentInfo().title()); - this.entry = entry; - if (jsonValue.isJsonArray()) { - this.jsonValue = jsonValue.getAsJsonArray(); - } else { - if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON array", jsonValue); - } - this.jsonValue = new JsonArray(); - } - this.update = update; - this.ops = ops; + list.addSingle(addLayout); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java similarity index 82% rename from src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java rename to src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java index de50918..579b46c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordConfigScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java @@ -7,6 +7,7 @@ import com.mojang.serialization.DynamicOps; import java.util.List; import java.util.function.Consumer; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.Tooltip; @@ -14,7 +15,7 @@ import net.minecraft.client.gui.screens.Screen; import org.slf4j.Logger; -class RecordConfigScreen extends EntryListScreen { +class RecordScreenEntryProvider implements ScreenEntryProvider { private static final Logger LOGGER = LogUtils.getLogger(); private final List> entries; @@ -22,8 +23,7 @@ class RecordConfigScreen extends EntryListScreen { private final Consumer update; private final DynamicOps ops; - public RecordConfigScreen(Screen screen, EntryCreationInfo creationInfo, List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { - super(screen, creationInfo.componentInfo().title()); + RecordScreenEntryProvider(List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { this.entries = entries; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); @@ -38,25 +38,25 @@ public RecordConfigScreen(Screen screen, EntryCreationInfo creationInfo, List } @Override - protected void onExit() { + public void onExit() { this.update.accept(jsonValue); } @Override - protected void addEntries() { + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { for (var entry: this.entries) { JsonElement specificValue = this.jsonValue.has(entry.key()) ? this.jsonValue.get(entry.key()) : JsonNull.INSTANCE; - var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), font).alignLeft(); - var contents = createEntryWidget(entry, specificValue); + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var contents = createEntryWidget(entry, specificValue, parent); entry.entry().entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); label.setTooltip(tooltip); }); - this.list.addPair(label, contents); + list.addPair(label, contents); } } - private LayoutElement createEntryWidget(RecordEntry entry, JsonElement specificValue) { + private LayoutElement createEntryWidget(RecordEntry entry, JsonElement specificValue, Screen parent) { // If this is missing, missing values are just not allowed var defaultValue = entry.missingBehavior().map(behavior -> { var value = behavior.missing().get(); @@ -68,7 +68,7 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp return encoded.result().orElseThrow(); }); JsonElement specificValueWithDefault = specificValue.isJsonNull() && defaultValue.isPresent() ? defaultValue.get() : specificValue; - return entry.entry().widget().create(this, Button.DEFAULT_WIDTH, ops, specificValueWithDefault, newValue -> { + return entry.entry().widget().create(parent, Button.DEFAULT_WIDTH, ops, specificValueWithDefault, newValue -> { if (shouldUpdate(newValue, entry)) { this.jsonValue.add(entry.key(), newValue); } else { @@ -77,7 +77,7 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp }, entry.entry().entryCreationInfo(), defaultValue.isPresent() && defaultValue.get().isJsonNull()); } - boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { + private boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { if (newValue.isJsonNull()) { return false; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java new file mode 100644 index 0000000..4ab873c --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java @@ -0,0 +1,9 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; + +public interface ScreenEntryFactory { + ScreenEntryProvider open(DynamicOps ops, JsonElement original, Consumer onClose, EntryCreationInfo entry); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryList.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryList.java new file mode 100644 index 0000000..8a62571 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryList.java @@ -0,0 +1,8 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import net.minecraft.client.gui.layouts.LayoutElement; + +public interface ScreenEntryList { + void addPair(LayoutElement left, LayoutElement right); + void addSingle(LayoutElement layoutElement); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java new file mode 100644 index 0000000..db86d24 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import net.minecraft.client.gui.screens.Screen; + +public interface ScreenEntryProvider { + void onExit(); + void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent); + + static Screen create(ScreenEntryProvider provider, Screen parent, EntryCreationInfo info) { + return new EntryListScreen(parent, info.componentInfo().title(), provider); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java deleted file mode 100644 index 025bd9b..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; -import java.util.function.Consumer; -import net.minecraft.client.gui.screens.Screen; - -public interface ScreenFactory { - Screen open(Screen parent, DynamicOps ops, JsonElement original, Consumer onClose, EntryCreationInfo entry); -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java new file mode 100644 index 0000000..3ac1f12 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java @@ -0,0 +1,34 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; + +class SingleScreenEntryProvider implements ScreenEntryProvider { + private static final int FULL_WIDTH = Button.DEFAULT_WIDTH * 2 + EntryListScreen.Entry.SPACING; + private final DynamicOps ops; + private final EntryCreationInfo creationInfo; + private final Consumer update; + private JsonElement value; + private final LayoutFactory first; + + SingleScreenEntryProvider(JsonElement original, LayoutFactory first, DynamicOps ops, EntryCreationInfo creationInfo, Consumer update) { + this.value = original; + this.first = first; + this.ops = ops; + this.creationInfo = creationInfo; + this.update = update; + } + + @Override + public void onExit() { + update.accept(value); + } + + @Override + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + list.addSingle(first.create(parent, FULL_WIDTH, ops, value, newValue -> value = newValue, creationInfo, false)); + } +} diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index 85412bd..d89cce2 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -2,6 +2,7 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; @@ -10,7 +11,10 @@ import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; @@ -19,7 +23,51 @@ @Mod("codecextras_testmod") public class CodecExtrasTest { - private record TestRecord(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings) { + private interface Dispatches { + Map> MAP = new HashMap<>(); + Structure STRUCTURE = Structure.STRING.dispatch( + "type", + d -> DataResult.success(d.key()), + MAP::keySet, + MAP::get + ).annotate(SchemaAnnotations.REUSE_KEY, "dispatches"); + String key(); + } + + private record Abc(int a, String b, float c) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var a = i.addOptional("a", Structure.INT, Abc::a, () -> 123); + var b = i.addOptional("b", Structure.STRING, Abc::b, () ->"gizmo"); + var c = i.addOptional("c", Structure.FLOAT, Abc::c, () -> 1.23f); + return container -> new Abc(a.apply(container), b.apply(container), c.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "abc"); + + @Override + public String key() { + return "abc"; + } + } + + private record Xyz(String x, int y, float z) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var x = i.addOptional("x", Structure.STRING, Xyz::x, () -> "gadget"); + var y = i.addOptional("y", Structure.INT, Xyz::y, () -> 345); + var z = i.addOptional("z", Structure.FLOAT, Xyz::z, () -> 3.45f); + return container -> new Xyz(x.apply(container), y.apply(container), z.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "xyz"); + + @Override + public String key() { + return "xyz"; + } + } + + static { + Dispatches.MAP.put("abc", Abc.STRUCTURE); + Dispatches.MAP.put("xyz", Xyz.STRUCTURE); + } + + private record TestRecord(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches) { private static final Structure STRUCTURE = Structure.record(builder -> { var a = builder.add("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestRecord::a); var b = builder.add("b", Structure.FLOAT, TestRecord::b); @@ -29,10 +77,11 @@ private record TestRecord(int a, float b, boolean c, String d, Optional var f = builder.addOptional("f", Structure.STRING, TestRecord::f); var g = builder.add("g", Structure.UNIT, TestRecord::g); var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestRecord::strings, () -> List.of("test1", "test2")); + var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestRecord::dispatches, () -> new Abc(1, "test", 1.0f)); return container -> new TestRecord( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), - g.apply(container), strings.apply(container) + g.apply(container), strings.apply(container), dispatches.apply(container) ); }); @@ -47,7 +96,7 @@ public Codec codec() { @Override public TestRecord defaultConfig() { - return new TestRecord(1, 2.0f, true, "test", Optional.empty(), Optional.empty(), Unit.INSTANCE, List.of("test1", "test2")); + return new TestRecord(1, 2.0f, true, "test", Optional.empty(), Optional.empty(), Unit.INSTANCE, List.of("test1", "test2"), new Abc(1, "test", 1.0f)); } }.handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); From 78e02a2ad521ed2313058272dcc9f0ed58478a0b Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 01:56:34 -0500 Subject: [PATCH 41/76] Remove unneeded annotation --- .../lukebemish/codecextras/test/neoforge/CodecExtrasTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index d89cce2..d8825a2 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -25,12 +25,12 @@ public class CodecExtrasTest { private interface Dispatches { Map> MAP = new HashMap<>(); - Structure STRUCTURE = Structure.STRING.dispatch( + Structure STRUCTURE = Structure.STRING.dispatch( "type", d -> DataResult.success(d.key()), MAP::keySet, MAP::get - ).annotate(SchemaAnnotations.REUSE_KEY, "dispatches"); + ); String key(); } From 892da0d5f1cf608a526b6ae6f082a4fd51f6bf33 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 02:17:41 -0500 Subject: [PATCH 42/76] Add identity interpreter --- .../structured/IdentityInterpreter.java | 76 +++++++++++++++++++ .../test/neoforge/CodecExtrasTest.java | 15 ++-- 2 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java new file mode 100644 index 0000000..5639483 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -0,0 +1,76 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.types.Identity; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; + +/** + * Attempts to recover a default value from a structure by evaluating missing behaviours as if the value is missing + */ +public class IdentityInterpreter implements Interpreter { + public static final IdentityInterpreter INSTANCE = new IdentityInterpreter(); + + @Override + public DataResult>> list(App single) { + return DataResult.error(() -> "No default value available for a list"); + } + + @Override + public DataResult> keyed(Key key) { + return DataResult.error(() -> "No default value available for a key"); + } + + @Override + public DataResult> record(List> fields, Function creator) { + var builder = RecordStructure.Container.builder(); + for (var field : fields) { + DataResult> result = forField(field, builder); + if (result != null) return result; + } + return DataResult.success(new Identity<>(creator.apply(builder.build()))); + } + + private @Nullable DataResult> forField(RecordStructure.Field field, RecordStructure.Container.Builder builder) { + var missingBehavior = field.missingBehavior(); + if (missingBehavior.isPresent()) { + builder.add(field.key(), missingBehavior.get().missing().get()); + } else { + var result = field.structure().interpret(this).map(i -> Identity.unbox(i).value()); + if (result.error().isPresent()) { + return DataResult.error(() -> "No default value available for field " + field.name() + ": " + result.error().orElseThrow().message()); + } + builder.add(field.key(), result.result().orElseThrow()); + } + return null; + } + + @Override + public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + var value = Identity.unbox(input).value(); + return deserializer.apply(value).map(Identity::new); + } + + @Override + public DataResult> annotate(Structure original, Keys annotations) { + return original.interpret(this); + } + + @Override + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + return DataResult.error(() -> "No default value available for a dispatch"); + } + + @Override + public DataResult>> parametricallyKeyed(Key2 key, App parameter) { + return DataResult.error(() -> "No default value available for a parametric key"); + } + + public DataResult interpret(Structure structure) { + return structure.interpret(this).map(i -> Identity.unbox(i).value()); + } +} diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index d8825a2..7a7eed8 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -10,6 +10,7 @@ import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.structured.Annotation; +import dev.lukebemish.codecextras.structured.IdentityInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import java.util.HashMap; @@ -69,15 +70,15 @@ public String key() { private record TestRecord(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches) { private static final Structure STRUCTURE = Structure.record(builder -> { - var a = builder.add("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestRecord::a); - var b = builder.add("b", Structure.FLOAT, TestRecord::b); - var c = builder.add("c", Structure.BOOL, TestRecord::c); - var d = builder.add("d", Structure.STRING, TestRecord::d); + var a = builder.addOptional("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestRecord::a, () -> 34); + var b = builder.addOptional("b", Structure.FLOAT, TestRecord::b, () -> 1.2f); + var c = builder.addOptional("c", Structure.BOOL, TestRecord::c, () -> true); + var d = builder.addOptional("d", Structure.STRING, TestRecord::d, () -> "test"); var e = builder.addOptional("e", Structure.BOOL, TestRecord::e); var f = builder.addOptional("f", Structure.STRING, TestRecord::f); - var g = builder.add("g", Structure.UNIT, TestRecord::g); + var g = builder.addOptional("g", Structure.UNIT, TestRecord::g, () -> Unit.INSTANCE); var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestRecord::strings, () -> List.of("test1", "test2")); - var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestRecord::dispatches, () -> new Abc(1, "test", 1.0f)); + var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestRecord::dispatches, () -> IdentityInterpreter.INSTANCE.interpret(Abc.STRUCTURE).getOrThrow()); return container -> new TestRecord( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), @@ -96,7 +97,7 @@ public Codec codec() { @Override public TestRecord defaultConfig() { - return new TestRecord(1, 2.0f, true, "test", Optional.empty(), Optional.empty(), Unit.INSTANCE, List.of("test1", "test2"), new Abc(1, "test", 1.0f)); + return IdentityInterpreter.INSTANCE.interpret(TestRecord.STRUCTURE).getOrThrow(); } }.handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); From 5e02a6cb185fd5bddf901a7ff5aa6f2b6e940002 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 02:58:05 -0500 Subject: [PATCH 43/76] Shared test mod and test with modmenu --- build.gradle | 11 +++ gradle/libs.versions.toml | 6 ++ settings.gradle | 14 ++- .../codecextras/test/common/TestConfig.java | 93 ++++++++++++++++++ .../test/fabric/CodecExtrasModMenu.java | 26 +++++ src/testFabric/resources/fabric.mod.json | 11 +++ .../test/neoforge/CodecExtrasTest.java | 94 +------------------ 7 files changed, 164 insertions(+), 91 deletions(-) create mode 100644 src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java create mode 100644 src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java create mode 100644 src/testFabric/resources/fabric.mod.json diff --git a/build.gradle b/build.gradle index 1dc4987..42b7542 100644 --- a/build.gradle +++ b/build.gradle @@ -136,8 +136,10 @@ java { withSourcesJar() withJavadocJar() capability(project.group as String, "$project.name-minecraft", project.version as String) + capability(project.group as String, "$project.name-minecraft-mojmap", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) + capability(project.group as String, "$project.name-stream-mojmap", project.version as String) } registerFeature("minecraftIntermediary") { usingSourceSet sourceSets.minecraftIntermediary @@ -195,6 +197,15 @@ dependencies { testNeoforgeCompileOnly sourceSets.minecraft.output testFabricCompileOnly sourceSets.minecraftIntermediary.output + testCommonCompileOnly(project(':')) { + capabilities { + requireCapability 'dev.lukebemish:codecextras-minecraft-mojmap' + } + } + + modTestFabricImplementation libs.fabric.loader + modTestFabricLocalImplementation libs.modmenu + modTestFabricLocalImplementation libs.fabric.api } ['minecraftJar', 'minecraftIntermediaryJar', 'jar'].each { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a4ac91..b4a41b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,14 @@ minecraft = "1.21.1" neoforge = "21.1.31" +fabric_loader = "0.16.3" +fabric_api = "0.103.0+1.21.1" +modmenu = "11.0.2" [libraries] minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } neoforge = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge" } +fabric_loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric_loader" } +fabric_api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric_api" } +modmenu = { group = "com.terraformersmc", name = "modmenu", version.ref = "modmenu" } diff --git a/settings.gradle b/settings.gradle index fc5cc26..97cf155 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,16 +27,26 @@ plugins { } multisource.of(':') { + repositories { + maven { + name = "Terraformers" + url = "https://maven.terraformersmc.com/" + content { + includeModule 'com.terraformersmc', 'modmenu' + } + } + } configureEach { minecraft.add project.libs.minecraft mappings.add loom.officialMojangMappings() } common('minecraft', []) {} fabric('minecraftIntermediary', ['minecraft']) {} - neoforge('testNeoforge', []) { + common('testCommon', []) {} + neoforge('testNeoforge', ['testCommon']) { neoForge.add project.libs.neoforge } - fabric('testFabric', []) {} + fabric('testFabric', ['testCommon']) {} repositories { it.removeIf { it.name == 'Forge' } } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java new file mode 100644 index 0000000..cfb7faa --- /dev/null +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -0,0 +1,93 @@ +package dev.lukebemish.codecextras.test.common; + +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.config.ConfigType; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.structured.Annotation; +import dev.lukebemish.codecextras.structured.IdentityInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public record TestConfig(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches) { + private static final Map> DISPATCHES = new HashMap<>(); + + public interface Dispatches { + Structure STRUCTURE = Structure.STRING.dispatch( + "type", + d -> DataResult.success(d.key()), + DISPATCHES::keySet, + DISPATCHES::get + ); + String key(); + } + + private record Abc(int a, String b, float c) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var a = i.addOptional("a", Structure.INT, Abc::a, () -> 123); + var b = i.addOptional("b", Structure.STRING, Abc::b, () ->"gizmo"); + var c = i.addOptional("c", Structure.FLOAT, Abc::c, () -> 1.23f); + return container -> new Abc(a.apply(container), b.apply(container), c.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "abc"); + + @Override + public String key() { + return "abc"; + } + } + + private record Xyz(String x, int y, float z) implements Dispatches { + private static final Structure STRUCTURE = Structure.record(i -> { + var x = i.addOptional("x", Structure.STRING, Xyz::x, () -> "gadget"); + var y = i.addOptional("y", Structure.INT, Xyz::y, () -> 345); + var z = i.addOptional("z", Structure.FLOAT, Xyz::z, () -> 3.45f); + return container -> new Xyz(x.apply(container), y.apply(container), z.apply(container)); + }).annotate(SchemaAnnotations.REUSE_KEY, "xyz"); + + @Override + public String key() { + return "xyz"; + } + } + + static { + DISPATCHES.put("abc", Abc.STRUCTURE); + DISPATCHES.put("xyz", Xyz.STRUCTURE); + } + + public static final Structure STRUCTURE = Structure.record(builder -> { + var a = builder.addOptional("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestConfig::a, () -> 34); + var b = builder.addOptional("b", Structure.FLOAT, TestConfig::b, () -> 1.2f); + var c = builder.addOptional("c", Structure.BOOL, TestConfig::c, () -> true); + var d = builder.addOptional("d", Structure.STRING, TestConfig::d, () -> "test"); + var e = builder.addOptional("e", Structure.BOOL, TestConfig::e); + var f = builder.addOptional("f", Structure.STRING, TestConfig::f); + var g = builder.addOptional("g", Structure.UNIT, TestConfig::g, () -> Unit.INSTANCE); + var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestConfig::strings, () -> List.of("test1", "test2")); + var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestConfig::dispatches, () -> IdentityInterpreter.INSTANCE.interpret(Abc.STRUCTURE).getOrThrow()); + return container -> new TestConfig( + a.apply(container), b.apply(container), c.apply(container), + d.apply(container), e.apply(container), f.apply(container), + g.apply(container), strings.apply(container), dispatches.apply(container) + ); + }); + + public static final Codec CODEC = MinecraftStructures.CODEC_INTERPRETER.interpret(STRUCTURE).getOrThrow(); + + public static final ConfigType CONFIG = new ConfigType<>() { + @Override + public Codec codec() { + return TestConfig.CODEC; + } + + @Override + public TestConfig defaultConfig() { + return IdentityInterpreter.INSTANCE.interpret(TestConfig.STRUCTURE).getOrThrow(); + } + }; +} diff --git a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java new file mode 100644 index 0000000..458160d --- /dev/null +++ b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java @@ -0,0 +1,26 @@ +package dev.lukebemish.codecextras.test.fabric; + +import com.mojang.serialization.JsonOps; +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import dev.lukebemish.codecextras.config.ConfigType; +import dev.lukebemish.codecextras.config.GsonOpsIo; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; +import dev.lukebemish.codecextras.test.common.TestConfig; +import net.fabricmc.loader.api.FabricLoader; + +public class CodecExtrasModMenu implements ModMenuApi { + private static final ConfigType.ConfigHandle CONFIG = TestConfig.CONFIG + .handle(FabricLoader.getInstance().getConfigDir().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + ConfigScreenEntry entry = new ConfigScreenInterpreter( + MinecraftStructures.CODEC_INTERPRETER + ).interpret(TestConfig.STRUCTURE).getOrThrow(); + + return parent -> entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()); + } +} diff --git a/src/testFabric/resources/fabric.mod.json b/src/testFabric/resources/fabric.mod.json new file mode 100644 index 0000000..5c7f5c7 --- /dev/null +++ b/src/testFabric/resources/fabric.mod.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "id": "codecextras_testmod", + "version": "1.0.0", + "name": "CodecExtras Test Mod", + "entrypoints": { + "modmenu": [ + "dev.lukebemish.codecextras.test.fabric.CodecExtrasModMenu" + ] + } +} diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index 7a7eed8..f19a63b 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -1,22 +1,12 @@ package dev.lukebemish.codecextras.test.neoforge; -import com.mojang.datafixers.util.Unit; -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; -import dev.lukebemish.codecextras.structured.Annotation; -import dev.lukebemish.codecextras.structured.IdentityInterpreter; -import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import dev.lukebemish.codecextras.test.common.TestConfig; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLPaths; @@ -24,87 +14,13 @@ @Mod("codecextras_testmod") public class CodecExtrasTest { - private interface Dispatches { - Map> MAP = new HashMap<>(); - Structure STRUCTURE = Structure.STRING.dispatch( - "type", - d -> DataResult.success(d.key()), - MAP::keySet, - MAP::get - ); - String key(); - } - - private record Abc(int a, String b, float c) implements Dispatches { - private static final Structure STRUCTURE = Structure.record(i -> { - var a = i.addOptional("a", Structure.INT, Abc::a, () -> 123); - var b = i.addOptional("b", Structure.STRING, Abc::b, () ->"gizmo"); - var c = i.addOptional("c", Structure.FLOAT, Abc::c, () -> 1.23f); - return container -> new Abc(a.apply(container), b.apply(container), c.apply(container)); - }).annotate(SchemaAnnotations.REUSE_KEY, "abc"); - - @Override - public String key() { - return "abc"; - } - } - - private record Xyz(String x, int y, float z) implements Dispatches { - private static final Structure STRUCTURE = Structure.record(i -> { - var x = i.addOptional("x", Structure.STRING, Xyz::x, () -> "gadget"); - var y = i.addOptional("y", Structure.INT, Xyz::y, () -> 345); - var z = i.addOptional("z", Structure.FLOAT, Xyz::z, () -> 3.45f); - return container -> new Xyz(x.apply(container), y.apply(container), z.apply(container)); - }).annotate(SchemaAnnotations.REUSE_KEY, "xyz"); - - @Override - public String key() { - return "xyz"; - } - } - - static { - Dispatches.MAP.put("abc", Abc.STRUCTURE); - Dispatches.MAP.put("xyz", Xyz.STRUCTURE); - } - - private record TestRecord(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches) { - private static final Structure STRUCTURE = Structure.record(builder -> { - var a = builder.addOptional("a", Structure.INT.annotate(Annotation.DESCRIPTION, "Describes the field!").annotate(Annotation.TITLE, "Field A"), TestRecord::a, () -> 34); - var b = builder.addOptional("b", Structure.FLOAT, TestRecord::b, () -> 1.2f); - var c = builder.addOptional("c", Structure.BOOL, TestRecord::c, () -> true); - var d = builder.addOptional("d", Structure.STRING, TestRecord::d, () -> "test"); - var e = builder.addOptional("e", Structure.BOOL, TestRecord::e); - var f = builder.addOptional("f", Structure.STRING, TestRecord::f); - var g = builder.addOptional("g", Structure.UNIT, TestRecord::g, () -> Unit.INSTANCE); - var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestRecord::strings, () -> List.of("test1", "test2")); - var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestRecord::dispatches, () -> IdentityInterpreter.INSTANCE.interpret(Abc.STRUCTURE).getOrThrow()); - return container -> new TestRecord( - a.apply(container), b.apply(container), c.apply(container), - d.apply(container), e.apply(container), f.apply(container), - g.apply(container), strings.apply(container), dispatches.apply(container) - ); - }); - - private static final Codec CODEC = MinecraftStructures.CODEC_INTERPRETER.interpret(STRUCTURE).getOrThrow(); - } - - private static final ConfigType.ConfigHandle CONFIG = new ConfigType() { - @Override - public Codec codec() { - return TestRecord.CODEC; - } - - @Override - public TestRecord defaultConfig() { - return IdentityInterpreter.INSTANCE.interpret(TestRecord.STRUCTURE).getOrThrow(); - } - }.handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); + private static final ConfigType.ConfigHandle CONFIG = TestConfig.CONFIG + .handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); public CodecExtrasTest(ModContainer modContainer) { - ConfigScreenEntry entry = new ConfigScreenInterpreter( + ConfigScreenEntry entry = new ConfigScreenInterpreter( MinecraftStructures.CODEC_INTERPRETER - ).interpret(TestRecord.STRUCTURE).getOrThrow(); + ).interpret(TestConfig.STRUCTURE).getOrThrow(); modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()) From be06ac0b32d096f70d858a537617dddcf4bc05e5 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 03:57:26 -0500 Subject: [PATCH 44/76] Work on more types --- .../structured/CodecInterpreter.java | 20 +- .../codecextras/structured/Interpreter.java | 8 + .../codecextras/structured/Key.java | 14 +- .../codecextras/structured/Range.java | 22 ++ .../codecextras/structured/Structure.java | 61 +++- .../structured/MinecraftInterpreters.java | 166 +++++++++ .../minecraft/structured/MinecraftKeys.java | 74 ++++ .../structured/MinecraftStructures.java | 320 ++++-------------- .../config/ConfigScreenInterpreter.java | 15 + .../structured/StreamCodecInterpreter.java | 45 ++- .../codecextras/test/common/TestConfig.java | 4 +- .../test/fabric/CodecExtrasModMenu.java | 4 +- .../test/neoforge/CodecExtrasTest.java | 4 +- 13 files changed, 490 insertions(+), 267 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/Range.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 9830e64..2d14dde 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -2,6 +2,7 @@ import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; @@ -30,7 +31,24 @@ public CodecInterpreter(Keys keys, Keys2(Codec.DOUBLE)) .add(Interpreter.STRING, new Holder<>(Codec.STRING)) .build() - ), parametricKeys); + ), parametricKeys.join(Keys2., K1, K1>builder() + .add(Interpreter.INT_IN_RANGE, numberRangeCodecParameter(Codec.INT)) + .add(Interpreter.BYTE_IN_RANGE, numberRangeCodecParameter(Codec.BYTE)) + .add(Interpreter.SHORT_IN_RANGE, numberRangeCodecParameter(Codec.SHORT)) + .add(Interpreter.LONG_IN_RANGE, numberRangeCodecParameter(Codec.LONG)) + .add(Interpreter.FLOAT_IN_RANGE, numberRangeCodecParameter(Codec.FLOAT)) + .add(Interpreter.DOUBLE_IN_RANGE, numberRangeCodecParameter(Codec.DOUBLE)) + .build() + )); + } + + private static > ParametricKeyedValue>, Const.Mu> numberRangeCodecParameter(Codec codec) { + return new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + return new Holder<>(codec.validate(Codec.checkRange(Const.unbox(parameter).min(), Const.unbox(parameter).max())).xmap(Const::create, Const::unbox)); + } + }; } public static CodecInterpreter create( diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 76bcd93..ff88b4c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -1,6 +1,7 @@ package dev.lukebemish.codecextras.structured; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; @@ -38,4 +39,11 @@ default Optional> key() { Key STRING = Key.create("STRING"); DataResult>> parametricallyKeyed(Key2 key, App parameter); + + Key2>, Const.Mu> INT_IN_RANGE = Key2.create("int_in_range"); + Key2>, Const.Mu> BYTE_IN_RANGE = Key2.create("byte_in_range"); + Key2>, Const.Mu> SHORT_IN_RANGE = Key2.create("short_in_range"); + Key2>, Const.Mu> LONG_IN_RANGE = Key2.create("long_in_range"); + Key2>, Const.Mu> FLOAT_IN_RANGE = Key2.create("float_in_range"); + Key2>, Const.Mu> DOUBLE_IN_RANGE = Key2.create("double_in_range"); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key.java b/src/main/java/dev/lukebemish/codecextras/structured/Key.java index 5cb84c4..43fbd07 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Key.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key.java @@ -1,6 +1,18 @@ package dev.lukebemish.codecextras.structured; -public final class Key { +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +public final class Key implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static Key unbox(App box) { + return (Key) box; + } + private final String name; private Key(String name) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Range.java b/src/main/java/dev/lukebemish/codecextras/structured/Range.java new file mode 100644 index 0000000..9cd84aa --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/Range.java @@ -0,0 +1,22 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; + +/** + * A range of values + * + * @param min minimum value, inclusive + * @param max maximum value, inclusive + * @param number type of the range + */ +public record Range>(N min, N max) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static > Range unbox(App app) { + return (Range) app; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index f807d3e..a9b5b68 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -1,6 +1,7 @@ package dev.lukebemish.codecextras.structured; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; @@ -130,6 +131,19 @@ public DataResult> interpret(Interpreter interpre }; } + static Structure keyed(Key key, Structure fallback) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + var result = interpreter.keyed(key); + if (result.error().isPresent()) { + return fallback.interpret(interpreter); + } + return result; + } + }; + } + static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @Override @@ -146,12 +160,27 @@ static > Structure p @Override public DataResult> interpret(Interpreter interpreter) { return interpreter.parametricallyKeyed(key, parameter).flatMap(app -> - interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) + interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) ); } }; } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Structure fallback) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + var result = interpreter.parametricallyKeyed(key, parameter).flatMap(app -> + interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) + ); + if (result.error().isPresent()) { + return fallback.interpret(interpreter); + } + return result; + } + }; + } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys) { return new Structure<>() { @Override @@ -178,4 +207,34 @@ static Structure record(RecordStructure.Builder builder) { Structure FLOAT = keyed(Interpreter.FLOAT); Structure DOUBLE = keyed(Interpreter.DOUBLE); Structure STRING = keyed(Interpreter.STRING); + + static Structure intInRange(int min, int max) { + return Structure.parametricallyKeyed(Interpreter.INT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } + + static Structure byteInRange(byte min, byte max) { + return Structure.parametricallyKeyed(Interpreter.BYTE_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } + + static Structure shortInRange(short min, short max) { + return Structure.parametricallyKeyed(Interpreter.SHORT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } + + static Structure longInRange(long min, long max) { + return Structure.parametricallyKeyed(Interpreter.LONG_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } + + static Structure floatInRange(float min, float max) { + return Structure.parametricallyKeyed(Interpreter.FLOAT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } + + static Structure doubleInRange(double min, double max) { + return Structure.parametricallyKeyed(Interpreter.DOUBLE_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + .xmap(Const::unbox, Const::create); + } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java new file mode 100644 index 0000000..5d67600 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -0,0 +1,166 @@ +package dev.lukebemish.codecextras.minecraft.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K1; +import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.Keys2; +import dev.lukebemish.codecextras.structured.MapCodecInterpreter; +import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; + +public final class MinecraftInterpreters { + private MinecraftInterpreters() {} + + public static final Keys MAP_CODEC_KEYS = Keys.builder() + .build(); + + public static final Keys2, K1, K1> MAP_CODEC_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .build(); + + public static final Keys CODEC_KEYS = MAP_CODEC_KEYS.map(new Keys.Converter<>() { + @Override + public App convert(App app) { + return new CodecInterpreter.Holder<>(MapCodecInterpreter.unbox(app).codec()); + } + }).join(Keys.builder() + .add(MinecraftKeys.RESOURCE_LOCATION, new CodecInterpreter.Holder<>(ResourceLocation.CODEC)) + .build() + ); + + public static final Keys2, K1, K1> CODEC_PARAMETRIC_KEYS = MAP_CODEC_PARAMETRIC_KEYS.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { + @Override + public App2, A, B> convert(App2, A, B> input) { + var unboxed = ParametricKeyedValue.unbox(input); + return new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var mapCodec = MapCodecInterpreter.unbox(unboxed.convert(parameter)); + return new CodecInterpreter.Holder<>(mapCodec.codec()); + } + }; + } + }).join(Keys2., K1, K1>builder() + .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(ResourceKey.codec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.ResourceKeyHolder::new, a -> MinecraftKeys.ResourceKeyHolder.unbox(a).value())); + } + }) + .add(MinecraftKeys.TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(TagKey.codec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.TagKeyHolder::new, a -> MinecraftKeys.TagKeyHolder.unbox(a).value())); + } + }) + .add(MinecraftKeys.HASHED_TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(TagKey.hashedCodec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.TagKeyHolder::new, a -> MinecraftKeys.TagKeyHolder.unbox(a).value())); + } + }) + .add(MinecraftKeys.HOMOGENOUS_LIST_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new CodecInterpreter.Holder<>(RegistryCodecs.homogeneousList(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.HolderSetHolder::new, a -> MinecraftKeys.HolderSetHolder.unbox(a).value())); + } + }) + .build() + ); + + public static final CodecInterpreter CODEC_INTERPRETER = CodecInterpreter.create().with( + CODEC_KEYS, + MAP_CODEC_KEYS, + CODEC_PARAMETRIC_KEYS, + MAP_CODEC_PARAMETRIC_KEYS + ); + + public static final MapCodecInterpreter MAP_CODEC_INTERPRETER = MapCodecInterpreter.create().with( + CODEC_KEYS, + MAP_CODEC_KEYS, + CODEC_PARAMETRIC_KEYS, + MAP_CODEC_PARAMETRIC_KEYS + ); + + public static final Keys, Object> FRIENDLY_STREAM_KEYS = Keys., Object>builder() + .add(MinecraftKeys.RESOURCE_LOCATION, new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast())) + .build(); + + public static final Keys2>, K1, K1> FRIENDLY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() + .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App, App> convert(App parameter) { + return new StreamCodecInterpreter.Holder<>( + ResourceKey.streamCodec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).>map(MinecraftKeys.ResourceKeyHolder::new, a -> MinecraftKeys.ResourceKeyHolder.unbox(a).value()).cast() + ); + } + }) + .build(); + + public static final StreamCodecInterpreter FRIENDLY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + FRIENDLY_STREAM_KEYS, + FRIENDLY_STREAM_PARAMETRIC_KEYS + ); + + public static final Keys, Object> REGISTRY_STREAM_KEYS = FRIENDLY_STREAM_KEYS.map(new Keys.Converter, StreamCodecInterpreter.Holder.Mu, Object>() { + @Override + public App, A> convert(App, A> input) { + return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(input).cast()); + } + }).join(Keys., Object>builder() + .build() + ); + + public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = FRIENDLY_STREAM_PARAMETRIC_KEYS.map(new Keys2.Converter>, ParametricKeyedValue.Mu>, K1, K1>() { + @Override + public App2>, A, B> convert(App2>, A, B> input) { + return new ParametricKeyedValue<>() { + @Override + public App, App> convert(App parameter) { + return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(ParametricKeyedValue.unbox(input).convert(parameter)).cast()); + } + }; + } + }).join(Keys2.>, K1, K1>builder() + .build() + ); + + public static final StreamCodecInterpreter REGISTRY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + REGISTRY_STREAM_KEYS, + REGISTRY_STREAM_PARAMETRIC_KEYS + ); + + public static final Keys JSON_SCHEMA_KEYS = Keys.builder() + .add(MinecraftKeys.RESOURCE_LOCATION, new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get())) + .build(); + + // TODO: Add regex fo schemas + public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) + .add(MinecraftKeys.TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) + .add(MinecraftKeys.HASHED_TAG_KEY, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); + } + }) + .build(); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java new file mode 100644 index 0000000..18ac697 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -0,0 +1,74 @@ +package dev.lukebemish.codecextras.minecraft.structured; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Key2; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; + +public final class MinecraftKeys { + private MinecraftKeys() { + } + + public static final Key RESOURCE_LOCATION = Key.create("resource_location"); + // TODO: implement config color picker + public static final Key ARGB_COLOR = Key.create("argb_color"); + public static final Key RGB_COLOR = Key.create("rgb_color"); + + public record ResourceKeyHolder(ResourceKey value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static ResourceKeyHolder unbox(App box) { + return (ResourceKeyHolder) box; + } + } + + public record TagKeyHolder(TagKey value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static TagKeyHolder unbox(App box) { + return (TagKeyHolder) box; + } + } + + public record HolderSetHolder(HolderSet value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static HolderSetHolder unbox(App box) { + return (HolderSetHolder) box; + } + } + + public record RegistryKeyHolder( + ResourceKey> value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static RegistryKeyHolder unbox(App box) { + return (RegistryKeyHolder) box; + } + } + + public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); + + public static final Key2 TAG_KEY = Key2.create("tag_key"); + + public static final Key2 HASHED_TAG_KEY = Key2.create("#tag_key"); + + public static final Key2 HOMOGENOUS_LIST_KEY = Key2.create("homogenous_list"); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index dcbd1db..a597e44 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -1,290 +1,96 @@ package dev.lukebemish.codecextras.minecraft.structured; -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.DataResult; -import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; import dev.lukebemish.codecextras.structured.CodecInterpreter; -import dev.lukebemish.codecextras.structured.Key; -import dev.lukebemish.codecextras.structured.Key2; import dev.lukebemish.codecextras.structured.Keys; -import dev.lukebemish.codecextras.structured.Keys2; -import dev.lukebemish.codecextras.structured.MapCodecInterpreter; -import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; import java.util.function.Function; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryCodecs; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; public final class MinecraftStructures { - private MinecraftStructures() {} - - public static final Keys MAP_CODEC_KEYS = Keys.builder() - .build(); - - public static final Keys2, K1, K1> MAP_CODEC_PARAMETRIC_KEYS = Keys2., K1, K1>builder() - .build(); - - public static final Keys CODEC_KEYS = MAP_CODEC_KEYS.map(new Keys.Converter<>() { - @Override - public App convert(App app) { - return new CodecInterpreter.Holder<>(MapCodecInterpreter.unbox(app).codec()); - } - }).join(Keys.builder() - .add(Types.RESOURCE_LOCATION, new CodecInterpreter.Holder<>(ResourceLocation.CODEC)) - .build() - ); - - public static final Keys2, K1, K1> CODEC_PARAMETRIC_KEYS = MAP_CODEC_PARAMETRIC_KEYS.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { - @Override - public App2, A, B> convert(App2, A, B> input) { - var unboxed = ParametricKeyedValue.unbox(input); - return new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - var mapCodec = MapCodecInterpreter.unbox(unboxed.convert(parameter)); - return new CodecInterpreter.Holder<>(mapCodec.codec()); - } - }; - } - }).join(Keys2., K1, K1>builder() - .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(ResourceKey.codec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value())); - } - }) - .add(Types.TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(TagKey.codec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.TagKeyHolder::new, a -> Types.TagKeyHolder.unbox(a).value())); - } - }) - .add(Types.HASHED_TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(TagKey.hashedCodec(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.TagKeyHolder::new, a -> Types.TagKeyHolder.unbox(a).value())); - } - }) - .add(Types.HOMOGENOUS_LIST_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(RegistryCodecs.homogeneousList(Types.RegistryKeyHolder.unbox(parameter).value()).xmap(Types.HolderSetHolder::new, a -> Types.HolderSetHolder.unbox(a).value())); - } - }) - .build() - ); - - public static final CodecInterpreter CODEC_INTERPRETER = CodecInterpreter.create().with( - CODEC_KEYS, - MAP_CODEC_KEYS, - CODEC_PARAMETRIC_KEYS, - MAP_CODEC_PARAMETRIC_KEYS - ); - - public static final MapCodecInterpreter MAP_CODEC_INTERPRETER = MapCodecInterpreter.create().with( - CODEC_KEYS, - MAP_CODEC_KEYS, - CODEC_PARAMETRIC_KEYS, - MAP_CODEC_PARAMETRIC_KEYS - ); - - public static final Keys, Object> FRIENDLY_STREAM_KEYS = Keys., Object>builder() - .add(Types.RESOURCE_LOCATION, new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast())) - .build(); - - public static final Keys2>, K1, K1> FRIENDLY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() - .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App, App> convert(App parameter) { - return new StreamCodecInterpreter.Holder<>( - ResourceKey.streamCodec(Types.RegistryKeyHolder.unbox(parameter).value()).>map(Types.ResourceKeyHolder::new, a -> Types.ResourceKeyHolder.unbox(a).value()).cast() - ); - } - }) - .build(); - - public static final StreamCodecInterpreter FRIENDLY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( - FRIENDLY_STREAM_KEYS, - FRIENDLY_STREAM_PARAMETRIC_KEYS - ); + private MinecraftStructures() { + } - public static final Keys, Object> REGISTRY_STREAM_KEYS = FRIENDLY_STREAM_KEYS.map(new Keys.Converter, StreamCodecInterpreter.Holder.Mu, Object>() { - @Override - public App, A> convert(App, A> input) { - return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(input).cast()); - } - }).join(Keys., Object>builder() - .build() + public static final Structure RESOURCE_LOCATION = Structure.keyed( + MinecraftKeys.RESOURCE_LOCATION, Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) + .build() ); - public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = FRIENDLY_STREAM_PARAMETRIC_KEYS.map(new Keys2.Converter>, ParametricKeyedValue.Mu>, K1, K1>() { - @Override - public App2>, A, B> convert(App2>, A, B> input) { - return new ParametricKeyedValue<>() { - @Override - public App, App> convert(App parameter) { - return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(ParametricKeyedValue.unbox(input).convert(parameter)).cast()); - } - }; - } - }).join(Keys2.>, K1, K1>builder() - .build() + public static final Structure ARGB_COLOR = Structure.keyed( + MinecraftKeys.ARGB_COLOR, + Structure.INT ); - public static final StreamCodecInterpreter REGISTRY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( - REGISTRY_STREAM_KEYS, - REGISTRY_STREAM_PARAMETRIC_KEYS + public static final Structure RGB_COLOR = Structure.keyed( + MinecraftKeys.RGB_COLOR, + Structure.INT ); - public static final Keys JSON_SCHEMA_KEYS = Keys.builder() - .add(Types.RESOURCE_LOCATION, new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get())) - .build(); - - // TODO: Add regex fo schemas - public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() - .add(Types.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) - .add(Types.TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) - .add(Types.HASHED_TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) - .build(); - - public static final class Types { - private Types() {} - - public static final Key RESOURCE_LOCATION = Key.create("resource_location"); - - public record ResourceKeyHolder(ResourceKey value) implements App { - public static final class Mu implements K1 { private Mu() {} } - - public static ResourceKeyHolder unbox(App box) { - return (ResourceKeyHolder) box; - } - } - - public record TagKeyHolder(TagKey value) implements App { - public static final class Mu implements K1 { private Mu() {} } - - public static TagKeyHolder unbox(App box) { - return (TagKeyHolder) box; - } - } - - public record HolderSetHolder(HolderSet value) implements App { - public static final class Mu implements K1 { private Mu() {} } - - public static HolderSetHolder unbox(App box) { - return (HolderSetHolder) box; - } - } - - public record RegistryKeyHolder(ResourceKey> value) implements App { - public static final class Mu implements K1 { private Mu() {} } - - public static RegistryKeyHolder unbox(App box) { - return (RegistryKeyHolder) box; - } - } - - public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); - - public static final Key2 TAG_KEY = Key2.create("tag_key"); - - public static final Key2 HASHED_TAG_KEY = Key2.create("#tag_key"); + public static Structure> resourceKey(ResourceKey> registry) { + return Structure.parametricallyKeyed( + MinecraftKeys.RESOURCE_KEY, + new MinecraftKeys.RegistryKeyHolder<>(registry), + MinecraftKeys.ResourceKeyHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + ResourceKey.codec(registry).xmap(MinecraftKeys.ResourceKeyHolder::new, MinecraftKeys.ResourceKeyHolder::value) + )) + ) + .build() + ).xmap(MinecraftKeys.ResourceKeyHolder::value, MinecraftKeys.ResourceKeyHolder::new); + } - public static final Key2 HOMOGENOUS_LIST_KEY = Key2.create("homogenous_list"); + public static Structure> homogenousList(ResourceKey> registry) { + return Structure.parametricallyKeyed( + MinecraftKeys.HOMOGENOUS_LIST_KEY, + new MinecraftKeys.RegistryKeyHolder<>(registry), + MinecraftKeys.HolderSetHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + RegistryCodecs.homogeneousList(registry).xmap(MinecraftKeys.HolderSetHolder::new, MinecraftKeys.HolderSetHolder::value) + )) + ) + .build() + ).xmap(MinecraftKeys.HolderSetHolder::value, MinecraftKeys.HolderSetHolder::new); } - public static final class Structures { - private Structures() {} + public static Structure> tagKey(ResourceKey> registry, boolean hashPrefix) { + return Structure.parametricallyKeyed( + hashPrefix ? MinecraftKeys.HASHED_TAG_KEY : MinecraftKeys.TAG_KEY, + new MinecraftKeys.RegistryKeyHolder<>(registry), + MinecraftKeys.TagKeyHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + (hashPrefix ? TagKey.hashedCodec(registry) : TagKey.codec(registry)).xmap(MinecraftKeys.TagKeyHolder::new, MinecraftKeys.TagKeyHolder::value) + )) + ) + .build() + ).xmap(MinecraftKeys.TagKeyHolder::value, MinecraftKeys.TagKeyHolder::new); + } - public static final Structure RESOURCE_LOCATION = Structure.keyed( - Types.RESOURCE_LOCATION, Keys., K1>builder() - .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) - .build() + public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { + var keyStructure = resourceKey(registry.key()); + return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> + registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry()) + "::" + toDefsKey(k.location())) ); + } - public static Structure> resourceKey(ResourceKey> registry) { - return Structure.parametricallyKeyed( - Types.RESOURCE_KEY, - new Types.RegistryKeyHolder<>(registry), - Types.ResourceKeyHolder::unbox, - Keys.>, K1>builder() - .add( - CodecInterpreter.KEY, - new Flip<>(new CodecInterpreter.Holder<>( - ResourceKey.codec(registry).xmap(Types.ResourceKeyHolder::new, Types.ResourceKeyHolder::value) - )) - ) - .build() - ).xmap(Types.ResourceKeyHolder::value, Types.ResourceKeyHolder::new); - } - - public static Structure> homogenousList(ResourceKey> registry) { - return Structure.parametricallyKeyed( - Types.HOMOGENOUS_LIST_KEY, - new Types.RegistryKeyHolder<>(registry), - Types.HolderSetHolder::unbox, - Keys.>, K1>builder() - .add( - CodecInterpreter.KEY, - new Flip<>(new CodecInterpreter.Holder<>( - RegistryCodecs.homogeneousList(registry).xmap(Types.HolderSetHolder::new, Types.HolderSetHolder::value) - )) - ) - .build() - ).xmap(Types.HolderSetHolder::value, Types.HolderSetHolder::new); - } - - public static Structure> tagKey(ResourceKey> registry, boolean hashPrefix) { - return Structure.parametricallyKeyed( - hashPrefix ? Types.HASHED_TAG_KEY : Types.TAG_KEY, - new Types.RegistryKeyHolder<>(registry), - Types.TagKeyHolder::unbox, - Keys.>, K1>builder() - .add( - CodecInterpreter.KEY, - new Flip<>(new CodecInterpreter.Holder<>( - (hashPrefix ? TagKey.hashedCodec(registry) : TagKey.codec(registry)).xmap(Types.TagKeyHolder::new, Types.TagKeyHolder::value) - )) - ) - .build() - ).xmap(Types.TagKeyHolder::value, Types.TagKeyHolder::new); - } - - public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { - var keyStructure = resourceKey(registry.key()); - return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> - registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry())+"::"+toDefsKey(k.location())) - ); - } - - private static String toDefsKey(ResourceLocation location) { - return location.getNamespace().replace('/', '.') + ":" + location.getPath().replace('/', '.'); - } + private static String toDefsKey(ResourceLocation location) { + return location.getNamespace().replace('/', '.') + ":" + location.getPath().replace('/', '.'); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 239437a..0c248f1 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -9,9 +9,11 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; @@ -23,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -30,6 +33,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; public class ConfigScreenInterpreter extends KeyStoringInterpreter { private final CodecInterpreter codecInterpreter; @@ -113,6 +117,10 @@ public ConfigScreenInterpreter( Widgets.canHandleOptional(Widgets.text(DataResult::success, DataResult::success, false)), new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) )) + .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( + Widgets.canHandleOptional(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), false)), + new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) + )) .build()), parametricKeys.join(Keys2., K1, K1>builder() .build()) @@ -130,6 +138,13 @@ public ConfigScreenInterpreter( ); } + public static final Key KEY = Key.create("ConfigScreenInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } + @Override public ConfigScreenInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new ConfigScreenInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 7fd2eac..4b95407 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -2,6 +2,7 @@ import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; @@ -10,10 +11,12 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.Range; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.DecoderException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -39,7 +42,47 @@ public StreamCodecInterpreter(Keys, Object> keys, Keys2(ByteBufCodecs.DOUBLE.cast())) .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) .build() - ), parametricKeys); + ), parametricKeys.join(Keys2.>, K1, K1>builder() + .add(Interpreter.INT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.VAR_INT.cast())) + .add(Interpreter.BYTE_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.BYTE.cast())) + .add(Interpreter.SHORT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.SHORT.cast())) + .add(Interpreter.LONG_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.VAR_LONG.cast())) + .add(Interpreter.FLOAT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.FLOAT.cast())) + .add(Interpreter.DOUBLE_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.DOUBLE.cast())) + .build() + )); + } + + private static , B extends ByteBuf> ParametricKeyedValue, Const.Mu>, Const.Mu> numberRangeCodecParameter(StreamCodec codec) { + return new ParametricKeyedValue<>() { + @Override + public App, App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + return new StreamCodecInterpreter.Holder<>(new StreamCodec, T>>() { + @Override + public App, T> decode(B buffer) { + var n = codec.decode(buffer); + if (n.compareTo(range.min()) < 0) { + throw new DecoderException("Value " + n + " is larger than max " + range.max()); + } else if (n.compareTo(range.max()) > 0) { + throw new DecoderException("Value " + n + " is smaller than min " + range.min()); + } + return Const.create(n); + } + + @Override + public void encode(B buffer, App, T> object2) { + var value = Const.unbox(object2); + if (value.compareTo(range.min()) < 0) { + throw new DecoderException("Value " + value + " is larger than max " + range.max()); + } else if (value.compareTo(range.max()) > 0) { + throw new DecoderException("Value " + value + " is smaller than min " + range.min()); + } + codec.encode(buffer, value); + } + }); + } + }; } public StreamCodecInterpreter() { diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index cfb7faa..a366f8f 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -4,7 +4,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.config.ConfigType; -import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.IdentityInterpreter; import dev.lukebemish.codecextras.structured.Structure; @@ -77,7 +77,7 @@ public String key() { ); }); - public static final Codec CODEC = MinecraftStructures.CODEC_INTERPRETER.interpret(STRUCTURE).getOrThrow(); + public static final Codec CODEC = MinecraftInterpreters.CODEC_INTERPRETER.interpret(STRUCTURE).getOrThrow(); public static final ConfigType CONFIG = new ConfigType<>() { @Override diff --git a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java index 458160d..b25ec06 100644 --- a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java +++ b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java @@ -5,7 +5,7 @@ import com.terraformersmc.modmenu.api.ModMenuApi; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; -import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.test.common.TestConfig; @@ -18,7 +18,7 @@ public class CodecExtrasModMenu implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { ConfigScreenEntry entry = new ConfigScreenInterpreter( - MinecraftStructures.CODEC_INTERPRETER + MinecraftInterpreters.CODEC_INTERPRETER ).interpret(TestConfig.STRUCTURE).getOrThrow(); return parent -> entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()); diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index f19a63b..c2cd57d 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -3,7 +3,7 @@ import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; -import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.test.common.TestConfig; @@ -19,7 +19,7 @@ public class CodecExtrasTest { public CodecExtrasTest(ModContainer modContainer) { ConfigScreenEntry entry = new ConfigScreenInterpreter( - MinecraftStructures.CODEC_INTERPRETER + MinecraftInterpreters.CODEC_INTERPRETER ).interpret(TestConfig.STRUCTURE).getOrThrow(); modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> From f601e66b54c08c626ef5b293b4691d0357f58d43 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 12:45:38 -0500 Subject: [PATCH 45/76] Working range sliders in configs --- .../codecextras/structured/Range.java | 16 +++ .../codecextras/structured/Structure.java | 12 +- .../config/ConfigScreenInterpreter.java | 135 ++++++++++++++++++ .../minecraft/structured/config/Widgets.java | 70 +++++++++ .../test/structured/TestParametricKeys.java | 4 +- .../codecextras/test/common/TestConfig.java | 12 +- 6 files changed, 239 insertions(+), 10 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Range.java b/src/main/java/dev/lukebemish/codecextras/structured/Range.java index 9cd84aa..78015c5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Range.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Range.java @@ -11,6 +11,22 @@ * @param number type of the range */ public record Range>(N min, N max) implements App { + public Range { + if (min.compareTo(max) >= 0) { + throw new IllegalArgumentException("min >= max"); + } + } + + public N clamp(N value) { + if (value.compareTo(min) < 0) { + return min; + } + if (value.compareTo(max) > 0) { + return max; + } + return value; + } + public static final class Mu implements K1 { private Mu() { } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index a9b5b68..6190ffa 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -209,32 +209,32 @@ static Structure record(RecordStructure.Builder builder) { Structure STRING = keyed(Interpreter.STRING); static Structure intInRange(int min, int max) { - return Structure.parametricallyKeyed(Interpreter.INT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.INT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } static Structure byteInRange(byte min, byte max) { - return Structure.parametricallyKeyed(Interpreter.BYTE_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.BYTE_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } static Structure shortInRange(short min, short max) { - return Structure.parametricallyKeyed(Interpreter.SHORT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.SHORT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } static Structure longInRange(long min, long max) { - return Structure.parametricallyKeyed(Interpreter.LONG_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.LONG_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } static Structure floatInRange(float min, float max) { - return Structure.parametricallyKeyed(Interpreter.FLOAT_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.FLOAT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } static Structure doubleInRange(double min, double max) { - return Structure.parametricallyKeyed(Interpreter.DOUBLE_IN_RANGE, Const.create(new Range<>(min, max)), app -> Const.create(Const.unbox(app))) + return Structure.parametricallyKeyed(Interpreter.DOUBLE_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 0c248f1..0d2c064 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -4,7 +4,9 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; @@ -18,6 +20,7 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.Range; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; @@ -123,6 +126,138 @@ public ConfigScreenInterpreter( )) .build()), parametricKeys.join(Keys2., K1, K1>builder() + .add(Interpreter.INT_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.INT.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsInt()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not an integer: " + json); + } + } + return DataResult.error(() -> "Not an integer: " + json); + }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) + .add(Interpreter.BYTE_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.BYTE.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsByte()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a byte: " + json); + } + } + return DataResult.error(() -> "Not a byte: " + json); + }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) + .add(Interpreter.SHORT_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.SHORT.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsShort()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a short: " + json); + } + } + return DataResult.error(() -> "Not a short: " + json); + }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) + .add(Interpreter.LONG_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.LONG.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsLong()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a long: " + json); + } + } + return DataResult.error(() -> "Not a long: " + json); + }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) + .add(Interpreter.FLOAT_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.FLOAT.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsFloat()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a float: " + json); + } + } + return DataResult.error(() -> "Not a float: " + json); + }, true), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) + .add(Interpreter.DOUBLE_IN_RANGE, new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + var range = Const.unbox(parameter); + var codec = Codec.DOUBLE.validate(Codec.checkRange(range.min(), range.max())); + return ConfigScreenEntry.single( + Widgets.slider(range, i -> DataResult.success(new JsonPrimitive(i)), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isNumber()) { + try { + return DataResult.success(json.getAsJsonPrimitive().getAsDouble()); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a double: " + json); + } + } + return DataResult.error(() -> "Not a double: " + json); + }, true), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo( + info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), + info -> info.withCodec(codec) + ); + } + }) .build()) ); this.codecInterpreter = codecInterpreter; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 4d962b9..350340a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -1,12 +1,15 @@ package dev.lukebemish.codecextras.minecraft.structured.config; +import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; import com.mojang.logging.LogUtils; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.structured.Range; import java.util.function.Function; import java.util.function.Predicate; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractSliderButton; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Checkbox; import net.minecraft.client.gui.components.EditBox; @@ -142,6 +145,73 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon }; } + public static > LayoutFactory slider(Range range, Function> toJson, Function> fromJson, boolean isDoubleLike) { + return canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonPrimitive(range.min()); + update.accept(original); + } + + var valueResult = fromJson.apply(original); + N value; + if (valueResult.error().isPresent()) { + LOGGER.warn("Failed to decode `{}`: {}", original, valueResult.error().get().message()); + value = range.min(); + } else { + value = valueResult.getOrThrow(); + } + + AbstractSliderButton widget = new AbstractSliderButton(0, 0, width, Button.DEFAULT_HEIGHT, Component.empty(), valueInRange(range, value)) { + { + this.updateMessage(); + } + + @Override + protected void updateMessage() { + N value = calculateValue(); + this.setMessage(Component.literal(isDoubleLike ? String.format("%.2f", value.doubleValue()) : String.valueOf(value.intValue()))); + } + + private N calculateValue() { + JsonElement valueElement; + var realValue = this.value * (range.max().doubleValue() - range.min().doubleValue()) + range.min().doubleValue(); + if (isDoubleLike) { + valueElement = new JsonPrimitive(realValue); + } else { + valueElement = new JsonPrimitive(Math.round(realValue)); + } + var valueResult = fromJson.apply(valueElement); + N value; + if (valueResult.error().isPresent()) { + LOGGER.warn("Failed to decode `{}`: {}", valueElement, valueResult.error().get().message()); + value = range.min(); + } else { + value = valueResult.getOrThrow(); + } + return value; + } + + @Override + protected void applyValue() { + N value = calculateValue(); + var jsonResult = toJson.apply(value); + if (jsonResult.error().isPresent()) { + LOGGER.warn("Failed to encode `{}`: {}", value, jsonResult.error().get().message()); + } else { + update.accept(jsonResult.getOrThrow()); + } + this.value = valueInRange(range, value); + } + }; + widget.setTooltip(Tooltip.create(creationInfo.componentInfo().description())); + return widget; + }); + } + + private static > double valueInRange(Range range, N value) { + return (value.doubleValue() - range.min().doubleValue()) / (range.max().doubleValue() - range.min().doubleValue()); + } + public static LayoutFactory bool(boolean falseIfMissing) { LayoutFactory widget = (parent, width, ops, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java index fcf0ab2..d231fcc 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestParametricKeys.java @@ -18,7 +18,7 @@ public class TestParametricKeys { private record WithType(String string) implements App { - public static class Mu implements K1 { private Mu() {} } + public static final class Mu implements K1 { private Mu() {} } private static WithType unbox(App box) { return (WithType) box; @@ -26,7 +26,7 @@ private static WithType unbox(App box) { } private record Prefix(String string) implements App { - public static class Mu implements K1 { private Mu() {} } + public static final class Mu implements K1 { private Mu() {} } private static Prefix unbox(App box) { return (Prefix) box; diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index a366f8f..0176405 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -14,7 +14,12 @@ import java.util.Map; import java.util.Optional; -public record TestConfig(int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches) { +public record TestConfig( + int a, float b, boolean c, + String d, Optional e, Optional f, + Unit g, List strings, Dispatches dispatches, + int intInRange, float floatInRange +) { private static final Map> DISPATCHES = new HashMap<>(); public interface Dispatches { @@ -70,10 +75,13 @@ public String key() { var g = builder.addOptional("g", Structure.UNIT, TestConfig::g, () -> Unit.INSTANCE); var strings = builder.addOptional("strings", Structure.STRING.listOf(), TestConfig::strings, () -> List.of("test1", "test2")); var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestConfig::dispatches, () -> IdentityInterpreter.INSTANCE.interpret(Abc.STRUCTURE).getOrThrow()); + var intInRange = builder.addOptional("intInRange", Structure.intInRange(10, 60), TestConfig::intInRange, () -> 50); + var floatInRange = builder.addOptional("floatInRange", Structure.floatInRange(1.0f, 5.0f), TestConfig::floatInRange, () -> 3.0f); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), - g.apply(container), strings.apply(container), dispatches.apply(container) + g.apply(container), strings.apply(container), dispatches.apply(container), + intInRange.apply(container), floatInRange.apply(container) ); }); From 7cb8de904d8b9e016dc20535e5f416404edb18c5 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 22:17:38 -0500 Subject: [PATCH 46/76] Color pickers in configs --- .../minecraft/structured/MinecraftKeys.java | 1 - .../structured/config/ColorPickScreen.java | 115 +++++++++ .../structured/config/ColorPickWidget.java | 219 ++++++++++++++++++ .../config/ConfigScreenInterpreter.java | 8 + .../minecraft/structured/config/Widgets.java | 63 +++++ .../textures/gui/sprites/widget/hue.png | Bin 0 -> 4550 bytes .../gui/sprites/widget/hue.png.mcmeta | 7 + .../gui/sprites/widget/transparent.png | Bin 0 -> 4335 bytes .../gui/sprites/widget/transparent.png.mcmeta | 10 + .../codecextras/test/common/TestConfig.java | 9 +- 10 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java create mode 100644 src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/hue.png create mode 100644 src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/hue.png.mcmeta create mode 100644 src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/transparent.png create mode 100644 src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/transparent.png.mcmeta diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index 18ac697..d324666 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -15,7 +15,6 @@ private MinecraftKeys() { } public static final Key RESOURCE_LOCATION = Key.create("resource_location"); - // TODO: implement config color picker public static final Key ARGB_COLOR = Key.create("argb_color"); public static final Key RGB_COLOR = Key.create("rgb_color"); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java new file mode 100644 index 0000000..7627138 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java @@ -0,0 +1,115 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.logging.LogUtils; +import java.util.Locale; +import java.util.function.Consumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +class ColorPickScreen extends Screen { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final Screen backgroundScreen; + private final LinearLayout layout = LinearLayout.vertical(); + private final Consumer consumer; + private final boolean hasAlpha; + int color; + private EditBox textField; + private ColorPickWidget pick; + + protected ColorPickScreen(Screen backgroundScreen, Component component, Consumer consumer, boolean hasAlpha) { + super(component); + this.backgroundScreen = backgroundScreen; + this.consumer = consumer; + this.hasAlpha = hasAlpha; + } + + @Override + public void added() { + super.added(); + this.backgroundScreen.clearFocus(); + } + + protected void init() { + this.backgroundScreen.init(this.minecraft, this.width, this.height); + this.layout.spacing(12).defaultCellSetting().alignHorizontallyCenter(); + this.pick = new ColorPickWidget(0, 0, Component.empty(), this::setColor, hasAlpha); + this.layout.addChild(pick); + var bottomLayout = new EqualSpacingLayout(pick.getWidth(), Button.DEFAULT_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL); + this.textField = new EditBox(this.font, 0, 0, 80, Button.DEFAULT_HEIGHT, Component.empty()); + textField.setResponder(string -> { + try { + if (hasAlpha && string.length() != 8) { + return; + } else if (!hasAlpha && string.length() != 6) { + return; + } + var color = Integer.parseUnsignedInt(string, 16); + setColor(color); + } catch (NumberFormatException e) { + LOGGER.warn("Invalid hex number: {}", string, e); + } + }); + textField.setFilter(string -> string.matches("[0-9a-fA-F]*")); + var button = Button.builder(CommonComponents.GUI_DONE, b -> this.onClose()).width(pick.getWidth() - 80 - 8).build(); + bottomLayout.addChild(textField); + bottomLayout.addChild(button); + this.layout.addChild(bottomLayout); + this.layout.visitWidgets(this::addRenderableWidget); + this.updateColor(color); + this.repositionElements(); + } + + public void onClose() { + this.consumer.accept(this.color); + this.minecraft.setScreen(this.backgroundScreen); + } + + @Override + protected void repositionElements() { + this.backgroundScreen.resize(this.minecraft, this.width, this.height); + this.layout.arrangeElements(); + FrameLayout.centerInRectangle(this.layout, this.getRectangle()); + } + + public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { + this.backgroundScreen.render(guiGraphics, -1, -1, f); + guiGraphics.flush(); + RenderSystem.clear(256, Minecraft.ON_OSX); + this.renderTransparentBackground(guiGraphics); + } + + public void setColor(int color) { + if (color != this.color) { + updateColor(color); + } + } + + private void updateColor(int color) { + this.color = color; + var string = Integer.toHexString(hasAlpha ? color : color & 0xFFFFFF); + if (hasAlpha) { + string = "00000000".substring(string.length()) + string; + } else { + string = "000000".substring(string.length()) + string; + } + if (this.textField != null) { + if (!this.textField.getValue().equalsIgnoreCase(string)) { + this.textField.setValue(string.toUpperCase(Locale.ROOT)); + } + } + if (this.pick != null) { + this.pick.setColor(color); + } + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java new file mode 100644 index 0000000..551f123 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java @@ -0,0 +1,219 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import java.util.function.Consumer; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.joml.Matrix4f; + +class ColorPickWidget extends AbstractWidget { + private static final ResourceLocation HUE = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/hue"); + private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); + private static final int BORDER_COLOR = 0xFFA0A0A0; + + private final Consumer consumeClick; + private final boolean hasAlpha; + + private int color; + private double alpha; + private double hue; + private double saturation; + private double value; + + private int fullySaturated; + + ColorPickWidget(int i, int j, Component component, Consumer consumer, boolean hasAlpha) { + super(i, j, calculateWidth(hasAlpha), 128 + 2, component); + this.consumeClick = consumer; + this.hasAlpha = hasAlpha; + } + + private static int calculateWidth(boolean alpha) { + return 128 + 8*(alpha ? 4 : 2) + 2*(alpha ? 3 : 2); + } + + public void setColor(int argbColor) { + this.color = argbColor; + + double r = (argbColor >> 16 & 255) / 255.0F; + double g = (argbColor >> 8 & 255) / 255.0F; + double b = (argbColor & 255) / 255.0F; + + this.alpha = (argbColor >> 24 & 255) / 255.0F; + this.value = value(r, g, b); + var saturation = saturation(r, g, b); + + if (toRgb(this.hue, saturation, this.value) != argbColor) { + this.hue = hue(r, g, b); + } + + if (toRgb(this.hue, this.saturation, this.value) != argbColor) { + this.saturation = saturation; + } + + this.fullySaturated = 0xFF000000 | toRgb(hue, 1.0, 1.0); + } + + private static int toRgb(double hue, double saturation, double value) { + double prime = hue / (1d/6d); + double c = value * saturation; + double x = c * (1 - Math.abs(prime % 2 - 1)); + double r = 0; + double g = 0; + double b = 0; + if (prime < 1) { + r = c; + g = x; + } else if (prime < 2) { + r = x; + g = c; + } else if (prime < 3) { + g = c; + b = x; + } else if (prime < 4) { + g = x; + b = c; + } else if (prime < 5) { + r = x; + b = c; + } else { + r = c; + b = x; + } + + r += value - c; + g += value - c; + b += value - c; + + int rI = (int) Math.round(r * 255); + int gI = (int) Math.round(g * 255); + int bI = (int) Math.round(b * 255); + return (rI & 0xFF) << 16 | (gI & 0xFF) << 8 | (bI & 0xFF); + } + + private static double value(double r, double g, double b) { + return Math.max(r, Math.max(g, b)); + } + + private static double saturation(double r, double g, double b) { + double max = Math.max(r, Math.max(g, b)); + double min = Math.min(r, Math.min(g, b)); + double diff = max - min; + return max == 0 ? 0 : diff / max; + } + + private static double hue(double r, double g, double b) { + double max = Math.max(r, Math.max(g, b)); + double min = Math.min(r, Math.min(g, b)); + double diff = max - min; + double h; + + if (diff == 0) { + h = 0; + } else if (max == r) { + h = (1d/6d * ((g - b) / diff) + 1.0) % 1.0; + } else if (max == g) { + h = (1d/6d * ((b - r) / diff) + 1d/3d) % 1.0; + } else { + h = (1d/6d * ((r - g) / diff) + 2d/3d) % 1.0; + } + return h; + } + + private int invert(int rgb) { + int r = 255 - (rgb >> 16 & 255); + int g = 255 - (rgb >> 8 & 255); + int b = 255 - (rgb & 255); + return r << 16 | g << 8 | b; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { + if (!this.visible) { + return; + } + int x1 = getX()+1; + int y1 = getY()+1; + int x2 = x1 + 128; + int y2 = y1 + 128; + guiGraphics.renderOutline(getX(), getY(), 128+2, 128+2, BORDER_COLOR); + guiGraphics.renderOutline(getX()+128+8+2, getY(), 8+2, 128+2, BORDER_COLOR); + if (hasAlpha) { + guiGraphics.renderOutline(getX() + 128 + 8 * 3 + 2 * 2, getY(), 8 + 2, 128 + 2, BORDER_COLOR); + } + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + VertexConsumer vertexConsumer = guiGraphics.bufferSource().getBuffer(RenderType.gui()); + vertexConsumer.addVertex(matrix4f, x1, y1, 0).setColor(0xFFFFFFFF); + vertexConsumer.addVertex(matrix4f, x1, y2, 0).setColor(0xFFFFFFFF); + vertexConsumer.addVertex(matrix4f, x2, y2, 0).setColor(fullySaturated); + vertexConsumer.addVertex(matrix4f, x2, y1, 0).setColor(fullySaturated); + guiGraphics.flush(); + guiGraphics.fillGradient(x1, y1, x2, y2, 0, 0xFF000000); + + guiGraphics.enableScissor(x1, y1, x2, y2); + int xCenter = (int) (x1 + saturation * 127); + int yCenter = (int) (y1 + (1 - value) * 127); + guiGraphics.fill(xCenter-2, yCenter, xCenter+3, yCenter+1, invert(color) | 0xFF000000); + guiGraphics.fill(xCenter, yCenter-2, xCenter+1, yCenter+3, invert(color) | 0xFF000000); + guiGraphics.disableScissor(); + + guiGraphics.blitSprite(HUE, x1+128+8+2, y1, 8, 128); + + guiGraphics.fill(x1+128+8+2, y1+(int)(hue*127), x1+128+8*2+2, y1+(int)(hue*127)+1, 0xFF000000); + + var ax1 = x1 + 128 + 8 * 3 + 2 * 2; + var ax2 = ax1 + 8; + if (this.hasAlpha) { + guiGraphics.blitSprite(TRANSPARENT, ax1, y1, 8, 128); + + guiGraphics.fillGradient(ax1, y1, ax2, y2, 0x00000000, color | 0xFF000000); + + guiGraphics.fill(ax1, y1+(int)(alpha*127), ax2, y1+(int)(alpha*127)+1, 0xFF000000); + } + } + + @Override + protected void onDrag(double x, double y, double oldX, double oldY) { + this.fromPosition(x, y); + super.onDrag(x, y, oldX, oldY); + } + + @Override + public void onClick(double x, double y) { + this.fromPosition(x, y); + super.onClick(x, y); + } + + private void fromPosition(double x, double y) { + x = x - getX(); + y = y - getY(); + if (x > 1 && x < 128+1 && y > 1 && y < 129) { + saturation = Math.max(0, Math.min(1, (x - 1) / 128)); + value = Math.max(0, Math.min(1, 1 - (y - 1) / 128)); + updateColor(); + } else if (x > 128+2+8 && x < 128+2+8*2+2 && y > 1 && y < 129) { + hue = Math.max(0, Math.min(1, (y - 1) / 128)); + updateColor(); + } else if (hasAlpha && x > 128+2*2+8*3 && x < 128+2*2+8*4+2 && y > 1 && y < 129) { + alpha = Math.max(0, Math.min(1, (y - 1) / 128)); + updateColor(); + } + } + + private void updateColor() { + int color = toRgb(hue, saturation, value); + if (hasAlpha) { + color |= (int) (alpha * 255) << 24; + } + consumeClick.accept(color); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 0d2c064..8658ee8 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -124,6 +124,14 @@ public ConfigScreenInterpreter( Widgets.canHandleOptional(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), false)), new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) )) + .add(MinecraftKeys.ARGB_COLOR, ConfigScreenEntry.single( + Widgets.color(true), + new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + )) + .add(MinecraftKeys.RGB_COLOR, ConfigScreenEntry.single( + Widgets.color(false), + new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + )) .build()), parametricKeys.join(Keys2., K1, K1>builder() .add(Interpreter.INT_IN_RANGE, new ParametricKeyedValue<>() { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 350340a..8c3d119 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -9,6 +9,8 @@ import java.util.function.Function; import java.util.function.Predicate; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.AbstractSliderButton; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Checkbox; @@ -18,11 +20,15 @@ import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; public final class Widgets { private static final Logger LOGGER = LogUtils.getLogger(); + private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); + private static final int BORDER_COLOR = 0xFFA0A0A0; private Widgets() {} @@ -145,6 +151,63 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon }; } + public static LayoutFactory color(boolean includeAlpha) { + return canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + if (!handleOptional && original.isJsonNull()) { + original = new JsonPrimitive(0); + update.accept(original); + } + + int[] value = new int[1]; + if (original.isJsonPrimitive()) { + try { + value[0] = original.getAsInt(); + } catch (NumberFormatException e) { + LOGGER.warn("Failed to decode `{}`: {}", original, e.getMessage()); + } + } else { + LOGGER.warn("Failed to decode `{}`: not a primitive", original); + } + + Function message = color -> Component.literal("0x"+Integer.toHexString(color)).withColor(color | 0xFF000000); + + return new AbstractButton(0, 0, width, Button.DEFAULT_HEIGHT, Component.empty()) { + { + setTooltip(Tooltip.create(creationInfo.componentInfo().description())); + } + + @Override + public void onPress() { + var screen = new ColorPickScreen(parent, creationInfo.componentInfo().title(), color -> { + update.accept(new JsonPrimitive(color)); + value[0] = color; + }, includeAlpha); + screen.setColor(value[0]); + Minecraft.getInstance().setScreen(screen); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + this.defaultButtonNarrationText(narrationElementOutput); + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { + super.renderWidget(guiGraphics, i, j, f); + int rectangleHeight = 12; + int startY = getY() + getHeight()/2 - rectangleHeight/2; + int startX = getX() + (startY - getY()); + int endY = getY() + getHeight()/2 + rectangleHeight/2; + int endX = getX() + getWidth() - (startX - getX()); + if (includeAlpha) { + guiGraphics.blitSprite(TRANSPARENT, startX, startY, endX - startX, endY - startY); + } + guiGraphics.fill(startX, startY, endX, endY, includeAlpha ? value[0] : value[0] | 0xFF000000); + } + }; + }); + } + public static > LayoutFactory slider(Range range, Function> toJson, Function> fromJson, boolean isDoubleLike) { return canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { diff --git a/src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/hue.png b/src/minecraft/resources/assets/codecextras_minecraft/textures/gui/sprites/widget/hue.png new file mode 100644 index 0000000000000000000000000000000000000000..69bae57d363e6f1e7fe0953694154c0a852b11d2 GIT binary patch literal 4550 zcmeHKe^3)w9$!EKQ7Cv!t<{d}iml3RHrZ@`Z3&hV3Yi+rKq^$*@ix1gu$pAUW+8!5 zwAR{_TB~+EYpeBl#W=z$^FNO^9%I=7yQ; ze{AOEefRsm-_QHL&-=db>|5?EFU`z&JOe?HOnaHF0{SEbNk-C>K|1~MYEUHQ^3`^a z-98OT2i-_e3{z}kEiQuxlAM%+q=0@6w5+6LWHj{eKpPGEKKsX$hO6?2pdS~P4}d%+ zZln0?z&8{6R%j&1xzL}4CcGY-_X5buldk@XC7zLa4h;)(4O(hd)dG(|%2%hseJW5K5zTGHkR@xp83d3dHnJ*OMNeyLDg z?6embqd`gZ2tFP`A{!R2e!A?B&fI0aA2dHQEvr5|XH><;uC3V0Lm4G4+4EkT^~8)o z_mU?w?agm&>zp0@aesAPu=cNCAA2Et*aY-UR(Z3g;?iOJmXZ}|ntPXzUh6%4@1i{KK{>9d~Z^9PB>3;l$#_ zW#idp!mq^a4&V9IYYQ)F^V_H0w%lH7ACuQ~rS;P7ao3idO70l--i@;bweM8_`s4k7 z`FvN+yyIUT&w1gisdG<8c1ag6?OOZMu7;UE{lQs2_r!Z`s^Dx_w{H zj+dW0@oe*%%Y{EpJV&g4eqz;#72`*?Jo9Q-_p#p2d%oTG&u(9r+nlnISQ*u&%0DE( z`R?Tf6P_-=JS@F1rK<7N+~21h+_e4N?Ace+uYaGa*)!+e18FbaL|&b_%-6mrcl+ck z(a(CuF06d?u}_CLo;z~b&{5lsY!_zzXz5m%A z?*hoQ4pa4z|YGJ8u9xAAv$8kR9qTlMNMF+Y9i0KV%3ms z#iGQcl!nr1@sfy8Phy1`Xo19e=nC6&aR_*`Vl}cHqzNJ%4r{`CjVO5uoyB4yv?M{2 zI9TAJhJegO@IYvi0ujTo@gY_cg0dh6Pz955i*>RU!(bfki_af)I1=!IP@DzegNQIe zLZ{IZem~LQBP5s914ulepY;g28iG7g!H2{;iRDY``G7pBKLp1n{DXCpPhAej615=EEWUK7)@qe?*(w{8A6~g^1#$3`1wkiQP|X(NCQbxI+8T&3?vy_w(32LmqL(<3Z`0N zRd;1uXjlvo%P4sY0ID9=LaSv|lw6|dvtkN_TALHY4meI6BQrLr>HwOk)~>#4jcX0G zHbENg5IMoq@V~5;!-E#Yw!BOT$)ePts$wUqlCO=u#a?}adX!L9Jrp#<#+DFb>UmC$ z6R=`KYz-6e@=!hE>Ds3kekK(x1|4Ozn0cHejV7F8I2KX?@_{#UZnKGZ8#uFhAbLpj z$YDm}i@d-ia0Th9a)nM&H)`rY>u?RP>;e#mYc04|_uqsO@dXn~&FEWk0r3zg1*$>6 zA_IOg9TYF9g+!tl#yL~U&M)}H_u?0v0icH_85F-mbPdrpC)Jd|BSbPbAuK^YHa z*Z+;Kj0e{#J^)`qVYn=fnRmVvu3D+?=Syu!uX5?qKeZqV-H2e>iy;KjOj24>WXJS+ zP^QUtM@icC5y{Epr|n(ytrJvZ<&tW-SoAAj<;X*K?y6U^#(+_`-B#o>pW6M_oZlTs z(`|cS>zTX1{eGT4tLW_wH{YVu|Ao|M8@FJx>%EJiL(4l>32VFdt=f^7-}Z&E+vJ_`mcj;OdgwmYsc$<-x{@~Zk>Fv@Jse+bko1v_0b<% z4^mgY-I%%dm4b`^O!ePLu?$~#tO|Ah?QF{-n`!>m@H+2uV_WUo!_h8jlxyUgUdgPv ze#_bVB8sg5Te{8$i^~Ik*5ej?GV_skVi3|Hrj$5*_vI)sMn$z>mTF1G3W^ZML4>x>&g|@NcW2UVExV;%=s=;R4?*ng%$;^Z_UpKjP z@0@$ich3Fpxo78YtXf*0oi#HH!?5gNMPLQ=nP$sOgXfZP-X|Dl%SH7;IT*~hX3ht} zIhZY}XGgr89Mw^e9qUO>hYoCtD9?g$iH2I@x5^3Qk{+cV#YQ4&22C7r)$ls|v+<%g>F{9o z;12%dfy2vYA=EIhc>Y85x9eZoyt=OFrq5Sv4FfxtJ0ILJ|DI`^_SkC=ZNHztJHFHV z*jnz(^V!WSPps3Le-7yG)lh&Ffgz1NnUnser%{VTh7{<`(0;O>+CZN;y49l3V*`_b+p@ik=^ zcC=|=@zALjzHxZluZzkz&9>e0abJIXdG&DD8?zfv9lw|V=>GKwZw*}-FL^|LJ>B2i z*SFKtJIpSf({+dO+XYDtA$vR=wRW+nBpwyS7G&Td)TBgw#Lx>b61XDyh&ne<^HD!)Rw~wMsCwPf z8gX5V$Vx;>aaK{B1As6x1Uw#Yjp$t5M_9NVj7>2`;FgNf;v?!I+x)7AaF^X>r^&Lo z(!mhLS$L5q$=r&-q9g>o`G{u2h;kGai^c3Qr(M;WC( zCPV@wfOJt)qK2YIa1&DqsqKc3AYdLJi7y=G`BC_Yo@4>|pyEQ5a@c7q9Hvq&bfc^T zK#~FdqlI468Aa3zq^s?kh{`%p#F(23A&H~*(RQuXT8<=As1=1lRR^z*aW2hsIci~2 z(4>T;mKBgaPSa52F|x+RX3kj4Neu+dM{&n#kF4EN1}&cF0;-~cFwCD}rF z(6WP{+uqx?Ji8Qu0@&-nP_5K71lC0w}pen0I`CZrvPBdVJ)0h zMpdmwRa-1%?X=HbDMKp(ce zLeULX>$F6P6IG4c5^ssuR>e9>IBp#ZP7o7I(1i{pS#bhZVoGclB25UYM>1VUiLr7*kA&1~~d&Z;ds%*pr4V5+lkH8hAr}aOEFR(T$ zZ@hM_8JW8Pgpo94h2!spQON~UX3ZE`aS?SECqu#Pp1C-_l1En>YABf$oer5Uv-M;ImpO8a+wu%QDOA9~v@WbuX Z0leb*jKKcEFY7@a3zjVnJiV}S{a^9hw=n e, Optional f, Unit g, List strings, Dispatches dispatches, - int intInRange, float floatInRange + int intInRange, float floatInRange, int argb, + int rgb ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -77,11 +79,14 @@ public String key() { var dispatches = builder.addOptional("dispatches", Dispatches.STRUCTURE, TestConfig::dispatches, () -> IdentityInterpreter.INSTANCE.interpret(Abc.STRUCTURE).getOrThrow()); var intInRange = builder.addOptional("intInRange", Structure.intInRange(10, 60), TestConfig::intInRange, () -> 50); var floatInRange = builder.addOptional("floatInRange", Structure.floatInRange(1.0f, 5.0f), TestConfig::floatInRange, () -> 3.0f); + var argb = builder.addOptional("argb", MinecraftStructures.ARGB_COLOR, TestConfig::argb, () -> 0xFF0000FF); + var rgb = builder.addOptional("rgb", MinecraftStructures.RGB_COLOR, TestConfig::rgb, () -> 0xFF0000); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), g.apply(container), strings.apply(container), dispatches.apply(container), - intInRange.apply(container), floatInRange.apply(container) + intInRange.apply(container), floatInRange.apply(container), argb.apply(container), + rgb.apply(container) ); }); From e0a45a9f572d4286a436f36e0bc2cf771d2def73 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 22:32:12 -0500 Subject: [PATCH 47/76] Support multiple config screens in one mod --- .../config/ConfigScreenBuilder.java | 78 +++++++++++++++++++ .../structured/config/ConfigScreenEntry.java | 9 +-- .../config/ConfigScreenInterpreter.java | 6 +- .../config/ScreenEntryProvider.java | 4 +- .../test/fabric/CodecExtrasModMenu.java | 5 +- .../test/neoforge/CodecExtrasTest.java | 5 +- 6 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java new file mode 100644 index 0000000..0165340 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java @@ -0,0 +1,78 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConfigScreenBuilder { + private record SingleScreen(ConfigScreenEntry screenEntry, Consumer onClose, DynamicOps ops, Supplier initialData) {} + + private final List> screens = new ArrayList<>(); + private Logger logger = LoggerFactory.getLogger(ConfigScreenBuilder.class); + + private ConfigScreenBuilder() {} + + public static ConfigScreenBuilder create() { + return new ConfigScreenBuilder(); + } + + public ConfigScreenBuilder logger(Logger logger) { + this.logger = logger; + return this; + } + + public ConfigScreenBuilder add(ConfigScreenEntry entry, Consumer onClose, DynamicOps ops, Supplier initialData) { + screens.add(new SingleScreen<>(entry, onClose, ops, initialData)); + return this; + } + + public UnaryOperator factory() { + if (screens.isEmpty()) { + throw new IllegalStateException("No screens have been added to the builder"); + } + return parent -> { + if (screens.size() == 1) { + var entry = screens.getFirst(); + return openSingleScreen(parent, entry); + } else { + return ScreenEntryProvider.create(new ScreenEntryProvider() { + @Override + public void onExit() {} + + @Override + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + for (var screen : screens) { + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, screen.screenEntry().entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var button = Button.builder(Component.translatable("codecextras.config.configurerecord"), b -> { + var newScreen = openSingleScreen(parent, screen); + Minecraft.getInstance().setScreen(newScreen); + }).width(Button.DEFAULT_WIDTH).build(); + screen.screenEntry().entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + label.setTooltip(tooltip); + button.setTooltip(tooltip); + }); + list.addPair(label, button); + } + } + }, parent, ComponentInfo.empty()); + } + }; + } + + private Screen openSingleScreen(Screen parent, SingleScreen entry) { + return entry.screenEntry().rootScreen(parent, entry.onClose(), entry.ops(), entry.initialData().get(), logger); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 8f943d3..a319a1b 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -10,7 +10,6 @@ import java.util.function.UnaryOperator; import net.minecraft.client.gui.screens.Screen; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public record ConfigScreenEntry(LayoutFactory widget, ScreenEntryFactory screenEntryProvider, EntryCreationInfo entryCreationInfo) implements App { @@ -42,11 +41,7 @@ public ConfigScreenEntry withEntryCreationInfo(Function onClose, DynamicOps ops, T initialData) { - return this.rootScreen(parent, onClose, ops, initialData, LoggerFactory.getLogger(ConfigScreenEntry.class)); - } - - public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, T initialData, Logger logger) { + Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, T initialData, Logger logger) { var initial = entryCreationInfo.codec().encodeStart(ops, initialData); JsonElement initialJson; if (initial.error().isPresent()) { @@ -63,6 +58,6 @@ public Screen rootScreen(Screen parent, Consumer onClose, DynamicOps DataResult>> list(App Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( ops, finalOriginal, update, creationInfo - ), parent, creationInfo)) + ), parent, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -348,7 +348,7 @@ public DataResult> record(List Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( ops, finalOriginal, update, creationInfo - ), parent, creationInfo)) + ), parent, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -448,7 +448,7 @@ public DataResult> dispatch(String key, Stru Component.translatable("codecextras.config.configurerecord"), b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( ops, finalOriginal, update, creationInfo - ), parent, creationInfo)) + ), parent, creationInfo.componentInfo())) ).width(width).build(); }), factory, diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java index db86d24..c40dc00 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java @@ -6,7 +6,7 @@ public interface ScreenEntryProvider { void onExit(); void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent); - static Screen create(ScreenEntryProvider provider, Screen parent, EntryCreationInfo info) { - return new EntryListScreen(parent, info.componentInfo().title(), provider); + static Screen create(ScreenEntryProvider provider, Screen parent, ComponentInfo componentInfo) { + return new EntryListScreen(parent, componentInfo.title(), provider); } } diff --git a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java index b25ec06..daa82a6 100644 --- a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java +++ b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java @@ -6,6 +6,7 @@ import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenBuilder; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.test.common.TestConfig; @@ -21,6 +22,8 @@ public ConfigScreenFactory getModConfigScreenFactory() { MinecraftInterpreters.CODEC_INTERPRETER ).interpret(TestConfig.STRUCTURE).getOrThrow(); - return parent -> entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()); + return parent -> ConfigScreenBuilder.create() + .add(entry, CONFIG::save, JsonOps.INSTANCE, CONFIG::load) + .factory().apply(parent); } } diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index c2cd57d..c1bff24 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -4,6 +4,7 @@ import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; +import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenBuilder; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; import dev.lukebemish.codecextras.test.common.TestConfig; @@ -23,7 +24,9 @@ public CodecExtrasTest(ModContainer modContainer) { ).interpret(TestConfig.STRUCTURE).getOrThrow(); modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> - entry.rootScreen(parent, CONFIG::save, JsonOps.INSTANCE, CONFIG.load()) + ConfigScreenBuilder.create() + .add(entry, CONFIG::save, JsonOps.INSTANCE, CONFIG::load) + .factory().apply(parent) ); } } From 4d08e7e4a4d5aa46e9c07811891aed413eea27ed Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 1 Sep 2024 23:45:02 -0500 Subject: [PATCH 48/76] Some javadoc work --- .../codecextras/structured/Annotation.java | 14 ++ .../structured/IdentityInterpreter.java | 16 +- .../codecextras/structured/Key.java | 16 ++ .../codecextras/structured/Keys.java | 43 +++++ .../codecextras/structured/Structure.java | 151 ++++++++++++++++-- .../codecextras/structured/package-info.java | 21 +++ .../structured/MinecraftInterpreters.java | 2 + .../structured/StreamCodecInterpreter.java | 21 ++- 8 files changed, 269 insertions(+), 15 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java index 581b2b9..1ea7a40 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java @@ -3,6 +3,10 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.Optional; +/** + * Annotations are metadata that can be attached to parts of structures to provide additional information to interpreters. + * This class contains some annotation keys recognized by built-in interpreters in CodecExtras. + */ public class Annotation { /** * A comment that a field in a structure should be serialized with. @@ -17,10 +21,20 @@ public class Annotation { */ public static final Key DESCRIPTION = Key.create("description"); + /** + * Retrieve an annotation value, if present, from a set of annotations. + * @param keys the annotations to search + * @param key the key of the annotation to retrieve + * @return the value of the annotation, if present + * @param the type of the annotation value + */ public static Optional get(Keys keys, Key key) { return keys.get(key).map(app -> Identity.unbox(app).value()); } + /** + * {@return an empty annotation set} + */ public static Keys empty() { return EMPTY; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 5639483..71a4ba1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -5,16 +5,30 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import org.jspecify.annotations.Nullable; /** - * Attempts to recover a default value from a structure by evaluating missing behaviours as if the value is missing + * Attempts to recover a default value from a structure by evaluating missing behaviours as if the value is missing. */ public class IdentityInterpreter implements Interpreter { + /** + * The singleton instance of this interpreter. + */ public static final IdentityInterpreter INSTANCE = new IdentityInterpreter(); + /** + * The key for this interpreter. + */ + public static final Key KEY = Key.create("IdentityInterpreter"); + + @Override + public Optional> key() { + return Optional.of(KEY); + } + @Override public DataResult>> list(App single) { return DataResult.error(() -> "No default value available for a list"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key.java b/src/main/java/dev/lukebemish/codecextras/structured/Key.java index 43fbd07..0485869 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Key.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key.java @@ -3,6 +3,10 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +/** + * A key which might be associated with a value. Keys carry a type parameter, and are compared by identity. + * @param the type parameter carried by the key, which may determine the type of the associated value + */ public final class Key implements App { public static final class Mu implements K1 { private Mu() { @@ -19,11 +23,23 @@ private Key(String name) { this.name = name; } + /** + * {@return a new key with the given name} + * Names are used for debugging purposes only, as keys are compared by identity. The name of the calling class will + * also be included in the key's name. + * @param name the name of the key + * @param the type parameter carried by the key + */ public static Key create(String name) { var className = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getSimpleName(); return new Key<>(className + ":" + name); } + /** + * {@return the name of the key} + * Names are used for debugging purposes only, as keys are compared by identity; two keys with the same name are not + * necessarily the same key. + */ public String name() { return name; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index fcf4d2e..fb24140 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -6,6 +6,12 @@ import java.util.Map; import java.util.Optional; +/** + * A collection of keys and their associated values. Each key is parameterized by a type extending {@code L}, and a + * value matching a given key will be of the type of {@code Mu} applied to the key's type. + * @param the type function mapping key type parameters to value types + * @param the bound on the key type parameters + */ public final class Keys { private final IdentityHashMap, App> keys; @@ -13,31 +19,68 @@ private Keys(IdentityHashMap, App> keys) { this.keys = keys; } + /** + * {@return the value associated with a key, if present} + * @param key the key to search for + * @param the type parameter of the value associated with the key + */ @SuppressWarnings("unchecked") public Optional> get(Key key) { return Optional.ofNullable((App) keys.get(key)); } + /** + * {@return a new instance with the same keys, with values whose type is the application of a different type function} + * @param converter converts {@code Mu} to {@code N} for each key's type parameter {@code T extends L} + * @param the type function associated with the new {@link Keys} + */ public Keys map(Converter converter) { var map = new IdentityHashMap, App>(); keys.forEach((key, value) -> map.put(key, converter.convert(value))); return new Keys<>(map); } + /** + * Effectively "lifts" values from {@code Mu} to {@code N}. Type parameters are bounded by {@code L}. + * @param + * @param + * @param + */ public interface Converter { + /** + * {@return a single value, converted} + * @param input the value to convert + * @param the type parameter of the value to convert + */ App convert(App input); } + /** + * {@return a new key set builder} + * @param the type function mapping key type parameters to value types + * @param the bound on the key type parameters + */ public static Builder builder() { return new Builder<>(); } + /** + * {@return a new key set combining this set with another} + * Values in the other set will overwrite values in this set if they share a key. + * @param other the other key set to combine with this one + */ public Keys join(Keys other) { var map = new IdentityHashMap<>(this.keys); map.putAll(other.keys); return new Keys<>(map); } + /** + * {@return a new key set with the single key-value pair provided added} + * @param key the key to add + * @param value the value to associate with the key + * @param the type parameter for the key + */ public Keys with(Key key, App value) { var map = new IdentityHashMap<>(this.keys); map.put(key, value); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6190ffa..0583725 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -14,18 +14,49 @@ import java.util.function.Supplier; import org.jspecify.annotations.Nullable; +/** + * Represents the structure of a data type in a generic form. This structure can then be interpreted into any number of + * specific representations, such as a {@link com.mojang.serialization.Codec}, by using the appropriate {@link Interpreter}. + * @param the type of data this structure represents + */ public interface Structure { + /** + * Use the structure to create a representation of the data type. + * @param interpreter contains the logic to create a specific type of representation from a structure + * @return the specific representation of the data type, boxed as an {@link App}, or an error if one could not be created + * @param the type function of the sort of specific representation + */ DataResult> interpret(Interpreter interpreter); + + /** + * {@return the annotations attached to this structure} + * Annotations are pieces of metadata attached to a structure which an interpreter may optionally use to mark up the + * result it produces; examples would include comments to attach to a codec that would show up in supported serialized + * data formats, or the like. + * @see Annotation + */ default Keys annotations() { return Annotation.empty(); } + /** + * {@return a new structure with the single provided annotation added} + * @param key the annotation key + * @param value the annotation value + * @param the type of the annotation + * @see Annotation + */ default Structure annotate(Key key, T value) { var outer = this; var annotations = annotations().with(key, new Identity<>(value)); return annotatedDelegatingStructure(outer, annotations); } + /** + * {@return a new structure with the provided annotations added} + * @param annotations the annotations to add + * @see Annotation + */ default Structure annotate(Keys annotations) { var outer = this; var combined = annotations().join(annotations); @@ -58,6 +89,9 @@ public Keys annotations() { return new AnnotatedDelegatingStructure(outer instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure ? annotatedDelegatingStructure : null); } + /** + * {@return a new structure representing a list of the current structure} + */ default Structure> listOf() { var outer = this; return new Structure<>() { @@ -80,16 +114,31 @@ default RecordStructure.Builder optionalFieldOf(String name, Supplier defa return builder -> builder.addOptional(name, this, Function.identity(), defaultValue); } + /** + * Like codecs, the type a structure represents can be changed without changing the actual underlying data structure, + * by providing conversion functions to and from the new type. + * @param deserializer converts the old type to the new type, if possible + * @param serializer converts the new type to the old type, if possible + * @return a new structure representing the new type + * @param the new type to represent + */ default Structure flatXmap(Function> deserializer, Function> serializer) { var outer = this; - return new Structure<>() { + return annotatedDelegatingStructure(new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); } - }; + }, outer.annotations()); } + /** + * Similar to {@link #flatXmap(Function, Function)} (Function, Function)}, except that the conversion functions are not allowed to fail. + * @param deserializer converts the old type to the new type + * @param serializer converts the new type to the old type + * @return a new structure representing the new type + * @param the new type to represent + */ default Structure xmap(Function deserializer, Function serializer) { return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); } @@ -104,6 +153,12 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * It might be necessary to lazily-initialize a structure to avoid circular static field dependencies. + * @param supplier the structure to lazily initialize + * @return a new structure that will initialize the wrapped structure when interpreted + * @param the type of data the structure represents + */ static Structure lazyInitialized(Supplier> supplier) { return new Structure<>() { @Override @@ -113,15 +168,14 @@ public DataResult> interpret(Interpreter interpre }; } - static Structure lazy(Supplier> supplier) { - return new Structure<>() { - @Override - public DataResult> interpret(Interpreter interpreter) { - return supplier.get().interpret(interpreter); - } - }; - } - + /** + * Keys provide a way of representing the smallest building blocks of a structure. Interpreters are responsible for + * finding a matching specific representation given a key when interpreting a structure. + * @param key the key which will be matched to a specific representation + * @return a new structure + * @param the type of data the structure represents + * @see Key + */ static Structure keyed(Key key) { return new Structure<>() { @Override @@ -131,6 +185,13 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Similar to {@link #keyed(Key)}, except a fallback structure is provided in case the interpreter cannot resolve the key. + * @param key the key which will be matched to a specific representation + * @param fallback the structure to interpret if the key cannot be resolved + * @return a new structure + * @param the type of data the structure represents + */ static Structure keyed(Key key, Structure fallback) { return new Structure<>() { @Override @@ -144,6 +205,17 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Similar to {@link #keyed(Key)}, except that specific representations may also be stored on the structure, by + * resolved interpreter-specific keys. The key set used is {@link Flip}ed so that the type parameterizing the key can + * be the type function of the interpreter. Interpreter keys are matched against the provided key set, and if missing + * the provided key is resolved by the interpreter. + * @param key the key which will be matched to a specific representation + * @param keys the set of specific representations to match against + * @return a new structure + * @param the type of data the structure represents + * @see Interpreter#key() + */ static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @Override @@ -198,41 +270,98 @@ static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } + /** + * Represents a {@link Unit} value. + */ Structure UNIT = keyed(Interpreter.UNIT); + /** + * Represents a {@code boolean} value. + */ Structure BOOL = keyed(Interpreter.BOOL); + /** + * Represents a {@code byte} value. + */ Structure BYTE = keyed(Interpreter.BYTE); + /** + * Represents a {@code short} value. + */ Structure SHORT = keyed(Interpreter.SHORT); + /** + * Represents an {@code int} value. + */ Structure INT = keyed(Interpreter.INT); + /** + * Represents a {@code long} value. + */ Structure LONG = keyed(Interpreter.LONG); + /** + * Represents a {@code float} value. + */ Structure FLOAT = keyed(Interpreter.FLOAT); + /** + * Represents a {@code double} value. + */ Structure DOUBLE = keyed(Interpreter.DOUBLE); + /** + * Represents a {@link String} value. + */ Structure STRING = keyed(Interpreter.STRING); + /** + * {@return a structure representing integer values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure intInRange(int min, int max) { return Structure.parametricallyKeyed(Interpreter.INT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + /** + * {@return a structure representing byte values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure byteInRange(byte min, byte max) { return Structure.parametricallyKeyed(Interpreter.BYTE_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + /** + * {@return a structure representing short values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure shortInRange(short min, short max) { return Structure.parametricallyKeyed(Interpreter.SHORT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + /** + * {@return a structure representing long values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure longInRange(long min, long max) { return Structure.parametricallyKeyed(Interpreter.LONG_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + /** + * {@return a structure representing float values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure floatInRange(float min, float max) { return Structure.parametricallyKeyed(Interpreter.FLOAT_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + /** + * {@return a structure representing double values within a range} + * @param min the minimum value (inclusive) + * @param max the maximum value (inclusive) + */ static Structure doubleInRange(double min, double max) { return Structure.parametricallyKeyed(Interpreter.DOUBLE_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/package-info.java index d2476f7..46903d8 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/package-info.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/package-info.java @@ -1,3 +1,24 @@ +/** + * CodecExtras' structure API allows you to create any number of representations of the structure of a type of data from + * a single shared representation. + *

+ * The core piece of this API is {@link dev.lukebemish.codecextras.structured.Structure}. + * If you define a {@code Structure} representing a data structure for some type {@code T}, you can then interpret + * that structure into any number of types parameterized by {@code T} dependent on that structure, say {@code F}, by + * using an appropriate {@link dev.lukebemish.codecextras.structured.Interpreter} of type {@code Interpreter}. + * For instance, you can turn a {@code Structure} into a {@link com.mojang.serialization.Codec} by using a + * {@link dev.lukebemish.codecextras.structured.CodecInterpreter}. + *

+ * CodecExtras' core module has a number of interpreter representations built in, including: + *

    + *
  • {@link dev.lukebemish.codecextras.structured.CodecInterpreter}, for creating a {@link com.mojang.serialization.Codec} + *
  • {@link dev.lukebemish.codecextras.structured.MapCodecInterpreter}, for creating a {@link com.mojang.serialization.MapCodec} + *
  • {@link dev.lukebemish.codecextras.structured.IdentityInterpreter}, which extracts the default value from a structure made up of optional components + *
  • {@link dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter}, which creates a JSON schema describing how a structure would be (de)serialized by a {@link com.mojang.serialization.Codec} + *
+ * The interpreter system is extensible, so you can implement your own interpreters for your own types. The {@code codecextras-minecraft} + * module provides a number of interpreters for Minecraft-specific types, including stream codecs and config screens. + */ @NullMarked @ApiStatus.Experimental package dev.lukebemish.codecextras.structured; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java index 5d67600..db8892a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -106,6 +106,7 @@ public App, App FRIENDLY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + StreamCodecInterpreter.FRIENDLY_BYTE_BUF_KEY, FRIENDLY_STREAM_KEYS, FRIENDLY_STREAM_PARAMETRIC_KEYS ); @@ -134,6 +135,7 @@ public App, App REGISTRY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( + StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, REGISTRY_STREAM_KEYS, REGISTRY_STREAM_PARAMETRIC_KEYS ); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 4b95407..bfe65a9 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -7,6 +7,7 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.structured.Interpreter; +import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; @@ -25,12 +26,16 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import org.jspecify.annotations.Nullable; public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { - public StreamCodecInterpreter(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { + private final Key> key; + + public StreamCodecInterpreter(Key> key, Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { super(keys.join(Keys., Object>builder() .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) @@ -51,8 +56,12 @@ public StreamCodecInterpreter(Keys, Object> keys, Keys2> FRIENDLY_BYTE_BUF_KEY = Key.create("StreamCodecInterpreter"); + public static final Key> REGISTRY_FRIENDLY_BYTE_BUF_KEY = Key.create("StreamCodecInterpreter"); + private static , B extends ByteBuf> ParametricKeyedValue, Const.Mu>, Const.Mu> numberRangeCodecParameter(StreamCodec codec) { return new ParametricKeyedValue<>() { @Override @@ -85,8 +94,9 @@ public void encode(B buffer, App, T> object2) { }; } - public StreamCodecInterpreter() { + public StreamCodecInterpreter(Key> key) { this( + key, Keys., Object>builder().build(), Keys2.>, K1, K1>builder().build() ); @@ -94,7 +104,7 @@ public StreamCodecInterpreter() { @Override public StreamCodecInterpreter with(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { - return new StreamCodecInterpreter<>(keys().join(keys), parametricKeys().join(parametricKeys)); + return new StreamCodecInterpreter<>(key, keys().join(keys), parametricKeys().join(parametricKeys)); } @Override @@ -212,6 +222,11 @@ public DataResult> interpret(Structure structure) { return structure.interpret(this).map(StreamCodecInterpreter::unbox); } + @Override + public Optional>> key() { + return Optional.of(key); + } + public record Holder(StreamCodec streamCodec) implements App, T> { public static final class Mu implements K1 {} From d8be8eed04325b7dce2982b57cde989783274428 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 2 Sep 2024 00:16:47 -0500 Subject: [PATCH 49/76] Fix edge case for missing values --- .../minecraft/structured/config/Widgets.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 8c3d119..977103c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -94,7 +94,11 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - Button.DEFAULT_SPACING; var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var object = new Object() { - private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, ops, original, update, creationInfo, false); + private JsonElement value = original; + private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, ops, original, json -> { + this.value = json; + update.accept(json); + }, creationInfo, false); private final Button disabled = Button.builder(Component.translatable("codecextras.config.missing"), b -> {}) .width(remainingWidth) .build(); @@ -113,6 +117,7 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon disabled.setHeight(maxHeight); disabled.visible = true; } else { + update.accept(value); wrapped.visitWidgets(w -> { w.visible = true; w.active = true; @@ -140,6 +145,10 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon lock.setTooltip(tooltip); disabled.setTooltip(tooltip); }); + + if (missing) { + update.accept(JsonNull.INSTANCE); + } } }; layout.addChild(object.lock, LayoutSettings.defaults().alignVerticallyMiddle()); From 2bc360304d0c76652942b4910cc418c8d89e7a61 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 2 Sep 2024 13:17:07 -0500 Subject: [PATCH 50/76] string-representable values and resource key support in config screens --- .../codecextras/StringRepresentation.java | 55 ++++++ .../structured/CodecInterpreter.java | 8 + .../codecextras/structured/Interpreter.java | 2 + .../codecextras/structured/Structure.java | 12 ++ .../schema/JsonSchemaInterpreter.java | 22 ++- .../structured/config/ChoiceScreen.java | 161 ++++++++++++++++++ .../config/ConfigScreenBuilder.java | 10 +- .../structured/config/ConfigScreenEntry.java | 19 +-- .../config/ConfigScreenInterpreter.java | 72 ++++++-- .../structured/config/DispatchPickScreen.java | 120 ------------- .../config/DispatchScreenEntryProvider.java | 13 +- .../config/EntryCreationContext.java | 51 ++++++ .../structured/config/LayoutFactory.java | 3 +- .../config/ListScreenEntryProvider.java | 10 +- .../config/RecordScreenEntryProvider.java | 13 +- .../structured/config/ScreenEntryFactory.java | 3 +- .../config/SingleScreenEntryProvider.java | 9 +- .../minecraft/structured/config/Widgets.java | 69 ++++++-- .../structured/StreamCodecInterpreter.java | 46 +++++ .../codecextras/test/common/TestConfig.java | 11 +- .../test/fabric/CodecExtrasModMenu.java | 4 +- .../test/neoforge/CodecExtrasTest.java | 4 +- 22 files changed, 516 insertions(+), 201 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/StringRepresentation.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ChoiceScreen.java delete mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java diff --git a/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java new file mode 100644 index 0000000..f2ca37e --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java @@ -0,0 +1,55 @@ +package dev.lukebemish.codecextras; + +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A representation of a type with finite possible values as strings. Values of the type should be comparable by identity + * @param values provides the possible (ordered) values of the type + * @param representation converts a value to a string + * @param the type of the values + */ +public record StringRepresentation(Supplier values, Function representation) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static StringRepresentation unbox(App box) { + return (StringRepresentation) box; + } + + public Codec codec() { + return Codec.lazyInitialized(() -> { + var values = this.values().get(); + var map = new HashMap(); + for (var value : values) { + map.put(this.representation().apply(value), value); + } + Function toString; + if (values.length > 16) { + toString = this.representation(); + } else { + Map representationMap = new IdentityHashMap<>(); + for (var entry : map.entrySet()) { + representationMap.put(entry.getValue(), entry.getKey()); + } + toString = representationMap::get; + } + return Codec.STRING.comapFlatMap(string -> { + T value = map.get(string); + if (value == null) { + return DataResult.error(() -> "Unknown string representation value: " + string); + } + return DataResult.success(value); + }, toString); + }); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 2d14dde..7ab7358 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -8,6 +8,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.HashMap; @@ -38,6 +39,13 @@ public CodecInterpreter(Keys keys, Keys2() { + @Override + public App> convert(App parameter) { + var representation = StringRepresentation.unbox(parameter); + return new Holder<>(representation.codec().xmap(Identity::new, app -> Identity.unbox(app).value())); + } + }) .build() )); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index ff88b4c..752a6b4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -5,6 +5,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Optional; @@ -46,4 +47,5 @@ default Optional> key() { Key2>, Const.Mu> LONG_IN_RANGE = Key2.create("long_in_range"); Key2>, Const.Mu> FLOAT_IN_RANGE = Key2.create("float_in_range"); Key2>, Const.Mu> DOUBLE_IN_RANGE = Key2.create("double_in_range"); + Key2 STRING_REPRESENTABLE = Key2.create("enum"); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 0583725..f1f4d77 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -5,6 +5,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; import java.util.List; @@ -366,4 +367,15 @@ static Structure doubleInRange(double min, double max) { return Structure.parametricallyKeyed(Interpreter.DOUBLE_IN_RANGE, Const.create(new Range<>(min, max)), app -> (Const) app) .xmap(Const::unbox, Const::create); } + + /** + * {@return a structure representing a type with finite possible values, each of which can be represented as a string} + * @param values provides the possible (ordered) values of the type + * @param representation converts a value to a string + * @param the type to represent + */ + static Structure stringRepresentable(Supplier values, Function representation) { + return Structure.parametricallyKeyed(Interpreter.STRING_REPRESENTABLE, new StringRepresentation<>(values, representation), app -> (Identity) app) + .xmap(i -> Identity.unbox(i).value(), Identity::new); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 098c1b5..6f93510 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -8,6 +8,7 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Interpreter; @@ -49,7 +50,24 @@ public JsonSchemaInterpreter( .add(Interpreter.DOUBLE, new Holder<>(NUMBER.get())) .add(Interpreter.STRING, new Holder<>(STRING.get())) .build() - ), parametricKeys); + ), parametricKeys.join(Keys2., K1, K1>builder() + .add(Interpreter.STRING_REPRESENTABLE, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var representation = StringRepresentation.unbox(parameter); + JsonArray oneOf = new JsonArray(); + for (var value : representation.values().get()) { + JsonObject object = new JsonObject(); + object.addProperty("const", representation.representation().apply(value)); + oneOf.add(object); + } + JsonObject schema = new JsonObject(); + schema.add("oneOf", oneOf); + return new Holder<>(schema); + } + }) + .build() + )); this.codecInterpreter = codecInterpreter; this.ops = ops; } @@ -222,7 +240,7 @@ private static Map> definitions(App box) { return Holder.unbox(box).definition; } - public DataResult partialInterpret(Structure structure) { + public DataResult nestedSchema(Structure structure) { return structure.interpret(this).map(JsonSchemaInterpreter::schemaValue); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ChoiceScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ChoiceScreen.java new file mode 100644 index 0000000..5928f01 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ChoiceScreen.java @@ -0,0 +1,161 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +class ChoiceScreen extends Screen { + private static final int KEYS_NEED_SEARCH = 10; + + private final Screen lastScreen; + private final HeaderAndFooterLayout layout; + private final Consumer<@Nullable String> onClose; + private @Nullable EntryList list; + private final List keys; + private @Nullable String selectedKey; + private String filter = ""; + private final Button doneButton = Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()).width(200).build(); + + public ChoiceScreen(Screen screen, Component title, List keys, @Nullable String selectedKey, Consumer<@Nullable String> onClose) { + super(title); + this.lastScreen = screen; + this.keys = keys; + this.selectedKey = selectedKey; + this.onClose = onClose; + if (keys.size() > KEYS_NEED_SEARCH) { + layout = new HeaderAndFooterLayout(this, HeaderAndFooterLayout.DEFAULT_HEADER_AND_FOOTER_HEIGHT + Button.DEFAULT_HEIGHT + Button.DEFAULT_SPACING, HeaderAndFooterLayout.DEFAULT_HEADER_AND_FOOTER_HEIGHT); + } else { + layout = new HeaderAndFooterLayout(this); + } + } + + protected void init() { + this.addHeader(); + + this.list = new EntryList(); + this.addContents(); + this.layout.addToContents(this.list); + this.updateButtonValidity(); + + this.addFooter(); + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + this.lastScreen.resize(this.minecraft, this.width, this.height); + this.layout.arrangeElements(); + if (this.list != null) { + this.list.updateSize(this.width, this.layout); + } + } + + public void added() { + super.added(); + } + + protected void addHeader() { + var title = new StringWidget(this.title, this.font); + var layout = LinearLayout.vertical().spacing(Button.DEFAULT_SPACING); + layout.addChild(title, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + if (keys.size() > KEYS_NEED_SEARCH) { + var search = new EditBox(this.font, 0, 0, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, Component.empty()); + search.setValue(this.filter); + search.setResponder(string -> { + if (this.list != null) { + this.filter = string; + this.list.clear(); + this.addContents(); + this.repositionElements(); + } + }); + layout.addChild(search, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); + } + this.layout.addToHeader(layout); + } + + protected void addContents() { + this.keys.forEach(key -> { + if (filter.isBlank() || key.contains(filter)) { + this.list.addEntry(list.new Entry(key)); + } + }); + this.list.setSelected(this.list.children().stream() + .filter(entry -> filter.isBlank() || entry.key.contains(filter)) + .filter(entry -> Objects.equals(entry.key, this.selectedKey)).findFirst().orElse(null) + ); + } + + protected void addFooter() { + this.layout.addToFooter(this.doneButton); + } + + public void onClose() { + this.onClose.accept(this.selectedKey); + this.minecraft.setScreen(this.lastScreen); + } + + private final class EntryList extends ObjectSelectionList { + private EntryList() { + super(ChoiceScreen.this.minecraft, ChoiceScreen.this.width, ChoiceScreen.this.layout.getContentHeight(), ChoiceScreen.this.layout.getHeaderHeight(), 16); + } + + public void setSelected(EntryList.@Nullable Entry entry) { + super.setSelected(entry); + if (entry != null) { + ChoiceScreen.this.selectedKey = entry.key; + } + + ChoiceScreen.this.updateButtonValidity(); + } + + @Override + public int addEntry(Entry entry) { + return super.addEntry(entry); + } + + public void clear() { + this.clearEntries(); + } + + private class Entry extends ObjectSelectionList.Entry { + final String key; + final Component name; + + public Entry(final String key) { + this.key = key; + this.name = Component.literal(key); + } + + public Component getNarration() { + return Component.translatable("narrator.select", this.name); + } + + public void render(GuiGraphics guiGraphics, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { + guiGraphics.drawString(ChoiceScreen.this.font, this.name, k + 5, j + 2, 0xFFFFFF); + } + + public boolean mouseClicked(double d, double e, int i) { + ChoiceScreen.EntryList.this.setSelected(this); + return super.mouseClicked(d, e, i); + } + } + } + + private void updateButtonValidity() { + this.doneButton.active = this.list.getSelected() != null; + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java index 0165340..caf48d2 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java @@ -1,7 +1,5 @@ package dev.lukebemish.codecextras.minecraft.structured.config; -import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -17,7 +15,7 @@ import org.slf4j.LoggerFactory; public class ConfigScreenBuilder { - private record SingleScreen(ConfigScreenEntry screenEntry, Consumer onClose, DynamicOps ops, Supplier initialData) {} + private record SingleScreen(ConfigScreenEntry screenEntry, Consumer onClose, Supplier context, Supplier initialData) {} private final List> screens = new ArrayList<>(); private Logger logger = LoggerFactory.getLogger(ConfigScreenBuilder.class); @@ -33,8 +31,8 @@ public ConfigScreenBuilder logger(Logger logger) { return this; } - public ConfigScreenBuilder add(ConfigScreenEntry entry, Consumer onClose, DynamicOps ops, Supplier initialData) { - screens.add(new SingleScreen<>(entry, onClose, ops, initialData)); + public ConfigScreenBuilder add(ConfigScreenEntry entry, Consumer onClose, Supplier context, Supplier initialData) { + screens.add(new SingleScreen<>(entry, onClose, context, initialData)); return this; } @@ -73,6 +71,6 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { } private Screen openSingleScreen(Screen parent, SingleScreen entry) { - return entry.screenEntry().rootScreen(parent, entry.onClose(), entry.ops(), entry.initialData().get(), logger); + return entry.screenEntry().rootScreen(parent, entry.onClose(), entry.context().get(), entry.initialData().get(), logger); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index a319a1b..5d11029 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -4,7 +4,6 @@ import com.google.gson.JsonNull; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; -import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -20,7 +19,7 @@ public static ConfigScreenEntry unbox(App app) { } public static ConfigScreenEntry single(LayoutFactory first, EntryCreationInfo entryCreationInfo) { - return new ConfigScreenEntry<>(first, (ops, original, onClose, creationInfo) -> new SingleScreenEntryProvider<>(original, first, ops, creationInfo, onClose), entryCreationInfo); + return new ConfigScreenEntry<>(first, (context, original, onClose, creationInfo) -> new SingleScreenEntryProvider<>(original, first, context, creationInfo, onClose), entryCreationInfo); } public ConfigScreenEntry withComponentInfo(UnaryOperator function) { @@ -29,20 +28,20 @@ public ConfigScreenEntry withComponentInfo(UnaryOperator funct public
ConfigScreenEntry withEntryCreationInfo(Function, EntryCreationInfo> function, Function, EntryCreationInfo> reverse) { return new ConfigScreenEntry<>( - (parent, width, ops, original, update, entry, handleOptional) -> { + (parent, width, context, original, update, entry, handleOptional) -> { var entryCreationInfo = reverse.apply(entry); - return this.widget.create(parent, width, ops, original, update, entryCreationInfo, handleOptional); + return this.widget.create(parent, width, context, original, update, entryCreationInfo, handleOptional); }, - (ops, original, onClose, entry) -> { + (context, original, onClose, entry) -> { var entryCreationInfo = reverse.apply(entry); - return this.screenEntryProvider.open(ops, original, onClose, entryCreationInfo); + return this.screenEntryProvider.open(context, original, onClose, entryCreationInfo); }, function.apply(this.entryCreationInfo) ); } - Screen rootScreen(Screen parent, Consumer onClose, DynamicOps ops, T initialData, Logger logger) { - var initial = entryCreationInfo.codec().encodeStart(ops, initialData); + Screen rootScreen(Screen parent, Consumer onClose, EntryCreationContext context, T initialData, Logger logger) { + var initial = entryCreationInfo.codec().encodeStart(context.ops(), initialData); JsonElement initialJson; if (initial.error().isPresent()) { logger.warn("Failed to encode `{}`: {}", initialData, initial.error().get().message()); @@ -50,8 +49,8 @@ Screen rootScreen(Screen parent, Consumer onClose, DynamicOps op } else { initialJson = initial.getOrThrow(); } - var provider = screenEntryProvider().open(ops, initialJson, json -> { - var decoded = entryCreationInfo.codec().parse(ops, json); + var provider = screenEntryProvider().open(context, initialJson, json -> { + var decoded = entryCreationInfo.codec().parse(context.ops(), json); if (decoded.error().isPresent()) { logger.warn("Failed to decode `{}`: {}", json, decoded.error().get().message()); } else { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index ad1e7aa..a8f87e3 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -11,6 +11,7 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; @@ -25,6 +26,7 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; public class ConfigScreenInterpreter extends KeyStoringInterpreter { @@ -117,11 +120,11 @@ public ConfigScreenInterpreter( new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), ComponentInfo.empty()) )) .add(Interpreter.STRING, ConfigScreenEntry.single( - Widgets.canHandleOptional(Widgets.text(DataResult::success, DataResult::success, false)), + Widgets.wrapWithOptionalHandling(Widgets.text(DataResult::success, DataResult::success, false)), new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) )) .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( - Widgets.canHandleOptional(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), false)), + Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), false)), new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) )) .add(MinecraftKeys.ARGB_COLOR, ConfigScreenEntry.single( @@ -266,6 +269,47 @@ public App, T>> convert(App() { + @Override + public App> convert(App parameter) { + var representation = StringRepresentation.unbox(parameter); + var codec = representation.codec(); + var identityCodec = codec.>xmap(Identity::new, app -> Identity.unbox(app).value()); + return ConfigScreenEntry.single( + Widgets.pickWidget(representation), + new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo(i -> i.withCodec(identityCodec), i -> i.withCodec(codec)); + } + }) + .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { + @SuppressWarnings("unchecked") + @Override + public App> convert(App parameter) { + var registryKey = MinecraftKeys.RegistryKeyHolder.unbox(parameter).value(); + var codec = ResourceKey.codec(registryKey); + var holderCodec = codec.>xmap(MinecraftKeys.ResourceKeyHolder::new, app -> MinecraftKeys.ResourceKeyHolder.unbox(app).value()); + return ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + var registry = context.registryAccess().registry(registryKey); + LayoutFactory> wrapped; + Function, String> mapper = key -> key.location().toString(); + if (registry.isPresent()) { + Supplier[]> values = () -> registry.get().registryKeySet().stream().sorted(Comparator., String>comparing(key -> key.location().getNamespace()).thenComparing(key -> key.location().getPath())).toArray(ResourceKey[]::new); + wrapped = Widgets.pickWidget(new StringRepresentation<>(values, mapper)); + } else { + wrapped = (parent2, width2, context2, original2, update2, creationInfo2, handleOptional2) -> Widgets.text( + ResourceLocation::read, + rl -> DataResult.success(rl.toString()), + string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), + false + ).create(parent2, width2, context2, original2, update2, creationInfo2.withCodec(ResourceLocation.CODEC), handleOptional2); + } + return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); + }, + new EntryCreationInfo<>(codec, ComponentInfo.empty()) + ).withEntryCreationInfo(i -> i.withCodec(holderCodec), i -> i.withCodec(codec)); + } + }) .build()) ); this.codecInterpreter = codecInterpreter; @@ -300,10 +344,10 @@ public DataResult>> list(App> factory = (ops, original, onClose, creationInfo) -> - new ListScreenEntryProvider<>(unwrapped, ops, original, onClose); + ScreenEntryFactory> factory = (context, original, onClose, creationInfo) -> + new ListScreenEntryProvider<>(unwrapped, context, original, onClose); return DataResult.success(new ConfigScreenEntry<>( - Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonArray(); update.accept(original); @@ -312,7 +356,7 @@ public DataResult>> list(App Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - ops, finalOriginal, update, creationInfo + context, finalOriginal, update, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), @@ -331,14 +375,14 @@ public DataResult> record(List "Errors crating record screen: "+errors.stream().map(Supplier::get).collect(Collectors.joining(", "))); } - ScreenEntryFactory factory = (ops, original, onClose, creationInfo) -> - new RecordScreenEntryProvider(entries, ops, original, onClose); + ScreenEntryFactory factory = (context, original, onClose, creationInfo) -> + new RecordScreenEntryProvider(entries, context, original, onClose); var codecResult = codecInterpreter.record(fields, creator); if (codecResult.isError()) { return DataResult.error(() -> "Error creating record codec: "+codecResult.error().orElseThrow().messageSupplier()); } return DataResult.success(new ConfigScreenEntry<>( - Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonObject(); update.accept(original); @@ -347,7 +391,7 @@ public DataResult> record(List Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - ops, finalOriginal, update, creationInfo + context, finalOriginal, update, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), @@ -435,10 +479,10 @@ public DataResult> dispatch(String key, Stru if (codecResult.isError()) { return DataResult.error(() -> "Error creating dispatch codec: "+codecResult.error().orElseThrow().messageSupplier()); } - ScreenEntryFactory factory = (ops, original, onClose, creationInfo) -> - new DispatchScreenEntryProvider<>(keyResult.getOrThrow().entryCreationInfo(), original, key, onClose, ops, entries.get()); + ScreenEntryFactory factory = (context, original, onClose, creationInfo) -> + new DispatchScreenEntryProvider<>(keyResult.getOrThrow().entryCreationInfo(), original, key, onClose, context, entries.get()); return DataResult.success(new ConfigScreenEntry<>( - Widgets.canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonObject(); update.accept(original); @@ -447,7 +491,7 @@ public DataResult> dispatch(String key, Stru return Button.builder( Component.translatable("codecextras.config.configurerecord"), b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - ops, finalOriginal, update, creationInfo + context, finalOriginal, update, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java deleted file mode 100644 index 9e63fc6..0000000 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchPickScreen.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.lukebemish.codecextras.minecraft.structured.config; - -import com.ibm.icu.text.Collator; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.function.Consumer; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.ObjectSelectionList; -import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.CommonComponents; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.Nullable; - -class DispatchPickScreen extends Screen { - private final Screen lastScreen; - private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); - private final Consumer<@Nullable String> onClose; - private @Nullable EntryList list; - private final List keys; - private @Nullable String selectedKey; - private final Button doneButton = Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()).width(200).build(); - - public DispatchPickScreen(Screen screen, Component title, List keys, @Nullable String selectedKey, Consumer<@Nullable String> onClose) { - super(title); - this.lastScreen = screen; - this.keys = keys; - this.selectedKey = selectedKey; - this.onClose = onClose; - } - - protected void init() { - this.addTitle(); - this.addContents(); - this.addFooter(); - this.layout.visitWidgets(this::addRenderableWidget); - this.repositionElements(); - } - - @Override - protected void repositionElements() { - this.lastScreen.resize(this.minecraft, this.width, this.height); - this.layout.arrangeElements(); - if (this.list != null) { - this.list.updateSize(this.width, this.layout); - } - } - - public void added() { - super.added(); - } - - protected void addTitle() { - this.layout.addTitleHeader(this.title, this.font); - } - - protected void addContents() { - this.list = new EntryList(); - this.list.setSelected(this.list.children().stream().filter((entry) -> Objects.equals(entry.key, this.selectedKey)).findFirst().orElse(null)); - this.layout.addToContents(this.list); - this.updateButtonValidity(); - } - - protected void addFooter() { - this.layout.addToFooter(this.doneButton); - } - - public void onClose() { - this.onClose.accept(this.selectedKey); - this.minecraft.setScreen(this.lastScreen); - } - - private final class EntryList extends ObjectSelectionList { - private EntryList() { - super(DispatchPickScreen.this.minecraft, DispatchPickScreen.this.width, DispatchPickScreen.this.layout.getContentHeight(), DispatchPickScreen.this.layout.getHeaderHeight(), 16); - Collator collator = Collator.getInstance(Locale.getDefault()); - DispatchPickScreen.this.keys.forEach(key -> { - this.addEntry(new Entry(key)); - }); - } - - public void setSelected(EntryList.@Nullable Entry entry) { - super.setSelected(entry); - if (entry != null) { - DispatchPickScreen.this.selectedKey = entry.key; - } - - DispatchPickScreen.this.updateButtonValidity(); - } - - private class Entry extends ObjectSelectionList.Entry { - final String key; - final Component name; - - public Entry(final String key) { - this.key = key; - this.name = Component.literal(key); - } - - public Component getNarration() { - return Component.translatable("narrator.select", this.name); - } - - public void render(GuiGraphics guiGraphics, int i, int j, int k, int l, int m, int n, int o, boolean bl, float f) { - guiGraphics.drawString(DispatchPickScreen.this.font, this.name, k + 5, j + 2, 0xFFFFFF); - } - - public boolean mouseClicked(double d, double e, int i) { - DispatchPickScreen.EntryList.this.setSelected(this); - return super.mouseClicked(d, e, i); - } - } - } - - private void updateButtonValidity() { - this.doneButton.active = this.list.getSelected() != null; - } -} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java index f3d1aa4..b4353bd 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java @@ -4,7 +4,6 @@ import com.google.gson.JsonObject; import com.mojang.logging.LogUtils; import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -32,9 +31,9 @@ class DispatchScreenEntryProvider implements ScreenEntryProvider { private final List keys; private final Map keyValues; private final Map> keyProviders; - private final DynamicOps ops; + private final EntryCreationContext context; - public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jsonValue, String key, Consumer update, DynamicOps ops, Map>> entries) { + public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jsonValue, String key, Consumer update, EntryCreationContext context, Map>> entries) { this.keyInfo = keyInfo; this.key = key; if (jsonValue.isJsonObject()) { @@ -46,12 +45,12 @@ public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jso this.jsonValue = new JsonObject(); } this.update = update; - this.ops = ops; + this.context = context; this.keys = new ArrayList<>(); this.keyValues = new HashMap<>(); this.keyProviders = new HashMap<>(); for (var entry : entries.entrySet()) { - var keyResult = keyInfo.codec().encodeStart(ops, entry.getKey()); + var keyResult = keyInfo.codec().encodeStart(context.ops(), entry.getKey()); if (keyResult.isError()) { LOGGER.warn("Failed to encode key {}", entry.getKey()); continue; @@ -95,7 +94,7 @@ public void onExit() { public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyInfo.componentInfo().title(), Minecraft.getInstance().font).alignLeft(); var contents = Button.builder(Component.literal(keyValue == null ? "" : keyValue), b -> { - Minecraft.getInstance().setScreen(new DispatchPickScreen(parent, keyInfo.componentInfo().title(), keys, keyValue, newKeyValue -> { + Minecraft.getInstance().setScreen(new ChoiceScreen(parent, keyInfo.componentInfo().title(), keys, keyValue, newKeyValue -> { if (!Objects.equals(newKeyValue, keyValue)) { keyValue = newKeyValue; jsonValue = new JsonObject(); @@ -123,7 +122,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { } private void addEntry(ConfigScreenEntry provider, JsonObject valueCopy, ScreenEntryList list, Runnable rebuild, Screen parent) { - var entryProvider = provider.screenEntryProvider().open(ops, valueCopy, newValue -> { + var entryProvider = provider.screenEntryProvider().open(context, valueCopy, newValue -> { if (newValue.isJsonObject()) { for (var entry : newValue.getAsJsonObject().entrySet()) { if (entry.getKey().equals(key)) { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java new file mode 100644 index 0000000..59af829 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java @@ -0,0 +1,51 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import java.util.Objects; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; + +public class EntryCreationContext { + private final DynamicOps ops; + private final RegistryAccess registryAccess; + + private EntryCreationContext(DynamicOps ops, RegistryAccess registryAccess) { + this.ops = ops; + this.registryAccess = registryAccess; + } + + public DynamicOps ops() { + return ops; + } + + public RegistryAccess registryAccess() { + return registryAccess; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private DynamicOps ops = JsonOps.INSTANCE; + private RegistryAccess registryAccess = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + + private Builder() {} + + public Builder ops(DynamicOps ops) { + this.ops = ops; + return this; + } + + public Builder registryAccess(RegistryAccess registryAccess) { + this.registryAccess = registryAccess; + return this; + } + + public EntryCreationContext build() { + return new EntryCreationContext(Objects.requireNonNull(ops), registryAccess); + } + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java index 26545b3..6624732 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/LayoutFactory.java @@ -1,11 +1,10 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.screens.Screen; public interface LayoutFactory { - LayoutElement create(Screen parent, int width, DynamicOps ops, JsonElement original, Consumer update, EntryCreationInfo creationInfo, boolean handleOptional); + LayoutElement create(Screen parent, int width, EntryCreationContext context, JsonElement original, Consumer update, EntryCreationInfo creationInfo, boolean handleOptional); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java index 75a811c..0ad1483 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java @@ -4,7 +4,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.mojang.logging.LogUtils; -import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; @@ -21,9 +20,9 @@ class ListScreenEntryProvider implements ScreenEntryProvider { private final ConfigScreenEntry entry; private final JsonArray jsonValue; private final Consumer update; - private final DynamicOps ops; + private final EntryCreationContext context; - ListScreenEntryProvider(ConfigScreenEntry entry, DynamicOps ops, JsonElement jsonValue, Consumer update) { + ListScreenEntryProvider(ConfigScreenEntry entry, EntryCreationContext context, JsonElement jsonValue, Consumer update) { this.entry = entry; if (jsonValue.isJsonArray()) { this.jsonValue = jsonValue.getAsJsonArray(); @@ -34,7 +33,7 @@ class ListScreenEntryProvider implements ScreenEntryProvider { this.jsonValue = new JsonArray(); } this.update = update; - this.ops = ops; + this.context = context; } @Override @@ -85,8 +84,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { layout.addChild(entry.widget().create( parent, remainingWidth, - ops, - jsonValue.get(index), + context, jsonValue.get(index), newValue -> this.jsonValue.set(index, newValue), entry.entryCreationInfo(), false ), LayoutSettings.defaults().alignVerticallyMiddle()); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java index 579b46c..c42556d 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java @@ -4,7 +4,6 @@ import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.mojang.logging.LogUtils; -import com.mojang.serialization.DynamicOps; import java.util.List; import java.util.function.Consumer; import net.minecraft.client.Minecraft; @@ -21,9 +20,9 @@ class RecordScreenEntryProvider implements ScreenEntryProvider { private final List> entries; private final JsonObject jsonValue; private final Consumer update; - private final DynamicOps ops; + private final EntryCreationContext context; - RecordScreenEntryProvider(List> entries, DynamicOps ops, JsonElement jsonValue, Consumer update) { + RecordScreenEntryProvider(List> entries, EntryCreationContext context, JsonElement jsonValue, Consumer update) { this.entries = entries; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); @@ -34,7 +33,7 @@ class RecordScreenEntryProvider implements ScreenEntryProvider { this.jsonValue = new JsonObject(); } this.update = update; - this.ops = ops; + this.context = context; } @Override @@ -60,7 +59,7 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp // If this is missing, missing values are just not allowed var defaultValue = entry.missingBehavior().map(behavior -> { var value = behavior.missing().get(); - var encoded = entry.codec().encodeStart(ops, value); + var encoded = entry.codec().encodeStart(context.ops(), value); if (encoded.error().isPresent()) { // The default value is unencodeable, so we have to handle missing values in the widget return JsonNull.INSTANCE; @@ -68,7 +67,7 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp return encoded.result().orElseThrow(); }); JsonElement specificValueWithDefault = specificValue.isJsonNull() && defaultValue.isPresent() ? defaultValue.get() : specificValue; - return entry.entry().widget().create(parent, Button.DEFAULT_WIDTH, ops, specificValueWithDefault, newValue -> { + return entry.entry().widget().create(parent, Button.DEFAULT_WIDTH, context, specificValueWithDefault, newValue -> { if (shouldUpdate(newValue, entry)) { this.jsonValue.add(entry.key(), newValue); } else { @@ -82,7 +81,7 @@ private boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { return false; } if (entry.missingBehavior().isPresent()) { - var decoded = entry.codec().parse(this.ops, newValue); + var decoded = entry.codec().parse(this.context.ops(), newValue); if (decoded.isError()) { LOGGER.warn("Could not encode new value {}", newValue); return false; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java index 4ab873c..76c7320 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java @@ -1,9 +1,8 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; public interface ScreenEntryFactory { - ScreenEntryProvider open(DynamicOps ops, JsonElement original, Consumer onClose, EntryCreationInfo entry); + ScreenEntryProvider open(EntryCreationContext context, JsonElement original, Consumer onClose, EntryCreationInfo entry); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java index 3ac1f12..db3effc 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java @@ -1,23 +1,22 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; -import com.mojang.serialization.DynamicOps; import java.util.function.Consumer; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.Screen; class SingleScreenEntryProvider implements ScreenEntryProvider { private static final int FULL_WIDTH = Button.DEFAULT_WIDTH * 2 + EntryListScreen.Entry.SPACING; - private final DynamicOps ops; + private final EntryCreationContext context; private final EntryCreationInfo creationInfo; private final Consumer update; private JsonElement value; private final LayoutFactory first; - SingleScreenEntryProvider(JsonElement original, LayoutFactory first, DynamicOps ops, EntryCreationInfo creationInfo, Consumer update) { + SingleScreenEntryProvider(JsonElement original, LayoutFactory first, EntryCreationContext context, EntryCreationInfo creationInfo, Consumer update) { this.value = original; this.first = first; - this.ops = ops; + this.context = context; this.creationInfo = creationInfo; this.update = update; } @@ -29,6 +28,6 @@ public void onExit() { @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { - list.addSingle(first.create(parent, FULL_WIDTH, ops, value, newValue -> value = newValue, creationInfo, false)); + list.addSingle(first.create(parent, FULL_WIDTH, context, value, newValue -> value = newValue, creationInfo, false)); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 977103c..43cdbf9 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -5,9 +5,14 @@ import com.google.gson.JsonPrimitive; import com.mojang.logging.LogUtils; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Range; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; @@ -28,12 +33,11 @@ public final class Widgets { private static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); - private static final int BORDER_COLOR = 0xFFA0A0A0; private Widgets() {} public static LayoutFactory text(Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { - return (parent, width, ops, original, update, creationInfo, handleOptional) -> { + return (parent, width, context, original, update, creationInfo, handleOptional) -> { var widget = new EditBox(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().title()); creationInfo.componentInfo().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); @@ -47,7 +51,7 @@ public static LayoutFactory text(Function> toData, update.accept(original); } - var decoded = creationInfo.codec().parse(ops, original); + var decoded = creationInfo.codec().parse(context.ops(), original); if (decoded.isError()) { LOGGER.warn("Failed to decode `{}`: {}", original, decoded.error().orElseThrow().message()); } else { @@ -69,7 +73,7 @@ public static LayoutFactory text(Function> toData, if (dataResult.error().isPresent()) { LOGGER.warn("Failed to encode `{}` as data: {}", string, dataResult.error().get().message()); } else { - var jsonResult = creationInfo.codec().encodeStart(ops, dataResult.getOrThrow()); + var jsonResult = creationInfo.codec().encodeStart(context.ops(), dataResult.getOrThrow()); if (jsonResult.error().isPresent()) { LOGGER.warn("Failed to encode `{}` as json: {}", dataResult.getOrThrow(), jsonResult.error().get().message()); } else { @@ -86,16 +90,53 @@ public static LayoutFactory text(Function> toData, return text(toData, fromData, s -> true, emptyIsMissing); } - public static LayoutFactory canHandleOptional(LayoutFactory assumesNonOptional) { - return (parent, fullWidth, ops, original, update, creationInfo, handleOptional) -> { + public static LayoutFactory pickWidget(StringRepresentation representation) { + return wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { + String[] stringValue = new String[1]; + if (original.isJsonPrimitive()) { + if (original.getAsJsonPrimitive().isString()) { + stringValue[0] = original.getAsJsonPrimitive().getAsString(); + } else { + LOGGER.warn("Failed to decode `{}`: not a string", original); + } + } else if (!original.isJsonNull()) { + LOGGER.warn("Failed to decode `{}`: not a primitive or null", original); + } + List values = new ArrayList<>(); + for (var value : representation.values().get()) { + var valueRepresentation = representation.representation().apply(value); + values.add(valueRepresentation); + } + Supplier calculateMessage = () -> Component.literal(stringValue[0] == null ? "" : stringValue[0]); + var holder = new Object() { + private final Button button = Button.builder(calculateMessage.get(), b -> { + Minecraft.getInstance().setScreen(new ChoiceScreen(parent, creationInfo.componentInfo().title(), values, stringValue[0], newKeyValue -> { + if (!Objects.equals(newKeyValue, stringValue[0])) { + stringValue[0] = newKeyValue; + if (newKeyValue == null) { + update.accept(JsonNull.INSTANCE); + } else { + update.accept(new JsonPrimitive(newKeyValue)); + } + this.button.setMessage(calculateMessage.get()); + } + })); + }).tooltip(Tooltip.create(creationInfo.componentInfo().description())).build(); + }; + return holder.button; + }); + } + + public static LayoutFactory wrapWithOptionalHandling(LayoutFactory assumesNonOptional) { + return (parent, fullWidth, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional) { - return assumesNonOptional.create(parent, fullWidth, ops, original, update, creationInfo, false); + return assumesNonOptional.create(parent, fullWidth, context, original, update, creationInfo, false); } var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - Button.DEFAULT_SPACING; var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var object = new Object() { private JsonElement value = original; - private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, ops, original, json -> { + private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, context, original, json -> { this.value = json; update.accept(json); }, creationInfo, false); @@ -161,7 +202,7 @@ public static LayoutFactory canHandleOptional(LayoutFactory assumesNon } public static LayoutFactory color(boolean includeAlpha) { - return canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + return wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonPrimitive(0); update.accept(original); @@ -218,7 +259,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { } public static > LayoutFactory slider(Range range, Function> toJson, Function> fromJson, boolean isDoubleLike) { - return canHandleOptional((parent, width, ops, original, update, creationInfo, handleOptional) -> { + return wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonPrimitive(range.min()); update.accept(original); @@ -285,7 +326,7 @@ private static > double valueInRange(Range r } public static LayoutFactory bool(boolean falseIfMissing) { - LayoutFactory widget = (parent, width, ops, original, update, creationInfo, handleOptional) -> { + LayoutFactory widget = (parent, width, context, original, update, creationInfo, handleOptional) -> { if (!handleOptional && original.isJsonNull()) { original = new JsonPrimitive(false); update.accept(original); @@ -308,11 +349,11 @@ public static LayoutFactory bool(boolean falseIfMissing) { return w; }; if (!falseIfMissing) { - return canHandleOptional(widget); + return wrapWithOptionalHandling(widget); } - return (parent, width, ops, original, update, entry, handleOptional) -> { + return (parent, width, context, original, update, entry, handleOptional) -> { if (handleOptional) { - return widget.create(parent, width, ops, original, update, entry, true); + return widget.create(parent, width, context, original, update, entry, true); } var button = Button.builder(Component.translatable("codecextras.config.unit"), b -> {}) .width(width) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index bfe65a9..e12e9f0 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -6,6 +6,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Interpreter; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.KeyStoringInterpreter; @@ -20,6 +21,7 @@ import io.netty.handler.codec.DecoderException; import java.util.ArrayList; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -54,6 +56,50 @@ public StreamCodecInterpreter(Key> key, Keys, Object> .add(Interpreter.LONG_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.VAR_LONG.cast())) .add(Interpreter.FLOAT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.FLOAT.cast())) .add(Interpreter.DOUBLE_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.DOUBLE.cast())) + .add(Interpreter.STRING_REPRESENTABLE, new ParametricKeyedValue<>() { + @Override + public App, App> convert(App parameter) { + var representation = StringRepresentation.unbox(parameter); + Supplier> lazy = Suppliers.memoize(() -> { + var values = representation.values().get(); + Map toIndexMap = new IdentityHashMap<>(); + for (int i = 0; i < values.length; i++) { + toIndexMap.put(values[i], i); + } + return new StreamCodec() { + @Override + public T decode(B buffer) { + var intValue = buffer.readInt(); + if (intValue < 0 || intValue >= values.length) { + throw new DecoderException("Unknown representation value: " + intValue); + } + return values[intValue]; + } + + @Override + public void encode(B buffer, T object) { + var index = toIndexMap.get(object); + if (index == null) { + throw new DecoderException("Unknown representation value: " + object); + } + buffer.writeInt(index); + } + }; + }); + return new Holder<>(new StreamCodec<>() { + @Override + public App decode(B buffer) { + return new Identity<>(lazy.get().decode(buffer)); + } + + @Override + public void encode(B buffer, App object) { + var value = Identity.unbox(object).value(); + lazy.get().encode(buffer, value); + } + }); + } + }) .build() )); this.key = key; diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 66e5cbb..b6498a7 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -14,13 +14,18 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import net.minecraft.core.registries.Registries; +import net.minecraft.references.Items; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Rarity; public record TestConfig( int a, float b, boolean c, String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches, int intInRange, float floatInRange, int argb, - int rgb + int rgb, ResourceKey item, Rarity rarity ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -81,12 +86,14 @@ public String key() { var floatInRange = builder.addOptional("floatInRange", Structure.floatInRange(1.0f, 5.0f), TestConfig::floatInRange, () -> 3.0f); var argb = builder.addOptional("argb", MinecraftStructures.ARGB_COLOR, TestConfig::argb, () -> 0xFF0000FF); var rgb = builder.addOptional("rgb", MinecraftStructures.RGB_COLOR, TestConfig::rgb, () -> 0xFF0000); + var item = builder.addOptional("item", MinecraftStructures.resourceKey(Registries.ITEM), TestConfig::item, () -> Items.MELON_SEEDS); + var rarity = builder.addOptional("rarity", Structure.stringRepresentable(Rarity::values, Rarity::getSerializedName), TestConfig::rarity, () -> Rarity.COMMON); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), g.apply(container), strings.apply(container), dispatches.apply(container), intInRange.apply(container), floatInRange.apply(container), argb.apply(container), - rgb.apply(container) + rgb.apply(container), item.apply(container), rarity.apply(container) ); }); diff --git a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java index daa82a6..dbab268 100644 --- a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java +++ b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/CodecExtrasModMenu.java @@ -1,6 +1,5 @@ package dev.lukebemish.codecextras.test.fabric; -import com.mojang.serialization.JsonOps; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import dev.lukebemish.codecextras.config.ConfigType; @@ -9,6 +8,7 @@ import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenBuilder; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; +import dev.lukebemish.codecextras.minecraft.structured.config.EntryCreationContext; import dev.lukebemish.codecextras.test.common.TestConfig; import net.fabricmc.loader.api.FabricLoader; @@ -23,7 +23,7 @@ public ConfigScreenFactory getModConfigScreenFactory() { ).interpret(TestConfig.STRUCTURE).getOrThrow(); return parent -> ConfigScreenBuilder.create() - .add(entry, CONFIG::save, JsonOps.INSTANCE, CONFIG::load) + .add(entry, CONFIG::save, () -> EntryCreationContext.builder().build(), CONFIG::load) .factory().apply(parent); } } diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index c1bff24..0c7bb58 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -1,12 +1,12 @@ package dev.lukebemish.codecextras.test.neoforge; -import com.mojang.serialization.JsonOps; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.config.GsonOpsIo; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenBuilder; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenEntry; import dev.lukebemish.codecextras.minecraft.structured.config.ConfigScreenInterpreter; +import dev.lukebemish.codecextras.minecraft.structured.config.EntryCreationContext; import dev.lukebemish.codecextras.test.common.TestConfig; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; @@ -25,7 +25,7 @@ public CodecExtrasTest(ModContainer modContainer) { modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> ConfigScreenBuilder.create() - .add(entry, CONFIG::save, JsonOps.INSTANCE, CONFIG::load) + .add(entry, CONFIG::save, () -> EntryCreationContext.builder().build(), CONFIG::load) .factory().apply(parent) ); } From 1147a34f02689de353e3104dc3810c07fb8c292c Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 2 Sep 2024 13:31:58 -0500 Subject: [PATCH 51/76] Slight tweak for better visibility in color pick widget --- .../structured/config/ColorPickWidget.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java index 551f123..8a47160 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java @@ -25,6 +25,7 @@ class ColorPickWidget extends AbstractWidget { private double value; private int fullySaturated; + private int inverted; ColorPickWidget(int i, int j, Component component, Consumer consumer, boolean hasAlpha) { super(i, j, calculateWidth(hasAlpha), 128 + 2, component); @@ -56,6 +57,7 @@ public void setColor(int argbColor) { } this.fullySaturated = 0xFF000000 | toRgb(hue, 1.0, 1.0); + this.inverted = 0xFF000000 | toRgb(1.0 - hue, 1.0, 1.0 - value); } private static int toRgb(double hue, double saturation, double value) { @@ -124,13 +126,6 @@ private static double hue(double r, double g, double b) { return h; } - private int invert(int rgb) { - int r = 255 - (rgb >> 16 & 255); - int g = 255 - (rgb >> 8 & 255); - int b = 255 - (rgb & 255); - return r << 16 | g << 8 | b; - } - @Override protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { if (!this.visible) { @@ -157,8 +152,8 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { guiGraphics.enableScissor(x1, y1, x2, y2); int xCenter = (int) (x1 + saturation * 127); int yCenter = (int) (y1 + (1 - value) * 127); - guiGraphics.fill(xCenter-2, yCenter, xCenter+3, yCenter+1, invert(color) | 0xFF000000); - guiGraphics.fill(xCenter, yCenter-2, xCenter+1, yCenter+3, invert(color) | 0xFF000000); + guiGraphics.fill(xCenter-2, yCenter, xCenter+3, yCenter+1, inverted); + guiGraphics.fill(xCenter, yCenter-2, xCenter+1, yCenter+3, inverted); guiGraphics.disableScissor(); guiGraphics.blitSprite(HUE, x1+128+8+2, y1, 8, 128); From 6168d5b96d7c7629fd390927c47fb9e7133904b8 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 2 Sep 2024 17:31:23 -0500 Subject: [PATCH 52/76] Either and unbounded map support in structures --- .../structured/CodecInterpreter.java | 15 ++ .../structured/IdentityInterpreter.java | 12 ++ .../codecextras/structured/Interpreter.java | 6 + .../structured/MapCodecInterpreter.java | 14 ++ .../codecextras/structured/Structure.java | 61 +++++++- .../schema/JsonSchemaInterpreter.java | 26 ++++ .../structured/config/ColorPickWidget.java | 49 +++++-- .../config/ConfigScreenInterpreter.java | 69 ++++++++- .../UnboundedMapScreenEntryProvider.java | 137 ++++++++++++++++++ .../minecraft/structured/config/Widgets.java | 120 ++++++++++++++- .../structured/StreamCodecInterpreter.java | 41 +++++- .../codecextras_minecraft/lang/en_us.json | 3 +- .../codecextras/test/common/TestConfig.java | 9 +- 13 files changed, 524 insertions(+), 38 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 7ab7358..922306f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -118,6 +119,20 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public DataResult>> unboundedMap(App key, App value) { + var keyCodec = unbox(key); + var valueCodec = unbox(value); + return DataResult.success(new Holder<>(Codec.unboundedMap(keyCodec, valueCodec))); + } + + @Override + public DataResult>> either(App left, App right) { + var leftCodec = unbox(left); + var rightCodec = unbox(right); + return DataResult.success(new Holder<>(Codec.either(leftCodec, rightCodec))); + } + @Override public CodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new CodecAndMapInterpreters(keys().join(keys), mapCodecInterpreter().keys(), parametricKeys().join(parametricKeys), mapCodecInterpreter().parametricKeys()).codecInterpreter(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 71a4ba1..67f2c02 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -2,9 +2,11 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -29,6 +31,11 @@ public Optional> key() { return Optional.of(KEY); } + @Override + public DataResult>> unboundedMap(App key, App value) { + return DataResult.error(() -> "No default value available for an unbounded map"); + } + @Override public DataResult>> list(App single) { return DataResult.error(() -> "No default value available for a list"); @@ -84,6 +91,11 @@ public DataResult "No default value available for a parametric key"); } + @Override + public DataResult>> either(App left, App right) { + return DataResult.error(() -> "No default value available for an either"); + } + public DataResult interpret(Structure structure) { return structure.interpret(this).map(i -> Identity.unbox(i).value()); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 752a6b4..e8464d3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -3,11 +3,13 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -29,6 +31,8 @@ default Optional> key() { return Optional.empty(); } + DataResult>> unboundedMap(App key, App value); + Key UNIT = Key.create("UNIT"); Key BOOL = Key.create("BOOL"); Key BYTE = Key.create("BYTE"); @@ -48,4 +52,6 @@ default Optional> key() { Key2>, Const.Mu> FLOAT_IN_RANGE = Key2.create("float_in_range"); Key2>, Const.Mu> DOUBLE_IN_RANGE = Key2.create("double_in_range"); Key2 STRING_REPRESENTABLE = Key2.create("enum"); + + DataResult>> either(App left, App right); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index c951ff1..ae76106 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -3,6 +3,8 @@ import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.KeyDispatchCodec; @@ -96,6 +98,18 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public DataResult>> unboundedMap(App key, App value) { + return DataResult.error(() -> "Cannot make a MapCodec for an unbounded map"); + } + + @Override + public DataResult>> either(App left, App right) { + var leftCodec = unbox(left); + var rightCodec = unbox(right); + return DataResult.success(new Holder<>(Codec.mapEither(leftCodec, rightCodec))); + } + @Override public MapCodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new CodecAndMapInterpreters(codecInterpreter().keys(), keys().join(keys), codecInterpreter().parametricKeys(), parametricKeys().join(parametricKeys)).mapCodecInterpreter(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index f1f4d77..c472471 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -3,12 +3,14 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -103,6 +105,28 @@ public DataResult>> interpret(Interpreter in }; } + static Structure> unboundedMap(Structure key, Structure value) { + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + return key.interpret(interpreter).flatMap(k -> value.interpret(interpreter).flatMap(v -> interpreter.unboundedMap(k, v))); + } + }; + } + + static Structure> either(Structure left, Structure right) { + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + var leftResult = left.interpret(interpreter); + var rightResult = right.interpret(interpreter); + return leftResult + .mapError(s -> rightResult.error().map(e -> s + "; " + e.message()).orElse(s)) + .flatMap(leftApp -> rightResult.flatMap(rightApp -> interpreter.either(leftApp, rightApp))); + } + }; + } + default RecordStructure.Builder fieldOf(String name) { return builder -> builder.add(name, this, Function.identity()); } @@ -199,7 +223,7 @@ static Structure keyed(Key key, Structure fallback) { public DataResult> interpret(Interpreter interpreter) { var result = interpreter.keyed(key); if (result.error().isPresent()) { - return fallback.interpret(interpreter); + return fallback.interpret(interpreter).mapError(s -> "Could not interpret keyed structure: "+s+"; "+result.error().orElseThrow().message()); } return result; } @@ -228,6 +252,22 @@ public DataResult> interpret(Interpreter interpre }; } + + static Structure keyed(Key key, Keys, K1> keys, Structure fallback) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + var result = interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) + .map(DataResult::success) + .orElseGet(() -> interpreter.keyed(key)); + if (result.error().isPresent()) { + return fallback.interpret(interpreter).mapError(s -> "Could not interpret keyed structure: "+s+"; "+result.error().orElseThrow().message()); + } + return result; + } + }; + } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer) { return new Structure<>() { @Override @@ -247,7 +287,7 @@ public DataResult> interpret(Interpreter interpre interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) ); if (result.error().isPresent()) { - return fallback.interpret(interpreter); + return fallback.interpret(interpreter).mapError(s -> "Could not interpret parametrically keyed structure: "+s+"; "+result.error().orElseThrow().message()); } return result; } @@ -267,6 +307,23 @@ public DataResult> interpret(Interpreter interpre }; } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys, Structure fallback) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + var result = interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) + .map(DataResult::success) + .orElseGet(() -> interpreter.parametricallyKeyed(key, parameter).flatMap(app -> + interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) + )); + if (result.error().isPresent()) { + return fallback.interpret(interpreter).mapError(s -> "Could not interpret parametrically keyed structure: "+s+"; "+result.error().orElseThrow().message()); + } + return result; + } + }; + } + static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 6f93510..94aa550 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -5,6 +5,7 @@ import com.google.gson.JsonObject; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; @@ -232,6 +233,31 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public DataResult>> unboundedMap(App key, App value) { + var schema = OBJECT.get(); + var definitions = new HashMap>(); + schema.add("additionalProperties", schemaValue(value)); + definitions.putAll(definitions(value)); + definitions.putAll(definitions(key)); + return DataResult.success(new Holder<>(schema, definitions)); + } + + @Override + public DataResult>> either(App left, App right) { + var schema = new JsonObject(); + var oneOf = new JsonArray(); + var definitions = new HashMap>(); + var leftSchema = schemaValue(left); + var rightSchema = schemaValue(right); + definitions.putAll(definitions(left)); + definitions.putAll(definitions(right)); + oneOf.add(leftSchema); + oneOf.add(rightSchema); + schema.add("oneOf", oneOf); + return DataResult.success(new Holder<>(schema, definitions)); + } + private static JsonObject schemaValue(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java index 8a47160..382370a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java @@ -37,6 +37,11 @@ private static int calculateWidth(boolean alpha) { return 128 + 8*(alpha ? 4 : 2) + 2*(alpha ? 3 : 2); } + private void recalculateInternal() { + this.fullySaturated = 0xFF000000 | toRgb(hue, 1.0, 1.0); + this.inverted = 0xFF000000 | toRgb((hue + 0.5) % 1, 1.0, 1.0 - value); + } + public void setColor(int argbColor) { this.color = argbColor; @@ -46,18 +51,10 @@ public void setColor(int argbColor) { this.alpha = (argbColor >> 24 & 255) / 255.0F; this.value = value(r, g, b); - var saturation = saturation(r, g, b); + this.saturation = saturation(r, g, b); + this.hue = hue(r, g, b); - if (toRgb(this.hue, saturation, this.value) != argbColor) { - this.hue = hue(r, g, b); - } - - if (toRgb(this.hue, this.saturation, this.value) != argbColor) { - this.saturation = saturation; - } - - this.fullySaturated = 0xFF000000 | toRgb(hue, 1.0, 1.0); - this.inverted = 0xFF000000 | toRgb(1.0 - hue, 1.0, 1.0 - value); + recalculateInternal(); } private static int toRgb(double hue, double saturation, double value) { @@ -187,15 +184,37 @@ private void fromPosition(double x, double y) { x = x - getX(); y = y - getY(); if (x > 1 && x < 128+1 && y > 1 && y < 129) { - saturation = Math.max(0, Math.min(1, (x - 1) / 128)); - value = Math.max(0, Math.min(1, 1 - (y - 1) / 128)); + var saturation = Math.max(0, Math.min(1, (x - 1) / 128)); + var value = Math.max(0, Math.min(1, 1 - (y - 1) / 128)); + this.saturation = saturation; + this.value = value; + var hue = this.hue; updateColor(); + this.hue = hue; + this.saturation = saturation; + this.value = value; + recalculateInternal(); } else if (x > 128+2+8 && x < 128+2+8*2+2 && y > 1 && y < 129) { - hue = Math.max(0, Math.min(1, (y - 1) / 128)); + var hue = Math.max(0, Math.min(1, (y - 1) / 128)); + this.hue = hue; + var saturation = this.saturation; + var value = this.value; updateColor(); + this.hue = hue; + this.saturation = saturation; + this.value = value; + recalculateInternal(); } else if (hasAlpha && x > 128+2*2+8*3 && x < 128+2*2+8*4+2 && y > 1 && y < 129) { - alpha = Math.max(0, Math.min(1, (y - 1) / 128)); + var alpha = Math.max(0, Math.min(1, (y - 1) / 128)); + this.alpha = alpha; + var saturation = this.saturation; + var value = this.value; + var hue = this.hue; updateColor(); + this.hue = hue; + this.saturation = saturation; + this.value = value; + recalculateInternal(); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index a8f87e3..408efca 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -8,6 +8,7 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -332,6 +333,23 @@ public Optional> key() { return Optional.of(KEY); } + @Override + public DataResult>> either(App left, App right) { + var codecLeft = ConfigScreenEntry.unbox(left).entryCreationInfo().codec(); + var codecRight = ConfigScreenEntry.unbox(right).entryCreationInfo().codec(); + var codecResult = codecInterpreter.either(new CodecInterpreter.Holder<>(codecLeft), new CodecInterpreter.Holder<>(codecRight)).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(codecResult.error().orElseThrow().messageSupplier()); + } + return DataResult.success(ConfigScreenEntry.single( + Widgets.either( + ConfigScreenEntry.unbox(left).widget(), + ConfigScreenEntry.unbox(right).widget() + ), + new EntryCreationInfo<>(codecResult.getOrThrow(), ComponentInfo.empty()) + )); + } + @Override public ConfigScreenInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new ConfigScreenInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter); @@ -348,9 +366,11 @@ public DataResult>> list(App(unwrapped, context, original, onClose); return DataResult.success(new ConfigScreenEntry<>( Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonArray(); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } JsonElement finalOriginal = original; return Button.builder( @@ -361,7 +381,38 @@ public DataResult>> list(App(codecResult.getOrThrow(), ComponentInfo.empty()) + )); + } + + @Override + public DataResult>> unboundedMap(App key, App value) { + var unwrappedKey = ConfigScreenEntry.unbox(key); + var unwrappedValue = ConfigScreenEntry.unbox(value); + var codecResult = codecInterpreter.unboundedMap(new CodecInterpreter.Holder<>(unwrappedKey.entryCreationInfo().codec()), new CodecInterpreter.Holder<>(unwrappedValue.entryCreationInfo().codec())).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(codecResult.error().orElseThrow().messageSupplier()); + } + ScreenEntryFactory> factory = (context, original, onClose, creationInfo) -> + new UnboundedMapScreenEntryProvider<>(unwrappedKey, unwrappedValue, context, original, onClose); + return DataResult.success(new ConfigScreenEntry<>( + Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { + if (original.isJsonNull()) { + original = new JsonObject(); + if (!handleOptional) { + update.accept(original); + } + } + JsonElement finalOriginal = original; + return Button.builder( + Component.translatable("codecextras.config.configurelist"), + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( + context, finalOriginal, update, creationInfo + ), parent, creationInfo.componentInfo())) + ).width(width).build(); + }), + factory, + new EntryCreationInfo<>(codecResult.getOrThrow(), ComponentInfo.empty()) )); } @@ -383,9 +434,11 @@ public DataResult> record(List( Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonObject(); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } JsonElement finalOriginal = original; return Button.builder( @@ -483,9 +536,11 @@ public DataResult> dispatch(String key, Stru new DispatchScreenEntryProvider<>(keyResult.getOrThrow().entryCreationInfo(), original, key, onClose, context, entries.get()); return DataResult.success(new ConfigScreenEntry<>( Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonObject(); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } JsonElement finalOriginal = original; return Button.builder( diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java new file mode 100644 index 0000000..b49f3f4 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java @@ -0,0 +1,137 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.mojang.datafixers.util.Pair; +import com.mojang.logging.LogUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +class UnboundedMapScreenEntryProvider implements ScreenEntryProvider { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final ConfigScreenEntry keyEntry; + private final ConfigScreenEntry valueEntry; + private final List> values = new ArrayList<>(); + private final JsonObject jsonValue = new JsonObject(); + private final Consumer update; + private final EntryCreationContext context; + + UnboundedMapScreenEntryProvider(ConfigScreenEntry keyEntry, ConfigScreenEntry valueEntry, EntryCreationContext context, JsonElement jsonValue, Consumer update) { + this.keyEntry = keyEntry; + this.valueEntry = valueEntry; + if (jsonValue.isJsonObject()) { + jsonValue.getAsJsonObject().entrySet().stream() + .>map(e -> Pair.of(new JsonPrimitive(e.getKey()), e.getValue())) + .forEach(values::add); + updateValue(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON array", jsonValue); + } + } + this.update = update; + this.context = context; + } + + private void updateValue() { + jsonValue.entrySet().clear(); + for (var pair : values) { + if (pair.getFirst().isJsonPrimitive()) { + if (pair.getFirst().getAsJsonPrimitive().isString()) { + jsonValue.add(pair.getFirst().getAsString(), pair.getSecond()); + } else { + LOGGER.warn("Key {} was not a JSON string", pair.getFirst()); + } + } else if (!pair.getFirst().isJsonNull()) { + LOGGER.warn("Key {} was not a JSON primitive", pair.getFirst()); + } + } + } + + @Override + public void onExit() { + this.update.accept(jsonValue); + } + + @Override + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + var fullWidth = Button.DEFAULT_WIDTH*2+ EntryListScreen.Entry.SPACING; + for (int i = 0; i < values.size(); i++) { + var index = i; + var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + // 5 gives us good spacing here + var keyAndValueWidth = (fullWidth - (Button.DEFAULT_HEIGHT + 5)*3 - 5)/2; + layout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.remove"), b -> { + values.remove(index); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.remove"))).build(), LayoutSettings.defaults().alignVerticallyMiddle()); + var upButton = Button.builder(Component.translatable("codecextras.config.list.icon.up"), b -> { + if (index == 0) { + return; + } + var oldAbove = values.get(index - 1); + var old = values.get(index); + values.set(index - 1, old); + values.set(index, oldAbove); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.up"))).build(); + if (index == 0) { + upButton.active = false; + } + layout.addChild(upButton, LayoutSettings.defaults().alignVerticallyMiddle()); + var downButton = Button.builder(Component.translatable("codecextras.config.list.icon.down"), b -> { + if (index == values.size()-1) { + return; + } + var oldBelow = values.get(index + 1); + var old = values.get(index); + values.set(index + 1, old); + values.set(index, oldBelow); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.down"))).build(); + if (index == values.size()-1) { + downButton.active = false; + } + layout.addChild(downButton, LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(keyEntry.widget().create( + parent, + keyAndValueWidth, + context, values.get(index).getFirst(), + newKey -> { + this.values.set(index, this.values.get(index).mapFirst(old -> newKey)); + updateValue(); + }, keyEntry.entryCreationInfo(), + false + ), LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(valueEntry.widget().create( + parent, + keyAndValueWidth, + context, values.get(index).getSecond(), + newValue -> { + this.values.set(index, this.values.get(index).mapSecond(old -> newValue)); + updateValue(); + }, valueEntry.entryCreationInfo(), + false + ), LayoutSettings.defaults().alignVerticallyMiddle()); + list.addSingle(layout); + } + var addLayout = new FrameLayout(fullWidth, 0); + addLayout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.add"), b -> { + values.add(new Pair<>(JsonNull.INSTANCE, JsonNull.INSTANCE)); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.add"))).build(), LayoutSettings.defaults().alignHorizontallyLeft().alignVerticallyMiddle()); + list.addSingle(addLayout); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 43cdbf9..be5b810 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -3,7 +3,9 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; +import com.mojang.datafixers.util.Either; import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Range; @@ -33,6 +35,7 @@ public final class Widgets { private static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); + private static final int DEFAULT_SPACING = 5; private Widgets() {} @@ -46,9 +49,11 @@ public static LayoutFactory text(Function> toData, widget.setFilter(filter); - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonPrimitive(""); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } var decoded = creationInfo.codec().parse(context.ops(), original); @@ -132,7 +137,7 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass if (!handleOptional) { return assumesNonOptional.create(parent, fullWidth, context, original, update, creationInfo, false); } - var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - Button.DEFAULT_SPACING; + var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - DEFAULT_SPACING; var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var object = new Object() { private JsonElement value = original; @@ -203,8 +208,11 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass public static LayoutFactory color(boolean includeAlpha) { return wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonPrimitive(0); + if (!handleOptional) { + update.accept(original); + } update.accept(original); } @@ -260,9 +268,11 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { public static > LayoutFactory slider(Range range, Function> toJson, Function> fromJson, boolean isDoubleLike) { return wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonPrimitive(range.min()); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } var valueResult = fromJson.apply(original); @@ -325,11 +335,105 @@ private static > double valueInRange(Range r return (value.doubleValue() - range.min().doubleValue()) / (range.max().doubleValue() - range.min().doubleValue()); } + public static LayoutFactory> either(LayoutFactory left, LayoutFactory right) { + return (parent, fullWidth, context, original, update, creationInfo, handleOptional) -> { + var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - DEFAULT_SPACING; + boolean[] isLeft = new boolean[1]; + boolean[] isMissing = new boolean[1]; + if (!original.isJsonNull()) { + if (handleOptional) { + isMissing[0] = true; + } else { + var result = creationInfo.codec().parse(context.ops(), original); + if (result.error().isPresent()) { + LOGGER.warn("Failed to decode `{}`: {}", original, result.error().get().message()); + } else { + isLeft[0] = result.getOrThrow().left().isPresent(); + } + } + } + Codec leftCodec = creationInfo.codec().comapFlatMap(e -> e.left().map(DataResult::success).orElse(DataResult.error(() -> "Expected left value")), Either::left); + Codec rightCodec = creationInfo.codec().comapFlatMap(e -> e.right().map(DataResult::success).orElse(DataResult.error(() -> "Expected right value")), Either::right); + var leftElement = left.create(parent, remainingWidth, context, isLeft[0] ? original : JsonNull.INSTANCE, update, creationInfo.withCodec(leftCodec), false); + var rightElement = right.create(parent, remainingWidth, context, isLeft[0] ? JsonNull.INSTANCE : original, update, creationInfo.withCodec(rightCodec), false); + var missingElement = handleOptional ? Button.builder(Component.translatable("codecextras.config.missing"), b -> {}).width(remainingWidth).build() : null; + if (handleOptional) { + missingElement.active = false; + } + update.accept(original); + var frame = new FrameLayout(remainingWidth, Button.DEFAULT_HEIGHT); + frame.addChild(leftElement); + frame.addChild(rightElement); + Runnable updateVisibility = () -> { + if (isMissing[0]) { + rightElement.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + leftElement.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + if (handleOptional) { + missingElement.visible = true; + } + } else if (isLeft[0]) { + rightElement.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + leftElement.visitWidgets(w -> { + w.visible = true; + w.active = true; + }); + if (handleOptional) { + missingElement.visible = false; + } + } else { + leftElement.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + rightElement.visitWidgets(w -> { + w.visible = true; + w.active = true; + }); + if (handleOptional) { + missingElement.visible = false; + } + } + }; + updateVisibility.run(); + var layout = new EqualSpacingLayout(fullWidth, Button.DEFAULT_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL); + var switchButton = Button.builder(Component.empty(), b -> { + if (handleOptional) { + if (isMissing[0]) { + isMissing[0] = false; + isLeft[0] = true; + update.accept(JsonNull.INSTANCE); + } else if (isLeft[0]) { + isLeft[0] = false; + } else { + isMissing[0] = true; + } + } else { + isLeft[0] = !isLeft[0]; + } + updateVisibility.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.either.switch"))).build(); + layout.addChild(switchButton, LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(frame, LayoutSettings.defaults().alignVerticallyMiddle()); + return layout; + }; + } + public static LayoutFactory bool(boolean falseIfMissing) { LayoutFactory widget = (parent, width, context, original, update, creationInfo, handleOptional) -> { - if (!handleOptional && original.isJsonNull()) { + if (original.isJsonNull()) { original = new JsonPrimitive(false); - update.accept(original); + if (!handleOptional) { + update.accept(original); + } } var w = Checkbox.builder(Component.empty(), Minecraft.getInstance().font) .maxWidth(width) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index e12e9f0..483a020 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; @@ -30,6 +31,7 @@ import java.util.function.Supplier; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.VarInt; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import org.jspecify.annotations.Nullable; @@ -66,10 +68,10 @@ public App, App> convert(App() { + return new StreamCodec<>() { @Override public T decode(B buffer) { - var intValue = buffer.readInt(); + var intValue = VarInt.read(buffer); if (intValue < 0 || intValue >= values.length) { throw new DecoderException("Unknown representation value: " + intValue); } @@ -82,7 +84,7 @@ public void encode(B buffer, T object) { if (index == null) { throw new DecoderException("Unknown representation value: " + object); } - buffer.writeInt(index); + VarInt.write(buffer, index); } }; }); @@ -273,6 +275,39 @@ public Optional>> key() { return Optional.of(key); } + @Override + public DataResult, Map>> unboundedMap(App, K> k, App, V> v) { + return DataResult.success(new Holder<>(new StreamCodec<>() { + @Override + public Map decode(B buffer) { + var map = new HashMap(); + int size = VarInt.read(buffer); + for (int i = 0; i < size; i++) { + K key = unbox(k).decode(buffer); + V value = unbox(v).decode(buffer); + map.put(key, value); + } + return map; + } + + @Override + public void encode(B buffer, Map object) { + VarInt.write(buffer, object.size()); + object.forEach((key, value) -> { + unbox(k).encode(buffer, key); + unbox(v).encode(buffer, value); + }); + } + })); + } + + @Override + public DataResult, Either>> either(App, L> left, App, R> right) { + var leftCodec = unbox(left); + var rightCodec = unbox(right); + return DataResult.success(new Holder<>(ByteBufCodecs.either(leftCodec, rightCodec))); + } + public record Holder(StreamCodec streamCodec) implements App, T> { public static final class Mu implements K1 {} diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json index 014923f..ce04554 100644 --- a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -10,5 +10,6 @@ "codecextras.config.list.add": "Add entry", "codecextras.config.list.up": "Move up", "codecextras.config.list.down": "Move down", - "codecextras.config.list.remove": "Remove entry" + "codecextras.config.list.remove": "Remove entry", + "codecextras.config.either.switch": "Switch entry type" } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index b6498a7..3f1acdf 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.test.common; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -25,7 +26,8 @@ public record TestConfig( String d, Optional e, Optional f, Unit g, List strings, Dispatches dispatches, int intInRange, float floatInRange, int argb, - int rgb, ResourceKey item, Rarity rarity + int rgb, ResourceKey item, Rarity rarity, + Map unbounded, Either either ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -88,12 +90,15 @@ public String key() { var rgb = builder.addOptional("rgb", MinecraftStructures.RGB_COLOR, TestConfig::rgb, () -> 0xFF0000); var item = builder.addOptional("item", MinecraftStructures.resourceKey(Registries.ITEM), TestConfig::item, () -> Items.MELON_SEEDS); var rarity = builder.addOptional("rarity", Structure.stringRepresentable(Rarity::values, Rarity::getSerializedName), TestConfig::rarity, () -> Rarity.COMMON); + var unbounded = builder.addOptional("unbounded", Structure.unboundedMap(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::unbounded, () -> Map.of("test", 123)); + var either = builder.addOptional("either", Structure.either(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::either, () -> Either.right(0x00FFAA)); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), g.apply(container), strings.apply(container), dispatches.apply(container), intInRange.apply(container), floatInRange.apply(container), argb.apply(container), - rgb.apply(container), item.apply(container), rarity.apply(container) + rgb.apply(container), item.apply(container), rarity.apply(container), + unbounded.apply(container), either.apply(container) ); }); From d075cd50303901e176712345fd1e058b0a27c3ef Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 2 Sep 2024 22:57:16 -0500 Subject: [PATCH 53/76] [no ci] initial work on dispatch maps and data components --- build.gradle | 65 ++++--- settings.gradle | 5 +- .../structured/CodecInterpreter.java | 4 +- .../structured/IdentityInterpreter.java | 3 +- .../codecextras/structured/Interpreter.java | 5 +- .../structured/MapCodecInterpreter.java | 6 +- .../codecextras/structured/Structure.java | 14 +- .../schema/JsonSchemaInterpreter.java | 6 +- src/main/resources/fabric.mod.json | 3 +- .../structured/CodecExtrasRegistries.java | 36 ++++ .../structured/MinecraftInterpreters.java | 5 + .../minecraft/structured/MinecraftKeys.java | 20 +++ .../structured/MinecraftStructures.java | 161 +++++++++++++++++- .../config/ConfigScreenInterpreter.java | 6 +- .../structured/StreamCodecInterpreter.java | 6 +- .../resources/META-INF/neoforge.mods.toml | 8 - src/minecraft/resources/fabric.mod.json | 6 - .../CodecExtrasRegistriesRegistrar.java | 13 ++ .../minecraft/fabric/package-info.java | 6 + src/minecraftFabric/resources/fabric.mod.json | 14 ++ .../neoforge/CodecExtrasNeoforge.java | 21 +++ .../minecraft/neoforge/package-info.java | 6 + .../resources/META-INF/neoforge.mods.toml | 17 ++ .../test/structured/TestDispatch.java | 2 +- .../codecextras/test/common/TestConfig.java | 2 +- testFabric/build.gradle | 2 +- testNeoforge/build.gradle | 2 +- 27 files changed, 380 insertions(+), 64 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java delete mode 100644 src/minecraft/resources/META-INF/neoforge.mods.toml delete mode 100644 src/minecraft/resources/fabric.mod.json create mode 100644 src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java create mode 100644 src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/package-info.java create mode 100644 src/minecraftFabric/resources/fabric.mod.json create mode 100644 src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java create mode 100644 src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/package-info.java create mode 100644 src/minecraftNeoforge/resources/META-INF/neoforge.mods.toml diff --git a/build.gradle b/build.gradle index 42b7542..1b7a55a 100644 --- a/build.gradle +++ b/build.gradle @@ -97,18 +97,18 @@ println "Building: $version" sourceSets { minecraft {} - minecraftIntermediary {} + minecraftFabric {} jmh {} } configurations { testNeoforgeRuntimeClasspath.extendsFrom minecraftRuntimeClasspath - testFabricRuntimeClasspath.extendsFrom minecraftIntermediaryRuntimeClasspath - testFabricToRemapRuntimeClasspath.extendsFrom minecraftIntermediaryToRemapRuntimeClasspath + testFabricRuntimeClasspath.extendsFrom minecraftFabricRuntimeClasspath + testFabricToRemapRuntimeClasspath.extendsFrom minecraftFabricToRemapRuntimeClasspath testNeoforgeCompileClasspath.extendsFrom minecraftCompileClasspath - testFabricCompileClasspath.extendsFrom minecraftIntermediaryCompileClasspath - testFabricToRemapCompileClasspath.extendsFrom minecraftIntermediaryToRemapCompileClasspath + testFabricCompileClasspath.extendsFrom minecraftFabricCompileClasspath + testFabricToRemapCompileClasspath.extendsFrom minecraftFabricToRemapCompileClasspath runtimeModClasses { canBeConsumed = true @@ -136,19 +136,25 @@ java { withSourcesJar() withJavadocJar() capability(project.group as String, "$project.name-minecraft", project.version as String) - capability(project.group as String, "$project.name-minecraft-mojmap", project.version as String) + capability(project.group as String, "$project.name-minecraft-common", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) - capability(project.group as String, "$project.name-stream-mojmap", project.version as String) } - registerFeature("minecraftIntermediary") { - usingSourceSet sourceSets.minecraftIntermediary + registerFeature("minecraftFabric") { + usingSourceSet sourceSets.minecraftFabric capability(project.group as String, "$project.name-minecraft", project.version as String) - capability(project.group as String, "$project.name-minecraft-intermediary", project.version as String) + capability(project.group as String, "$project.name-minecraft-fabric", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) capability(project.group as String, "$project.name-stream-intermediary", project.version as String) - } + } + registerFeature("minecraftNeoforge") { + usingSourceSet sourceSets.minecraftNeoforge + capability(project.group as String, "$project.name-minecraft", project.version as String) + capability(project.group as String, "$project.name-minecraft-neoforge", project.version as String) + // Old name + capability(project.group as String, "$project.name-stream", project.version as String) + } } repositories { @@ -192,23 +198,31 @@ dependencies { minecraftApi project(':') minecraftCompileOnly cLibs.bundles.compileonly minecraftAnnotationProcessor cLibs.bundles.annotationprocessor - minecraftIntermediaryApi project(':') - testImplementation sourceSets.minecraft.output - - testNeoforgeCompileOnly sourceSets.minecraft.output - testFabricCompileOnly sourceSets.minecraftIntermediary.output + minecraftFabricCompileOnly cLibs.bundles.compileonly + minecraftFabricAnnotationProcessor cLibs.bundles.annotationprocessor + minecraftNeoforgeCompileOnly cLibs.bundles.compileonly + minecraftNeoforgeAnnotationProcessor cLibs.bundles.annotationprocessor + minecraftFabricApi project(':') + minecraftNeoforgeApi project(':') + + testNeoforgeCompileOnly sourceSets.minecraftNeoforge.output + testFabricCompileOnly sourceSets.minecraftFabric.output testCommonCompileOnly(project(':')) { capabilities { - requireCapability 'dev.lukebemish:codecextras-minecraft-mojmap' + requireCapability 'dev.lukebemish:codecextras-minecraft-common' } } modTestFabricImplementation libs.fabric.loader - modTestFabricLocalImplementation libs.modmenu modTestFabricLocalImplementation libs.fabric.api + modTestFabricLocalImplementation libs.modmenu + + modMinecraftFabricImplementation libs.fabric.loader + modMinecraftFabricImplementation libs.fabric.api + modMinecraftFabricLocalImplementation libs.modmenu } -['minecraftJar', 'minecraftIntermediaryJar', 'jar'].each { +['minecraftJar', 'minecraftFabricJar', 'minecraftNeoforgeJar', 'jar'].each { tasks.named(it, Jar) { manifest { attributes( @@ -230,7 +244,7 @@ tasks.named('jar', Jar) { } } -['minecraftJar', 'minecraftIntermediaryJar'].each { +['minecraftJar', 'minecraftFabricJar', 'minecraftNeoforgeJar'].each { tasks.named(it, Jar) { manifest { attributes( @@ -262,8 +276,12 @@ tasks.register('jmhResults', FormatJmhOutput) { formattedResults.set project.file('build/reports/jmh/results.md') } -tasks.named('remapMinecraftIntermediaryJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> - task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-intermediary.jar") +tasks.named('remapMinecraftFabricJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> + task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-intermediary.jar") +} + +tasks.named('minecraftNeoforgeJar', Jar) { task -> + task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-neoforge.jar") } tasks.compileJava { @@ -276,12 +294,13 @@ tasks.compileJava { } } -['processResources', 'processMinecraftResources', 'processMinecraftIntermediaryResources'].each { +['processResources', 'processMinecraftResources', 'processMinecraftFabricResources', 'processMinecraftNeoforgeResources'].each { tasks.named(it, ProcessResources) { inputs.property "version", project.version.toString() filesMatching(["fabric.mod.json", "META-INF/neoforge.mods.toml"]) { expand "version": project.version.toString() + expand "minecraft_version": libs.versions.minecraft.get() } } } diff --git a/settings.gradle b/settings.gradle index 97cf155..192f03e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -41,7 +41,10 @@ multisource.of(':') { mappings.add loom.officialMojangMappings() } common('minecraft', []) {} - fabric('minecraftIntermediary', ['minecraft']) {} + fabric('minecraftFabric', ['minecraft']) {} + neoforge('minecraftNeoforge', ['minecraft']) { + neoForge.add project.libs.neoforge + } common('testCommon', []) {} neoforge('testNeoforge', ['testCommon']) { neoForge.add project.libs.neoforge diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 922306f..0cd40c3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -99,13 +99,13 @@ public DataResult> annotate(Structure original, Keys DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyCodec = unbox(keyCodecApp); // Object here as it's the furthest super A and we have only ? super A Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { Map>> codecMap = new HashMap<>(); - for (var entryKey : keys) { + for (var entryKey : keys.get()) { var result = structures.apply(entryKey).interpret(mapCodecInterpreter()); if (result.error().isPresent()) { codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 67f2c02..7d8b587 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -10,6 +10,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import org.jspecify.annotations.Nullable; /** @@ -82,7 +83,7 @@ public DataResult> annotate(Structure original, Keys< } @Override - public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return DataResult.error(() -> "No default value available for a dispatch"); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index e8464d3..c31cbc0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; public interface Interpreter { DataResult>> list(App single); @@ -25,7 +26,7 @@ public interface Interpreter { DataResult> annotate(Structure original, Keys annotations); - DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures); + DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures); default Optional> key() { return Optional.empty(); @@ -54,4 +55,6 @@ default Optional> key() { Key2 STRING_REPRESENTABLE = Key2.create("enum"); DataResult>> either(App left, App right); + + DataResult>> dispatchMap(Structure keyStructure, Supplier> keys, Function>> valueStructures); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index ae76106..a624873 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -78,14 +78,14 @@ public DataResult> interpret(Structure structure) { } @Override - public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(codecInterpreter()).flatMap(keyCodecApp -> { var keyCodec = CodecInterpreter.unbox(keyCodecApp); // Object here as it's the furthest super A and we have only ? super A Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { Map>> codecMap = new HashMap<>(); - for (var entryKey : keys) { - var result = structures.apply(entryKey).interpret(this); + for (var entryKey : keys.get()) { + var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); if (result.error().isPresent()) { codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); } else { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index c472471..c385631 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -168,12 +168,22 @@ default Structure xmap(Function deserializer, Function serial return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); } - default Structure dispatch(String key, Function> function, Supplier> keys, Function> structures) { + default Structure dispatch(String key, Function> function, Supplier> keys, Function>> structures) { var outer = this; return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.dispatch(key, outer, function, keys.get(), structures); + return interpreter.dispatch(key, outer, function, keys, structures); + } + }; + } + + default Structure> dispatchMap(Supplier> keys, Function>> structures) { + var outer = this; + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + return interpreter.dispatchMap(outer, keys, structures); } }; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 94aa550..f3aac46 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -185,7 +185,7 @@ public DataResult> annotate(Structure input, Keys DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keySchemaApp -> { var definitions = new HashMap<>(definitions(keySchemaApp)); var keySchema = schemaValue(keySchemaApp); @@ -202,8 +202,8 @@ public DataResult> dispatch(String key, Structure ke return DataResult.error(keyCodecResult.error().get().messageSupplier()); } var keyCodec = keyCodecResult.result().orElseThrow(); - for (A entryKey : keys) { - var result = structures.apply(entryKey).interpret(this); + for (A entryKey : keys.get()) { + var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); if (result.error().isPresent()) { return DataResult.error(result.error().get().messageSupplier()); } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 1e9e620..7efb632 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,5 +2,6 @@ "schemaVersion": 1, "id": "dev_lukebemish_codecextras", "version": "${version}", - "name": "CodecExtras" + "name": "CodecExtras", + "license": "LGPL-3.0-only" } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java new file mode 100644 index 0000000..0a9e586 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java @@ -0,0 +1,36 @@ +package dev.lukebemish.codecextras.minecraft.structured; + +import dev.lukebemish.codecextras.structured.Structure; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.Supplier; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +public class CodecExtrasRegistries { + private static final String NAMESPACE = "codecextras_minecraft"; + + public static final ResourceKey>>> DATA_COMPONENT_STRUCTURES = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(NAMESPACE, "data_component__type")); + + static { + // This MUST be the last static initializer in this class -- the registry registrar may depend on the keys defined earlier on + ServiceLoader.load(RegistryRegistrar.class).stream().map(ServiceLoader.Provider::get).forEach(RegistryRegistrar::setup); + } + + public static final class Registries { + private Registries() {} + + @SuppressWarnings("unchecked") + public static final Supplier>>> DATA_COMPONENT_STRUCTURES = () -> + (Registry>>) Objects.requireNonNull(BuiltInRegistries.REGISTRY.get(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES.location()), "Registry does not exist"); + } + + @ApiStatus.Internal + public interface RegistryRegistrar { + void setup(); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java index db8892a..1504aba 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -11,6 +11,8 @@ import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponentPatch; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; @@ -33,6 +35,8 @@ public App convert(Appbuilder() .add(MinecraftKeys.RESOURCE_LOCATION, new CodecInterpreter.Holder<>(ResourceLocation.CODEC)) + .add(MinecraftKeys.DATA_COMPONENT_MAP, new CodecInterpreter.Holder<>(DataComponentMap.CODEC)) + .add(MinecraftKeys.DATA_COMPONENT_PATCH, new CodecInterpreter.Holder<>(DataComponentPatch.CODEC)) .build() ); @@ -117,6 +121,7 @@ public App, A> con return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(input).cast()); } }).join(Keys., Object>builder() + .add(MinecraftKeys.DATA_COMPONENT_PATCH, new StreamCodecInterpreter.Holder<>(DataComponentPatch.STREAM_CODEC)) .build() ); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index d324666..35b23dd 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -4,13 +4,24 @@ import com.mojang.datafixers.kinds.K1; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; public final class MinecraftKeys { + private static final Map>, Key> DATA_COMPONENT_TYPE_KEYS = new ConcurrentHashMap<>(); + public static Key, Object>> VALUE_MAP = Key.create("value_map"); + public static Key DATA_COMPONENT_MAP = Key.create("data_component_map"); + public static Key DATA_COMPONENT_PATCH = Key.create("data_component_patch"); + private MinecraftKeys() { } @@ -18,6 +29,15 @@ private MinecraftKeys() { public static final Key ARGB_COLOR = Key.create("argb_color"); public static final Key RGB_COLOR = Key.create("rgb_color"); + @SuppressWarnings("unchecked") + public static Key dataComponentType(DataComponentType type) { + var location = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type); + if (location.isPresent()) { + return (Key) DATA_COMPONENT_TYPE_KEYS.computeIfAbsent(location.orElseThrow(), key -> Key.create(key.location().toString())); + } + throw new IllegalArgumentException("Data component type " + type + " is not registered"); + } + public record ResourceKeyHolder(ResourceKey value) implements App { public static final class Mu implements K1 { private Mu() { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index a597e44..e111bee 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -1,28 +1,43 @@ package dev.lukebemish.codecextras.minecraft.structured; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; +import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.TypedDataComponent; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; +import org.jspecify.annotations.Nullable; public final class MinecraftStructures { private MinecraftStructures() { } public static final Structure RESOURCE_LOCATION = Structure.keyed( - MinecraftKeys.RESOURCE_LOCATION, Keys., K1>builder() + MinecraftKeys.RESOURCE_LOCATION, + Keys., K1>builder() .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) - .build() + .build(), + Structure.STRING.flatXmap(ResourceLocation::read, rl -> DataResult.success(rl.toString())) ); public static final Structure ARGB_COLOR = Structure.keyed( @@ -35,6 +50,74 @@ private MinecraftStructures() { Structure.INT ); + private static final Structure, Object>> DATA_COMPONENT_VALUE_MAP_FALLBACK = resourceKey(Registries.DATA_COMPONENT_TYPE) + .>flatXmap(key -> { + var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(key); + if (type == null) { + return DataResult.error(() -> "Unknown data component type: " + key); + } + return DataResult.success(type); + }, type -> { + var location = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type); + if (location.isPresent()) { + return DataResult.success(location.orElseThrow()); + } + return DataResult.error(() -> "Data component type " + type + " is not registered"); + }) + .dispatchMap(() -> BuiltInRegistries.DATA_COMPONENT_TYPE.stream().collect(Collectors.toSet()), MinecraftStructures::dataComponentTypeStructure); + + public static final Structure, Object>> DATA_COMPONENT_VALUE_MAP = Structure.keyed( + MinecraftKeys.VALUE_MAP, + Keys., Object>>, K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(DataComponentType.VALUE_MAP_CODEC))) + .build(), + DATA_COMPONENT_VALUE_MAP_FALLBACK + ); + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Structure DATA_COMPONENT_MAP = Structure.keyed( + MinecraftKeys.DATA_COMPONENT_MAP, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(DataComponentMap.CODEC))) + .build(), + DATA_COMPONENT_VALUE_MAP.xmap(values -> { + var builder = DataComponentMap.builder(); + values.forEach((type, value) -> builder.set((DataComponentType) type, value)); + return builder.build(); + }, dataComponentMap -> dataComponentMap.stream().collect(Collectors.toMap(TypedDataComponent::type, TypedDataComponent::value))) + ); + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Structure DATA_COMPONENT_PATCH = Structure.keyed( + MinecraftKeys.DATA_COMPONENT_PATCH, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(DataComponentPatch.CODEC))) + .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(DataComponentPatch.STREAM_CODEC))) + .build(), + DataComponentPatchKey.STRUCTURE + .dispatchMap(DataComponentPatchKey::possibleKeys, DataComponentPatchKey::valueCodec) + .xmap(map -> { + var builder = DataComponentPatch.builder(); + map.forEach((key, value) -> { + if (key.removes) { + builder.remove(key.type); + } else { + builder.set((DataComponentType) key.type, value); + } + }); + return builder.build(); + }, patches -> patches.entrySet().stream().collect(Collectors.toMap(entry -> { + var key = entry.getKey(); + var removes = entry.getValue().isEmpty(); + return new DataComponentPatchKey<>(key, removes); + }, entry -> { + if (entry.getValue().isEmpty()) { + return Unit.INSTANCE; + } + return entry.getValue().get(); + }))) + ); + public static Structure> resourceKey(ResourceKey> registry) { return Structure.parametricallyKeyed( MinecraftKeys.RESOURCE_KEY, @@ -86,11 +169,83 @@ public static Structure> tagKey(ResourceKey> public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { var keyStructure = resourceKey(registry.key()); return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> - registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry()) + "::" + toDefsKey(k.location())) + DataResult.success(registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry()) + "::" + toDefsKey(k.location()))) ); } private static String toDefsKey(ResourceLocation location) { return location.getNamespace().replace('/', '.') + ":" + location.getPath().replace('/', '.'); } + + private static DataResult> dataComponentTypeStructure(DataComponentType type) { + var resourceKey = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type); + if (resourceKey.isEmpty()) { + return DataResult.error(() -> "Unregistered data component type: " + type); + } + var structure = CodecExtrasRegistries.Registries.DATA_COMPONENT_STRUCTURES.get().get(resourceKey.orElseThrow().location()); + return fallbackDataComponentTypeStructure(type, structure); + } + + @SuppressWarnings("unchecked") + private static DataResult> fallbackDataComponentTypeStructure(DataComponentType type, @Nullable Structure> fallback) { + var key = MinecraftKeys.dataComponentType(type); + var codec = type.codec(); + var streamCodec = type.streamCodec(); + var keysBuilder = Keys., K1>builder() + .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(streamCodec.cast())));; + if (codec != null) { + keysBuilder.add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(codec))); + } + var keys = keysBuilder.build(); + if (fallback == null) { + return DataResult.success(Structure.keyed( + key, + keys + )); + } + return DataResult.success(Structure.keyed( + key, + keys, + (Structure) fallback + )); + } + + public static final class Types { + private Types() {} + + } + + private record DataComponentPatchKey(DataComponentType type, boolean removes) { + private static Set> possibleKeys() { + return BuiltInRegistries.DATA_COMPONENT_TYPE.stream() + .flatMap(type -> Stream.of(new DataComponentPatchKey<>(type, false), new DataComponentPatchKey<>(type, true))) + .collect(Collectors.toSet()); + } + + private static final Structure> STRUCTURE = Structure.STRING + .flatXmap(string -> { + boolean removes = string.startsWith("!"); + string = removes ? string.substring(1) : string; + return ResourceLocation.read(string).flatMap(rl -> { + var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(rl); + if (type == null) { + return DataResult.error(() -> "Unknown data component type: " + rl); + } + return DataResult.success(new DataComponentPatchKey<>(type, removes)); + }); + }, key -> { + var rl = BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(key.type); + if (rl == null) { + return DataResult.error(() -> "Unknown data component type: " + key.type); + } + return DataResult.success((key.removes ? "!" : "") + rl); + }); + + private DataResult> valueCodec() { + if (removes) { + return DataResult.success(Structure.UNIT); + } + return dataComponentTypeStructure(type); + } + } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 408efca..c00e954 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -511,15 +511,15 @@ public DataResult> annotate(Structure origin } @Override - public DataResult> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { var keyResult = interpret(keyStructure).map(entry -> entry.withComponentInfo(info -> info.fallbackTitle(Component.literal(key)))); if (keyResult.error().isPresent()) { return DataResult.error(keyResult.error().get().messageSupplier()); } Supplier>>> entries = Suppliers.memoize(() -> { Map>> map = new HashMap<>(); - for (var entryKey : keys) { - var result = structures.apply(entryKey).interpret(this); + for (var entryKey : keys.get()) { + var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); if (result.error().isPresent()) { map.put(entryKey, DataResult.error(result.error().get().messageSupplier())); } else { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 483a020..bf2362c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -203,13 +203,13 @@ public DataResult, A>> annotate(Structure original, Keys } @Override - public DataResult, E>> dispatch(String key, Structure keyStructure, Function> function, Set keys, Function> structures) { + public DataResult, E>> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyStreamCodec = unbox(keyCodecApp); Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { Map>> codecMap = new HashMap<>(); - for (var entryKey : keys) { - var result = structures.apply(entryKey).interpret(this); + for (var entryKey : keys.get()) { + var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); if (result.error().isPresent()) { codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); } diff --git a/src/minecraft/resources/META-INF/neoforge.mods.toml b/src/minecraft/resources/META-INF/neoforge.mods.toml deleted file mode 100644 index 5d6457e..0000000 --- a/src/minecraft/resources/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,8 +0,0 @@ -modLoader="lowcodefml" -loaderVersion="[1,)" -license="LGPL-3.0-only" -[[mods]] -modId="codecextras_minecraft" -version="${version}" -displayName="CodecExtras - Minecraft Adapters" -description="" diff --git a/src/minecraft/resources/fabric.mod.json b/src/minecraft/resources/fabric.mod.json deleted file mode 100644 index 6fe53aa..0000000 --- a/src/minecraft/resources/fabric.mod.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaVersion": 1, - "id": "codecextras_minecraft", - "version": "${version}", - "name": "CodecExtras - Minecraft Adapters" -} diff --git a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java new file mode 100644 index 0000000..70e9fd6 --- /dev/null +++ b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java @@ -0,0 +1,13 @@ +package dev.lukebemish.codecextras.minecraft.fabric; + +import com.google.auto.service.AutoService; +import dev.lukebemish.codecextras.minecraft.structured.CodecExtrasRegistries; +import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder; + +@AutoService(CodecExtrasRegistries.RegistryRegistrar.class) +public final class CodecExtrasRegistriesRegistrar implements CodecExtrasRegistries.RegistryRegistrar { + @Override + public void setup() { + FabricRegistryBuilder.createSimple(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES).buildAndRegister(); + } +} diff --git a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/package-info.java b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/package-info.java new file mode 100644 index 0000000..3e08c0d --- /dev/null +++ b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.minecraft.fabric; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/minecraftFabric/resources/fabric.mod.json b/src/minecraftFabric/resources/fabric.mod.json new file mode 100644 index 0000000..72e71b3 --- /dev/null +++ b/src/minecraftFabric/resources/fabric.mod.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "id": "codecextras_minecraft", + "version": "${version}", + "name": "CodecExtras - Minecraft Adapters", + "license": "LGPL-3.0-only", + "description": "Minecraft-specific adapters for CodecExtras", + "authors": [ + "Luke Bemish" + ], + "depends": { + "minecraft": ">=${minecraft_version}" + } +} diff --git a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java new file mode 100644 index 0000000..1f4dd92 --- /dev/null +++ b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java @@ -0,0 +1,21 @@ +package dev.lukebemish.codecextras.minecraft.neoforge; + +import dev.lukebemish.codecextras.minecraft.structured.CodecExtrasRegistries; +import dev.lukebemish.codecextras.structured.Structure; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponentType; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.registries.NewRegistryEvent; +import net.neoforged.neoforge.registries.RegistryBuilder; + +@Mod("codecextras_minecraft") +public final class CodecExtrasNeoforge { + private static final Registry>> DATA_COMPONENT_STRUCTURE_REGISTRY = new RegistryBuilder<>(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES).create(); + + CodecExtrasNeoforge(IEventBus modBus) { + modBus.addListener(NewRegistryEvent.class, event -> { + event.register(DATA_COMPONENT_STRUCTURE_REGISTRY); + }); + } +} diff --git a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/package-info.java b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/package-info.java new file mode 100644 index 0000000..e63db2a --- /dev/null +++ b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.minecraft.neoforge; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/minecraftNeoforge/resources/META-INF/neoforge.mods.toml b/src/minecraftNeoforge/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..531499e --- /dev/null +++ b/src/minecraftNeoforge/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,17 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="LGPL-3.0-only" +authors="Luke Bemish" + +[[mods]] +modId="codecextras_minecraft" +version="${version}" +displayName="CodecExtras - Minecraft Adapters" +description="Minecraft-specific adapters for CodecExtras" + +[[dependencies.codecextras_minecraft]] +modId="minecraft" +type="required" +versionRange="[${minecraft_version},)" +ordering="NONE" +side="BOTH" diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java index 18f6330..1fae90b 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java @@ -20,7 +20,7 @@ private interface Dispatches { "type", d -> DataResult.success(d.key()), MAP::keySet, - MAP::get + k -> DataResult.success(MAP.get(k)) ).annotate(SchemaAnnotations.REUSE_KEY, "dispatches"); String key(); } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 3f1acdf..e9c7126 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -36,7 +36,7 @@ public interface Dispatches { "type", d -> DataResult.success(d.key()), DISPATCHES::keySet, - DISPATCHES::get + k -> DataResult.success(DISPATCHES.get(k)) ); String key(); } diff --git a/testFabric/build.gradle b/testFabric/build.gradle index 30b2273..0a1313e 100644 --- a/testFabric/build.gradle +++ b/testFabric/build.gradle @@ -16,7 +16,7 @@ loom { } dependencies { - normalRuntimeModClasses(project(path: ':', configuration: 'minecraftIntermediaryRuntimeModClasses')) { + normalRuntimeModClasses(project(path: ':', configuration: 'minecraftFabricRuntimeModClasses')) { transitive = false } } diff --git a/testNeoforge/build.gradle b/testNeoforge/build.gradle index 06aaed3..0264af2 100644 --- a/testNeoforge/build.gradle +++ b/testNeoforge/build.gradle @@ -25,7 +25,7 @@ dependencies { normalRuntimeModClasses(project(path: ':', configuration: 'runtimeModClasses')) { transitive = false } - minecraftRuntimeModClasses(project(path: ':', configuration: 'minecraftRuntimeModClasses')) { + minecraftRuntimeModClasses(project(path: ':', configuration: 'minecraftNeoforgeRuntimeModClasses')) { transitive = false } } From 95e808e8be267ab37e04a4e0b5818df8a3f44c32 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 5 Sep 2024 15:25:58 -0500 Subject: [PATCH 54/76] Better dispatch behaviour, custom bounds behaviour, and work on itemstack structure support --- build.gradle | 12 +- .../PartialDispatchedMapCodec.java | 87 ++++++++++ .../codecextras/StringRepresentation.java | 10 +- .../structured/CodecInterpreter.java | 30 ++-- .../structured/IdentityInterpreter.java | 5 + .../codecextras/structured/Interpreter.java | 12 +- .../structured/MapCodecInterpreter.java | 25 +-- .../codecextras/structured/Structure.java | 89 +++++++--- .../schema/JsonSchemaInterpreter.java | 29 +++- .../structured/MinecraftInterpreters.java | 2 +- .../minecraft/structured/MinecraftKeys.java | 6 +- .../structured/MinecraftStructures.java | 95 +++++------ .../structured/config/ConfigScreenEntry.java | 6 +- .../config/ConfigScreenInterpreter.java | 122 +++++++++++-- .../config/DispatchScreenEntryProvider.java | 83 ++++----- .../DispatchedMapScreenEntryProvider.java | 161 ++++++++++++++++++ .../structured/config/JsonComparator.java | 64 +++++++ .../config/ListScreenEntryProvider.java | 2 +- .../config/RecordScreenEntryProvider.java | 2 +- .../UnboundedMapScreenEntryProvider.java | 38 +---- .../minecraft/structured/config/Widgets.java | 6 +- .../structured/StreamCodecInterpreter.java | 62 +++++-- .../codecextras_minecraft/lang/en_us.json | 3 +- .../codecextras/test/common/TestConfig.java | 5 +- 24 files changed, 719 insertions(+), 237 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/PartialDispatchedMapCodec.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/JsonComparator.java diff --git a/build.gradle b/build.gradle index 1b7a55a..d95a13b 100644 --- a/build.gradle +++ b/build.gradle @@ -296,11 +296,17 @@ tasks.compileJava { ['processResources', 'processMinecraftResources', 'processMinecraftFabricResources', 'processMinecraftNeoforgeResources'].each { tasks.named(it, ProcessResources) { - inputs.property "version", project.version.toString() + var version = project.version.toString() + var minecraftVersion = libs.versions.minecraft.get() + + inputs.property "version", version + inputs.property "minecraft_version", minecraftVersion filesMatching(["fabric.mod.json", "META-INF/neoforge.mods.toml"]) { - expand "version": project.version.toString() - expand "minecraft_version": libs.versions.minecraft.get() + expand([ + "version": version, + "minecraft_version": minecraftVersion + ]) } } } diff --git a/src/main/java/dev/lukebemish/codecextras/PartialDispatchedMapCodec.java b/src/main/java/dev/lukebemish/codecextras/PartialDispatchedMapCodec.java new file mode 100644 index 0000000..5d8b6b0 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/PartialDispatchedMapCodec.java @@ -0,0 +1,87 @@ +package dev.lukebemish.codecextras; + +import com.google.common.collect.ImmutableMap; +import com.mojang.datafixers.util.Pair; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.Lifecycle; +import com.mojang.serialization.RecordBuilder; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +/* +This class adapted from DispatchedMapCodec.java from DFU (https://github.com/Mojang/DataFixerUpper), under the MIT license: + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +public record PartialDispatchedMapCodec( + Codec keyCodec, + Function>> valueCodecFunction +) implements Codec> { + @Override + public DataResult encode(final Map input, final DynamicOps ops, final T prefix) { + final RecordBuilder mapBuilder = ops.mapBuilder(); + for (final Map.Entry entry : input.entrySet()) { + mapBuilder.add(keyCodec.encodeStart(ops, entry.getKey()), valueCodecFunction.apply(entry.getKey()).flatMap(codec -> encodeValue(codec, entry.getValue(), ops))); + } + return mapBuilder.build(prefix); + } + + @SuppressWarnings("unchecked") + private DataResult encodeValue(final Codec codec, final V input, final DynamicOps ops) { + return codec.encodeStart(ops, (V2) input); + } + + @Override + public DataResult, T>> decode(final DynamicOps ops, final T input) { + return ops.getMap(input).flatMap(map -> { + final Map entries = new Object2ObjectArrayMap<>(); + final Stream.Builder> failed = Stream.builder(); + + final DataResult finalResult = map.entries().reduce( + DataResult.success(Unit.INSTANCE, Lifecycle.stable()), + (result, entry) -> parseEntry(result, ops, entry, entries, failed), + (r1, r2) -> r1.apply2stable((u1, u2) -> u1, r2) + ); + + final Pair, T> pair = Pair.of(ImmutableMap.copyOf(entries), input); + final T errors = ops.createMap(failed.build()); + + return finalResult.map(ignored -> pair).setPartial(pair).mapError(error -> error + " missed input: " + errors); + }); + } + + private DataResult parseEntry(final DataResult result, final DynamicOps ops, final Pair input, final Map entries, final Stream.Builder> failed) { + final DataResult keyResult = keyCodec.parse(ops, input.getFirst()); + final DataResult valueResult = keyResult.flatMap(valueCodecFunction).flatMap(valueCodec -> valueCodec.parse(ops, input.getSecond()).map(Function.identity())); + final DataResult> entryResult = keyResult.apply2stable(Pair::of, valueResult); + + final Optional> entry = entryResult.resultOrPartial(); + if (entry.isPresent()) { + final K key = entry.get().getFirst(); + final V value = entry.get().getSecond(); + if (entries.putIfAbsent(key, value) != null) { + failed.add(input); + return result.apply2stable((u, p) -> u, DataResult.error(() -> "Duplicate entry for key: '" + key + "'")); + } + } + if (entryResult.isError()) { + failed.add(input); + } + + return result.apply2stable((u, p) -> u, entryResult); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java index f2ca37e..9e58180 100644 --- a/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java +++ b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java @@ -6,6 +6,7 @@ import com.mojang.serialization.DataResult; import java.util.HashMap; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; @@ -16,12 +17,17 @@ * @param representation converts a value to a string * @param the type of the values */ -public record StringRepresentation(Supplier values, Function representation) implements App { +public record StringRepresentation(Supplier> values, Function representation) implements App { public static final class Mu implements K1 { private Mu() { } } + public static StringRepresentation ofArray(Supplier values, Function representation) { + Supplier> listSupplier = () -> List.of(values.get()); + return new StringRepresentation<>(listSupplier, representation); + } + public static StringRepresentation unbox(App box) { return (StringRepresentation) box; } @@ -34,7 +40,7 @@ public Codec codec() { map.put(this.representation().apply(value), value); } Function toString; - if (values.length > 16) { + if (values.size() > 16) { toString = this.representation(); } else { Map representationMap = new IdentityHashMap<>(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 0cd40c3..1d8992c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -1,6 +1,5 @@ package dev.lukebemish.codecextras.structured; -import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; @@ -9,14 +8,15 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; +import dev.lukebemish.codecextras.PartialDispatchedMapCodec; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.comments.CommentFirstListCodec; import dev.lukebemish.codecextras.types.Identity; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; @@ -102,20 +102,9 @@ public DataResult> annotate(Structure original, Keys DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyCodec = unbox(keyCodecApp); - // Object here as it's the furthest super A and we have only ? super A - Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { - Map>> codecMap = new HashMap<>(); - for (var entryKey : keys.get()) { - var result = structures.apply(entryKey).interpret(mapCodecInterpreter()); - if (result.error().isPresent()) { - codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); - } else { - codecMap.put(entryKey, DataResult.success(MapCodecInterpreter.unbox(result.result().orElseThrow()))); - } - } - return codecMap; - }); - return DataResult.success(new Holder<>(keyCodec.partialDispatch(key, function, k -> codecMapSupplier.get().get(k)))); + var map = new ConcurrentHashMap>>(); + Function>> cache = k -> map.computeIfAbsent(k , structures.andThen(result -> result.flatMap(s -> s.interpret(mapCodecInterpreter())).map(MapCodecInterpreter::unbox))); + return DataResult.success(new Holder<>(keyCodec.partialDispatch(key, function, cache))); }); } @@ -133,6 +122,15 @@ public DataResult>> either(App return DataResult.success(new Holder<>(Codec.either(leftCodec, rightCodec))); } + @Override + public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + return keyStructure.interpret(this).map(CodecInterpreter::unbox).flatMap(keyCodec -> { + var map = new ConcurrentHashMap>>(); + Function>> cache = k -> map.computeIfAbsent(k , valueStructures.andThen(result -> result.flatMap(s -> s.interpret(this)).map(CodecInterpreter::unbox))); + return DataResult.success(new Holder<>(new PartialDispatchedMapCodec<>(keyCodec, cache))); + }); + } + @Override public CodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new CodecAndMapInterpreters(keys().join(keys), mapCodecInterpreter().keys(), parametricKeys().join(parametricKeys), mapCodecInterpreter().parametricKeys()).codecInterpreter(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 7d8b587..a3773fe 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -87,6 +87,11 @@ public DataResult> dispatch(String key, Structure return DataResult.error(() -> "No default value available for a dispatch"); } + @Override + public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + return DataResult.error(() -> "No default value available for a dispatched map"); + } + @Override public DataResult>> parametricallyKeyed(Key2 key, App parameter) { return DataResult.error(() -> "No default value available for a parametric key"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index c31cbc0..4b66ea5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -32,6 +32,16 @@ default Optional> key() { return Optional.empty(); } + default DataResult> bounded(App input, Supplier> values) { + Function> verifier = a -> { + if (values.get().contains(a)) { + return DataResult.success(a); + } + return DataResult.error(() -> "Invalid value: " + a); + }; + return flatXmap(input, verifier, verifier); + } + DataResult>> unboundedMap(App key, App value); Key UNIT = Key.create("UNIT"); @@ -56,5 +66,5 @@ default Optional> key() { DataResult>> either(App left, App right); - DataResult>> dispatchMap(Structure keyStructure, Supplier> keys, Function>> valueStructures); + DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index a624873..ca49112 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -1,6 +1,5 @@ package dev.lukebemish.codecextras.structured; -import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; @@ -10,11 +9,11 @@ import com.mojang.serialization.codecs.KeyDispatchCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; @@ -81,23 +80,17 @@ public DataResult> interpret(Structure structure) { public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(codecInterpreter()).flatMap(keyCodecApp -> { var keyCodec = CodecInterpreter.unbox(keyCodecApp); - // Object here as it's the furthest super A and we have only ? super A - Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { - Map>> codecMap = new HashMap<>(); - for (var entryKey : keys.get()) { - var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); - if (result.error().isPresent()) { - codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); - } else { - codecMap.put(entryKey, DataResult.success(MapCodecInterpreter.unbox(result.result().orElseThrow()))); - } - } - return codecMap; - }); - return DataResult.success(new MapCodecInterpreter.Holder<>(new KeyDispatchCodec<>(key, keyCodec, function, k -> codecMapSupplier.get().get(k)))); + var map = new ConcurrentHashMap>>(); + Function>> cache = k -> map.computeIfAbsent(k , structures.andThen(result -> result.flatMap(s -> s.interpret(this)).map(MapCodecInterpreter::unbox))); + return DataResult.success(new MapCodecInterpreter.Holder<>(new KeyDispatchCodec<>(key, keyCodec, function, cache))); }); } + @Override + public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + return DataResult.error(() -> "Cannot make a MapCodec for a dispatched map"); + } + @Override public DataResult>> unboundedMap(App key, App value) { return DataResult.error(() -> "Cannot make a MapCodec for an unbounded map"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index c385631..6da4290 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.collect.Sets; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; @@ -15,7 +16,6 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; /** * Represents the structure of a data type in a generic form. This structure can then be interpreted into any number of @@ -52,7 +52,7 @@ default Keys annotations() { default Structure annotate(Key key, T value) { var outer = this; var annotations = annotations().with(key, new Identity<>(value)); - return annotatedDelegatingStructure(outer, annotations); + return annotatedDelegatingStructure(Function.identity(), outer, annotations); } /** @@ -63,24 +63,23 @@ default Structure annotate(Key key, T value) { default Structure annotate(Keys annotations) { var outer = this; var combined = annotations().join(annotations); - return annotatedDelegatingStructure(outer, combined); + return annotatedDelegatingStructure(Function.identity(), outer, combined); } - private static Structure annotatedDelegatingStructure(Structure outer, Keys annotations) { - final class AnnotatedDelegatingStructure implements Structure { - final @Nullable AnnotatedDelegatingStructure delegate; + private static Structure annotatedDelegatingStructure(Function, Structure> outerFunction, Structure outer, Keys annotations) { + final class AnnotatedDelegatingStructure implements Structure { + final Structure original; - AnnotatedDelegatingStructure(@Nullable AnnotatedDelegatingStructure delegate) { - this.delegate = delegate; + AnnotatedDelegatingStructure(Function, Structure> function, Structure original) { + while (original instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure) { + original = annotatedDelegatingStructure.original; + } + this.original = function.apply(original); } @Override - public DataResult> interpret(Interpreter interpreter) { - return interpreter.annotate(original(), annotations); - } - - private Structure original() { - return delegate != null ? delegate.original() : outer; + public DataResult> interpret(Interpreter interpreter) { + return interpreter.annotate(original, annotations); } @Override @@ -89,7 +88,7 @@ public Keys annotations() { } } - return new AnnotatedDelegatingStructure(outer instanceof AnnotatedDelegatingStructure annotatedDelegatingStructure ? annotatedDelegatingStructure : null); + return new AnnotatedDelegatingStructure<>(outerFunction, outer); } /** @@ -148,13 +147,13 @@ default RecordStructure.Builder optionalFieldOf(String name, Supplier defa * @param the new type to represent */ default Structure flatXmap(Function> deserializer, Function> serializer) { - var outer = this; - return annotatedDelegatingStructure(new Structure<>() { + Function, Structure> structureMaker = outer -> new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); } - }, outer.annotations()); + }; + return annotatedDelegatingStructure(structureMaker, this, this.annotations()); } /** @@ -169,7 +168,15 @@ default Structure xmap(Function deserializer, Function serial } default Structure dispatch(String key, Function> function, Supplier> keys, Function>> structures) { - var outer = this; + return dispatch(key, function, keys, structures, true); + } + + default Structure dispatchUnbounded(String key, Function> function, Supplier> keys, Function>> structures) { + return dispatch(key, function, keys, structures, false); + } + + private Structure dispatch(String key, Function> function, Supplier> keys, Function>> structures, boolean bounded) { + var outer = bounded ? this.bounded(keys) : this; return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { @@ -178,12 +185,20 @@ public DataResult> interpret(Interpreter interpre }; } - default Structure> dispatchMap(Supplier> keys, Function>> structures) { - var outer = this; + default Structure> dispatchedMap(Supplier> keys, Function>> structures) { + return dispatchedMap(keys, structures, true); + } + + default Structure> dispatchedUnboundedMap(Supplier> keys, Function>> structures) { + return dispatchedMap(keys, structures, false); + } + + private Structure> dispatchedMap(Supplier> keys, Function>> structures, boolean bounded) { + var outer = bounded ? this.bounded(keys) : this; return new Structure<>() { @Override public DataResult>> interpret(Interpreter interpreter) { - return interpreter.dispatchMap(outer, keys, structures); + return interpreter.dispatchedMap(outer, keys, structures); } }; } @@ -334,6 +349,34 @@ public DataResult> interpret(Interpreter interpre }; } + default Structure bounded(Supplier> available) { + final class BoundedStructure implements Structure { + private final Structure outer; + private final Supplier> totalAvailable; + + BoundedStructure(Structure outer) { + if (outer instanceof BoundedStructure boundedStructure) { + this.outer = boundedStructure.outer; + this.totalAvailable = () -> Sets.union(boundedStructure.totalAvailable.get(), available.get()); + } else { + this.outer = outer; + this.totalAvailable = available; + } + } + + @Override + public DataResult> interpret(Interpreter interpreter) { + return outer.interpret(interpreter).flatMap(app -> interpreter.bounded(app, totalAvailable)); + } + } + + return annotatedDelegatingStructure(BoundedStructure::new, this, this.annotations()); + } + + default Structure validate(Function> verifier) { + return this.flatXmap(verifier, verifier); + } + static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } @@ -442,7 +485,7 @@ static Structure doubleInRange(double min, double max) { * @param the type to represent */ static Structure stringRepresentable(Supplier values, Function representation) { - return Structure.parametricallyKeyed(Interpreter.STRING_REPRESENTABLE, new StringRepresentation<>(values, representation), app -> (Identity) app) + return Structure.parametricallyKeyed(Interpreter.STRING_REPRESENTABLE, StringRepresentation.ofArray(values, representation), app -> (Identity) app) .xmap(i -> Identity.unbox(i).value(), Identity::new); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index f3aac46..04f91b0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -246,18 +246,39 @@ public DataResult>> unboundedMap(App DataResult>> either(App left, App right) { var schema = new JsonObject(); - var oneOf = new JsonArray(); + var anyOf = new JsonArray(); var definitions = new HashMap>(); var leftSchema = schemaValue(left); var rightSchema = schemaValue(right); definitions.putAll(definitions(left)); definitions.putAll(definitions(right)); - oneOf.add(leftSchema); - oneOf.add(rightSchema); - schema.add("oneOf", oneOf); + anyOf.add(leftSchema); + anyOf.add(rightSchema); + schema.add("anyOf", anyOf); return DataResult.success(new Holder<>(schema, definitions)); } + @Override + public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + var definitions = new HashMap>(); + return codecInterpreter.interpret(keyStructure).flatMap(keyCodec -> { + var schema = OBJECT.get(); + for (var key : keys.get()) { + var keyValue = keyCodec.encodeStart(ops, key).flatMap(ops::getStringValue); + if (keyValue.error().isPresent()) { + return DataResult.error(keyValue.error().get().messageSupplier()); + } + var valueSchema = valueStructures.apply(key).flatMap(it -> it.interpret(this)); + if (valueSchema.error().isPresent()) { + return DataResult.error(valueSchema.error().get().messageSupplier()); + } + schema.add(keyValue.result().orElseThrow(), schemaValue(valueSchema.result().orElseThrow())); + definitions.putAll(definitions(valueSchema.result().orElseThrow())); + } + return DataResult.success(schema); + }).map(schema -> new Holder<>(schema, definitions)); + } + private static JsonObject schemaValue(App box) { return Holder.unbox(box).jsonObject; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java index 1504aba..c577e48 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -71,7 +71,7 @@ public App return new CodecInterpreter.Holder<>(TagKey.hashedCodec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.TagKeyHolder::new, a -> MinecraftKeys.TagKeyHolder.unbox(a).value())); } }) - .add(MinecraftKeys.HOMOGENOUS_LIST_KEY, new ParametricKeyedValue<>() { + .add(MinecraftKeys.HOMOGENOUS_LIST, new ParametricKeyedValue<>() { @Override public App> convert(App parameter) { return new CodecInterpreter.Holder<>(RegistryCodecs.homogeneousList(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.HolderSetHolder::new, a -> MinecraftKeys.HolderSetHolder.unbox(a).value())); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index 35b23dd..e774209 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -83,11 +83,15 @@ public static RegistryKeyHolder unbox(App box) { } } + public static final Key> DATA_COMPONENT_PATCH_KEY = Key.create("data_component_patch_key"); + + public record DataComponentPatchKey(DataComponentType type, boolean removes) {} + public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); public static final Key2 TAG_KEY = Key2.create("tag_key"); public static final Key2 HASHED_TAG_KEY = Key2.create("#tag_key"); - public static final Key2 HOMOGENOUS_LIST_KEY = Key2.create("homogenous_list"); + public static final Key2 HOMOGENOUS_LIST = Key2.create("homogenous_list"); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index e111bee..2e98397 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -64,7 +64,7 @@ private MinecraftStructures() { } return DataResult.error(() -> "Data component type " + type + " is not registered"); }) - .dispatchMap(() -> BuiltInRegistries.DATA_COMPONENT_TYPE.stream().collect(Collectors.toSet()), MinecraftStructures::dataComponentTypeStructure); + .dispatchedMap(() -> BuiltInRegistries.DATA_COMPONENT_TYPE.stream().collect(Collectors.toSet()), MinecraftStructures::dataComponentTypeStructure); public static final Structure, Object>> DATA_COMPONENT_VALUE_MAP = Structure.keyed( MinecraftKeys.VALUE_MAP, @@ -87,6 +87,29 @@ private MinecraftStructures() { }, dataComponentMap -> dataComponentMap.stream().collect(Collectors.toMap(TypedDataComponent::type, TypedDataComponent::value))) ); + public static final Structure> DATA_COMPONENT_PATCH_KEY = Structure.keyed( + MinecraftKeys.DATA_COMPONENT_PATCH_KEY, + Structure.STRING + .>flatXmap(string -> { + boolean removes = string.startsWith("!"); + string = removes ? string.substring(1) : string; + return ResourceLocation.read(string).flatMap(rl -> { + var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(rl); + if (type == null) { + return DataResult.error(() -> "Unknown data component type: " + rl); + } + return DataResult.success(new MinecraftKeys.DataComponentPatchKey<>(type, removes)); + }); + }, key -> { + var rl = BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(key.type()); + if (rl == null) { + return DataResult.error(() -> "Unknown data component type: " + key.type()); + } + return DataResult.success((key.removes() ? "!" : "") + rl); + }) + .bounded(MinecraftStructures::possibleDataComponentPatchKeys) + ); + @SuppressWarnings({"rawtypes", "unchecked"}) public static final Structure DATA_COMPONENT_PATCH = Structure.keyed( MinecraftKeys.DATA_COMPONENT_PATCH, @@ -94,22 +117,22 @@ private MinecraftStructures() { .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(DataComponentPatch.CODEC))) .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(DataComponentPatch.STREAM_CODEC))) .build(), - DataComponentPatchKey.STRUCTURE - .dispatchMap(DataComponentPatchKey::possibleKeys, DataComponentPatchKey::valueCodec) + DATA_COMPONENT_PATCH_KEY + .dispatchedUnboundedMap(MinecraftStructures::possibleDataComponentPatchKeys, MinecraftStructures::dataComponentPatchValueCodec) .xmap(map -> { var builder = DataComponentPatch.builder(); map.forEach((key, value) -> { - if (key.removes) { - builder.remove(key.type); + if (key.removes()) { + builder.remove(key.type()); } else { - builder.set((DataComponentType) key.type, value); + builder.set((DataComponentType) key.type(), value); } }); return builder.build(); }, patches -> patches.entrySet().stream().collect(Collectors.toMap(entry -> { var key = entry.getKey(); var removes = entry.getValue().isEmpty(); - return new DataComponentPatchKey<>(key, removes); + return new MinecraftKeys.DataComponentPatchKey<>(key, removes); }, entry -> { if (entry.getValue().isEmpty()) { return Unit.INSTANCE; @@ -136,7 +159,7 @@ public static Structure> resourceKey(ResourceKey Structure> homogenousList(ResourceKey> registry) { return Structure.parametricallyKeyed( - MinecraftKeys.HOMOGENOUS_LIST_KEY, + MinecraftKeys.HOMOGENOUS_LIST, new MinecraftKeys.RegistryKeyHolder<>(registry), MinecraftKeys.HolderSetHolder::unbox, Keys.>, K1>builder() @@ -186,8 +209,10 @@ private static DataResult> dataComponentTypeStructure(DataComponent return fallbackDataComponentTypeStructure(type, structure); } - @SuppressWarnings("unchecked") private static DataResult> fallbackDataComponentTypeStructure(DataComponentType type, @Nullable Structure> fallback) { + if (fallback != null) { + return DataResult.success(fallback); + } var key = MinecraftKeys.dataComponentType(type); var codec = type.codec(); var streamCodec = type.streamCodec(); @@ -197,55 +222,23 @@ private static DataResult> fallbackDataComponentTypeStructure(D keysBuilder.add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(codec))); } var keys = keysBuilder.build(); - if (fallback == null) { - return DataResult.success(Structure.keyed( - key, - keys - )); - } return DataResult.success(Structure.keyed( key, - keys, - (Structure) fallback + keys )); } - public static final class Types { - private Types() {} - + private static Set> possibleDataComponentPatchKeys() { + return BuiltInRegistries.DATA_COMPONENT_TYPE.stream() + .flatMap(type -> Stream.of(new MinecraftKeys.DataComponentPatchKey<>(type, false), new MinecraftKeys.DataComponentPatchKey<>(type, true))) + .collect(Collectors.toSet()); } - private record DataComponentPatchKey(DataComponentType type, boolean removes) { - private static Set> possibleKeys() { - return BuiltInRegistries.DATA_COMPONENT_TYPE.stream() - .flatMap(type -> Stream.of(new DataComponentPatchKey<>(type, false), new DataComponentPatchKey<>(type, true))) - .collect(Collectors.toSet()); - } - - private static final Structure> STRUCTURE = Structure.STRING - .flatXmap(string -> { - boolean removes = string.startsWith("!"); - string = removes ? string.substring(1) : string; - return ResourceLocation.read(string).flatMap(rl -> { - var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(rl); - if (type == null) { - return DataResult.error(() -> "Unknown data component type: " + rl); - } - return DataResult.success(new DataComponentPatchKey<>(type, removes)); - }); - }, key -> { - var rl = BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(key.type); - if (rl == null) { - return DataResult.error(() -> "Unknown data component type: " + key.type); - } - return DataResult.success((key.removes ? "!" : "") + rl); - }); - - private DataResult> valueCodec() { - if (removes) { - return DataResult.success(Structure.UNIT); - } - return dataComponentTypeStructure(type); + private static DataResult> dataComponentPatchValueCodec(MinecraftKeys.DataComponentPatchKey key) { + if (key.removes()) { + return DataResult.success(Structure.UNIT); } + return dataComponentTypeStructure(key.type()); } + } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 5d11029..0d6cab8 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -10,7 +10,7 @@ import net.minecraft.client.gui.screens.Screen; import org.slf4j.Logger; -public record ConfigScreenEntry(LayoutFactory widget, ScreenEntryFactory screenEntryProvider, EntryCreationInfo entryCreationInfo) implements App { +public record ConfigScreenEntry(LayoutFactory layout, ScreenEntryFactory screenEntryProvider, EntryCreationInfo entryCreationInfo) implements App { public static final class Mu implements K1 { private Mu() {} } @@ -23,14 +23,14 @@ public static ConfigScreenEntry single(LayoutFactory first, EntryCreat } public ConfigScreenEntry withComponentInfo(UnaryOperator function) { - return new ConfigScreenEntry<>(this.widget, this.screenEntryProvider, this.entryCreationInfo.withComponentInfo(function)); + return new ConfigScreenEntry<>(this.layout, this.screenEntryProvider, this.entryCreationInfo.withComponentInfo(function)); } public ConfigScreenEntry withEntryCreationInfo(Function, EntryCreationInfo> function, Function, EntryCreationInfo> reverse) { return new ConfigScreenEntry<>( (parent, width, context, original, update, entry, handleOptional) -> { var entryCreationInfo = reverse.apply(entry); - return this.widget.create(parent, width, context, original, update, entryCreationInfo, handleOptional); + return this.layout.create(parent, width, context, original, update, entryCreationInfo, handleOptional); }, (context, original, onClose, entry) -> { var entryCreationInfo = reverse.apply(entry); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index c00e954..f83c353 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -10,6 +10,7 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; +import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; @@ -41,8 +42,11 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; public class ConfigScreenInterpreter extends KeyStoringInterpreter { + private static final Logger LOGGER = LogUtils.getLogger(); + private final CodecInterpreter codecInterpreter; public ConfigScreenInterpreter( @@ -283,7 +287,6 @@ public App> convert(App() { - @SuppressWarnings("unchecked") @Override public App> convert(App parameter) { var registryKey = MinecraftKeys.RegistryKeyHolder.unbox(parameter).value(); @@ -295,7 +298,7 @@ public App> LayoutFactory> wrapped; Function, String> mapper = key -> key.location().toString(); if (registry.isPresent()) { - Supplier[]> values = () -> registry.get().registryKeySet().stream().sorted(Comparator., String>comparing(key -> key.location().getNamespace()).thenComparing(key -> key.location().getPath())).toArray(ResourceKey[]::new); + Supplier>> values = () -> registry.get().registryKeySet().stream().sorted(Comparator., String>comparing(key -> key.location().getNamespace()).thenComparing(key -> key.location().getPath())).toList(); wrapped = Widgets.pickWidget(new StringRepresentation<>(values, mapper)); } else { wrapped = (parent2, width2, context2, original2, update2, creationInfo2, handleOptional2) -> Widgets.text( @@ -343,13 +346,46 @@ public DataResult>> either(App(codecResult.getOrThrow(), ComponentInfo.empty()) )); } + @Override + public DataResult> bounded(App input, Supplier> values) { + var codec = codecInterpreter.bounded(new CodecInterpreter.Holder<>(ConfigScreenEntry.unbox(input).entryCreationInfo().codec()), values).map(CodecInterpreter::unbox); + if (codec.isError()) { + return DataResult.error(codec.error().orElseThrow().messageSupplier()); + } + return DataResult.success(ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + List knownValues = new ArrayList<>(); + Map stringValues = new HashMap<>(); + for (var value : values.get()) { + var encoded = codec.getOrThrow().encodeStart(context.ops(), value); + if (encoded.error().isPresent()) { + LOGGER.error("Error encoding value `{}`: {}", value, encoded.error().get()); + continue; + } + String string; + var result = encoded.getOrThrow(); + if (result.isJsonPrimitive()) { + string = result.getAsString(); + } else { + string = result.toString(); + } + knownValues.add(value); + stringValues.put(value, string); + } + var wrapped = Widgets.pickWidget(new StringRepresentation<>(() -> knownValues, stringValues::get)); + return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); + }, + ConfigScreenEntry.unbox(input).entryCreationInfo().withCodec(codec.getOrThrow()) + )); + } + @Override public ConfigScreenInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new ConfigScreenInterpreter(keys().join(keys), parametricKeys().join(parametricKeys), this.codecInterpreter); @@ -372,11 +408,14 @@ public DataResult>> list(App Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal, update, creationInfo + context, finalOriginal[0], value -> { + finalOriginal[0] = value; + update.accept(value); + }, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), @@ -403,11 +442,14 @@ public DataResult>> unboundedMap(App< update.accept(original); } } - JsonElement finalOriginal = original; + JsonElement[] finalOriginal = new JsonElement[] {original}; return Button.builder( Component.translatable("codecextras.config.configurelist"), b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal, update, creationInfo + context, finalOriginal[0], jsonValue -> { + finalOriginal[0] = jsonValue; + update.accept(jsonValue); + }, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), @@ -440,11 +482,14 @@ public DataResult> record(List Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal, update, creationInfo + context, finalOriginal[0], value -> { + finalOriginal[0] = value; + update.accept(value); + }, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), @@ -533,7 +578,55 @@ public DataResult> dispatch(String key, Stru return DataResult.error(() -> "Error creating dispatch codec: "+codecResult.error().orElseThrow().messageSupplier()); } ScreenEntryFactory factory = (context, original, onClose, creationInfo) -> - new DispatchScreenEntryProvider<>(keyResult.getOrThrow().entryCreationInfo(), original, key, onClose, context, entries.get()); + new DispatchScreenEntryProvider<>(keyResult.getOrThrow(), original, key, onClose, context, entries.get()); + return DataResult.success(new ConfigScreenEntry<>( + Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { + if (original.isJsonNull()) { + original = new JsonObject(); + if (!handleOptional) { + update.accept(original); + } + } + JsonElement[] finalOriginal = new JsonElement[] {original}; + return Button.builder( + Component.translatable("codecextras.config.configurerecord"), + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( + context, finalOriginal[0], value -> { + finalOriginal[0] = value; + update.accept(value); + }, creationInfo + ), parent, creationInfo.componentInfo())) + ).width(width).build(); + }), + factory, + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + )); + } + + @Override + public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + var keyResult = interpret(keyStructure); + if (keyResult.error().isPresent()) { + return DataResult.error(keyResult.error().get().messageSupplier()); + } + Supplier>>> entries = Suppliers.memoize(() -> { + Map>> map = new HashMap<>(); + for (var entryKey : keys.get()) { + var result = valueStructures.apply(entryKey).flatMap(it -> it.interpret(this)); + if (result.error().isPresent()) { + map.put(entryKey, DataResult.error(result.error().get().messageSupplier())); + } else { + map.put(entryKey, DataResult.success(ConfigScreenEntry.unbox(result.getOrThrow()))); + } + } + return map; + }); + var codecResult = codecInterpreter.dispatchedMap(keyStructure, keys, valueStructures); + if (codecResult.isError()) { + return DataResult.error(() -> "Error creating dispatch codec: "+codecResult.error().orElseThrow().messageSupplier()); + } + ScreenEntryFactory> factory = (context, original, onClose, creationInfo) -> + new DispatchedMapScreenEntryProvider<>(keyResult.getOrThrow(), original, onClose, context, entries.get()); return DataResult.success(new ConfigScreenEntry<>( Widgets.wrapWithOptionalHandling((parent, width, context, original, update, creationInfo, handleOptional) -> { if (original.isJsonNull()) { @@ -542,11 +635,14 @@ public DataResult> dispatch(String key, Stru update.accept(original); } } - JsonElement finalOriginal = original; + JsonElement[] finalOriginal = new JsonElement[] {original}; return Button.builder( Component.translatable("codecextras.config.configurerecord"), b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal, update, creationInfo + context, finalOriginal[0], value -> { + finalOriginal[0] = value; + update.accept(value); + }, creationInfo ), parent, creationInfo.componentInfo())) ).width(width).build(); }), diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java index b4353bd..62b1067 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java @@ -1,11 +1,11 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.mojang.logging.LogUtils; import com.mojang.serialization.DataResult; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,25 +16,24 @@ import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; class DispatchScreenEntryProvider implements ScreenEntryProvider { private static final Logger LOGGER = LogUtils.getLogger(); - private final EntryCreationInfo keyInfo; + private final ConfigScreenEntry keyEntry; private final String key; - private @Nullable String keyValue; + private JsonElement keyValue = JsonNull.INSTANCE; + private JsonElement oldKeyValue = JsonNull.INSTANCE; private JsonObject jsonValue; private final Consumer update; - private final List keys; - private final Map keyValues; - private final Map> keyProviders; + private final List keys; + private final Map> keyProviders; private final EntryCreationContext context; - public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jsonValue, String key, Consumer update, EntryCreationContext context, Map>> entries) { - this.keyInfo = keyInfo; + public DispatchScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, String key, Consumer update, EntryCreationContext context, Map>> entries) { + this.keyEntry = keyEntry; this.key = key; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); @@ -47,39 +46,26 @@ public DispatchScreenEntryProvider(EntryCreationInfo keyInfo, JsonElement jso this.update = update; this.context = context; this.keys = new ArrayList<>(); - this.keyValues = new HashMap<>(); this.keyProviders = new HashMap<>(); for (var entry : entries.entrySet()) { - var keyResult = keyInfo.codec().encodeStart(context.ops(), entry.getKey()); + var keyResult = keyEntry.entryCreationInfo().codec().encodeStart(context.ops(), entry.getKey()); if (keyResult.isError()) { LOGGER.warn("Failed to encode key {}", entry.getKey()); continue; } JsonElement keyElement = keyResult.getOrThrow(); - String keyAsString = stringify(keyElement); - keyValues.put(keyAsString, keyElement); if (entry.getValue().isError()) { LOGGER.warn("Failed to create screen entry for key {}: {}", entry.getKey(), entry.getValue().error().orElseThrow().message()); } - keyProviders.put(keyAsString, entry.getValue().getOrThrow()); - this.keys.add(keyAsString); + keyProviders.put(keyElement, entry.getValue().getOrThrow()); + this.keys.add(keyElement); } - this.keys.sort(Comparator.naturalOrder()); + this.keys.sort(JsonComparator.INSTANCE); if (this.jsonValue.has(key)) { - this.keyValue = stringify(this.jsonValue.get(key)); + this.keyValue = this.jsonValue.get(key); } } - private String stringify(JsonElement keyElement) { - String keyAsString; - if (keyElement.isJsonPrimitive()) { - keyAsString = keyElement.getAsString(); - } else { - keyAsString = keyElement.toString(); - } - return keyAsString; - } - @Override public void onExit() { if (nestedOnExit != null) { @@ -92,32 +78,37 @@ public void onExit() { @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { - var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyInfo.componentInfo().title(), Minecraft.getInstance().font).alignLeft(); - var contents = Button.builder(Component.literal(keyValue == null ? "" : keyValue), b -> { - Minecraft.getInstance().setScreen(new ChoiceScreen(parent, keyInfo.componentInfo().title(), keys, keyValue, newKeyValue -> { - if (!Objects.equals(newKeyValue, keyValue)) { - keyValue = newKeyValue; + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyEntry.entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var contents = keyEntry.layout().create(parent, Button.DEFAULT_WIDTH, context, keyValue, newKeyValue -> { + if (!Objects.equals(newKeyValue, oldKeyValue)) { + keyValue = newKeyValue; + boolean shouldRebuild = keyProviders.containsKey(keyValue) && !Objects.equals(oldKeyValue, keyValue); + if (shouldRebuild) { + oldKeyValue = keyValue; jsonValue = new JsonObject(); - if (keyValue != null) { - jsonValue.add(key, keyValues.get(keyValue)); - } else { - jsonValue.remove(key); - } + } + if (!keyValue.isJsonNull()) { + jsonValue.add(key, keyValue); + } else { + jsonValue.remove(key); + } + if (shouldRebuild) { rebuild.run(); } - })); - }).build(); - keyInfo.componentInfo().maybeDescription().ifPresent(description -> { + } + }, keyEntry.entryCreationInfo(), false); + keyEntry.entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); label.setTooltip(tooltip); - contents.setTooltip(tooltip); }); list.addPair(label, contents); - if (keyValue != null) { + if (!keyValue.isJsonNull()) { var provider = keyProviders.get(keyValue); - JsonObject valueCopy = new JsonObject(); - valueCopy.asMap().putAll(jsonValue.asMap()); - addEntry(provider, valueCopy, list, rebuild, parent); + if (provider != null) { + JsonObject valueCopy = new JsonObject(); + valueCopy.asMap().putAll(jsonValue.asMap()); + addEntry(provider, valueCopy, list, rebuild, parent); + } } } @@ -128,7 +119,7 @@ private void addEntry(ConfigScreenEntry provider, JsonObject va if (entry.getKey().equals(key)) { continue; } - jsonValue.add(entry.getKey(), entry.getValue()); + this.jsonValue.add(entry.getKey(), entry.getValue()); } } else { LOGGER.warn("Value {} was not a JSON object", newValue); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java new file mode 100644 index 0000000..d9161da --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java @@ -0,0 +1,161 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.mojang.datafixers.util.Pair; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.DataResult; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +class DispatchedMapScreenEntryProvider implements ScreenEntryProvider { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final ConfigScreenEntry keyEntry; + private final List keys; + private final Map> keyProviders; + private final List, JsonElement>> values = new ArrayList<>(); + private final JsonObject jsonValue = new JsonObject(); + private final Consumer update; + private final EntryCreationContext context; + + public DispatchedMapScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, Consumer update, EntryCreationContext context, Map>> entries) { + this.keyEntry = keyEntry; + if (jsonValue.isJsonObject()) { + jsonValue.getAsJsonObject().entrySet().stream() + .map(e -> Pair.of(Optional.of(e.getKey()), e.getValue())) + .forEach(values::add); + updateValue(); + } else { + if (!jsonValue.isJsonNull()) { + LOGGER.warn("Value {} was not a JSON array", jsonValue); + } + } + this.update = update; + this.context = context; + this.keys = new ArrayList<>(); + this.keyProviders = new HashMap<>(); + for (var entry : entries.entrySet()) { + if (entry.getValue().isError()) { + LOGGER.warn("Failed to create screen entry for key {}: {}", entry.getKey(), entry.getValue().error().orElseThrow().message()); + } + var keyResult = keyEntry.entryCreationInfo().codec().encodeStart(context.ops(), entry.getKey()); + if (keyResult.isError()) { + LOGGER.warn("Failed to encode key {}", entry.getKey()); + continue; + } + JsonElement keyElement = keyResult.getOrThrow(); + if (!keyElement.isJsonPrimitive()) { + LOGGER.warn("Key {} was not a JSON primitive", keyElement); + continue; + } else if (!keyElement.getAsJsonPrimitive().isString()) { + LOGGER.warn("Key {} was not a JSON string", keyElement); + continue; + } + String keyAsString = keyElement.getAsString(); + keyProviders.put(keyAsString, entry.getValue().getOrThrow()); + this.keys.add(keyAsString); + } + this.keys.sort(Comparator.naturalOrder()); + } + + private void updateValue() { + jsonValue.entrySet().clear(); + for (var pair : values) { + if (pair.getFirst().isEmpty()) { + continue; + } + if (jsonValue.has(pair.getFirst().get())) { + LOGGER.warn("Duplicate key {}", pair.getFirst().get()); + } + jsonValue.add(pair.getFirst().get(), pair.getSecond()); + } + } + + @Override + public void onExit() { + this.update.accept(jsonValue); + } + + @Override + public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { + var fullWidth = Button.DEFAULT_WIDTH*2+ EntryListScreen.Entry.SPACING; + for (int i = 0; i < values.size(); i++) { + var index = i; + var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + // 5 gives us good spacing here + var keyAndValueWidth = (fullWidth - (Button.DEFAULT_HEIGHT + 5) - 5)/2; + layout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.remove"), b -> { + values.remove(index); + updateValue(); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.remove"))).build(), LayoutSettings.defaults().alignVerticallyMiddle()); + var keyValue = values.get(index).getFirst(); + var keyLayout = keyEntry.layout().create( + parent, + keyAndValueWidth, + context, + keyValue.map(JsonPrimitive::new).orElse(JsonNull.INSTANCE), + newKeyValue -> { + String stringKeyValue = newKeyValue.isJsonNull() ? null : newKeyValue.isJsonPrimitive() && newKeyValue.getAsJsonPrimitive().isString() ? newKeyValue.getAsJsonPrimitive().getAsString() : null; + boolean shouldRebuild = keyProviders.containsKey(stringKeyValue); + if (!Objects.equals(stringKeyValue, keyValue.orElse(null))) { + values.set(index, Pair.of(Optional.ofNullable(stringKeyValue), JsonNull.INSTANCE)); + updateValue(); + if (shouldRebuild) { + rebuild.run(); + } + } + }, + keyEntry.entryCreationInfo(), + false + ); + layout.addChild(keyLayout, LayoutSettings.defaults().alignVerticallyMiddle()); + var valueEntry = keyValue.map(keyProviders::get).orElse(null); + if (valueEntry != null) { + addValueEntry(parent, layout, valueEntry, keyAndValueWidth, index); + } else { + var disabled = Button.builder(Component.translatable("codecextras.config.dispatchedmap.icon.disabled"), b -> {}).width(keyAndValueWidth).build(); + disabled.active = false; + layout.addChild(disabled, LayoutSettings.defaults().alignVerticallyMiddle()); + } + list.addSingle(layout); + } + var addLayout = new FrameLayout(fullWidth, 0); + addLayout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.add"), b -> { + values.add(new Pair<>(Optional.empty(), JsonNull.INSTANCE)); + updateValue(); + rebuild.run(); + }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.add"))).build(), LayoutSettings.defaults().alignHorizontallyLeft().alignVerticallyMiddle()); + list.addSingle(addLayout); + } + + private void addValueEntry(Screen parent, EqualSpacingLayout layout, ConfigScreenEntry valueEntry, int keyAndValueWidth, int index) { + layout.addChild(valueEntry.layout().create( + parent, + keyAndValueWidth, + context, values.get(index).getSecond(), + newValue -> { + this.values.set(index, this.values.get(index).mapSecond(old -> newValue)); + updateValue(); + }, valueEntry.entryCreationInfo(), + false + ), LayoutSettings.defaults().alignVerticallyMiddle()); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/JsonComparator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/JsonComparator.java new file mode 100644 index 0000000..d5d0a1e --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/JsonComparator.java @@ -0,0 +1,64 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.Map; + +class JsonComparator implements Comparator { + public static final JsonComparator INSTANCE = new JsonComparator(); + + private JsonComparator() {} + + @Override + public int compare(JsonElement o1, JsonElement o2) { + if (o1.isJsonPrimitive() && o2.isJsonPrimitive()) { + var p1 = o1.getAsJsonPrimitive(); + var p2 = o2.getAsJsonPrimitive(); + if (p1.isString() || p2.isString()) { + return p1.getAsString().compareTo(p2.getAsString()); + } else if (p1.isNumber() || p2.isNumber()) { + BigDecimal p1d = p1.isNumber() ? p1.getAsBigDecimal() : p1.getAsBoolean() ? BigDecimal.ONE : BigDecimal.ZERO; + BigDecimal p2d = p2.isNumber() ? p2.getAsBigDecimal() : p2.getAsBoolean() ? BigDecimal.ONE : BigDecimal.ZERO; + return p1d.compareTo(p2d); + } else { + return Boolean.compare(p1.getAsBoolean(), p2.getAsBoolean()); + } + } else if (o1.isJsonArray()) { + if (!o2.isJsonArray()) { + return 1; + } + var a1 = o1.getAsJsonArray(); + var a2 = o2.getAsJsonArray(); + int size = Math.min(a1.size(), a2.size()); + for (int i = 0; i < size; i++) { + int cmp = compare(a1.get(i), a2.get(i)); + if (cmp != 0) { + return cmp; + } + } + return Integer.compare(a1.size(), a2.size()); + } else if (o1.isJsonObject()) { + if (!o2.isJsonObject()) { + return 1; + } + var o1e = o1.getAsJsonObject().entrySet().stream().sorted(Map.Entry.comparingByKey()).iterator(); + var o2e = o2.getAsJsonObject().entrySet().stream().sorted(Map.Entry.comparingByKey()).iterator(); + while (o1e.hasNext() && o2e.hasNext()) { + var e1 = o1e.next(); + var e2 = o2e.next(); + int cmp = e1.getKey().compareTo(e2.getKey()); + if (cmp != 0) { + return cmp; + } + cmp = compare(e1.getValue(), e2.getValue()); + if (cmp != 0) { + return cmp; + } + } + return Boolean.compare(o1e.hasNext(), o2e.hasNext()); + } else { + return 0; + } + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java index 0ad1483..c6c1e68 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java @@ -81,7 +81,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { downButton.active = false; } layout.addChild(downButton, LayoutSettings.defaults().alignVerticallyMiddle()); - layout.addChild(entry.widget().create( + layout.addChild(entry.layout().create( parent, remainingWidth, context, jsonValue.get(index), diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java index c42556d..edacd17 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java @@ -67,7 +67,7 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp return encoded.result().orElseThrow(); }); JsonElement specificValueWithDefault = specificValue.isJsonNull() && defaultValue.isPresent() ? defaultValue.get() : specificValue; - return entry.entry().widget().create(parent, Button.DEFAULT_WIDTH, context, specificValueWithDefault, newValue -> { + return entry.entry().layout().create(parent, Button.DEFAULT_WIDTH, context, specificValueWithDefault, newValue -> { if (shouldUpdate(newValue, entry)) { this.jsonValue.add(entry.key(), newValue); } else { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java index b49f3f4..7535c0f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java @@ -67,45 +67,18 @@ public void onExit() { @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { - var fullWidth = Button.DEFAULT_WIDTH*2+ EntryListScreen.Entry.SPACING; + var fullWidth = Button.DEFAULT_WIDTH*2 + EntryListScreen.Entry.SPACING; for (int i = 0; i < values.size(); i++) { var index = i; var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); // 5 gives us good spacing here - var keyAndValueWidth = (fullWidth - (Button.DEFAULT_HEIGHT + 5)*3 - 5)/2; + var keyAndValueWidth = (fullWidth - (Button.DEFAULT_HEIGHT + 5) - 5)/2; layout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.remove"), b -> { values.remove(index); + updateValue(); rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.remove"))).build(), LayoutSettings.defaults().alignVerticallyMiddle()); - var upButton = Button.builder(Component.translatable("codecextras.config.list.icon.up"), b -> { - if (index == 0) { - return; - } - var oldAbove = values.get(index - 1); - var old = values.get(index); - values.set(index - 1, old); - values.set(index, oldAbove); - rebuild.run(); - }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.up"))).build(); - if (index == 0) { - upButton.active = false; - } - layout.addChild(upButton, LayoutSettings.defaults().alignVerticallyMiddle()); - var downButton = Button.builder(Component.translatable("codecextras.config.list.icon.down"), b -> { - if (index == values.size()-1) { - return; - } - var oldBelow = values.get(index + 1); - var old = values.get(index); - values.set(index + 1, old); - values.set(index, oldBelow); - rebuild.run(); - }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.down"))).build(); - if (index == values.size()-1) { - downButton.active = false; - } - layout.addChild(downButton, LayoutSettings.defaults().alignVerticallyMiddle()); - layout.addChild(keyEntry.widget().create( + layout.addChild(keyEntry.layout().create( parent, keyAndValueWidth, context, values.get(index).getFirst(), @@ -115,7 +88,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { }, keyEntry.entryCreationInfo(), false ), LayoutSettings.defaults().alignVerticallyMiddle()); - layout.addChild(valueEntry.widget().create( + layout.addChild(valueEntry.layout().create( parent, keyAndValueWidth, context, values.get(index).getSecond(), @@ -130,6 +103,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { var addLayout = new FrameLayout(fullWidth, 0); addLayout.addChild(Button.builder(Component.translatable("codecextras.config.list.icon.add"), b -> { values.add(new Pair<>(JsonNull.INSTANCE, JsonNull.INSTANCE)); + updateValue(); rebuild.run(); }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.list.add"))).build(), LayoutSettings.defaults().alignHorizontallyLeft().alignVerticallyMiddle()); list.addSingle(addLayout); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index be5b810..c575a15 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -126,7 +126,7 @@ public static LayoutFactory pickWidget(StringRepresentation representa this.button.setMessage(calculateMessage.get()); } })); - }).tooltip(Tooltip.create(creationInfo.componentInfo().description())).build(); + }).width(width).tooltip(Tooltip.create(creationInfo.componentInfo().description())).build(); }; return holder.button; }); @@ -138,7 +138,7 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass return assumesNonOptional.create(parent, fullWidth, context, original, update, creationInfo, false); } var remainingWidth = fullWidth - Button.DEFAULT_HEIGHT - DEFAULT_SPACING; - var layout = new EqualSpacingLayout(Button.DEFAULT_WIDTH, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var object = new Object() { private JsonElement value = original; private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, context, original, json -> { @@ -227,8 +227,6 @@ public static LayoutFactory color(boolean includeAlpha) { LOGGER.warn("Failed to decode `{}`: not a primitive", original); } - Function message = color -> Component.literal("0x"+Integer.toHexString(color)).withColor(color | 0xFF000000); - return new AbstractButton(0, 0, width, Button.DEFAULT_HEIGHT, Component.empty()) { { setTooltip(Tooltip.create(creationInfo.componentInfo().description())); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index bf2362c..8847035 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -20,6 +20,7 @@ import dev.lukebemish.codecextras.types.Identity; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.EncoderException; import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; @@ -27,6 +28,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; import net.minecraft.network.FriendlyByteBuf; @@ -65,17 +67,17 @@ public App, App> convert(App> lazy = Suppliers.memoize(() -> { var values = representation.values().get(); Map toIndexMap = new IdentityHashMap<>(); - for (int i = 0; i < values.length; i++) { - toIndexMap.put(values[i], i); + for (int i = 0; i < values.size(); i++) { + toIndexMap.put(values.get(i), i); } return new StreamCodec<>() { @Override public T decode(B buffer) { var intValue = VarInt.read(buffer); - if (intValue < 0 || intValue >= values.length) { + if (intValue < 0 || intValue >= values.size()) { throw new DecoderException("Unknown representation value: " + intValue); } - return values[intValue]; + return values.get(intValue); } @Override @@ -206,23 +208,51 @@ public DataResult, A>> annotate(Structure original, Keys public DataResult, E>> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keyCodecApp -> { var keyStreamCodec = unbox(keyCodecApp); - Supplier>>> codecMapSupplier = Suppliers.memoize(() -> { - Map>> codecMap = new HashMap<>(); - for (var entryKey : keys.get()) { - var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); - if (result.error().isPresent()) { - codecMap.put(entryKey, DataResult.error(result.error().get().messageSupplier())); - } - codecMap.put(entryKey, DataResult.success(StreamCodecInterpreter.unbox(result.result().orElseThrow()))); - } - return codecMap; - }); + var map = new ConcurrentHashMap>>(); + Function>> cache = k -> map.computeIfAbsent(k , structures.andThen(result -> result.flatMap(s -> s.interpret(this)).map(StreamCodecInterpreter::unbox))); return DataResult.success(new Holder<>( - keyStreamCodec.dispatch(function.andThen(DataResult::getOrThrow), k -> codecMapSupplier.get().get(k).getOrThrow()) + keyStreamCodec.dispatch(function.andThen(DataResult::getOrThrow), cache.andThen(DataResult::getOrThrow)) )); }); } + @Override + public DataResult, Map>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { + return keyStructure.interpret(this).map(StreamCodecInterpreter::unbox).flatMap(keyCodec -> { + var map = new ConcurrentHashMap>>(); + Function>> cache = k -> map.computeIfAbsent(k , valueStructures.andThen(result -> result.flatMap(s -> s.interpret(this)).map(StreamCodecInterpreter::unbox))); + return DataResult.success(new Holder<>(new StreamCodec<>() { + @Override + public Map decode(B buffer) { + var map = new HashMap(); + var size = VarInt.read(buffer); + for (int i = 0; i < size; i++) { + var key = keyCodec.decode(buffer); + var valueCodec = cache.apply(key).getOrThrow(s -> new DecoderException("Could not find StreamCodec for key "+key+": "+s)); + var value = valueCodec.decode(buffer); + map.put(key, value); + } + return map; + } + + @Override + public void encode(B buffer, Map object) { + buffer.writeInt(object.size()); + object.forEach((key, value) -> { + keyCodec.encode(buffer, key); + var valueCodec = cache.apply(key).getOrThrow(s -> new EncoderException("Could not find StreamCodec for key "+ key +": "+s)); + encodeValue(buffer, valueCodec, value); + }); + } + + @SuppressWarnings("unchecked") + private void encodeValue(B buffer, StreamCodec valueCodec, V value) { + valueCodec.encode(buffer, (X) value); + } + })); + }); + } + private static void encodeSingleField(B buf, Field field, A data) { var missingBehaviour = field.missingBehavior(); if (missingBehaviour.isEmpty()) { diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json index ce04554..4938d7e 100644 --- a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -11,5 +11,6 @@ "codecextras.config.list.up": "Move up", "codecextras.config.list.down": "Move down", "codecextras.config.list.remove": "Remove entry", - "codecextras.config.either.switch": "Switch entry type" + "codecextras.config.either.switch": "Switch entry type", + "codecextras.config.dispatchedmap.icon.disabled": "Pick a type to configure" } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index e9c7126..5643076 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -27,7 +27,7 @@ public record TestConfig( Unit g, List strings, Dispatches dispatches, int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, - Map unbounded, Either either + Map unbounded, Either either, Map dispatchedMap ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -92,13 +92,14 @@ public String key() { var rarity = builder.addOptional("rarity", Structure.stringRepresentable(Rarity::values, Rarity::getSerializedName), TestConfig::rarity, () -> Rarity.COMMON); var unbounded = builder.addOptional("unbounded", Structure.unboundedMap(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::unbounded, () -> Map.of("test", 123)); var either = builder.addOptional("either", Structure.either(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::either, () -> Either.right(0x00FFAA)); + var dispatchedMap = builder.addOptional("dispatchedMap", Structure.STRING.dispatchedMap(DISPATCHES::keySet, k -> DataResult.success(DISPATCHES.get(k))), TestConfig::dispatchedMap, Map::of); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), g.apply(container), strings.apply(container), dispatches.apply(container), intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), - unbounded.apply(container), either.apply(container) + unbounded.apply(container), either.apply(container), dispatchedMap.apply(container) ); }); From 10325f7ff8dcd2d42445ca110a025d43dfa0b86c Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 5 Sep 2024 20:59:25 -0500 Subject: [PATCH 55/76] Working component patch key config --- build.gradle | 3 - .../codecextras/StringRepresentation.java | 31 +++++-- .../codecextras/structured/Structure.java | 5 +- .../config/ConfigScreenInterpreter.java | 86 ++++++++++++++++++- .../codecextras_minecraft/lang/en_us.json | 4 +- .../codecextras/test/common/TestConfig.java | 9 +- 6 files changed, 119 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index d95a13b..f1d7016 100644 --- a/build.gradle +++ b/build.gradle @@ -289,9 +289,6 @@ tasks.compileJava { '-Aautoextension.name=CodecExtras', "-Aautoextension.version=${version}".toString() ] - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) - } } ['processResources', 'processMinecraftResources', 'processMinecraftFabricResources', 'processMinecraftNeoforgeResources'].each { diff --git a/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java index 9e58180..ccf42eb 100644 --- a/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java +++ b/src/main/java/dev/lukebemish/codecextras/StringRepresentation.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.serialization.Codec; @@ -10,6 +11,7 @@ import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; /** * A representation of a type with finite possible values as strings. Values of the type should be comparable by identity @@ -17,12 +19,27 @@ * @param representation converts a value to a string * @param the type of the values */ -public record StringRepresentation(Supplier> values, Function representation) implements App { +public record StringRepresentation(Supplier> values, Function representation, Function inverse, boolean identity) implements App { public static final class Mu implements K1 { private Mu() { } } + public StringRepresentation(Supplier> values, Function representation) { + this(values, representation, memoizeInverse(representation, values), false); + } + + private static Function memoizeInverse(Function representation, Supplier> values) { + Supplier> mapSupplier = Suppliers.memoize(() -> { + var map = new HashMap(); + for (var value : values.get()) { + map.put(representation.apply(value), value); + } + return map; + }); + return t -> mapSupplier.get().get(t); + } + public static StringRepresentation ofArray(Supplier values, Function representation) { Supplier> listSupplier = () -> List.of(values.get()); return new StringRepresentation<>(listSupplier, representation); @@ -35,22 +52,18 @@ public static StringRepresentation unbox(App box) { public Codec codec() { return Codec.lazyInitialized(() -> { var values = this.values().get(); - var map = new HashMap(); - for (var value : values) { - map.put(this.representation().apply(value), value); - } Function toString; if (values.size() > 16) { toString = this.representation(); } else { - Map representationMap = new IdentityHashMap<>(); - for (var entry : map.entrySet()) { - representationMap.put(entry.getValue(), entry.getKey()); + Map representationMap = identity() ? new IdentityHashMap<>() : new HashMap<>(); + for (var value : values) { + representationMap.put(value, this.representation().apply(value)); } toString = representationMap::get; } return Codec.STRING.comapFlatMap(string -> { - T value = map.get(string); + T value = inverse.apply(string); if (value == null) { return DataResult.error(() -> "Unknown string representation value: " + string); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 6da4290..9fef9d3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -147,13 +147,12 @@ default RecordStructure.Builder optionalFieldOf(String name, Supplier defa * @param the new type to represent */ default Structure flatXmap(Function> deserializer, Function> serializer) { - Function, Structure> structureMaker = outer -> new Structure<>() { + return annotatedDelegatingStructure(outer -> new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); } - }; - return annotatedDelegatingStructure(structureMaker, this, this.annotations()); + }, this, this.annotations()); } /** diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index f83c353..e794b84 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -3,6 +3,7 @@ import com.google.common.base.Suppliers; import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.mojang.datafixers.kinds.App; @@ -14,7 +15,9 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Interpreter; @@ -39,6 +42,13 @@ import java.util.stream.Collectors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -140,6 +150,78 @@ public ConfigScreenInterpreter( Widgets.color(false), new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) )) + .add(MinecraftKeys.DATA_COMPONENT_PATCH_KEY, ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + boolean[] removes = new boolean[1]; + DataComponentType[] type = new DataComponentType[1]; + if (!original.isJsonNull()) { + var initialResult = creationInfo.codec().parse(context.ops(), original); + if (initialResult.error().isPresent()) { + LOGGER.error("Error parsing data component patch key: {}", initialResult.error().get()); + } else { + removes[0] = initialResult.getOrThrow().removes(); + type[0] = initialResult.getOrThrow().type(); + } + } + var remainingWidth = width - Button.DEFAULT_HEIGHT - 5; + var cycle = CycleButton.builder(bool -> { + if (bool == Boolean.TRUE) { + return Component.translatable("codecextras.config.datacomponent.keytoggle.removes"); + } + return Component.empty(); + }).withValues(List.of(true, false)) + .withInitialValue(removes[0]) + .displayOnlyValue() + .create(0, 0, Button.DEFAULT_HEIGHT, Button.DEFAULT_HEIGHT, Component.translatable("codecextras.config.datacomponent.keytoggle"), (b, bool) -> { + removes[0] = bool; + if (type[0] != null) { + var result = creationInfo.codec().encodeStart(context.ops(), new MinecraftKeys.DataComponentPatchKey<>(type[0], removes[0])); + if (result.error().isPresent()) { + LOGGER.error("Error encoding data component patch key: {}", result.error().get()); + } else { + update.accept(result.getOrThrow()); + } + } + }); + var typeKeys = BuiltInRegistries.DATA_COMPONENT_TYPE.registryKeySet().stream().toList(); + var actual = Widgets.pickWidget( + new StringRepresentation<>(() -> typeKeys, key -> key.location().toString()) + ).create(parent, remainingWidth, context, type[0] == null ? JsonNull.INSTANCE : new JsonPrimitive(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(type[0]).toString()), json -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + String string = json.getAsJsonPrimitive().getAsString(); + var rlResult = ResourceLocation.read(string); + if (rlResult.error().isPresent()) { + LOGGER.error("Error reading resource location: {}", rlResult.error().get()); + return; + } + var key = rlResult.getOrThrow(); + var typeResult = BuiltInRegistries.DATA_COMPONENT_TYPE.getOptional(key); + if (typeResult.isEmpty()) { + LOGGER.error("Unknown data component type: {}", key); + return; + } + type[0] = typeResult.get(); + var result = creationInfo.codec().encodeStart(context.ops(), new MinecraftKeys.DataComponentPatchKey<>(type[0], removes[0])); + if (result.error().isPresent()) { + LOGGER.error("Error encoding data component patch key: {}", result.error().get()); + } else { + update.accept(result.getOrThrow()); + } + } else { + LOGGER.error("Not a string: {}", json); + } + }, creationInfo.withCodec(ResourceKey.codec(Registries.DATA_COMPONENT_TYPE)), false); + var tooltipToggle = Tooltip.create(Component.translatable("codecextras.config.datacomponent.keytoggle")); + var tooltipType = Tooltip.create(creationInfo.componentInfo().description()); + cycle.setTooltip(tooltipToggle); + actual.visitWidgets(w -> w.setTooltip(tooltipType)); + var layout = new EqualSpacingLayout(width, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + layout.addChild(cycle, LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(actual, LayoutSettings.defaults().alignVerticallyMiddle()); + return layout; + }, + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(MinecraftStructures.DATA_COMPONENT_PATCH_KEY).getOrThrow(), ComponentInfo.empty()) + )) .build()), parametricKeys.join(Keys2., K1, K1>builder() .add(Interpreter.INT_IN_RANGE, new ParametricKeyedValue<>() { @@ -363,6 +445,7 @@ public DataResult> bounded(App { List knownValues = new ArrayList<>(); Map stringValues = new HashMap<>(); + Map inverse = new HashMap<>(); for (var value : values.get()) { var encoded = codec.getOrThrow().encodeStart(context.ops(), value); if (encoded.error().isPresent()) { @@ -378,8 +461,9 @@ public DataResult> bounded(App(() -> knownValues, stringValues::get)); + var wrapped = Widgets.pickWidget(new StringRepresentation<>(() -> knownValues, stringValues::get, inverse::get, false)); return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); }, ConfigScreenEntry.unbox(input).entryCreationInfo().withCodec(codec.getOrThrow()) diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json index 4938d7e..dba9c68 100644 --- a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -12,5 +12,7 @@ "codecextras.config.list.down": "Move down", "codecextras.config.list.remove": "Remove entry", "codecextras.config.either.switch": "Switch entry type", - "codecextras.config.dispatchedmap.icon.disabled": "Pick a type to configure" + "codecextras.config.dispatchedmap.icon.disabled": "Pick a type to configure", + "codecextras.config.datacomponent.keytoggle": "Toggle remove/add", + "codecextras.config.datacomponent.keytoggle.removes": "!" } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 5643076..a5f19ae 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -6,6 +6,7 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; +import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.IdentityInterpreter; @@ -15,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.Registries; import net.minecraft.references.Items; import net.minecraft.resources.ResourceKey; @@ -27,7 +29,8 @@ public record TestConfig( Unit g, List strings, Dispatches dispatches, int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, - Map unbounded, Either either, Map dispatchedMap + Map unbounded, Either either, Map dispatchedMap, + MinecraftKeys.DataComponentPatchKey patchKey ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -93,13 +96,15 @@ public String key() { var unbounded = builder.addOptional("unbounded", Structure.unboundedMap(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::unbounded, () -> Map.of("test", 123)); var either = builder.addOptional("either", Structure.either(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::either, () -> Either.right(0x00FFAA)); var dispatchedMap = builder.addOptional("dispatchedMap", Structure.STRING.dispatchedMap(DISPATCHES::keySet, k -> DataResult.success(DISPATCHES.get(k))), TestConfig::dispatchedMap, Map::of); + var patchKey = builder.addOptional("patchKey", MinecraftStructures.DATA_COMPONENT_PATCH_KEY, TestConfig::patchKey, () -> new MinecraftKeys.DataComponentPatchKey<>(DataComponents.BEES, false)); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), g.apply(container), strings.apply(container), dispatches.apply(container), intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), - unbounded.apply(container), either.apply(container), dispatchedMap.apply(container) + unbounded.apply(container), either.apply(container), dispatchedMap.apply(container), + patchKey.apply(container) ); }); From c8f74cc3abd4bfcb4322d78f0abaf05d3aa09962 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 8 Sep 2024 00:24:21 -0500 Subject: [PATCH 56/76] Better error confirmation screens and data components --- build.gradle | 2 + minecraftFabric/build.gradle | 5 + .../codecextras/structured/Interpreter.java | 2 + .../codecextras/structured/Structure.java | 12 + .../codecextras/types/Preparable.java | 33 ++ .../codecextras/types/PreparableView.java | 7 + .../structured/CodecExtrasRegistries.java | 33 +- .../minecraft/structured/MinecraftKeys.java | 17 +- .../structured/MinecraftStructures.java | 22 +- .../structured/config/ColorPickScreen.java | 14 +- .../config/ConfigScreenBuilder.java | 4 +- .../structured/config/ConfigScreenEntry.java | 10 +- .../config/ConfigScreenInterpreter.java | 374 +++++++++++++++--- .../config/DispatchScreenEntryProvider.java | 37 +- .../DispatchedMapScreenEntryProvider.java | 27 +- .../config/EntryCreationContext.java | 29 ++ .../structured/config/EntryListScreen.java | 24 +- .../config/ListScreenEntryProvider.java | 4 +- .../config/RecordScreenEntryProvider.java | 10 +- .../structured/config/ScreenEntryFactory.java | 12 + .../config/ScreenEntryProvider.java | 6 +- .../config/SingleScreenEntryProvider.java | 2 +- .../UnboundedMapScreenEntryProvider.java | 11 +- .../minecraft/structured/config/Widgets.java | 44 ++- .../codecextras_minecraft/lang/en_us.json | 12 +- .../CodecExtrasRegistriesRegistrar.java | 7 +- .../fabric/mixin/MappedRegistryMixin.java | 23 ++ .../minecraft/fabric/mixin/package-info.java | 6 + .../codecextras.minecraft.mixin.json | 11 + src/minecraftFabric/resources/fabric.mod.json | 5 +- .../neoforge/CodecExtrasNeoforge.java | 4 + .../codecextras/test/common/TestConfig.java | 9 +- .../test/neoforge/CodecExtrasTest.java | 15 +- 33 files changed, 676 insertions(+), 157 deletions(-) create mode 100644 minecraftFabric/build.gradle create mode 100644 src/main/java/dev/lukebemish/codecextras/types/Preparable.java create mode 100644 src/main/java/dev/lukebemish/codecextras/types/PreparableView.java create mode 100644 src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/MappedRegistryMixin.java create mode 100644 src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/package-info.java create mode 100644 src/minecraftFabric/resources/codecextras.minecraft.mixin.json diff --git a/build.gradle b/build.gradle index f1d7016..39a5ab9 100644 --- a/build.gradle +++ b/build.gradle @@ -206,7 +206,9 @@ dependencies { minecraftNeoforgeApi project(':') testNeoforgeCompileOnly sourceSets.minecraftNeoforge.output + testNeoforgeCompileOnly sourceSets.minecraft.output testFabricCompileOnly sourceSets.minecraftFabric.output + testFabricCompileOnly sourceSets.minecraft.output testCommonCompileOnly(project(':')) { capabilities { requireCapability 'dev.lukebemish:codecextras-minecraft-common' diff --git a/minecraftFabric/build.gradle b/minecraftFabric/build.gradle new file mode 100644 index 0000000..92bc0a5 --- /dev/null +++ b/minecraftFabric/build.gradle @@ -0,0 +1,5 @@ +loom { + mixin { + useLegacyMixinAp = false + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 4b66ea5..6618290 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -6,6 +6,7 @@ import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Identity; import java.util.List; @@ -53,6 +54,7 @@ default DataResult> bounded(App input, Supplier> val Key FLOAT = Key.create("FLOAT"); Key DOUBLE = Key.create("DOUBLE"); Key STRING = Key.create("STRING"); + Key> PASSTHROUGH = Key.create("PASSTHROUGH"); DataResult>> parametricallyKeyed(Key2 key, App parameter); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 9fef9d3..ceb197b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -6,7 +6,9 @@ import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; @@ -417,6 +419,16 @@ static Structure record(RecordStructure.Builder builder) { */ Structure STRING = keyed(Interpreter.STRING); + /** + * Represents a {@link Dynamic} value. + */ + Structure> PASSTHROUGH = keyed( + Interpreter.PASSTHROUGH, + Keys.>, K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(Codec.PASSTHROUGH))) + .build() + ); + /** * {@return a structure representing integer values within a range} * @param min the minimum value (inclusive) diff --git a/src/main/java/dev/lukebemish/codecextras/types/Preparable.java b/src/main/java/dev/lukebemish/codecextras/types/Preparable.java new file mode 100644 index 0000000..4e45c2f --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/Preparable.java @@ -0,0 +1,33 @@ +package dev.lukebemish.codecextras.types; + +import com.google.common.base.Suppliers; +import java.util.function.Supplier; + +public interface Preparable extends PreparableView { + void prepare(); + + static Preparable memoize(Supplier supplier) { + var memoized = Suppliers.memoize(supplier::get); + return new Preparable<>() { + private volatile boolean prepared = false; + + @Override + public boolean isReady() { + return prepared; + } + + @Override + public T get() { + if (!prepared) { + throw new IllegalStateException("Not ready!"); + } + return memoized.get(); + } + + @Override + public void prepare() { + prepared = true; + } + }; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/types/PreparableView.java b/src/main/java/dev/lukebemish/codecextras/types/PreparableView.java new file mode 100644 index 0000000..2ac7702 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/types/PreparableView.java @@ -0,0 +1,7 @@ +package dev.lukebemish.codecextras.types; + +import java.util.function.Supplier; + +public interface PreparableView extends Supplier { + boolean isReady(); +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java index 0a9e586..a64d5d7 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java @@ -1,9 +1,10 @@ package dev.lukebemish.codecextras.minecraft.structured; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.types.Preparable; +import dev.lukebemish.codecextras.types.PreparableView; import java.util.Objects; import java.util.ServiceLoader; -import java.util.function.Supplier; import net.minecraft.core.Registry; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.BuiltInRegistries; @@ -13,24 +14,36 @@ public class CodecExtrasRegistries { private static final String NAMESPACE = "codecextras_minecraft"; + private static final RegistryRegistrar.RegistriesImpl REGISTRIES_IMPL = new RegistryRegistrar.RegistriesImpl(); - public static final ResourceKey>>> DATA_COMPONENT_STRUCTURES = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(NAMESPACE, "data_component__type")); + public static final ResourceKey>>> DATA_COMPONENT_STRUCTURES = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(NAMESPACE, "data_component_type")); + public static final Registries REGISTRIES = REGISTRIES_IMPL; static { // This MUST be the last static initializer in this class -- the registry registrar may depend on the keys defined earlier on - ServiceLoader.load(RegistryRegistrar.class).stream().map(ServiceLoader.Provider::get).forEach(RegistryRegistrar::setup); + ServiceLoader.load(RegistryRegistrar.class).stream().map(ServiceLoader.Provider::get).forEach(registryRegistrar -> registryRegistrar.setup(REGISTRIES_IMPL)); } - public static final class Registries { - private Registries() {} - - @SuppressWarnings("unchecked") - public static final Supplier>>> DATA_COMPONENT_STRUCTURES = () -> - (Registry>>) Objects.requireNonNull(BuiltInRegistries.REGISTRY.get(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES.location()), "Registry does not exist"); + public abstract sealed static class Registries { + public abstract PreparableView>>> dataComponentStructures(); } @ApiStatus.Internal public interface RegistryRegistrar { - void setup(); + @ApiStatus.Internal + final class RegistriesImpl extends Registries { + private RegistriesImpl() {} + + @Override + public PreparableView>>> dataComponentStructures() { + return dataComponentStructures; + } + + @SuppressWarnings("unchecked") + public final Preparable>>> dataComponentStructures = Preparable.memoize(() -> + (Registry>>) Objects.requireNonNull(BuiltInRegistries.REGISTRY.get(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES.location()), "Registry does not exist")); + } + + void setup(RegistriesImpl registries); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index e774209..0317fb9 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.kinds.K1; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Key2; +import dev.lukebemish.codecextras.types.Identity; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.core.HolderSet; @@ -11,7 +12,6 @@ import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentType; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; @@ -21,6 +21,7 @@ public final class MinecraftKeys { public static Key, Object>> VALUE_MAP = Key.create("value_map"); public static Key DATA_COMPONENT_MAP = Key.create("data_component_map"); public static Key DATA_COMPONENT_PATCH = Key.create("data_component_patch"); + public static Key2 FALLBACK_DATA_COMPONENT_TYPE = Key2.create("data_component_type"); private MinecraftKeys() { } @@ -29,13 +30,15 @@ private MinecraftKeys() { public static final Key ARGB_COLOR = Key.create("argb_color"); public static final Key RGB_COLOR = Key.create("rgb_color"); - @SuppressWarnings("unchecked") - public static Key dataComponentType(DataComponentType type) { - var location = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type); - if (location.isPresent()) { - return (Key) DATA_COMPONENT_TYPE_KEYS.computeIfAbsent(location.orElseThrow(), key -> Key.create(key.location().toString())); + public record DataComponentTypeHolder(DataComponentType value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static DataComponentTypeHolder unbox(App box) { + return (DataComponentTypeHolder) box; } - throw new IllegalArgumentException("Data component type " + type + " is not registered"); } public record ResourceKeyHolder(ResourceKey value) implements App { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 2e98397..a75718f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -9,6 +9,7 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; +import dev.lukebemish.codecextras.types.Identity; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -23,6 +24,7 @@ import net.minecraft.core.component.TypedDataComponent; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; @@ -205,7 +207,10 @@ private static DataResult> dataComponentTypeStructure(DataComponent if (resourceKey.isEmpty()) { return DataResult.error(() -> "Unregistered data component type: " + type); } - var structure = CodecExtrasRegistries.Registries.DATA_COMPONENT_STRUCTURES.get().get(resourceKey.orElseThrow().location()); + if (!CodecExtrasRegistries.REGISTRIES.dataComponentStructures().isReady()) { + return DataResult.error(() -> "Data component structures registry is not frozen"); + } + var structure = CodecExtrasRegistries.REGISTRIES.dataComponentStructures().get().get(resourceKey.orElseThrow().location()); return fallbackDataComponentTypeStructure(type, structure); } @@ -213,17 +218,20 @@ private static DataResult> fallbackDataComponentTypeStructure(D if (fallback != null) { return DataResult.success(fallback); } - var key = MinecraftKeys.dataComponentType(type); + var key2 = MinecraftKeys.FALLBACK_DATA_COMPONENT_TYPE; + var codec = type.codec(); var streamCodec = type.streamCodec(); - var keysBuilder = Keys., K1>builder() - .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(streamCodec.cast())));; + var keysBuilder = Keys.>, K1>builder() + .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(streamCodec.cast().map(Identity::new, i -> Identity.unbox(i).value()))));; if (codec != null) { - keysBuilder.add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(codec))); + keysBuilder.add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(codec.xmap(Identity::new, i -> Identity.unbox(i).value())))); } var keys = keysBuilder.build(); - return DataResult.success(Structure.keyed( - key, + return DataResult.success(Structure.parametricallyKeyed( + key2, + new MinecraftKeys.DataComponentTypeHolder<>(type), + Identity::unbox, keys )); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java index 7627138..75ed708 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java @@ -2,8 +2,6 @@ import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.logging.LogUtils; -import java.util.Locale; -import java.util.function.Consumer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -14,8 +12,12 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; +import java.util.Locale; +import java.util.function.Consumer; + class ColorPickScreen extends Screen { private static final Logger LOGGER = LogUtils.getLogger(); @@ -24,8 +26,8 @@ class ColorPickScreen extends Screen { private final Consumer consumer; private final boolean hasAlpha; int color; - private EditBox textField; - private ColorPickWidget pick; + private @Nullable EditBox textField; + private @Nullable ColorPickWidget pick; protected ColorPickScreen(Screen backgroundScreen, Component component, Consumer consumer, boolean hasAlpha) { super(component); @@ -57,10 +59,10 @@ protected void init() { var color = Integer.parseUnsignedInt(string, 16); setColor(color); } catch (NumberFormatException e) { - LOGGER.warn("Invalid hex number: {}", string, e); + LOGGER.error("Invalid hex number: {}", string, e); } }); - textField.setFilter(string -> string.matches("[0-9a-fA-F]*")); + textField.setFilter(string -> string.matches("^[0-9a-fA-F]{0,"+(hasAlpha ? 8 : 6)+"}$")); var button = Button.builder(CommonComponents.GUI_DONE, b -> this.onClose()).width(pick.getWidth() - 80 - 8).build(); bottomLayout.addChild(textField); bottomLayout.addChild(button); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java index caf48d2..11aee57 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java @@ -47,7 +47,7 @@ public UnaryOperator factory() { } else { return ScreenEntryProvider.create(new ScreenEntryProvider() { @Override - public void onExit() {} + public void onExit(EntryCreationContext context) {} @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { @@ -65,7 +65,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { list.addPair(label, button); } } - }, parent, ComponentInfo.empty()); + }, parent, EntryCreationContext.builder().build(), ComponentInfo.empty()); } }; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 0d6cab8..1393ec3 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -34,7 +34,7 @@ public ConfigScreenEntry withEntryCreationInfo(Function { var entryCreationInfo = reverse.apply(entry); - return this.screenEntryProvider.open(context, original, onClose, entryCreationInfo); + return this.screenEntryProvider.openChecked(context, original, onClose, entryCreationInfo); }, function.apply(this.entryCreationInfo) ); @@ -44,19 +44,19 @@ Screen rootScreen(Screen parent, Consumer onClose, EntryCreationContext conte var initial = entryCreationInfo.codec().encodeStart(context.ops(), initialData); JsonElement initialJson; if (initial.error().isPresent()) { - logger.warn("Failed to encode `{}`: {}", initialData, initial.error().get().message()); + logger.error("Failed to encode `{}`: {}", initialData, initial.error().get().message()); initialJson = JsonNull.INSTANCE; } else { initialJson = initial.getOrThrow(); } - var provider = screenEntryProvider().open(context, initialJson, json -> { + var provider = screenEntryProvider().openChecked(context, initialJson, json -> { var decoded = entryCreationInfo.codec().parse(context.ops(), json); if (decoded.error().isPresent()) { - logger.warn("Failed to decode `{}`: {}", json, decoded.error().get().message()); + logger.error("Failed to decode `{}`: {}", json, decoded.error().get().message()); } else { onClose.accept(decoded.getOrThrow()); } }, this.entryCreationInfo()); - return ScreenEntryProvider.create(provider, parent, entryCreationInfo.componentInfo()); + return ScreenEntryProvider.create(provider, parent, context, entryCreationInfo.componentInfo()); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index e794b84..1c392a0 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -1,11 +1,14 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.common.base.Suppliers; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; @@ -14,6 +17,9 @@ import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.codecs.PrimitiveCodec; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; @@ -30,13 +36,16 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -45,13 +54,17 @@ import net.minecraft.client.gui.components.CycleButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class ConfigScreenInterpreter extends KeyStoringInterpreter { @@ -73,7 +86,7 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not an integer: "+string); } - }, integer -> DataResult.success(integer+""), string -> string.matches("-?[0-9]*"), true), + }, integer -> DataResult.success(integer+""), string -> string.matches("^-?[0-9]*$"), true), new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) )) .add(Interpreter.BYTE, ConfigScreenEntry.single( @@ -83,7 +96,7 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not a byte: "+string); } - }, byteValue -> DataResult.success(byteValue+""), string -> string.matches("-?[0-9]*"), true), + }, byteValue -> DataResult.success(byteValue+""), string -> string.matches("^-?[0-9]*$"), true), new EntryCreationInfo<>(Codec.BYTE, ComponentInfo.empty()) )) .add(Interpreter.SHORT, ConfigScreenEntry.single( @@ -93,7 +106,7 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not a short: "+string); } - }, shortValue -> DataResult.success(shortValue+""), string -> string.matches("-?[0-9]*"), true), + }, shortValue -> DataResult.success(shortValue+""), string -> string.matches("^-?[0-9]*$"), true), new EntryCreationInfo<>(Codec.SHORT, ComponentInfo.empty()) )) .add(Interpreter.LONG, ConfigScreenEntry.single( @@ -103,7 +116,7 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not a long: "+string); } - }, longValue -> DataResult.success(longValue+""), string -> string.matches("-?[0-9]*"), true), + }, longValue -> DataResult.success(longValue+""), string -> string.matches("^-?[0-9]*$"), true), new EntryCreationInfo<>(Codec.LONG, ComponentInfo.empty()) )) .add(Interpreter.DOUBLE, ConfigScreenEntry.single( @@ -113,7 +126,7 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not a double: "+string); } - }, doubleValue -> DataResult.success(doubleValue+""), string -> string.matches("-?[0-9]*(\\.[0-9]*)?"), true), + }, doubleValue -> DataResult.success(doubleValue+""), string -> string.matches("^-?[0-9]*(\\.[0-9]*)?$"), true), new EntryCreationInfo<>(Codec.DOUBLE, ComponentInfo.empty()) )) .add(Interpreter.FLOAT, ConfigScreenEntry.single( @@ -123,23 +136,27 @@ public ConfigScreenInterpreter( } catch (NumberFormatException e) { return DataResult.error(() -> "Not a float: "+string); } - }, floatValue -> DataResult.success(floatValue+""), string -> string.matches("-?[0-9]*(\\.[0-9]*)?"), true), + }, floatValue -> DataResult.success(floatValue+""), string -> string.matches("^-?[0-9]*(\\.[0-9]*)?$"), true), new EntryCreationInfo<>(Codec.FLOAT, ComponentInfo.empty()) )) .add(Interpreter.BOOL, ConfigScreenEntry.single( - Widgets.bool(false), + Widgets.bool(), new EntryCreationInfo<>(Codec.BOOL, ComponentInfo.empty()) )) .add(Interpreter.UNIT, ConfigScreenEntry.single( - Widgets.bool(true), + Widgets.unit(), new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), ComponentInfo.empty()) )) .add(Interpreter.STRING, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(DataResult::success, DataResult::success, false)), new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) )) + .add(Interpreter.PASSTHROUGH, ConfigScreenEntry.single( + Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), + new EntryCreationInfo<>(Codec.PASSTHROUGH, ComponentInfo.empty()) + )) .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( - Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), false)), + Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("^([a-z0-9._-]+:)?[a-z0-9/._-]*$"), false)), new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) )) .add(MinecraftKeys.ARGB_COLOR, ConfigScreenEntry.single( @@ -154,6 +171,7 @@ public ConfigScreenInterpreter( (parent, width, context, original, update, creationInfo, handleOptional) -> { boolean[] removes = new boolean[1]; DataComponentType[] type = new DataComponentType[1]; + var problems = new EntryCreationContext.ProblemMarker[2]; if (!original.isJsonNull()) { var initialResult = creationInfo.codec().parse(context.ops(), original); if (initialResult.error().isPresent()) { @@ -165,11 +183,11 @@ public ConfigScreenInterpreter( } var remainingWidth = width - Button.DEFAULT_HEIGHT - 5; var cycle = CycleButton.builder(bool -> { - if (bool == Boolean.TRUE) { - return Component.translatable("codecextras.config.datacomponent.keytoggle.removes"); - } - return Component.empty(); - }).withValues(List.of(true, false)) + if (bool == Boolean.TRUE) { + return Component.translatable("codecextras.config.datacomponent.keytoggle.removes"); + } + return Component.empty(); + }).withValues(List.of(true, false)) .withInitialValue(removes[0]) .displayOnlyValue() .create(0, 0, Button.DEFAULT_HEIGHT, Button.DEFAULT_HEIGHT, Component.translatable("codecextras.config.datacomponent.keytoggle"), (b, bool) -> { @@ -177,8 +195,9 @@ public ConfigScreenInterpreter( if (type[0] != null) { var result = creationInfo.codec().encodeStart(context.ops(), new MinecraftKeys.DataComponentPatchKey<>(type[0], removes[0])); if (result.error().isPresent()) { - LOGGER.error("Error encoding data component patch key: {}", result.error().get()); + problems[0] = context.problem(problems[0], "Error encoding data component patch key: "+result.error().get().message()); } else { + context.resolve(problems[0]); update.accept(result.getOrThrow()); } } @@ -191,24 +210,25 @@ public ConfigScreenInterpreter( String string = json.getAsJsonPrimitive().getAsString(); var rlResult = ResourceLocation.read(string); if (rlResult.error().isPresent()) { - LOGGER.error("Error reading resource location: {}", rlResult.error().get()); + problems[1] = context.problem(problems[1], "Error reading resource location: "+rlResult.error().get().message()); return; } var key = rlResult.getOrThrow(); var typeResult = BuiltInRegistries.DATA_COMPONENT_TYPE.getOptional(key); if (typeResult.isEmpty()) { - LOGGER.error("Unknown data component type: {}", key); + problems[1] = context.problem(problems[1], "Unknown data component type: "+key); return; } type[0] = typeResult.get(); var result = creationInfo.codec().encodeStart(context.ops(), new MinecraftKeys.DataComponentPatchKey<>(type[0], removes[0])); if (result.error().isPresent()) { - LOGGER.error("Error encoding data component patch key: {}", result.error().get()); + problems[1] = context.problem(problems[1], "Error encoding data component patch key: "+result.error().get().message()); } else { + context.resolve(problems[1]); update.accept(result.getOrThrow()); } } else { - LOGGER.error("Not a string: {}", json); + problems[1] = context.problem(problems[1], "Not a string: "+json); } }, creationInfo.withCodec(ResourceKey.codec(Registries.DATA_COMPONENT_TYPE)), false); var tooltipToggle = Tooltip.create(Component.translatable("codecextras.config.datacomponent.keytoggle")); @@ -221,8 +241,7 @@ public ConfigScreenInterpreter( return layout; }, new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(MinecraftStructures.DATA_COMPONENT_PATCH_KEY).getOrThrow(), ComponentInfo.empty()) - )) - .build()), + )).build()), parametricKeys.join(Keys2., K1, K1>builder() .add(Interpreter.INT_IN_RANGE, new ParametricKeyedValue<>() { @Override @@ -386,7 +405,7 @@ public App> wrapped = (parent2, width2, context2, original2, update2, creationInfo2, handleOptional2) -> Widgets.text( ResourceLocation::read, rl -> DataResult.success(rl.toString()), - string -> string.matches("([a-z0-9._-]+:)?[a-z0-9/._-]*"), + string -> string.matches("^([a-z0-9._-]+:)?[a-z0-9/._-]*$"), false ).create(parent2, width2, context2, original2, update2, creationInfo2.withCodec(ResourceLocation.CODEC), handleOptional2); } @@ -396,11 +415,265 @@ public App> ).withEntryCreationInfo(i -> i.withCodec(holderCodec), i -> i.withCodec(codec)); } }) + .add(MinecraftKeys.FALLBACK_DATA_COMPONENT_TYPE, new ParametricKeyedValue<>() { + @Override + public App> convert(App parameter) { + var type = MinecraftKeys.DataComponentTypeHolder.unbox(parameter).value(); + var codec = type.codec(); + if (codec == null) { + LOGGER.error("{} is not a persistent component", type); + return ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + var button = Button.builder( + Component.translatable("codecextras.config.datacomponent.notpersistent", type), + b -> { + } + ).width(width).build(); + button.active = false; + return button; + }, + new EntryCreationInfo<>(Codec.EMPTY.codec().flatXmap( + ignored -> DataResult.error(() -> type + " is not a persistent component"), + ignored -> DataResult.error(() -> type + " is not a persistent component") + ), ComponentInfo.empty()) + ); + } + var identityCodec = codec.>xmap(Identity::new, app -> Identity.unbox(app).value()); + return ConfigScreenEntry.single( + Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), + new EntryCreationInfo<>(identityCodec, ComponentInfo.empty()) + ); + } + }) .build()) ); this.codecInterpreter = codecInterpreter; } + private static final Codec BIG_DECIMAL_CODEC = new PrimitiveCodec<>() { + @Override + public DataResult read(final DynamicOps ops, final T input) { + return ops + .getNumberValue(input) + .map(number -> number instanceof BigDecimal bigDecimal ? bigDecimal : new BigDecimal(number.toString())); + } + + @Override + public T write(final DynamicOps ops, final BigDecimal value) { + return ops.createNumeric(value); + } + + @Override + public String toString() { + return "BigDecimal"; + } + }; + + private static LayoutElement byJson(Screen parentOuter, int widthOuter, EntryCreationContext contextOuter, JsonElement originalOuter, Consumer updateOuter, EntryCreationInfo creationInfoOuter, boolean handleOptionalOuter) { + var entryHolder = new Object() { + final EntryCreationInfo jsonInfo = new EntryCreationInfo<>( + Codec.PASSTHROUGH.xmap(d -> d.convert(contextOuter.ops()).getValue(), v -> new Dynamic<>(contextOuter.ops(), v)), + ComponentInfo.empty() + ); + final EntryCreationInfo stringInfo = new EntryCreationInfo<>( + Codec.STRING, + ComponentInfo.empty() + ); + final EntryCreationInfo numberInfo = new EntryCreationInfo<>( + BIG_DECIMAL_CODEC.xmap(Function.identity(), number -> number instanceof BigDecimal bigDecimal ? bigDecimal : new BigDecimal(number.toString())), + ComponentInfo.empty() + ); + final EntryCreationInfo booleanInfo = new EntryCreationInfo<>( + Codec.BOOL, + ComponentInfo.empty() + ); + final ConfigScreenEntry stringEntry = ConfigScreenEntry.single( + Widgets.text(DataResult::success, DataResult::success, false), + stringInfo + ); + final ConfigScreenEntry numberEntry = ConfigScreenEntry.single( + Widgets.text(s -> { + if (s.isEmpty()) { + return DataResult.success(BigDecimal.ZERO); + } + try { + return DataResult.success(new BigDecimal(s)); + } catch (NumberFormatException e) { + return DataResult.error(() -> "Not a number: "+s); + } + }, n -> DataResult.success(n.toString()), s -> s.matches("^-?[0-9]*(\\.[0-9]*)?$"), false), + numberInfo + ); + final ConfigScreenEntry booleanEntry = ConfigScreenEntry.single( + Widgets.bool(), + booleanInfo + ); + static final Gson GSON = new GsonBuilder().create(); + final ConfigScreenEntry rawJsonEntry = ConfigScreenEntry.single( + Widgets.text(s -> { + try { + return DataResult.success(GSON.fromJson(s, JsonElement.class)); + } catch (JsonSyntaxException e) { + return DataResult.error(() -> "Invalid JSON `"+s+"`: "+e.getMessage()); + } + }, n -> DataResult.success(n.toString()), false), + jsonInfo + ); + final ConfigScreenEntry jsonEntry = ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + enum JsonType { + OBJECT("codecextras.config.json.object"), + ARRAY("codecextras.config.json.array"), + STRING("codecextras.config.json.string"), + NUMBER("codecextras.config.json.number"), + BOOLEAN("codecextras.config.json.boolean"), + RAW("codecextras.config.json.raw"); + + private final Component component; + + + JsonType(String translationKey) { + component = Component.translatable(translationKey); + } + + static @Nullable JsonType of(JsonElement element) { + if (element.isJsonObject()) { + return OBJECT; + } + if (element.isJsonArray()) { + return ARRAY; + } + if (element.isJsonPrimitive()) { + var primitive = element.getAsJsonPrimitive(); + if (primitive.isString()) { + return STRING; + } + if (primitive.isNumber()) { + return NUMBER; + } + if (primitive.isBoolean()) { + return BOOLEAN; + } + } + return null; + } + } + + if (original.isJsonNull()) { + original = new JsonObject(); + if (!handleOptional) { + update.accept(original); + } + } + + var remainingWidth = width - Button.DEFAULT_HEIGHT - 5; + JsonType[] type = new JsonType[] {JsonType.of(original)}; + if (type[0] == null) { + type[0] = JsonType.OBJECT; + } + Map elements = new EnumMap<>(JsonType.class); + elements.put(JsonType.RAW, original); + elements.put(JsonType.OBJECT, new JsonObject()); + elements.put(JsonType.ARRAY, new JsonArray()); + elements.put(JsonType.STRING, new JsonPrimitive("")); + elements.put(JsonType.NUMBER, new JsonPrimitive(0)); + elements.put(JsonType.BOOLEAN, new JsonPrimitive(false)); + elements.put(type[0], original); + var outerEntryHolder = this; + var holder = new Object() { + private final Runnable onTypeUpdate = () -> { + var currentType = type[0]; + var component = currentType.component; + var tooltip = Tooltip.create(component); + this.cycle.setTooltip(tooltip); + for (var entry : this.layouts.entrySet()) { + var layout = entry.getValue(); + if (entry.getKey() == currentType) { + layout.visitWidgets(w -> { + w.visible = true; + w.active = true; + }); + } else { + layout.visitWidgets(w -> { + w.visible = false; + w.active = false; + }); + } + } + }; + private final EntryCreationContext.ProblemMarker[] problems = new EntryCreationContext.ProblemMarker[1]; + private final Consumer checkedUpdate = newJsonValue -> creationInfo.codec().parse(context.ops(), newJsonValue).ifError(error -> { + problems[0] = context.problem(problems[0], "Coult not encode: "+error.message()); + }).ifSuccess(json -> { + context.resolve(problems[0]); + update.accept(json); + }); + private final CycleButton cycle = CycleButton.builder(t -> Component.empty()) + .withValues(List.of(JsonType.RAW, JsonType.OBJECT, JsonType.ARRAY, JsonType.STRING, JsonType.NUMBER, JsonType.BOOLEAN)) + .withInitialValue(type[0] == null ? JsonType.OBJECT : type[0]) + .displayOnlyValue() + .create(0, 0, Button.DEFAULT_HEIGHT, Button.DEFAULT_HEIGHT, Component.translatable("codecextras.config.json.type"), (b, t) -> { + type[0] = t; + onTypeUpdate.run(); + checkedUpdate.accept(elements.get(type[0])); + }); + private final Map layouts = new EnumMap<>(JsonType.class); + + { + layouts.put(JsonType.OBJECT, Button.builder(Component.translatable("codecextras.config.configurerecord"), b -> { + Minecraft.getInstance().setScreen(ScreenEntryProvider.create( + new UnboundedMapScreenEntryProvider<>(stringEntry, jsonEntry, context, elements.get(JsonType.OBJECT), newJsonValue -> { + elements.put(JsonType.OBJECT, newJsonValue); + checkedUpdate.accept(newJsonValue); + }), parent, context, creationInfo.componentInfo() + )); + }).width(remainingWidth).build()); + layouts.put(JsonType.ARRAY, Button.builder(Component.translatable("codecextras.config.configurelist"), b -> { + Minecraft.getInstance().setScreen(ScreenEntryProvider.create( + new ListScreenEntryProvider<>(jsonEntry, context, elements.get(JsonType.ARRAY), newJsonValue -> { + elements.put(JsonType.ARRAY, newJsonValue); + checkedUpdate.accept(newJsonValue); + }), parent, context, creationInfo.componentInfo() + )); + }).width(remainingWidth).build()); + layouts.put(JsonType.STRING, stringEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.STRING), newJsonValue -> { + elements.put(JsonType.STRING, newJsonValue); + checkedUpdate.accept(newJsonValue); + }, outerEntryHolder.stringInfo, handleOptional)); + layouts.put(JsonType.NUMBER, numberEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.NUMBER), newJsonValue -> { + elements.put(JsonType.NUMBER, newJsonValue); + checkedUpdate.accept(newJsonValue); + }, outerEntryHolder.numberInfo, handleOptional)); + layouts.put(JsonType.BOOLEAN, booleanEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.BOOLEAN), newJsonValue -> { + elements.put(JsonType.BOOLEAN, newJsonValue); + checkedUpdate.accept(newJsonValue); + }, outerEntryHolder.booleanInfo, handleOptional)); + layouts.put(JsonType.RAW, outerEntryHolder.rawJsonEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.RAW), newJsonValue -> { + elements.put(JsonType.RAW, newJsonValue); + checkedUpdate.accept(newJsonValue); + }, outerEntryHolder.jsonInfo, handleOptional)); + onTypeUpdate.run(); + } + }; + var layout = new EqualSpacingLayout(width, 0, EqualSpacingLayout.Orientation.HORIZONTAL); + var frame = new FrameLayout(remainingWidth, Button.DEFAULT_HEIGHT); + for (var entry : holder.layouts.entrySet()) { + var layoutElement = entry.getValue(); + frame.addChild(layoutElement, LayoutSettings.defaults().alignVerticallyMiddle()); + } + layout.addChild(holder.cycle, LayoutSettings.defaults().alignVerticallyMiddle()); + layout.addChild(frame, LayoutSettings.defaults().alignVerticallyMiddle()); + return layout; + }, + jsonInfo + ); + }; + Codec jsonWrappingCodec = entryHolder.jsonInfo.codec().validate(json -> creationInfoOuter.codec().parse(contextOuter.ops(), json).map(t -> json)); + return entryHolder.jsonEntry.layout().create( + parentOuter, widthOuter, contextOuter, originalOuter, updateOuter, creationInfoOuter.withCodec(jsonWrappingCodec), handleOptionalOuter + ); + } + public ConfigScreenInterpreter( CodecInterpreter codecInterpreter ) { @@ -493,14 +766,15 @@ public DataResult>> list(App Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal[0], value -> { + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.openChecked( + subContext, finalOriginal[0], value -> { finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -527,14 +801,15 @@ public DataResult>> unboundedMap(App< } } JsonElement[] finalOriginal = new JsonElement[] {original}; + var subContext = context.subContext(); return Button.builder( Component.translatable("codecextras.config.configurelist"), - b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal[0], jsonValue -> { + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.openChecked( + subContext, finalOriginal[0], jsonValue -> { finalOriginal[0] = jsonValue; update.accept(jsonValue); }, creationInfo - ), parent, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -567,14 +842,15 @@ public DataResult> record(List Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal[0], value -> { + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.openChecked( + subContext, finalOriginal[0], value -> { finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -645,15 +921,10 @@ public DataResult> dispatch(String key, Stru if (keyResult.error().isPresent()) { return DataResult.error(keyResult.error().get().messageSupplier()); } - Supplier>>> entries = Suppliers.memoize(() -> { - Map>> map = new HashMap<>(); + Supplier>>>> entries = Suppliers.memoize(() -> { + Map>>> map = new HashMap<>(); for (var entryKey : keys.get()) { - var result = structures.apply(entryKey).flatMap(it -> it.interpret(this)); - if (result.error().isPresent()) { - map.put(entryKey, DataResult.error(result.error().get().messageSupplier())); - } else { - map.put(entryKey, DataResult.success(ConfigScreenEntry.unbox(result.getOrThrow()))); - } + map.put(entryKey, Suppliers.memoize(() -> structures.apply(entryKey).flatMap(it -> it.interpret(this)).map(ConfigScreenEntry::unbox))); } return map; }); @@ -672,14 +943,15 @@ public DataResult> dispatch(String key, Stru } } JsonElement[] finalOriginal = new JsonElement[] {original}; + var subContext = context.subContext(); return Button.builder( Component.translatable("codecextras.config.configurerecord"), - b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal[0], value -> { + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.openChecked( + subContext, finalOriginal[0], value -> { finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo())) ).width(width).build(); }), factory, @@ -693,15 +965,10 @@ public DataResult>> dispatchedMap(Str if (keyResult.error().isPresent()) { return DataResult.error(keyResult.error().get().messageSupplier()); } - Supplier>>> entries = Suppliers.memoize(() -> { - Map>> map = new HashMap<>(); + Supplier>>>> entries = Suppliers.memoize(() -> { + Map>>> map = new HashMap<>(); for (var entryKey : keys.get()) { - var result = valueStructures.apply(entryKey).flatMap(it -> it.interpret(this)); - if (result.error().isPresent()) { - map.put(entryKey, DataResult.error(result.error().get().messageSupplier())); - } else { - map.put(entryKey, DataResult.success(ConfigScreenEntry.unbox(result.getOrThrow()))); - } + map.put(entryKey, Suppliers.memoize(() -> valueStructures.apply(entryKey).flatMap(it -> it.interpret(this)).map(ConfigScreenEntry::unbox))); } return map; }); @@ -720,14 +987,15 @@ public DataResult>> dispatchedMap(Str } } JsonElement[] finalOriginal = new JsonElement[] {original}; + var subContext = context.subContext(); return Button.builder( Component.translatable("codecextras.config.configurerecord"), - b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.open( - context, finalOriginal[0], value -> { + b -> Minecraft.getInstance().setScreen(ScreenEntryProvider.create(factory.openChecked( + subContext, finalOriginal[0], value -> { finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo())) ).width(width).build(); }), factory, diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java index 62b1067..c04d35a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.StringWidget; @@ -29,17 +30,17 @@ class DispatchScreenEntryProvider implements ScreenEntryProvider { private JsonObject jsonValue; private final Consumer update; private final List keys; - private final Map> keyProviders; + private final Map>>> keyProviders; private final EntryCreationContext context; - public DispatchScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, String key, Consumer update, EntryCreationContext context, Map>> entries) { + public DispatchScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, String key, Consumer update, EntryCreationContext context, Map>>> entries) { this.keyEntry = keyEntry; this.key = key; if (jsonValue.isJsonObject()) { this.jsonValue = jsonValue.getAsJsonObject(); } else { if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON object", jsonValue); + LOGGER.error("Value {} was not a JSON object", jsonValue); } this.jsonValue = new JsonObject(); } @@ -50,14 +51,11 @@ public DispatchScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement js for (var entry : entries.entrySet()) { var keyResult = keyEntry.entryCreationInfo().codec().encodeStart(context.ops(), entry.getKey()); if (keyResult.isError()) { - LOGGER.warn("Failed to encode key {}", entry.getKey()); + LOGGER.error("Failed to encode key {}", entry.getKey()); continue; } JsonElement keyElement = keyResult.getOrThrow(); - if (entry.getValue().isError()) { - LOGGER.warn("Failed to create screen entry for key {}: {}", entry.getKey(), entry.getValue().error().orElseThrow().message()); - } - keyProviders.put(keyElement, entry.getValue().getOrThrow()); + keyProviders.put(keyElement, entry.getValue()); this.keys.add(keyElement); } this.keys.sort(JsonComparator.INSTANCE); @@ -67,14 +65,14 @@ public DispatchScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement js } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { if (nestedOnExit != null) { - nestedOnExit.run(); + nestedOnExit.accept(context); } update.accept(jsonValue); } - private @Nullable Runnable nestedOnExit; + private @Nullable Consumer nestedOnExit; @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { @@ -105,15 +103,20 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { if (!keyValue.isJsonNull()) { var provider = keyProviders.get(keyValue); if (provider != null) { - JsonObject valueCopy = new JsonObject(); - valueCopy.asMap().putAll(jsonValue.asMap()); - addEntry(provider, valueCopy, list, rebuild, parent); + var entry = provider.get(); + if (entry.isError()) { + LOGGER.error("Failed to create screen entry for key {}: {}", keyValue, entry.error().orElseThrow().message()); + } else { + JsonObject valueCopy = new JsonObject(); + valueCopy.asMap().putAll(jsonValue.asMap()); + addEntry(entry.getOrThrow(), valueCopy, list, rebuild, parent); + } } } } private void addEntry(ConfigScreenEntry provider, JsonObject valueCopy, ScreenEntryList list, Runnable rebuild, Screen parent) { - var entryProvider = provider.screenEntryProvider().open(context, valueCopy, newValue -> { + var entryProvider = provider.screenEntryProvider().openChecked(context, valueCopy, newValue -> { if (newValue.isJsonObject()) { for (var entry : newValue.getAsJsonObject().entrySet()) { if (entry.getKey().equals(key)) { @@ -122,10 +125,10 @@ private void addEntry(ConfigScreenEntry provider, JsonObject va this.jsonValue.add(entry.getKey(), entry.getValue()); } } else { - LOGGER.warn("Value {} was not a JSON object", newValue); + LOGGER.error("Value {} was not a JSON object", newValue); } }, provider.entryCreationInfo()); - this.nestedOnExit = entryProvider::onExit; + this.nestedOnExit = context -> entryProvider.onExit(context); entryProvider.addEntries(list, rebuild, parent); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java index d9161da..58f6f0b 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchedMapScreenEntryProvider.java @@ -15,6 +15,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Supplier; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.layouts.EqualSpacingLayout; @@ -29,13 +30,13 @@ class DispatchedMapScreenEntryProvider implements ScreenEntryProvider { private final ConfigScreenEntry keyEntry; private final List keys; - private final Map> keyProviders; + private final Map>>> keyProviders; private final List, JsonElement>> values = new ArrayList<>(); private final JsonObject jsonValue = new JsonObject(); private final Consumer update; private final EntryCreationContext context; - public DispatchedMapScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, Consumer update, EntryCreationContext context, Map>> entries) { + public DispatchedMapScreenEntryProvider(ConfigScreenEntry keyEntry, JsonElement jsonValue, Consumer update, EntryCreationContext context, Map>>> entries) { this.keyEntry = keyEntry; if (jsonValue.isJsonObject()) { jsonValue.getAsJsonObject().entrySet().stream() @@ -44,7 +45,7 @@ public DispatchedMapScreenEntryProvider(ConfigScreenEntry keyEntry, JsonEleme updateValue(); } else { if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON array", jsonValue); + LOGGER.error("Value {} was not a JSON array", jsonValue); } } this.update = update; @@ -52,24 +53,21 @@ public DispatchedMapScreenEntryProvider(ConfigScreenEntry keyEntry, JsonEleme this.keys = new ArrayList<>(); this.keyProviders = new HashMap<>(); for (var entry : entries.entrySet()) { - if (entry.getValue().isError()) { - LOGGER.warn("Failed to create screen entry for key {}: {}", entry.getKey(), entry.getValue().error().orElseThrow().message()); - } var keyResult = keyEntry.entryCreationInfo().codec().encodeStart(context.ops(), entry.getKey()); if (keyResult.isError()) { - LOGGER.warn("Failed to encode key {}", entry.getKey()); + LOGGER.error("Failed to encode key {}", entry.getKey()); continue; } JsonElement keyElement = keyResult.getOrThrow(); if (!keyElement.isJsonPrimitive()) { - LOGGER.warn("Key {} was not a JSON primitive", keyElement); + LOGGER.error("Key {} was not a JSON primitive", keyElement); continue; } else if (!keyElement.getAsJsonPrimitive().isString()) { - LOGGER.warn("Key {} was not a JSON string", keyElement); + LOGGER.error("Key {} was not a JSON string", keyElement); continue; } String keyAsString = keyElement.getAsString(); - keyProviders.put(keyAsString, entry.getValue().getOrThrow()); + keyProviders.put(keyAsString, entry.getValue()); this.keys.add(keyAsString); } this.keys.sort(Comparator.naturalOrder()); @@ -89,7 +87,7 @@ private void updateValue() { } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { this.update.accept(jsonValue); } @@ -129,7 +127,12 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { layout.addChild(keyLayout, LayoutSettings.defaults().alignVerticallyMiddle()); var valueEntry = keyValue.map(keyProviders::get).orElse(null); if (valueEntry != null) { - addValueEntry(parent, layout, valueEntry, keyAndValueWidth, index); + var valueScreenEntry = valueEntry.get(); + if (valueScreenEntry.isError()) { + LOGGER.error("Failed to create screen entry for key {}: {}", keyValue.orElseThrow(), valueScreenEntry.error().orElseThrow().message()); + } else { + addValueEntry(parent, layout, valueScreenEntry.getOrThrow(), keyAndValueWidth, index); + } } else { var disabled = Button.builder(Component.translatable("codecextras.config.dispatchedmap.icon.disabled"), b -> {}).width(keyAndValueWidth).build(); disabled.active = false; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java index 59af829..63aa933 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationContext.java @@ -1,15 +1,27 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.google.gson.JsonElement; +import com.mojang.logging.LogUtils; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; +import java.util.IdentityHashMap; +import java.util.Map; import java.util.Objects; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.BuiltInRegistries; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; public class EntryCreationContext { + private static final Logger LOGGER = LogUtils.getLogger(); + private final DynamicOps ops; private final RegistryAccess registryAccess; + final Map problems = new IdentityHashMap<>(); + + public static final class ProblemMarker { + private ProblemMarker() {} + } private EntryCreationContext(DynamicOps ops, RegistryAccess registryAccess) { this.ops = ops; @@ -24,6 +36,23 @@ public RegistryAccess registryAccess() { return registryAccess; } + public ProblemMarker problem(@Nullable ProblemMarker old, String message) { + ProblemMarker marker = old == null ? new ProblemMarker() : old; + problems.put(marker, message); + LOGGER.error(message); + return marker; + } + + public EntryCreationContext subContext() { + return new EntryCreationContext(ops, registryAccess); + } + + public void resolve(@Nullable ProblemMarker problem) { + if (problem != null) { + problems.remove(problem); + } + } + public static Builder builder() { return new Builder(); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java index 3e714bc..2245896 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryListScreen.java @@ -14,6 +14,7 @@ import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.layouts.LayoutSettings; import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; @@ -24,11 +25,13 @@ class EntryListScreen extends Screen { private final Screen lastScreen; private @Nullable EntryList list; private final ScreenEntryProvider screenEntries; + private final EntryCreationContext context; - public EntryListScreen(Screen screen, Component title, ScreenEntryProvider screenEntries) { + public EntryListScreen(Screen screen, Component title, ScreenEntryProvider screenEntries, EntryCreationContext context) { super(title); this.lastScreen = screen; this.screenEntries = screenEntries; + this.context = context; } protected void init() { @@ -65,7 +68,24 @@ protected void repositionElements() { } public void onClose() { - this.screenEntries.onExit(); + this.screenEntries.onExit(this.context); + var problems = this.context.problems; + if (!problems.isEmpty()) { + var issues = String.join("\n", problems.values()); + var screen = new ConfirmScreen(bl -> { + // Resolve everything in either case + for (var problem : problems.keySet().stream().toList()) { + this.context.resolve(problem); + } + if (bl) { + this.minecraft.setScreen(this.lastScreen); + } else { + this.minecraft.setScreen(this); + } + }, Component.translatable("codecextras.config.issue"), Component.translatable("codecextras.config.issue.message", issues)); + this.minecraft.setScreen(screen); + return; + } this.minecraft.setScreen(this.lastScreen); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java index c6c1e68..5bdbcca 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ListScreenEntryProvider.java @@ -28,7 +28,7 @@ class ListScreenEntryProvider implements ScreenEntryProvider { this.jsonValue = jsonValue.getAsJsonArray(); } else { if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON array", jsonValue); + LOGGER.error("Value {} was not a JSON array", jsonValue); } this.jsonValue = new JsonArray(); } @@ -37,7 +37,7 @@ class ListScreenEntryProvider implements ScreenEntryProvider { } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { this.update.accept(jsonValue); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java index edacd17..3c58783 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java @@ -12,6 +12,7 @@ import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.screens.Screen; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; class RecordScreenEntryProvider implements ScreenEntryProvider { @@ -28,7 +29,7 @@ class RecordScreenEntryProvider implements ScreenEntryProvider { this.jsonValue = jsonValue.getAsJsonObject(); } else { if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON object", jsonValue); + LOGGER.error("Value {} was not a JSON object", jsonValue); } this.jsonValue = new JsonObject(); } @@ -37,7 +38,7 @@ class RecordScreenEntryProvider implements ScreenEntryProvider { } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { this.update.accept(jsonValue); } @@ -76,6 +77,8 @@ private LayoutElement createEntryWidget(RecordEntry entry, JsonElement sp }, entry.entry().entryCreationInfo(), defaultValue.isPresent() && defaultValue.get().isJsonNull()); } + private EntryCreationContext.@Nullable ProblemMarker encodeValueProblem; + private boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { if (newValue.isJsonNull()) { return false; @@ -83,9 +86,10 @@ private boolean shouldUpdate(JsonElement newValue, RecordEntry entry) { if (entry.missingBehavior().isPresent()) { var decoded = entry.codec().parse(this.context.ops(), newValue); if (decoded.isError()) { - LOGGER.warn("Could not encode new value {}", newValue); + encodeValueProblem = context.problem(encodeValueProblem, decoded.error().orElseThrow().message()); return false; } + context.resolve(encodeValueProblem); return entry.missingBehavior().get().predicate().test(decoded.result().orElseThrow()); } return true; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java index 76c7320..9552200 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryFactory.java @@ -5,4 +5,16 @@ public interface ScreenEntryFactory { ScreenEntryProvider open(EntryCreationContext context, JsonElement original, Consumer onClose, EntryCreationInfo entry); + + default ScreenEntryProvider openChecked(EntryCreationContext context, JsonElement original, Consumer onClose, EntryCreationInfo entry) { + EntryCreationContext.ProblemMarker[] problems = {null}; + return open(context, original, jsonElement -> { + var codec = entry.codec(); + var result = codec.parse(context.ops(), jsonElement); + if (result.isError()) { + problems[0] = context.problem(problems[0], result.error().orElseThrow().message()); + } + onClose.accept(jsonElement); + }, entry); + } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java index c40dc00..6e4e2d3 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ScreenEntryProvider.java @@ -3,10 +3,10 @@ import net.minecraft.client.gui.screens.Screen; public interface ScreenEntryProvider { - void onExit(); + void onExit(EntryCreationContext context); void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent); - static Screen create(ScreenEntryProvider provider, Screen parent, ComponentInfo componentInfo) { - return new EntryListScreen(parent, componentInfo.title(), provider); + static Screen create(ScreenEntryProvider provider, Screen parent, EntryCreationContext context, ComponentInfo componentInfo) { + return new EntryListScreen(parent, componentInfo.title(), provider, context); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java index db3effc..50066ee 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/SingleScreenEntryProvider.java @@ -22,7 +22,7 @@ class SingleScreenEntryProvider implements ScreenEntryProvider { } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { update.accept(value); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java index 7535c0f..ae568e4 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/UnboundedMapScreenEntryProvider.java @@ -38,7 +38,7 @@ class UnboundedMapScreenEntryProvider implements ScreenEntryProvider { updateValue(); } else { if (!jsonValue.isJsonNull()) { - LOGGER.warn("Value {} was not a JSON array", jsonValue); + LOGGER.error("Value {} was not a JSON object", jsonValue); } } this.update = update; @@ -50,18 +50,21 @@ private void updateValue() { for (var pair : values) { if (pair.getFirst().isJsonPrimitive()) { if (pair.getFirst().getAsJsonPrimitive().isString()) { + if (jsonValue.has(pair.getFirst().getAsString())) { + LOGGER.warn("Duplicate key {}", pair.getFirst().getAsString()); + } jsonValue.add(pair.getFirst().getAsString(), pair.getSecond()); } else { - LOGGER.warn("Key {} was not a JSON string", pair.getFirst()); + LOGGER.error("Key {} was not a JSON string", pair.getFirst()); } } else if (!pair.getFirst().isJsonNull()) { - LOGGER.warn("Key {} was not a JSON primitive", pair.getFirst()); + LOGGER.error("Key {} was not a JSON primitive", pair.getFirst()); } } } @Override - public void onExit() { + public void onExit(EntryCreationContext context) { this.update.accept(jsonValue); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index c575a15..40e93ba 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -2,6 +2,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; +import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.mojang.datafixers.util.Either; import com.mojang.logging.LogUtils; @@ -425,7 +426,42 @@ public static LayoutFactory> either(LayoutFactory left, L }; } - public static LayoutFactory bool(boolean falseIfMissing) { + public static LayoutFactory unit() { + return (parent, width, context, original, update, creationInfo, handleOptional) -> { + if (original.isJsonNull()) { + original = new JsonObject(); + if (!handleOptional) { + update.accept(original); + } + } + if (handleOptional) { + var w = Checkbox.builder(Component.empty(), Minecraft.getInstance().font) + .maxWidth(width) + .onValueChange((checkbox, b) -> { + update.accept(b ? new JsonObject() : JsonNull.INSTANCE); + }) + .selected(original.isJsonPrimitive() && original.getAsJsonPrimitive().getAsBoolean()) + .build(); + creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + var tooltip = Tooltip.create(description); + w.setTooltip(tooltip); + }); + w.setMessage(creationInfo.componentInfo().title()); + return w; + } else { + var button = Button.builder(Component.translatable("codecextras.config.unit"), b -> { + }) + .width(width) + .build(); + var tooltip = Tooltip.create(creationInfo.componentInfo().description()); + button.setTooltip(tooltip); + button.active = false; + return button; + } + }; + } + + public static LayoutFactory bool() { LayoutFactory widget = (parent, width, context, original, update, creationInfo, handleOptional) -> { if (original.isJsonNull()) { original = new JsonPrimitive(false); @@ -436,9 +472,6 @@ public static LayoutFactory bool(boolean falseIfMissing) { var w = Checkbox.builder(Component.empty(), Minecraft.getInstance().font) .maxWidth(width) .onValueChange((checkbox, b) -> { - if (falseIfMissing && !b) { - update.accept(JsonNull.INSTANCE); - } update.accept(new JsonPrimitive(b)); }) .selected(original.isJsonPrimitive() && original.getAsJsonPrimitive().getAsBoolean()) @@ -450,9 +483,6 @@ public static LayoutFactory bool(boolean falseIfMissing) { w.setMessage(creationInfo.componentInfo().title()); return w; }; - if (!falseIfMissing) { - return wrapWithOptionalHandling(widget); - } return (parent, width, context, original, update, entry, handleOptional) -> { if (handleOptional) { return widget.create(parent, width, context, original, update, entry, true); diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json index dba9c68..5bbedc3 100644 --- a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -14,5 +14,15 @@ "codecextras.config.either.switch": "Switch entry type", "codecextras.config.dispatchedmap.icon.disabled": "Pick a type to configure", "codecextras.config.datacomponent.keytoggle": "Toggle remove/add", - "codecextras.config.datacomponent.keytoggle.removes": "!" + "codecextras.config.datacomponent.keytoggle.removes": "!", + "codecextras.config.datacomponent.notpersistent": "%s is not a persistent component", + "codecextras.config.json.object": "Object", + "codecextras.config.json.array": "Array", + "codecextras.config.json.string": "String", + "codecextras.config.json.number": "Number", + "codecextras.config.json.boolean": "Boolean", + "codecextras.config.json.raw": "Raw JSON", + "codecextras.config.json.type": "Change type", + "codecextras.config.issue": "Issues with configuration!", + "codecextras.config.issue.message": "Found issues:\n%s\nDo you wish to continue? Data may be lost if you do." } diff --git a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java index 70e9fd6..931a3b1 100644 --- a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java +++ b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/CodecExtrasRegistriesRegistrar.java @@ -6,8 +6,13 @@ @AutoService(CodecExtrasRegistries.RegistryRegistrar.class) public final class CodecExtrasRegistriesRegistrar implements CodecExtrasRegistries.RegistryRegistrar { + public static Runnable PREPARE_DATA_COMPONENT_STRUCTURES = () -> { + throw new IllegalStateException("Registry not created yet"); + }; + @Override - public void setup() { + public void setup(RegistriesImpl registries) { FabricRegistryBuilder.createSimple(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES).buildAndRegister(); + PREPARE_DATA_COMPONENT_STRUCTURES = registries.dataComponentStructures::prepare; } } diff --git a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/MappedRegistryMixin.java b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/MappedRegistryMixin.java new file mode 100644 index 0000000..0d3ea75 --- /dev/null +++ b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/MappedRegistryMixin.java @@ -0,0 +1,23 @@ +package dev.lukebemish.codecextras.minecraft.fabric.mixin; + +import dev.lukebemish.codecextras.minecraft.fabric.CodecExtrasRegistriesRegistrar; +import dev.lukebemish.codecextras.minecraft.structured.CodecExtrasRegistries; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.core.WritableRegistry; +import net.minecraft.resources.ResourceKey; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MappedRegistry.class) +public abstract class MappedRegistryMixin implements WritableRegistry { + @SuppressWarnings({"RedundantCast", "rawtypes"}) + @Inject(method = "freeze()Lnet/minecraft/core/Registry;", at = @At("HEAD")) + private void onFreeze(CallbackInfoReturnable> cir) { + if ((ResourceKey) this.key() == CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES) { + CodecExtrasRegistriesRegistrar.PREPARE_DATA_COMPONENT_STRUCTURES.run(); + } + } +} diff --git a/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/package-info.java b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/package-info.java new file mode 100644 index 0000000..c8660ed --- /dev/null +++ b/src/minecraftFabric/java/dev/lukebemish/codecextras/minecraft/fabric/mixin/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.minecraft.fabric.mixin; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/minecraftFabric/resources/codecextras.minecraft.mixin.json b/src/minecraftFabric/resources/codecextras.minecraft.mixin.json new file mode 100644 index 0000000..4fe4439 --- /dev/null +++ b/src/minecraftFabric/resources/codecextras.minecraft.mixin.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "dev.lukebemish.codecextras.minecraft.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "MappedRegistryMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/minecraftFabric/resources/fabric.mod.json b/src/minecraftFabric/resources/fabric.mod.json index 72e71b3..0fd15ac 100644 --- a/src/minecraftFabric/resources/fabric.mod.json +++ b/src/minecraftFabric/resources/fabric.mod.json @@ -10,5 +10,8 @@ ], "depends": { "minecraft": ">=${minecraft_version}" - } + }, + "mixins": [ + "codecextras.minecraft.mixin.json" + ] } diff --git a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java index 1f4dd92..f03fe3b 100644 --- a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java +++ b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java @@ -6,6 +6,7 @@ import net.minecraft.core.component.DataComponentType; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import net.neoforged.neoforge.registries.NewRegistryEvent; import net.neoforged.neoforge.registries.RegistryBuilder; @@ -17,5 +18,8 @@ public final class CodecExtrasNeoforge { modBus.addListener(NewRegistryEvent.class, event -> { event.register(DATA_COMPONENT_STRUCTURE_REGISTRY); }); + modBus.addListener(FMLCommonSetupEvent.class, event -> { + event.enqueueWork(((CodecExtrasRegistries.RegistryRegistrar.RegistriesImpl) CodecExtrasRegistries.REGISTRIES).dataComponentStructures::prepare); + }); } } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index a5f19ae..5aa7144 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -6,7 +6,6 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.config.ConfigType; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; -import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.IdentityInterpreter; @@ -16,7 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import net.minecraft.core.component.DataComponents; +import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.registries.Registries; import net.minecraft.references.Items; import net.minecraft.resources.ResourceKey; @@ -30,7 +29,7 @@ public record TestConfig( int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, Map unbounded, Either either, Map dispatchedMap, - MinecraftKeys.DataComponentPatchKey patchKey + DataComponentPatch patch ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -96,7 +95,7 @@ public String key() { var unbounded = builder.addOptional("unbounded", Structure.unboundedMap(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::unbounded, () -> Map.of("test", 123)); var either = builder.addOptional("either", Structure.either(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::either, () -> Either.right(0x00FFAA)); var dispatchedMap = builder.addOptional("dispatchedMap", Structure.STRING.dispatchedMap(DISPATCHES::keySet, k -> DataResult.success(DISPATCHES.get(k))), TestConfig::dispatchedMap, Map::of); - var patchKey = builder.addOptional("patchKey", MinecraftStructures.DATA_COMPONENT_PATCH_KEY, TestConfig::patchKey, () -> new MinecraftKeys.DataComponentPatchKey<>(DataComponents.BEES, false)); + var patch = builder.addOptional("patch", MinecraftStructures.DATA_COMPONENT_PATCH, TestConfig::patch, () -> DataComponentPatch.EMPTY); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), @@ -104,7 +103,7 @@ public String key() { intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), unbounded.apply(container), either.apply(container), dispatchedMap.apply(container), - patchKey.apply(container) + patch.apply(container) ); }); diff --git a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java index 0c7bb58..bd2f896 100644 --- a/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java +++ b/src/testNeoforge/java/dev/lukebemish/codecextras/test/neoforge/CodecExtrasTest.java @@ -19,14 +19,13 @@ public class CodecExtrasTest { .handle(FMLPaths.CONFIGDIR.get().resolve("codecextras_testmod.json"), GsonOpsIo.INSTANCE); public CodecExtrasTest(ModContainer modContainer) { - ConfigScreenEntry entry = new ConfigScreenInterpreter( - MinecraftInterpreters.CODEC_INTERPRETER - ).interpret(TestConfig.STRUCTURE).getOrThrow(); + modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> { + var interpreter = new ConfigScreenInterpreter(MinecraftInterpreters.CODEC_INTERPRETER); + ConfigScreenEntry entry = interpreter.interpret(TestConfig.STRUCTURE).getOrThrow(); - modContainer.registerExtensionPoint(IConfigScreenFactory.class, (container, parent) -> - ConfigScreenBuilder.create() - .add(entry, CONFIG::save, () -> EntryCreationContext.builder().build(), CONFIG::load) - .factory().apply(parent) - ); + return ConfigScreenBuilder.create() + .add(entry, CONFIG::save, () -> EntryCreationContext.builder().build(), CONFIG::load) + .factory().apply(parent); + }); } } From 457330aa6d539994bf5dd07862eddacee036b638 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 9 Sep 2024 17:28:10 -0500 Subject: [PATCH 57/76] Hopefully working ItemStack codecs and restructure interpreter keys --- .../structured/CodecInterpreter.java | 33 ++++- .../structured/IdentityInterpreter.java | 22 ++- .../codecextras/structured/Interpreter.java | 15 +- .../codecextras/structured/Keys.java | 5 + .../codecextras/structured/Keys2.java | 5 + .../structured/MapCodecInterpreter.java | 22 ++- .../codecextras/structured/Structure.java | 57 ++++++-- .../schema/JsonSchemaInterpreter.java | 20 ++- .../structured/MinecraftInterpreters.java | 118 ++-------------- .../minecraft/structured/MinecraftKeys.java | 38 +++++ .../structured/MinecraftStructures.java | 133 +++++++++++++++++- .../structured/config/ColorPickScreen.java | 5 +- .../config/ConfigScreenInterpreter.java | 22 ++- .../structured/StreamCodecInterpreter.java | 106 ++++++++++++-- 14 files changed, 434 insertions(+), 167 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 1d8992c..c091f6f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -14,11 +14,11 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; public abstract class CodecInterpreter extends KeyStoringInterpreter { public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { @@ -87,9 +87,9 @@ public DataResult> record(List } @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + public DataResult> flatXmap(App input, Function> to, Function> from) { var codec = Holder.unbox(input).codec(); - return DataResult.success(new Holder<>(codec.flatXmap(deserializer, serializer))); + return DataResult.success(new Holder<>(codec.flatXmap(to, from))); } @Override @@ -153,8 +153,31 @@ public CodecInterpreter with( public static final Key KEY = Key.create("CodecInterpreter"); @Override - public Optional> key() { - return Optional.of(KEY); + public Stream> keyConsumers() { + return Stream.of( + new KeyConsumer() { + @Override + public Key key() { + return KEY; + } + + @Override + public App convert(App input) { + return input; + } + }, + new KeyConsumer() { + @Override + public Key key() { + return MapCodecInterpreter.KEY; + } + + @Override + public App convert(App input) { + return new Holder<>(MapCodecInterpreter.unbox(input).codec()); + } + } + ); } public static Codec unbox(App box) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index a3773fe..39fc971 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -7,10 +7,10 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.jspecify.annotations.Nullable; /** @@ -28,8 +28,20 @@ public class IdentityInterpreter implements Interpreter { public static final Key KEY = Key.create("IdentityInterpreter"); @Override - public Optional> key() { - return Optional.of(KEY); + public Stream> keyConsumers() { + return Stream.of( + new KeyConsumer() { + @Override + public Key key() { + return KEY; + } + + @Override + public App convert(App input) { + return input; + } + } + ); } @Override @@ -72,9 +84,9 @@ public DataResult> record(List DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + public DataResult> flatXmap(App input, Function> to, Function> from) { var value = Identity.unbox(input).value(); - return deserializer.apply(value).map(Identity::new); + return to.apply(value).map(Identity::new); } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 6618290..d560db4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -11,10 +11,10 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; public interface Interpreter { DataResult>> list(App single); @@ -23,14 +23,19 @@ public interface Interpreter { DataResult> record(List> fields, Function creator); - DataResult> flatXmap(App input, Function> deserializer, Function> serializer); + DataResult> flatXmap(App input, Function> to, Function> from); DataResult> annotate(Structure original, Keys annotations); DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures); - default Optional> key() { - return Optional.empty(); + default Stream> keyConsumers() { + return Stream.of(); + } + + public interface KeyConsumer { + Key key(); + App convert(App input); } default DataResult> bounded(App input, Supplier> values) { @@ -55,6 +60,8 @@ default DataResult> bounded(App input, Supplier> val Key DOUBLE = Key.create("DOUBLE"); Key STRING = Key.create("STRING"); Key> PASSTHROUGH = Key.create("PASSTHROUGH"); + Key EMPTY_MAP = Key.create("EMPTY_MAP"); + Key EMPTY_LIST = Key.create("EMPTY_LIST"); DataResult>> parametricallyKeyed(Key2 key, App parameter); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index fb24140..23555e2 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -98,5 +98,10 @@ public Builder add(Key key, App value) { public Keys build() { return new Keys<>(new IdentityHashMap<>(keys)); } + + public Builder join(Keys other) { + keys.putAll(other.keys); + return this; + } } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java index cb77e34..d4645af 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys2.java @@ -55,5 +55,10 @@ public Builder add(Key2 key, App2 public Keys2 build() { return new Keys2<>(new IdentityHashMap<>(keys)); } + + public Builder join(Keys2 other) { + keys.putAll(other.keys); + return this; + } } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index ca49112..c167e73 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -11,11 +11,11 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( @@ -52,9 +52,9 @@ public DataResult> record(List } @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + public DataResult> flatXmap(App input, Function> to, Function> from) { var mapCodec = unbox(input); - return DataResult.success(new Holder<>(mapCodec.flatXmap(deserializer, serializer))); + return DataResult.success(new Holder<>(mapCodec.flatXmap(to, from))); } @Override @@ -133,7 +133,19 @@ static Holder unbox(App box) { public static final Key KEY = Key.create("MapCodecInterpreter"); @Override - public Optional> key() { - return Optional.of(KEY); + public Stream> keyConsumers() { + return Stream.of( + new KeyConsumer() { + @Override + public Key key() { + return KEY; + } + + @Override + public App convert(App input) { + return input; + } + } + ); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index ceb197b..464c468 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -143,29 +143,37 @@ default RecordStructure.Builder optionalFieldOf(String name, Supplier defa /** * Like codecs, the type a structure represents can be changed without changing the actual underlying data structure, * by providing conversion functions to and from the new type. - * @param deserializer converts the old type to the new type, if possible - * @param serializer converts the new type to the old type, if possible + * @param to converts the old type to the new type, if possible + * @param from converts the new type to the old type, if possible * @return a new structure representing the new type * @param the new type to represent */ - default Structure flatXmap(Function> deserializer, Function> serializer) { + default Structure flatXmap(Function> to, Function> from) { return annotatedDelegatingStructure(outer -> new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, deserializer, serializer)); + return outer.interpret(interpreter).flatMap(app -> interpreter.flatXmap(app, to, from)); } }, this, this.annotations()); } /** * Similar to {@link #flatXmap(Function, Function)} (Function, Function)}, except that the conversion functions are not allowed to fail. - * @param deserializer converts the old type to the new type - * @param serializer converts the new type to the old type + * @param to converts the old type to the new type + * @param from converts the new type to the old type * @return a new structure representing the new type * @param the new type to represent */ - default Structure xmap(Function deserializer, Function serializer) { - return flatXmap(a -> DataResult.success(deserializer.apply(a)), b -> DataResult.success(serializer.apply(b))); + default Structure xmap(Function to, Function from) { + return flatXmap(a -> DataResult.success(to.apply(a)), b -> DataResult.success(from.apply(b))); + } + + default Structure comapFlatMap(Function> to, Function from) { + return flatXmap(to, b -> DataResult.success(from.apply(b))); + } + + default Structure flatComapMap(Function to, Function> from) { + return flatXmap(a -> DataResult.success(to.apply(a)), from); } default Structure dispatch(String key, Function> function, Supplier> keys, Function>> structures) { @@ -265,13 +273,14 @@ public DataResult> interpret(Interpreter interpre * @param keys the set of specific representations to match against * @return a new structure * @param the type of data the structure represents - * @see Interpreter#key() + * @see Interpreter#keyConsumers() */ static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) + return interpreter.keyConsumers().flatMap(c -> convertedAppFromKeys(keys, c).stream()) + .findFirst() .map(DataResult::success) .orElseGet(() -> interpreter.keyed(key)); } @@ -283,7 +292,8 @@ static Structure keyed(Key key, Keys, K1> keys, Structure() { @Override public DataResult> interpret(Interpreter interpreter) { - var result = interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) + var result = interpreter.keyConsumers().flatMap(c -> convertedAppFromKeys(keys, c).stream()) + .findFirst() .map(DataResult::success) .orElseGet(() -> interpreter.keyed(key)); if (result.error().isPresent()) { @@ -324,8 +334,8 @@ static > Structure p return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - return interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) - .map(DataResult::success) + return interpreter.keyConsumers().flatMap(c -> convertedAppFromKeys(keys, c).stream()) + .findFirst().map(DataResult::success) .orElseGet(() -> interpreter.parametricallyKeyed(key, parameter).flatMap(app -> interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) )); @@ -333,11 +343,19 @@ public DataResult> interpret(Interpreter interpre }; } + private static Optional> convertedAppFromKeys(Keys, K1> keys, Interpreter.KeyConsumer c) { + return keys.get(c.key()) + .map(Flip::unbox) + .map(Flip::value) + .map(c::convert); + } + static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys, Structure fallback) { return new Structure<>() { @Override public DataResult> interpret(Interpreter interpreter) { - var result = interpreter.key().flatMap(k -> keys.get(k).>map(Flip::unbox).map(Flip::value)) + var result = interpreter.keyConsumers().flatMap(c -> convertedAppFromKeys(keys, c).stream()) + .findFirst() .map(DataResult::success) .orElseGet(() -> interpreter.parametricallyKeyed(key, parameter).flatMap(app -> interpreter.flatXmap(app, a -> DataResult.success(unboxer.apply(a)), DataResult::success) @@ -429,6 +447,17 @@ static Structure record(RecordStructure.Builder builder) { .build() ); + Structure EMPTY_MAP = keyed( + Interpreter.EMPTY_MAP, + unboundedMap(STRING, PASSTHROUGH) + .comapFlatMap(map -> map.isEmpty() ? DataResult.success(Unit.INSTANCE) : DataResult.error(() -> "Expected an empty map"), u -> Map.of()) + ); + Structure EMPTY_LIST = keyed( + Interpreter.EMPTY_LIST, + PASSTHROUGH.listOf() + .comapFlatMap(list -> list.isEmpty() ? DataResult.success(Unit.INSTANCE) : DataResult.error(() -> "Expected an empty list"), u -> List.of()) + ); + /** * {@return a structure representing integer values within a range} * @param min the minimum value (inclusive) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 04f91b0..b9041a6 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -24,10 +24,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.jspecify.annotations.Nullable; public class JsonSchemaInterpreter extends KeyStoringInterpreter { @@ -151,7 +151,7 @@ public DataResult> record(List } @Override - public DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + public DataResult> flatXmap(App input, Function> to, Function> from) { return DataResult.success(new Holder<>(schemaValue(input), definitions(input))); } @@ -322,8 +322,20 @@ public DataResult rootSchema(Structure structure) { public static final Key KEY = Key.create("JsonSchemaInterpreter"); @Override - public Optional> key() { - return Optional.of(KEY); + public Stream> keyConsumers() { + return Stream.of( + new KeyConsumer() { + @Override + public Key key() { + return KEY; + } + + @Override + public App convert(App input) { + return input; + } + } + ); } public record Holder(JsonObject jsonObject, Map> definition) implements App { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java index c577e48..5bb3c1a 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -1,7 +1,5 @@ package dev.lukebemish.codecextras.minecraft.structured; -import com.mojang.datafixers.kinds.App; -import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.K1; import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; import dev.lukebemish.codecextras.structured.CodecInterpreter; @@ -10,14 +8,9 @@ import dev.lukebemish.codecextras.structured.MapCodecInterpreter; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; -import net.minecraft.core.RegistryCodecs; -import net.minecraft.core.component.DataComponentMap; -import net.minecraft.core.component.DataComponentPatch; +import java.util.List; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.tags.TagKey; public final class MinecraftInterpreters { private MinecraftInterpreters() {} @@ -28,57 +21,11 @@ private MinecraftInterpreters() {} public static final Keys2, K1, K1> MAP_CODEC_PARAMETRIC_KEYS = Keys2., K1, K1>builder() .build(); - public static final Keys CODEC_KEYS = MAP_CODEC_KEYS.map(new Keys.Converter<>() { - @Override - public App convert(App app) { - return new CodecInterpreter.Holder<>(MapCodecInterpreter.unbox(app).codec()); - } - }).join(Keys.builder() - .add(MinecraftKeys.RESOURCE_LOCATION, new CodecInterpreter.Holder<>(ResourceLocation.CODEC)) - .add(MinecraftKeys.DATA_COMPONENT_MAP, new CodecInterpreter.Holder<>(DataComponentMap.CODEC)) - .add(MinecraftKeys.DATA_COMPONENT_PATCH, new CodecInterpreter.Holder<>(DataComponentPatch.CODEC)) - .build() - ); + public static final Keys CODEC_KEYS = Keys.builder() + .build(); - public static final Keys2, K1, K1> CODEC_PARAMETRIC_KEYS = MAP_CODEC_PARAMETRIC_KEYS.map(new Keys2.Converter, ParametricKeyedValue.Mu, K1, K1>() { - @Override - public App2, A, B> convert(App2, A, B> input) { - var unboxed = ParametricKeyedValue.unbox(input); - return new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - var mapCodec = MapCodecInterpreter.unbox(unboxed.convert(parameter)); - return new CodecInterpreter.Holder<>(mapCodec.codec()); - } - }; - } - }).join(Keys2., K1, K1>builder() - .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(ResourceKey.codec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.ResourceKeyHolder::new, a -> MinecraftKeys.ResourceKeyHolder.unbox(a).value())); - } - }) - .add(MinecraftKeys.TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(TagKey.codec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.TagKeyHolder::new, a -> MinecraftKeys.TagKeyHolder.unbox(a).value())); - } - }) - .add(MinecraftKeys.HASHED_TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(TagKey.hashedCodec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.TagKeyHolder::new, a -> MinecraftKeys.TagKeyHolder.unbox(a).value())); - } - }) - .add(MinecraftKeys.HOMOGENOUS_LIST, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new CodecInterpreter.Holder<>(RegistryCodecs.homogeneousList(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).xmap(MinecraftKeys.HolderSetHolder::new, a -> MinecraftKeys.HolderSetHolder.unbox(a).value())); - } - }) - .build() - ); + public static final Keys2, K1, K1> CODEC_PARAMETRIC_KEYS = Keys2., K1, K1>builder() + .build(); public static final CodecInterpreter CODEC_INTERPRETER = CodecInterpreter.create().with( CODEC_KEYS, @@ -95,18 +42,9 @@ public App, Object> FRIENDLY_STREAM_KEYS = Keys., Object>builder() - .add(MinecraftKeys.RESOURCE_LOCATION, new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast())) .build(); public static final Keys2>, K1, K1> FRIENDLY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() - .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App, App> convert(App parameter) { - return new StreamCodecInterpreter.Holder<>( - ResourceKey.streamCodec(MinecraftKeys.RegistryKeyHolder.unbox(parameter).value()).>map(MinecraftKeys.ResourceKeyHolder::new, a -> MinecraftKeys.ResourceKeyHolder.unbox(a).value()).cast() - ); - } - }) .build(); public static final StreamCodecInterpreter FRIENDLY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( @@ -115,59 +53,23 @@ public App, App, Object> REGISTRY_STREAM_KEYS = FRIENDLY_STREAM_KEYS.map(new Keys.Converter, StreamCodecInterpreter.Holder.Mu, Object>() { - @Override - public App, A> convert(App, A> input) { - return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(input).cast()); - } - }).join(Keys., Object>builder() - .add(MinecraftKeys.DATA_COMPONENT_PATCH, new StreamCodecInterpreter.Holder<>(DataComponentPatch.STREAM_CODEC)) - .build() - ); + public static final Keys, Object> REGISTRY_STREAM_KEYS = Keys., Object>builder() + .build(); - public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = FRIENDLY_STREAM_PARAMETRIC_KEYS.map(new Keys2.Converter>, ParametricKeyedValue.Mu>, K1, K1>() { - @Override - public App2>, A, B> convert(App2>, A, B> input) { - return new ParametricKeyedValue<>() { - @Override - public App, App> convert(App parameter) { - return new StreamCodecInterpreter.Holder<>(StreamCodecInterpreter.unbox(ParametricKeyedValue.unbox(input).convert(parameter)).cast()); - } - }; - } - }).join(Keys2.>, K1, K1>builder() - .build() - ); + public static final Keys2>, K1, K1> REGISTRY_STREAM_PARAMETRIC_KEYS = Keys2.>, K1, K1>builder() + .build(); public static final StreamCodecInterpreter REGISTRY_STREAM_CODEC_INTERPRETER = new StreamCodecInterpreter<>( StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, + List.of(FRIENDLY_STREAM_CODEC_INTERPRETER), REGISTRY_STREAM_KEYS, REGISTRY_STREAM_PARAMETRIC_KEYS ); public static final Keys JSON_SCHEMA_KEYS = Keys.builder() - .add(MinecraftKeys.RESOURCE_LOCATION, new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get())) .build(); // TODO: Add regex fo schemas public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() - .add(MinecraftKeys.RESOURCE_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) - .add(MinecraftKeys.TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) - .add(MinecraftKeys.HASHED_TAG_KEY, new ParametricKeyedValue<>() { - @Override - public App> convert(App parameter) { - return new JsonSchemaInterpreter.Holder<>(JsonSchemaInterpreter.STRING.get()); - } - }) .build(); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index 0317fb9..00ad089 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -7,6 +7,7 @@ import dev.lukebemish.codecextras.types.Identity; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.component.DataComponentMap; @@ -15,6 +16,8 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; public final class MinecraftKeys { private static final Map>, Key> DATA_COMPONENT_TYPE_KEYS = new ConcurrentHashMap<>(); @@ -30,6 +33,11 @@ private MinecraftKeys() { public static final Key ARGB_COLOR = Key.create("argb_color"); public static final Key RGB_COLOR = Key.create("rgb_color"); + public static final Key> ITEM_NON_AIR = Key.create("item_non_air"); + public static final Key OPTIONAL_ITEM_STACK = Key.create("optional_item_stack"); + public static final Key NON_EMPTY_ITEM_STACK = Key.create("non_empty_item_stack"); + public static final Key STRICT_NON_EMPTY_ITEM_STACK = Key.create("strict_non_empty_item_stack"); + public record DataComponentTypeHolder(DataComponentType value) implements App { public static final class Mu implements K1 { private Mu() { @@ -86,11 +94,41 @@ public static RegistryKeyHolder unbox(App box) { } } + public record RegistryHolder(Registry value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static RegistryHolder unbox(App box) { + return (RegistryHolder) box; + } + } + + public record HolderHolder(Holder value) implements App { + public static final class Mu implements K1 { + private Mu() { + } + } + + public static HolderHolder unbox(App box) { + return (HolderHolder) box; + } + } + public static final Key> DATA_COMPONENT_PATCH_KEY = Key.create("data_component_patch_key"); public record DataComponentPatchKey(DataComponentType type, boolean removes) {} public static final Key2 RESOURCE_KEY = Key2.create("resource_key"); + /** + * A key for structures handling a {@link Holder}, using an ID when handling non-permanent data. + */ + public static final Key2 ORDERED_HOLDER = Key2.create("holder"); + /** + * A key for structures handling a {@link Holder}, which does not use an ID. + */ + public static final Key2 UNORDERED_HOLDER = Key2.create("holder"); public static final Key2 TAG_KEY = Key2.create("tag_key"); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index a75718f..f201704 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -1,6 +1,7 @@ package dev.lukebemish.codecextras.minecraft.structured; import com.mojang.datafixers.kinds.K1; +import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; @@ -10,11 +11,14 @@ import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.EncoderException; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryCodecs; @@ -24,10 +28,15 @@ import net.minecraft.core.component.TypedDataComponent; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import org.jspecify.annotations.Nullable; public final class MinecraftStructures { @@ -38,6 +47,7 @@ private MinecraftStructures() { MinecraftKeys.RESOURCE_LOCATION, Keys., K1>builder() .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ResourceLocation.CODEC))) + .add(StreamCodecInterpreter.FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast()))) .build(), Structure.STRING.flatXmap(ResourceLocation::read, rl -> DataResult.success(rl.toString())) ); @@ -143,6 +153,61 @@ private MinecraftStructures() { }))) ); + @SuppressWarnings("deprecation") + public static final Structure> ITEM_NON_AIR = Structure.keyed( + MinecraftKeys.ITEM_NON_AIR, + Keys.>, K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.ITEM_NON_AIR_CODEC))) + .build(), + registryOrderedHolder(BuiltInRegistries.ITEM) + .validate(holder -> holder.is(Items.AIR.builtInRegistryHolder()) ? DataResult.error(() -> "Item must not be minecraft:air") : DataResult.success(holder)) + ); + + public static final Structure NON_EMPTY_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.NON_EMPTY_ITEM_STACK, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.CODEC))) + .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ItemStack.STREAM_CODEC))) + .build(), + Structure.record(builder -> { + var item = builder.add("id", ITEM_NON_AIR, ItemStack::getItemHolder); + var count = builder.addOptional("count", Structure.intInRange(1, 99), ItemStack::getCount, () -> 1); + var patch = builder.addOptional("components", DATA_COMPONENT_PATCH, ItemStack::getComponentsPatch, () -> DataComponentPatch.EMPTY); + return container -> new ItemStack( + item.apply(container), + count.apply(container), + patch.apply(container) + ); + }) + )); + + public static final Structure OPTIONAL_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.OPTIONAL_ITEM_STACK, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.OPTIONAL_CODEC))) + .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ItemStack.OPTIONAL_STREAM_CODEC))) + .build(), + Structure.either(Structure.EMPTY_MAP, NON_EMPTY_ITEM_STACK) + .xmap(e -> e.map(u -> ItemStack.EMPTY, Function.identity()), itemStack -> itemStack.isEmpty() ? Either.left(Unit.INSTANCE) : Either.right(itemStack)) + )); + + public static final Structure STRICT_NON_EMPTY_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.STRICT_NON_EMPTY_ITEM_STACK, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.STRICT_CODEC))) + .build(), + NON_EMPTY_ITEM_STACK.validate(MinecraftStructures::validateItemStackStrict) + )); + + private static DataResult validateItemStackStrict(ItemStack itemStack) { + return ItemStack.validateComponents(itemStack.getComponents()) + .flatMap( + u -> itemStack.getCount() > itemStack.getMaxStackSize() ? + DataResult.error(() -> "Item stack with stack size of " + itemStack.getCount() + " was larger than maximum: " + itemStack.getMaxStackSize()) : + DataResult.success(itemStack) + ); + } + public static Structure> resourceKey(ResourceKey> registry) { return Structure.parametricallyKeyed( MinecraftKeys.RESOURCE_KEY, @@ -155,10 +220,70 @@ public static Structure> resourceKey(ResourceKey(new StreamCodecInterpreter.Holder<>( + ResourceKey.streamCodec(registry).map(MinecraftKeys.ResourceKeyHolder::new, MinecraftKeys.ResourceKeyHolder::value).cast() + )) + ) + .build(), + RESOURCE_LOCATION.xmap(resourceLocation -> ResourceKey.create(registry, resourceLocation), ResourceKey::location) + .xmap(MinecraftKeys.ResourceKeyHolder::new, MinecraftKeys.ResourceKeyHolder::value) ).xmap(MinecraftKeys.ResourceKeyHolder::value, MinecraftKeys.ResourceKeyHolder::new); } + public static Structure> registryOrderedHolder(Registry registry) { + return Structure.parametricallyKeyed( + MinecraftKeys.ORDERED_HOLDER, + new MinecraftKeys.RegistryHolder<>(registry), + MinecraftKeys.HolderHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + registry.holderByNameCodec().xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) + )) + ) + .add( + StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, + new Flip<>(new StreamCodecInterpreter.Holder<>( + ByteBufCodecs.holderRegistry(registry.key()).map(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) + )) + ) + .build() + ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); + } + + public static Structure> registryUnorderedHolder(Registry registry) { + return Structure.parametricallyKeyed( + MinecraftKeys.UNORDERED_HOLDER, + new MinecraftKeys.RegistryHolder<>(registry), + MinecraftKeys.HolderHolder::unbox, + Keys.>, K1>builder() + .add( + CodecInterpreter.KEY, + new Flip<>(new CodecInterpreter.Holder<>( + registry.holderByNameCodec().xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) + )) + ) + .add( + StreamCodecInterpreter.FRIENDLY_BYTE_BUF_KEY, + new Flip<>(new StreamCodecInterpreter.Holder<>( + ResourceLocation.STREAM_CODEC.>map( + rl -> registry.getHolder(rl).orElseThrow(() -> new DecoderException("Unknown registry entry: " + rl)), + holder -> { + if (holder instanceof Holder.Reference reference) { + return reference.key().location(); + } + throw new EncoderException("Unknown registry entry: " + holder); + } + ).cast().map(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) + )) + ) + .build() + ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); + } + public static Structure> homogenousList(ResourceKey> registry) { return Structure.parametricallyKeyed( MinecraftKeys.HOMOGENOUS_LIST, @@ -187,7 +312,11 @@ public static Structure> tagKey(ResourceKey> (hashPrefix ? TagKey.hashedCodec(registry) : TagKey.codec(registry)).xmap(MinecraftKeys.TagKeyHolder::new, MinecraftKeys.TagKeyHolder::value) )) ) - .build() + .build(), + (hashPrefix ? + Structure.STRING.comapFlatMap(string -> string.startsWith("#") ? ResourceLocation.read(string.substring(1)).map(resourceLocation -> TagKey.create(registry, resourceLocation)) : DataResult.>error(() -> "Not a tag id"), tagKey -> "#" + tagKey.location()) : + RESOURCE_LOCATION.xmap(resourceLocation -> TagKey.create(registry, resourceLocation), TagKey::location)) + .xmap(MinecraftKeys.TagKeyHolder::new, MinecraftKeys.TagKeyHolder::value) ).xmap(MinecraftKeys.TagKeyHolder::value, MinecraftKeys.TagKeyHolder::new); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java index 75ed708..307c11f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java @@ -2,6 +2,8 @@ import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.logging.LogUtils; +import java.util.Locale; +import java.util.function.Consumer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -15,9 +17,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; -import java.util.Locale; -import java.util.function.Consumer; - class ColorPickScreen extends Screen { private static final Logger LOGGER = LogUtils.getLogger(); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 1c392a0..2aa4fb7 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -43,12 +43,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; @@ -687,8 +687,20 @@ public ConfigScreenInterpreter( public static final Key KEY = Key.create("ConfigScreenInterpreter"); @Override - public Optional> key() { - return Optional.of(KEY); + public Stream> keyConsumers() { + return Stream.of( + new KeyConsumer() { + @Override + public Key key() { + return KEY; + } + + @Override + public App convert(App input) { + return input; + } + } + ); } @Override @@ -878,10 +890,10 @@ private void handleEntry(RecordStructure.Field field, List DataResult> flatXmap(App input, Function> deserializer, Function> serializer) { + public DataResult> flatXmap(App input, Function> to, Function> from) { var original = ConfigScreenEntry.unbox(input); var codecOriginal = original.entryCreationInfo().codec(); - var codecMapped = codecInterpreter.flatXmap(new CodecInterpreter.Holder<>(codecOriginal), deserializer, serializer).map(CodecInterpreter::unbox); + var codecMapped = codecInterpreter.flatXmap(new CodecInterpreter.Holder<>(codecOriginal), to, from).map(CodecInterpreter::unbox); if (codecMapped.error().isPresent()) { return DataResult.error(codecMapped.error().get().messageSupplier()); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 8847035..6a4df78 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -2,6 +2,7 @@ import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.App2; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; @@ -31,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.VarInt; @@ -40,9 +42,11 @@ public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { private final Key> key; + private final List>> parentConsumers; + private final List> parents; - public StreamCodecInterpreter(Key> key, Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { - super(keys.join(Keys., Object>builder() + public StreamCodecInterpreter(Key> key, List> parents, Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { + super(Keys., Object>builder() .add(Interpreter.UNIT, new Holder<>(StreamCodec.of((buf, data) -> {}, buf -> Unit.INSTANCE))) .add(Interpreter.BOOL, new Holder<>(ByteBufCodecs.BOOL.cast())) .add(Interpreter.BYTE, new Holder<>(ByteBufCodecs.BYTE.cast())) @@ -52,8 +56,8 @@ public StreamCodecInterpreter(Key> key, Keys, Object> .add(Interpreter.FLOAT, new Holder<>(ByteBufCodecs.FLOAT.cast())) .add(Interpreter.DOUBLE, new Holder<>(ByteBufCodecs.DOUBLE.cast())) .add(Interpreter.STRING, new Holder<>(ByteBufCodecs.STRING_UTF8.cast())) - .build() - ), parametricKeys.join(Keys2.>, K1, K1>builder() + .build().join(buildCombinedKeys(parents)).join(keys), + Keys2.>, K1, K1>builder() .add(Interpreter.INT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.VAR_INT.cast())) .add(Interpreter.BYTE_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.BYTE.cast())) .add(Interpreter.SHORT_IN_RANGE, numberRangeCodecParameter(ByteBufCodecs.SHORT.cast())) @@ -104,11 +108,76 @@ public void encode(B buffer, App object) { }); } }) - .build() - )); + .build().join(buildCombinedParametricKeys(parents)).join(parametricKeys) + ); + this.parents = parents; + this.parentConsumers = new ArrayList<>(); + for (var parent : parents) { + parent.parentConsumers.forEach(c -> addKeyConsumer(this.parentConsumers, c)); + } this.key = key; } + private static Keys, Object> buildCombinedKeys(List> parents) { + var builder = Keys., Object>builder(); + for (var parent : parents) { + addConvertedKeysFromParent(builder, parent); + } + return builder.build(); + } + + private static Keys2>, K1, K1> buildCombinedParametricKeys(List> parents) { + var builder = Keys2.>, K1, K1>builder(); + for (var parent : parents) { + addConvertedParametricKeysFromParent(builder, parent); + } + return builder.build(); + } + + private static

void addConvertedParametricKeysFromParent(Keys2.Builder>, K1, K1> builder, StreamCodecInterpreter

parent) { + builder.join(parent.parametricKeys().map(new Keys2.Converter<>() { + @Override + public App2>, X, Y> convert(App2>, X, Y> input) { + var value = ParametricKeyedValue.unbox(input); + return value.map(new ParametricKeyedValue.Converter<>() { + @Override + public App, App> convert(App, App> app) { + return new Holder<>(unbox(app).cast()); + } + }); + } + })); + } + + private static

void addConvertedKeysFromParent(Keys.Builder, Object> builder, StreamCodecInterpreter

void addKeyConsumer(List>> keyConsumers, KeyConsumer> original) { + keyConsumers.add(new KeyConsumer>() { + @Override + public Key key() { + return original.key(); + } + + @Override + public App, T> convert(App input) { + var converted = original.convert(input); + var stream = unbox(converted); + return new StreamCodecInterpreter.Holder<>(stream.cast()); + } + }); + } + + public StreamCodecInterpreter(Key> key, Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { + this(key, List.of(), keys, parametricKeys); + } + public static final Key> FRIENDLY_BYTE_BUF_KEY = Key.create("StreamCodecInterpreter"); public static final Key> REGISTRY_FRIENDLY_BYTE_BUF_KEY = Key.create("StreamCodecInterpreter"); @@ -154,7 +223,7 @@ public StreamCodecInterpreter(Key> key) { @Override public StreamCodecInterpreter with(Keys, Object> keys, Keys2>, K1, K1> parametricKeys) { - return new StreamCodecInterpreter<>(key, keys().join(keys), parametricKeys().join(parametricKeys)); + return new StreamCodecInterpreter<>(key, parents, keys().join(keys), parametricKeys().join(parametricKeys)); } @Override @@ -190,11 +259,11 @@ public DataResult, A>> record(List DataResult, Y>> flatXmap(App, X> input, Function> deserializer, Function> serializer) { + public DataResult, Y>> flatXmap(App, X> input, Function> to, Function> from) { var streamCodec = unbox(input); return DataResult.success(new Holder<>(streamCodec.map( - x -> deserializer.apply(x).getOrThrow(), - y -> serializer.apply(y).getOrThrow() + x -> to.apply(x).getOrThrow(), + y -> from.apply(y).getOrThrow() ))); } @@ -301,8 +370,21 @@ public DataResult> interpret(Structure structure) { } @Override - public Optional>> key() { - return Optional.of(key); + public Stream>> keyConsumers() { + return Stream.concat( + Stream.of(new KeyConsumer, Holder.Mu>() { + @Override + public Key> key() { + return key; + } + + @Override + public App, T> convert(App, T> input) { + return input; + } + }), + parentConsumers.stream() + ); } @Override From b8852de5ce2018257200d5f7509e2115ae565bcf Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 9 Sep 2024 22:04:02 -0500 Subject: [PATCH 58/76] Fix up itemstack configuration --- .../structured/MinecraftStructures.java | 47 ++-- .../structured/config/ConfigAnnotations.java | 2 +- .../config/ConfigScreenInterpreter.java | 63 +++-- .../config/VisibilityWrapperElement.java | 232 ++++++++++++++++++ .../minecraft/structured/config/Widgets.java | 90 +++---- .../codecextras_minecraft/lang/en_us.json | 3 +- .../codecextras/test/common/TestConfig.java | 6 +- 7 files changed, 359 insertions(+), 84 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VisibilityWrapperElement.java diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index f201704..fb1645c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -13,11 +13,6 @@ import dev.lukebemish.codecextras.types.Identity; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; @@ -39,6 +34,13 @@ import net.minecraft.world.item.Items; import org.jspecify.annotations.Nullable; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + public final class MinecraftStructures { private MinecraftStructures() { } @@ -187,7 +189,10 @@ private MinecraftStructures() { .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.OPTIONAL_CODEC))) .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ItemStack.OPTIONAL_STREAM_CODEC))) .build(), - Structure.either(Structure.EMPTY_MAP, NON_EMPTY_ITEM_STACK) + Structure.either( + Structure.EMPTY_MAP, + NON_EMPTY_ITEM_STACK + ) .xmap(e -> e.map(u -> ItemStack.EMPTY, Function.identity()), itemStack -> itemStack.isEmpty() ? Either.left(Unit.INSTANCE) : Either.right(itemStack)) )); @@ -250,7 +255,12 @@ public static Structure> registryOrderedHolder(Registry registr ByteBufCodecs.holderRegistry(registry.key()).map(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) )) ) - .build() + .build(), + resourceKey(registry.key()) + .bounded(registry::registryKeySet) + .flatXmap(key -> registry.getHolder(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> + keyForEntry(holder, registry).map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + holder))) + .xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); } @@ -271,19 +281,28 @@ public static Structure> registryUnorderedHolder(Registry regis new Flip<>(new StreamCodecInterpreter.Holder<>( ResourceLocation.STREAM_CODEC.>map( rl -> registry.getHolder(rl).orElseThrow(() -> new DecoderException("Unknown registry entry: " + rl)), - holder -> { - if (holder instanceof Holder.Reference reference) { - return reference.key().location(); - } - throw new EncoderException("Unknown registry entry: " + holder); - } + holder -> keyForEntry(holder, registry).orElseThrow(() -> new EncoderException("Unknown registry entry: " + holder)).location() ).cast().map(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) )) ) - .build() + .build(), + resourceKey(registry.key()) + .bounded(registry::registryKeySet) + .flatXmap(key -> registry.getHolder(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> + keyForEntry(holder, registry).map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + holder))) + .xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); } + private static Optional> keyForEntry(Holder entry, Registry registry) { + if (entry instanceof Holder.Reference reference) { + return Optional.of(reference.key()); + } else { + var value = entry.value(); + return registry.getResourceKey(value); + } + } + public static Structure> homogenousList(ResourceKey> registry) { return Structure.parametricallyKeyed( MinecraftKeys.HOMOGENOUS_LIST, diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java index c851d41..c63963c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigAnnotations.java @@ -7,5 +7,5 @@ public final class ConfigAnnotations { private ConfigAnnotations() {} public static Key TITLE = Key.create("title"); - public static Key DESCRIPTOIN = Key.create("description"); + public static Key DESCRIPTION = Key.create("description"); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 2aa4fb7..6e7aabe 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -36,19 +36,6 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; @@ -58,15 +45,32 @@ import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.layouts.LayoutSettings; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + public class ConfigScreenInterpreter extends KeyStoringInterpreter { private static final Logger LOGGER = LogUtils.getLogger(); @@ -147,6 +151,14 @@ public ConfigScreenInterpreter( Widgets.unit(), new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), ComponentInfo.empty()) )) + .add(Interpreter.EMPTY_LIST, ConfigScreenEntry.single( + Widgets.unit(Component.translatable("codecextras.config.unit.empty")), + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_LIST).getOrThrow(), ComponentInfo.empty()) + )) + .add(Interpreter.EMPTY_MAP, ConfigScreenEntry.single( + Widgets.unit(Component.translatable("codecextras.config.unit.empty")), + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_MAP).getOrThrow(), ComponentInfo.empty()) + )) .add(Interpreter.STRING, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(DataResult::success, DataResult::success, false)), new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) @@ -155,6 +167,29 @@ public ConfigScreenInterpreter( Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), new EntryCreationInfo<>(Codec.PASSTHROUGH, ComponentInfo.empty()) )) + .add(MinecraftKeys.ITEM_NON_AIR, ConfigScreenEntry.single( + Widgets.pickWidget(new StringRepresentation<>( + () -> BuiltInRegistries.ITEM.holders().>map(Function.identity()).toList(), + holder -> { + if (holder instanceof Holder.Reference reference) { + return reference.key().location().toString(); + } else { + var value = holder.value(); + return BuiltInRegistries.ITEM.getKey(value).toString(); + } + }, + string -> { + var rlResult = ResourceLocation.read(string); + if (rlResult.error().isPresent()) { + return null; + } + var key = rlResult.getOrThrow(); + return BuiltInRegistries.ITEM.getHolder(key).orElse(null); + }, + false + )), + new EntryCreationInfo<>(ItemStack.ITEM_NON_AIR_CODEC, ComponentInfo.empty()) + )) .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("^([a-z0-9._-]+:)?[a-z0-9/._-]*$"), false)), new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) @@ -919,7 +954,7 @@ public DataResult> annotate(Structure origin .map(title -> entry.withComponentInfo(info -> info.withTitle(title))) .orElse(entry); return Annotation - .get(annotations, ConfigAnnotations.DESCRIPTOIN) + .get(annotations, ConfigAnnotations.DESCRIPTION) .or(() -> Annotation.get(annotations, Annotation.DESCRIPTION).map(Component::literal)) .or(() -> Annotation.get(annotations, Annotation.COMMENT).map(Component::literal)) .map(description -> withTitle.withComponentInfo(info -> info.withDescription(description))) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VisibilityWrapperElement.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VisibilityWrapperElement.java new file mode 100644 index 0000000..d7a20a2 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/VisibilityWrapperElement.java @@ -0,0 +1,232 @@ +package dev.lukebemish.codecextras.minecraft.structured.config; + +import java.util.IdentityHashMap; +import java.util.function.Consumer; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.layouts.Layout; +import net.minecraft.client.gui.layouts.LayoutElement; + +public interface VisibilityWrapperElement extends Layout { + void setVisible(boolean visible); + boolean visible(); + + void setActive(boolean visible); + boolean active(); + + static VisibilityWrapperElement ofInactive(LayoutElement child) { + return new VisibilityWrapperElement() { + private boolean visible = true; + private boolean active = true; + + private final IdentityHashMap isVisible = new IdentityHashMap<>(); + + private void visitChildren(Consumer widgetConsumer, Consumer wrapperConsumer) { + switch (child) { + case VisibilityWrapperElement wrapperElement -> wrapperConsumer.accept(wrapperElement); + case Layout layout -> layout.visitChildren(element -> visitChildren(widgetConsumer, wrapperConsumer)); + case AbstractWidget widget -> widgetConsumer.accept(widget); + default -> {} + } + } + + { + visitWidgets(widget -> { + widget.active = false; + }); + } + + @Override + public void setX(int i) { + child.setX(i); + } + + @Override + public void setY(int i) { + child.setY(i); + } + + @Override + public int getX() { + return child.getX(); + } + + @Override + public int getY() { + return child.getY(); + } + + @Override + public int getWidth() { + return child.getWidth(); + } + + @Override + public int getHeight() { + return child.getHeight(); + } + + @Override + public void visitChildren(Consumer consumer) { + consumer.accept(child); + } + + @Override + public void visitWidgets(Consumer consumer) { + child.visitWidgets(consumer); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + isVisible.forEach((element, wasVisible) -> { + if (element instanceof VisibilityWrapperElement wrapper) { + wrapper.setVisible(wasVisible); + } else if (element instanceof AbstractWidget widget) { + widget.visible = wasVisible; + } + }); + } else { + isVisible.clear(); + visitChildren(widget -> { + isVisible.put(widget, widget.visible); + widget.visible = false; + }, wrapper -> { + isVisible.put(wrapper, wrapper.visible()); + wrapper.setVisible(false); + }); + } + this.visible = visible; + } + + @Override + public boolean visible() { + return visible; + } + + @Override + public void setActive(boolean visible) { + this.active = visible; + } + + @Override + public boolean active() { + return active; + } + }; + } + + static VisibilityWrapperElement ofDirect(LayoutElement child) { + return new VisibilityWrapperElement() { + private boolean visible = true; + private boolean active = true; + + private final IdentityHashMap isVisible = new IdentityHashMap<>(); + private final IdentityHashMap isActive = new IdentityHashMap<>(); + + private void visitChildren(Consumer widgetConsumer, Consumer wrapperConsumer) { + switch (child) { + case VisibilityWrapperElement wrapperElement -> wrapperConsumer.accept(wrapperElement); + case Layout layout -> layout.visitChildren(element -> visitChildren(widgetConsumer, wrapperConsumer)); + case AbstractWidget widget -> widgetConsumer.accept(widget); + default -> {} + } + } + + @Override + public void setX(int i) { + child.setX(i); + } + + @Override + public void setY(int i) { + child.setY(i); + } + + @Override + public int getX() { + return child.getX(); + } + + @Override + public int getY() { + return child.getY(); + } + + @Override + public int getWidth() { + return child.getWidth(); + } + + @Override + public int getHeight() { + return child.getHeight(); + } + + @Override + public void visitChildren(Consumer consumer) { + consumer.accept(child); + } + + @Override + public void visitWidgets(Consumer consumer) { + child.visitWidgets(consumer); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + isVisible.forEach((element, wasVisible) -> { + if (element instanceof VisibilityWrapperElement wrapper) { + wrapper.setVisible(wasVisible); + } else if (element instanceof AbstractWidget widget) { + widget.visible = wasVisible; + } + }); + } else { + isVisible.clear(); + visitChildren(widget -> { + isVisible.put(widget, widget.visible); + widget.visible = false; + }, wrapper -> { + isVisible.put(wrapper, wrapper.visible()); + wrapper.setVisible(false); + }); + } + this.visible = visible; + } + + @Override + public boolean visible() { + return visible; + } + + @Override + public void setActive(boolean active) { + if (active) { + isActive.forEach((element, wasVisible) -> { + if (element instanceof VisibilityWrapperElement wrapper) { + wrapper.setActive(wasVisible); + } else if (element instanceof AbstractWidget widget) { + widget.active = wasVisible; + } + }); + } else { + isActive.clear(); + visitChildren(widget -> { + isActive.put(widget, widget.active); + widget.active = false; + }, wrapper -> { + isActive.put(wrapper, wrapper.active()); + wrapper.setActive(false); + }); + } + this.active = active; + } + + @Override + public boolean active() { + return active; + } + }; + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 40e93ba..1464285 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -10,12 +10,6 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Range; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; @@ -26,13 +20,19 @@ import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.layouts.EqualSpacingLayout; import net.minecraft.client.gui.layouts.FrameLayout; -import net.minecraft.client.gui.layouts.LayoutElement; import net.minecraft.client.gui.layouts.LayoutSettings; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + public final class Widgets { private static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); @@ -142,10 +142,10 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass var layout = new EqualSpacingLayout(fullWidth, 0, EqualSpacingLayout.Orientation.HORIZONTAL); var object = new Object() { private JsonElement value = original; - private final LayoutElement wrapped = assumesNonOptional.create(parent, remainingWidth, context, original, json -> { + private final VisibilityWrapperElement wrapped = VisibilityWrapperElement.ofDirect(assumesNonOptional.create(parent, remainingWidth, context, original, json -> { this.value = json; update.accept(json); - }, creationInfo, false); + }, creationInfo, false)); private final Button disabled = Button.builder(Component.translatable("codecextras.config.missing"), b -> {}) .width(remainingWidth) .build(); @@ -156,19 +156,15 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass missing = !b; if (missing) { update.accept(JsonNull.INSTANCE); - wrapped.visitWidgets(w -> { - w.visible = false; - w.active = false; - }); + wrapped.setVisible(false); + wrapped.setActive(false); var maxHeight = Math.max(disabled.getHeight(), wrapped.getHeight()); disabled.setHeight(maxHeight); disabled.visible = true; } else { update.accept(value); - wrapped.visitWidgets(w -> { - w.visible = true; - w.active = true; - }); + wrapped.setVisible(true); + wrapped.setActive(true); var maxHeight = Math.max(disabled.getHeight(), wrapped.getHeight()); disabled.setHeight(maxHeight); disabled.visible = false; @@ -182,10 +178,8 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass disabled.setHeight(maxHeight); disabled.active = false; disabled.visible = missing; - wrapped.visitWidgets(w -> { - w.visible = !missing; - w.active = !missing; - }); + wrapped.setVisible(!missing); + wrapped.setActive(!missing); creationInfo.componentInfo().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); @@ -203,7 +197,7 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass right.addChild(object.disabled, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); right.addChild(object.wrapped, LayoutSettings.defaults().alignVerticallyMiddle().alignHorizontallyCenter()); layout.addChild(right, LayoutSettings.defaults().alignVerticallyMiddle()); - return layout; + return VisibilityWrapperElement.ofDirect(layout); }; } @@ -353,8 +347,8 @@ public static LayoutFactory> either(LayoutFactory left, L } Codec leftCodec = creationInfo.codec().comapFlatMap(e -> e.left().map(DataResult::success).orElse(DataResult.error(() -> "Expected left value")), Either::left); Codec rightCodec = creationInfo.codec().comapFlatMap(e -> e.right().map(DataResult::success).orElse(DataResult.error(() -> "Expected right value")), Either::right); - var leftElement = left.create(parent, remainingWidth, context, isLeft[0] ? original : JsonNull.INSTANCE, update, creationInfo.withCodec(leftCodec), false); - var rightElement = right.create(parent, remainingWidth, context, isLeft[0] ? JsonNull.INSTANCE : original, update, creationInfo.withCodec(rightCodec), false); + var leftElement = VisibilityWrapperElement.ofDirect(left.create(parent, remainingWidth, context, isLeft[0] ? original : JsonNull.INSTANCE, update, creationInfo.withCodec(leftCodec), false)); + var rightElement = VisibilityWrapperElement.ofDirect(right.create(parent, remainingWidth, context, isLeft[0] ? JsonNull.INSTANCE : original, update, creationInfo.withCodec(rightCodec), false)); var missingElement = handleOptional ? Button.builder(Component.translatable("codecextras.config.missing"), b -> {}).width(remainingWidth).build() : null; if (handleOptional) { missingElement.active = false; @@ -365,38 +359,26 @@ public static LayoutFactory> either(LayoutFactory left, L frame.addChild(rightElement); Runnable updateVisibility = () -> { if (isMissing[0]) { - rightElement.visitWidgets(w -> { - w.visible = false; - w.active = false; - }); - leftElement.visitWidgets(w -> { - w.visible = false; - w.active = false; - }); + rightElement.setVisible(false); + rightElement.setActive(false); + leftElement.setVisible(false); + leftElement.setActive(false); if (handleOptional) { missingElement.visible = true; } } else if (isLeft[0]) { - rightElement.visitWidgets(w -> { - w.visible = false; - w.active = false; - }); - leftElement.visitWidgets(w -> { - w.visible = true; - w.active = true; - }); + rightElement.setVisible(false); + rightElement.setActive(false); + leftElement.setVisible(true); + leftElement.setActive(true); if (handleOptional) { missingElement.visible = false; } } else { - leftElement.visitWidgets(w -> { - w.visible = false; - w.active = false; - }); - rightElement.visitWidgets(w -> { - w.visible = true; - w.active = true; - }); + leftElement.setVisible(false); + leftElement.setActive(false); + rightElement.setVisible(true); + rightElement.setActive(true); if (handleOptional) { missingElement.visible = false; } @@ -422,11 +404,15 @@ public static LayoutFactory> either(LayoutFactory left, L }).width(Button.DEFAULT_HEIGHT).tooltip(Tooltip.create(Component.translatable("codecextras.config.either.switch"))).build(); layout.addChild(switchButton, LayoutSettings.defaults().alignVerticallyMiddle()); layout.addChild(frame, LayoutSettings.defaults().alignVerticallyMiddle()); - return layout; + return VisibilityWrapperElement.ofDirect(layout); }; } public static LayoutFactory unit() { + return unit(Component.translatable("codecextras.config.unit")); + } + + public static LayoutFactory unit(Component text) { return (parent, width, context, original, update, creationInfo, handleOptional) -> { if (original.isJsonNull()) { original = new JsonObject(); @@ -449,14 +435,14 @@ public static LayoutFactory unit() { w.setMessage(creationInfo.componentInfo().title()); return w; } else { - var button = Button.builder(Component.translatable("codecextras.config.unit"), b -> { + var button = Button.builder(text, b -> { }) .width(width) .build(); var tooltip = Tooltip.create(creationInfo.componentInfo().description()); button.setTooltip(tooltip); button.active = false; - return button; + return VisibilityWrapperElement.ofInactive(button); } }; } @@ -491,7 +477,7 @@ public static LayoutFactory bool() { .width(width) .build(); button.active = false; - return button; + return VisibilityWrapperElement.ofInactive(button); }; } } diff --git a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json index 5bbedc3..7301cf7 100644 --- a/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json +++ b/src/minecraft/resources/assets/codecextras_minecraft/lang/en_us.json @@ -24,5 +24,6 @@ "codecextras.config.json.raw": "Raw JSON", "codecextras.config.json.type": "Change type", "codecextras.config.issue": "Issues with configuration!", - "codecextras.config.issue.message": "Found issues:\n%s\nDo you wish to continue? Data may be lost if you do." + "codecextras.config.issue.message": "Found issues:\n%s\nDo you wish to continue? Data may be lost if you do.", + "codecextras.config.unit.empty": "Empty" } diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 5aa7144..6019151 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -20,6 +20,7 @@ import net.minecraft.references.Items; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Rarity; public record TestConfig( @@ -29,7 +30,7 @@ public record TestConfig( int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, Map unbounded, Either either, Map dispatchedMap, - DataComponentPatch patch + DataComponentPatch patch, ItemStack itemStack ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -96,6 +97,7 @@ public String key() { var either = builder.addOptional("either", Structure.either(Structure.STRING, MinecraftStructures.RGB_COLOR), TestConfig::either, () -> Either.right(0x00FFAA)); var dispatchedMap = builder.addOptional("dispatchedMap", Structure.STRING.dispatchedMap(DISPATCHES::keySet, k -> DataResult.success(DISPATCHES.get(k))), TestConfig::dispatchedMap, Map::of); var patch = builder.addOptional("patch", MinecraftStructures.DATA_COMPONENT_PATCH, TestConfig::patch, () -> DataComponentPatch.EMPTY); + var itemStack = builder.addOptional("itemStack", MinecraftStructures.OPTIONAL_ITEM_STACK, TestConfig::itemStack, () -> ItemStack.EMPTY); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), @@ -103,7 +105,7 @@ public String key() { intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), unbounded.apply(container), either.apply(container), dispatchedMap.apply(container), - patch.apply(container) + patch.apply(container), itemStack.apply(container) ); }); From 3b169d67808c37df9bf86d2bf5c45a6ce659d3c0 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 9 Sep 2024 22:11:49 -0500 Subject: [PATCH 59/76] Apply formatting --- .../structured/MinecraftStructures.java | 13 +++++---- .../config/ConfigScreenInterpreter.java | 27 +++++++++---------- .../minecraft/structured/config/Widgets.java | 13 +++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index fb1645c..042d57f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -13,6 +13,12 @@ import dev.lukebemish.codecextras.types.Identity; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; @@ -34,13 +40,6 @@ import net.minecraft.world.item.Items; import org.jspecify.annotations.Nullable; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - public final class MinecraftStructures { private MinecraftStructures() { } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 6e7aabe..28cb9cd 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -36,6 +36,19 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; @@ -57,20 +70,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - public class ConfigScreenInterpreter extends KeyStoringInterpreter { private static final Logger LOGGER = LogUtils.getLogger(); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 1464285..08f714d 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -10,6 +10,12 @@ import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.structured.Range; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; @@ -26,13 +32,6 @@ import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - public final class Widgets { private static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation TRANSPARENT = ResourceLocation.fromNamespaceAndPath("codecextras_minecraft", "widget/transparent"); From 3acb786a414122a86fd6060144e8285555a26121 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 13:06:21 -0500 Subject: [PATCH 60/76] Add remaining features --- .../codecextras/structured/Annotation.java | 9 +++ .../codecextras/structured/Interpreter.java | 6 +- .../structured/MapCodecInterpreter.java | 29 +++++++++ .../codecextras/structured/Structure.java | 2 +- .../structured/StructuredMapCodec.java | 9 +-- .../schema/JsonSchemaInterpreter.java | 48 +++++++++++++- .../structured/MinecraftStructures.java | 5 +- .../config/ConfigScreenInterpreter.java | 64 ++++++++++--------- .../neoforge/CodecExtrasNeoforge.java | 2 +- .../test/structured/TestStructured.java | 10 +-- 10 files changed, 138 insertions(+), 46 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java index 1ea7a40..b82b9d4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.structured; +import com.mojang.datafixers.util.Unit; import dev.lukebemish.codecextras.types.Identity; import java.util.Optional; @@ -20,6 +21,14 @@ public class Annotation { * A human-readable description for a part of a structure; if missing, falls back to {@link #COMMENT}. */ public static final Key DESCRIPTION = Key.create("description"); + /** + * A regex pattern that a string field or key in a structure should match. + */ + public static final Key PATTERN = Key.create("pattern"); + /** + * If present, the attached structure should be lenient as an optional field -- that is, if present but erroring, it is considered to be missing + */ + public static final Key LENIENT = Key.create("lenient"); /** * Retrieve an annotation value, if present, from a set of annotations. diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index d560db4..f29b1a8 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -38,14 +38,14 @@ public interface KeyConsumer { App convert(App input); } - default DataResult> bounded(App input, Supplier> values) { - Function> verifier = a -> { + default DataResult> bounded(Structure input, Supplier> values) { + Function> validator = a -> { if (values.get().contains(a)) { return DataResult.success(a); } return DataResult.error(() -> "Invalid value: " + a); }; - return flatXmap(input, verifier, verifier); + return input.interpret(this).flatMap(a -> flatXmap(a, validator, validator)); } DataResult>> unboundedMap(App key, App value); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index c167e73..224178b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -5,7 +5,10 @@ import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; import com.mojang.serialization.codecs.KeyDispatchCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; @@ -64,6 +67,32 @@ public DataResult> annotate(Structure original, Keys m = unbox(input); }; mapCodec.m = Annotation.get(annotations, Annotation.COMMENT).map(comment -> CommentMapCodec.of(mapCodec.m, comment)).orElse(mapCodec.m); + mapCodec.m = Annotation.get(annotations, Annotation.LENIENT).>map(ignored -> { + var outer = mapCodec.m; + return new MapCodec<>() { + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + return outer.encode(input, ops, prefix); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + var result = outer.decode(ops, input); + if (result.error().isPresent()) { + var fromEmpty = ops.getMap(ops.emptyMap()).flatMap(map -> outer.decode(ops, map)); + if (fromEmpty.error().isEmpty()) { + return fromEmpty; + } + } + return result; + } + + @Override + public Stream keys(DynamicOps ops) { + return outer.keys(ops); + } + }; + }).orElse(mapCodec.m); return new Holder<>(mapCodec.m); }); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 464c468..7fe0e57 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -385,7 +385,7 @@ final class BoundedStructure implements Structure { @Override public DataResult> interpret(Interpreter interpreter) { - return outer.interpret(interpreter).flatMap(app -> interpreter.bounded(app, totalAvailable)); + return interpreter.bounded(outer, totalAvailable); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 0b1c520..290c1aa 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -46,15 +46,16 @@ public static DataResult> of(List fieldCodec = unboxer.unbox(result.result().orElseThrow()); + boolean lenient = Annotation.get(field.structure().annotations(), Annotation.LENIENT).isPresent(); MapCodec fieldMapCodec = Annotation.get(field.structure().annotations(), Annotation.COMMENT) - .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field), comment)) - .orElseGet(() -> makeFieldCodec(fieldCodec, field)); + .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field, lenient), comment)) + .orElseGet(() -> makeFieldCodec(fieldCodec, field, lenient)); mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); return null; } - private static MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field) { - return field.missingBehavior().map(behavior -> fieldCodec.optionalFieldOf(field.name()).xmap( + private static MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field, boolean lenient) { + return field.missingBehavior().map(behavior -> (lenient ? fieldCodec.optionalFieldOf(field.name()) : fieldCodec.lenientOptionalFieldOf(field.name())).xmap( optional -> optional.orElseGet(behavior.missing()), value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() )).orElseGet(() -> fieldCodec.fieldOf(field.name())); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index b9041a6..12e2cb1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -3,6 +3,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; @@ -50,6 +51,17 @@ public JsonSchemaInterpreter( .add(Interpreter.FLOAT, new Holder<>(NUMBER.get())) .add(Interpreter.DOUBLE, new Holder<>(NUMBER.get())) .add(Interpreter.STRING, new Holder<>(STRING.get())) + .add(Interpreter.EMPTY_MAP, new Holder<>(() -> { + var json = OBJECT.get(); + json.add("additionalProperties", new JsonPrimitive(false)); + return json; + })) + .add(Interpreter.EMPTY_LIST, new Holder<>(() -> { + var json = ARRAY.get(); + json.add("prefixItems", new JsonArray()); + json.add("items", new JsonPrimitive(false)); + return json; + })) .build() ), parametricKeys.join(Keys2., K1, K1>builder() .add(Interpreter.STRING_REPRESENTABLE, new ParametricKeyedValue<>() { @@ -175,6 +187,9 @@ public DataResult> annotate(Structure input, Keys(definitions(result.result().orElseThrow())); } + Annotation.get(annotations, Annotation.PATTERN).ifPresent(pattern -> { + schema.addProperty("pattern", pattern); + }); Annotation.get(annotations, Annotation.DESCRIPTION).or(() -> Annotation.get(annotations, Annotation.COMMENT)).ifPresent(comment -> { schema.addProperty("description", comment); }); @@ -233,11 +248,38 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public DataResult> bounded(Structure input, Supplier> values) { + return codecInterpreter.interpret(input).flatMap(codec -> { + var types = new JsonArray(); + for (var value : values.get()) { + var result = codec.encodeStart(ops, value); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + types.add(result.result().orElseThrow()); + } + return input.interpret(this).flatMap(outer -> { + var schema = schemaValue(outer); + schema.add("enum", types); + return DataResult.success(new Holder<>(schema, definitions(outer))); + }); + }); + } + @Override public DataResult>> unboundedMap(App key, App value) { var schema = OBJECT.get(); var definitions = new HashMap>(); - schema.add("additionalProperties", schemaValue(value)); + var keyJson = schemaValue(key); + if (keyJson.has("pattern") && keyJson.get("pattern").isJsonPrimitive()) { + // if the key has a pattern, we use "patternProperties" + var patternProperties = new JsonObject(); + patternProperties.add(keyJson.get("pattern").getAsString(), schemaValue(value)); + schema.add("patternProperties", patternProperties); + } else { + schema.add("additionalProperties", schemaValue(value)); + } definitions.putAll(definitions(value)); definitions.putAll(definitions(key)); return DataResult.success(new Holder<>(schema, definitions)); @@ -343,6 +385,10 @@ public Holder(JsonObject object) { this(object, Map.of()); } + public Holder(Supplier objectCreator) { + this(objectCreator.get()); + } + public static final class Mu implements K1 { private Mu() {} } static Holder unbox(App box) { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 042d57f..40eeab1 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -5,6 +5,7 @@ import com.mojang.datafixers.util.Unit; import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.stream.structured.StreamCodecInterpreter; +import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Structure; @@ -51,6 +52,7 @@ private MinecraftStructures() { .add(StreamCodecInterpreter.FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ResourceLocation.STREAM_CODEC.cast()))) .build(), Structure.STRING.flatXmap(ResourceLocation::read, rl -> DataResult.success(rl.toString())) + .annotate(Annotation.PATTERN, "^([a-z0-9_.-]+:)?[a-z0-9_/.-]+$") ); public static final Structure ARGB_COLOR = Structure.keyed( @@ -121,6 +123,7 @@ private MinecraftStructures() { return DataResult.success((key.removes() ? "!" : "") + rl); }) .bounded(MinecraftStructures::possibleDataComponentPatchKeys) + .annotate(Annotation.PATTERN, "^[!]?([a-z0-9_.-]+:)?[a-z0-9_/.-]+$") ); @SuppressWarnings({"rawtypes", "unchecked"}) @@ -335,6 +338,7 @@ public static Structure> tagKey(ResourceKey> Structure.STRING.comapFlatMap(string -> string.startsWith("#") ? ResourceLocation.read(string.substring(1)).map(resourceLocation -> TagKey.create(registry, resourceLocation)) : DataResult.>error(() -> "Not a tag id"), tagKey -> "#" + tagKey.location()) : RESOURCE_LOCATION.xmap(resourceLocation -> TagKey.create(registry, resourceLocation), TagKey::location)) .xmap(MinecraftKeys.TagKeyHolder::new, MinecraftKeys.TagKeyHolder::value) + .annotate(Annotation.PATTERN, "^"+(hashPrefix ? "#" : "")+"([a-z0-9_.-]+:)?[a-z0-9_/.-]+$") ).xmap(MinecraftKeys.TagKeyHolder::value, MinecraftKeys.TagKeyHolder::new); } @@ -395,5 +399,4 @@ private static DataResult> dataComponentPatchValueCodec(MinecraftKe } return dataComponentTypeStructure(key.type()); } - } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 28cb9cd..8be4622 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -755,38 +755,40 @@ public DataResult>> either(App DataResult> bounded(App input, Supplier> values) { - var codec = codecInterpreter.bounded(new CodecInterpreter.Holder<>(ConfigScreenEntry.unbox(input).entryCreationInfo().codec()), values).map(CodecInterpreter::unbox); - if (codec.isError()) { - return DataResult.error(codec.error().orElseThrow().messageSupplier()); - } - return DataResult.success(ConfigScreenEntry.single( - (parent, width, context, original, update, creationInfo, handleOptional) -> { - List knownValues = new ArrayList<>(); - Map stringValues = new HashMap<>(); - Map inverse = new HashMap<>(); - for (var value : values.get()) { - var encoded = codec.getOrThrow().encodeStart(context.ops(), value); - if (encoded.error().isPresent()) { - LOGGER.error("Error encoding value `{}`: {}", value, encoded.error().get()); - continue; - } - String string; - var result = encoded.getOrThrow(); - if (result.isJsonPrimitive()) { - string = result.getAsString(); - } else { - string = result.toString(); + public DataResult> bounded(Structure inputSupplier, Supplier> values) { + return interpret(inputSupplier).flatMap(input -> { + var codec = codecInterpreter.bounded(inputSupplier, values).map(CodecInterpreter::unbox); + if (codec.isError()) { + return DataResult.error(codec.error().orElseThrow().messageSupplier()); + } + return DataResult.success(ConfigScreenEntry.single( + (parent, width, context, original, update, creationInfo, handleOptional) -> { + List knownValues = new ArrayList<>(); + Map stringValues = new HashMap<>(); + Map inverse = new HashMap<>(); + for (var value : values.get()) { + var encoded = codec.getOrThrow().encodeStart(context.ops(), value); + if (encoded.error().isPresent()) { + LOGGER.error("Error encoding value `{}`: {}", value, encoded.error().get()); + continue; + } + String string; + var result = encoded.getOrThrow(); + if (result.isJsonPrimitive()) { + string = result.getAsString(); + } else { + string = result.toString(); + } + knownValues.add(value); + stringValues.put(value, string); + inverse.put(string, value); } - knownValues.add(value); - stringValues.put(value, string); - inverse.put(string, value); - } - var wrapped = Widgets.pickWidget(new StringRepresentation<>(() -> knownValues, stringValues::get, inverse::get, false)); - return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); - }, - ConfigScreenEntry.unbox(input).entryCreationInfo().withCodec(codec.getOrThrow()) - )); + var wrapped = Widgets.pickWidget(new StringRepresentation<>(() -> knownValues, stringValues::get, inverse::get, false)); + return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); + }, + ConfigScreenEntry.unbox(input).entryCreationInfo().withCodec(codec.getOrThrow()) + )); + }); } @Override diff --git a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java index f03fe3b..64e9ab7 100644 --- a/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java +++ b/src/minecraftNeoforge/java/dev/lukebemish/codecextras/minecraft/neoforge/CodecExtrasNeoforge.java @@ -14,7 +14,7 @@ public final class CodecExtrasNeoforge { private static final Registry>> DATA_COMPONENT_STRUCTURE_REGISTRY = new RegistryBuilder<>(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES).create(); - CodecExtrasNeoforge(IEventBus modBus) { + public CodecExtrasNeoforge(IEventBus modBus) { modBus.addListener(NewRegistryEvent.class, event -> { event.register(DATA_COMPONENT_STRUCTURE_REGISTRY); }); diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index b66913e..31c7ab8 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -7,18 +7,19 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.schema.JsonSchemaInterpreter; import dev.lukebemish.codecextras.test.CodecAssertions; +import dev.lukebemish.codecextras.types.Identity; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; class TestStructured { - private record TestRecord(int a, String b, List c, Optional d, String e) { + private record TestRecord(int a, String b, List c, Optional d, Identity e) { private static final Structure STRUCTURE = Structure.record(i -> { var a = i.add("a", Structure.INT.annotate(Annotation.COMMENT, "Field A"), TestRecord::a); var b = i.add(Structure.STRING.fieldOf("b"), TestRecord::b); var c = i.add("c", Structure.BOOL.listOf(), TestRecord::c); var d = i.add(Structure.STRING.optionalFieldOf("d"), TestRecord::d); - var e = i.addOptional("e", Structure.STRING, TestRecord::e, () -> "default"); + var e = i.addOptional("e", Structure.STRING.annotate(Annotation.PATTERN, "^[a-z]+$").xmap(Identity::new, Identity::value), TestRecord::e, () -> new Identity<>("default")); return container -> new TestRecord(a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container)); }); @@ -54,13 +55,14 @@ private record TestRecord(int a, String b, List c, Optional d, }, "e": { "type": "string", - "default": "default" + "default": "default", + "pattern": "^[a-z]+$" } }, "required": ["a", "b", "c"] }"""; - private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty(), "default"); + private final TestRecord record = new TestRecord(1, "test", List.of(true, false, true), Optional.empty(), new Identity<>("default")); @Test void testDecodingCodec() { From e6832a795e07be9f50b9f3029c4fde19207bfaa2 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 13:26:49 -0500 Subject: [PATCH 61/76] Slight fixes for lenient fields and bounded values --- .../repair/FillMissingMapCodec.java | 93 ++++++++++++++----- .../structured/StructuredMapCodec.java | 3 +- .../schema/JsonSchemaInterpreter.java | 2 +- .../test/structured/TestDispatch.java | 3 +- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java index 7800c1f..f18e063 100644 --- a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java @@ -1,49 +1,66 @@ package dev.lukebemish.codecextras.repair; -import com.mojang.serialization.*; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.companion.AccompaniedOps; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; public final class FillMissingMapCodec extends MapCodec { private final MapCodec delegate; private final MapRepair fallback; + private final boolean lenient; - private FillMissingMapCodec(MapCodec delegate, MapRepair fallback) { + private FillMissingMapCodec(MapCodec delegate, MapRepair fallback, boolean lenient) { this.delegate = delegate; this.fallback = fallback; + this.lenient = lenient; } - public static MapCodec of(MapCodec codec, MapRepair fallback) { - return new FillMissingMapCodec<>(codec, fallback); + public static MapCodec fieldOf(MapCodec codec, MapRepair fallback, boolean lenient) { + return new FillMissingMapCodec<>(codec, fallback, lenient); + } + + public static MapCodec fieldOf(MapCodec codec, MapRepair fallback) { + return fieldOf(codec, fallback, true); + } + + public static MapCodec strictFieldOf(MapCodec codec, MapRepair fallback) { + return fieldOf(codec, fallback, false); } public static MapCodec fieldOf(Codec codec, String field, Repair fallback) { - return of(codec.fieldOf(field), new MapRepair<>() { - @Override - public A repair(DynamicOps ops, MapLike flawed) { - T value = flawed.get(field); - if (value == null) { - value = ops.empty(); - } - if (ops instanceof AccompaniedOps accompaniedOps) { - Optional> fillMissingLogOps = accompaniedOps.getCompanion(FillMissingLogOps.TOKEN); - if (fillMissingLogOps.isPresent()) { - fillMissingLogOps.get().logMissingField(field, value); - } - } - return fallback.repair(ops, value); - } - }); + return fieldOf(codec, field, fallback, true); + } + + public static MapCodec strictFieldOf(Codec codec, String field, Repair fallback) { + return fieldOf(codec, field, fallback, false); + } + + public static MapCodec fieldOf(Codec codec, String field, Repair fallback, boolean lenient) { + return fieldOf(codec.fieldOf(field), fallback.fieldOf(field), lenient); } public static MapCodec fieldOf(Codec codec, String field, A fallback) { + return fieldOf(codec, field, fallback, true); + } + + public static MapCodec strictFieldOf(Codec codec, String field, A fallback) { + return fieldOf(codec, field, fallback, false); + } + + public static MapCodec fieldOf(Codec codec, String field, A fallback, boolean lenient) { return fieldOf(codec, field, new Repair<>() { @Override public A repair(DynamicOps ops, T flawed) { return fallback; } - }); + }, lenient); } @Override @@ -53,8 +70,12 @@ public Stream keys(DynamicOps ops) { @Override public DataResult decode(DynamicOps ops, MapLike input) { + boolean allEmpty = delegate.keys(ops).allMatch(key -> input.get(key) == null); + if (allEmpty) { + return DataResult.success(fallback.repair(ops, input)); + } var original = delegate.decode(ops, input); - if (original.error().isPresent()) { + if (lenient && original.error().isPresent()) { return DataResult.success(fallback.repair(ops, input)); } return original; @@ -71,6 +92,34 @@ public interface MapRepair { public interface Repair { A repair(DynamicOps ops, T flawed); + + default MapRepair fieldOf(String field) { + return new MapRepair<>() { + @Override + public A repair(DynamicOps ops, MapLike flawed) { + T value = flawed.get(field); + if (value == null) { + value = ops.empty(); + } + if (ops instanceof AccompaniedOps accompaniedOps) { + Optional> fillMissingLogOps = accompaniedOps.getCompanion(FillMissingLogOps.TOKEN); + if (fillMissingLogOps.isPresent()) { + fillMissingLogOps.get().logMissingField(field, value); + } + } + return Repair.this.repair(ops, value); + } + }; + } + } + + public static Repair lazyRepair(Supplier supplier) { + return new Repair() { + @Override + public A repair(DynamicOps ops, T flawed) { + return supplier.get(); + } + }; } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 290c1aa..b4f9a74 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -9,6 +9,7 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentMapCodec; +import dev.lukebemish.codecextras.repair.FillMissingMapCodec; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -55,7 +56,7 @@ public static DataResult> of(List MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field, boolean lenient) { - return field.missingBehavior().map(behavior -> (lenient ? fieldCodec.optionalFieldOf(field.name()) : fieldCodec.lenientOptionalFieldOf(field.name())).xmap( + return field.missingBehavior().map(behavior -> FillMissingMapCodec.fieldOf(fieldCodec.optionalFieldOf(field.name()), FillMissingMapCodec.lazyRepair(Optional::empty).fieldOf(field.name()), lenient).xmap( optional -> optional.orElseGet(behavior.missing()), value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() )).orElseGet(() -> fieldCodec.fieldOf(field.name())); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 12e2cb1..ee5098c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -260,7 +260,7 @@ public DataResult> bounded(Structure input, Supplier { - var schema = schemaValue(outer); + var schema = copy(schemaValue(outer)); schema.add("enum", types); return DataResult.success(new Holder<>(schema, definitions(outer))); }); diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java index 1fae90b..e411474 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java @@ -84,7 +84,8 @@ public String key() { "dispatches": { "properties": { "type": { - "type": "string" + "type": "string", + "enum":["abc","xyz"] } }, "required": [ From 12538a9b190a6b758b118c30065fe72e2061af43 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 14:06:01 -0500 Subject: [PATCH 62/76] Hopefully get accompanied ops working with registry ops --- .../codecextras/jmh/LargeRecordsDecode.java | 10 +-- .../codecextras/jmh/LargeRecordsEncode.java | 10 +-- .../codecextras/jmh/TestRecord.java | 4 +- .../comments/CommentFirstListCodec.java | 7 +- .../codecextras/comments/CommentMapCodec.java | 6 +- .../codecextras/companion/AccompaniedOps.java | 10 +++ .../AlternateCompanionRetriever.java | 10 +++ .../codecextras/companion/DelegatingOps.java | 70 ++++++++++++++----- .../CurriedRecordCodecBuilder.java} | 31 ++++---- .../record/KeyedRecordCodecBuilder.java | 5 +- .../codecextras/repair/FillMissingLogOps.java | 3 +- .../repair/FillMissingMapCodec.java | 9 ++- .../RegistryOpsCompanionRetriever.java | 44 ++++++++++++ .../minecraft/companion/package-info.java | 4 ++ ...edRecords.java => TestCurriedRecords.java} | 6 +- 15 files changed, 163 insertions(+), 66 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/companion/AlternateCompanionRetriever.java rename src/main/java/dev/lukebemish/codecextras/{ExtendedRecordCodecBuilder.java => record/CurriedRecordCodecBuilder.java} (76%) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/package-info.java rename src/test/java/dev/lukebemish/codecextras/test/record/{TestExtendedRecords.java => TestCurriedRecords.java} (84%) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java index f045a99..56cedc6 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java @@ -39,9 +39,9 @@ public void keyedRecordCodecBuilder(Blackhole blackhole) { } @Benchmark - public void extendedRecordCodecBuilder(Blackhole blackhole) { + public void curriedRecordCodecBuilder(Blackhole blackhole) { JsonElement json = TestRecord.makeData(counter++); - var result = TestRecord.ERCB.decode(JsonOps.INSTANCE, json); + var result = TestRecord.CRCB.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } @@ -65,7 +65,7 @@ public void setup() { json = TestRecord.makeData(0); TestRecord.RCB.decode(JsonOps.INSTANCE, json); TestRecord.KRCB.decode(JsonOps.INSTANCE, json); - TestRecord.ERCB.decode(JsonOps.INSTANCE, json); + TestRecord.CRCB.decode(JsonOps.INSTANCE, json); } @Benchmark @@ -81,8 +81,8 @@ public void keyedRecordCodecBuilder(Blackhole blackhole) { } @Benchmark - public void extendedRecordCodecBuilder(Blackhole blackhole) { - var result = TestRecord.ERCB.decode(JsonOps.INSTANCE, json); + public void curriedRecordCodecBuilder(Blackhole blackhole) { + var result = TestRecord.CRCB.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java index 618292c..0113c05 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java @@ -38,9 +38,9 @@ public void keyedRecordCodecBuilder(Blackhole blackhole) { } @Benchmark - public void extendedRecordCodecBuilder(Blackhole blackhole) { + public void curriedRecordCodecBuilder(Blackhole blackhole) { TestRecord record = TestRecord.makeRecord(counter++); - var result = TestRecord.ERCB.encodeStart(JsonOps.INSTANCE, record); + var result = TestRecord.CRCB.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } @@ -64,7 +64,7 @@ public void setup() { record = TestRecord.makeRecord(0); TestRecord.RCB.encodeStart(JsonOps.INSTANCE, record); TestRecord.KRCB.encodeStart(JsonOps.INSTANCE, record); - TestRecord.ERCB.encodeStart(JsonOps.INSTANCE, record); + TestRecord.CRCB.encodeStart(JsonOps.INSTANCE, record); } @Benchmark @@ -80,8 +80,8 @@ public void keyedRecordCodecBuilder(Blackhole blackhole) { } @Benchmark - public void extendedRecordCodecBuilder(Blackhole blackhole) { - var result = TestRecord.ERCB.encodeStart(JsonOps.INSTANCE, record); + public void curriedRecordCodecBuilder(Blackhole blackhole) { + var result = TestRecord.CRCB.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java index ebf1ac8..b932aba 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -3,7 +3,7 @@ import com.google.gson.JsonObject; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.lukebemish.codecextras.ExtendedRecordCodecBuilder; +import dev.lukebemish.codecextras.record.CurriedRecordCodecBuilder; import dev.lukebemish.codecextras.record.KeyedRecordCodecBuilder; import dev.lukebemish.codecextras.record.MethodHandleRecordCodecBuilder; import java.lang.invoke.MethodHandles; @@ -33,7 +33,7 @@ record TestRecord( Codec.INT.fieldOf("p").forGetter(TestRecord::p) ).apply(i, TestRecord::new)); - public static final Codec ERCB = ExtendedRecordCodecBuilder + public static final Codec CRCB = CurriedRecordCodecBuilder .start(Codec.INT.fieldOf("a"), TestRecord::a) .field(Codec.INT.fieldOf("b"), TestRecord::b) .field(Codec.INT.fieldOf("c"), TestRecord::c) diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentFirstListCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentFirstListCodec.java index 7633d3d..ed76d9e 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentFirstListCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentFirstListCodec.java @@ -35,12 +35,7 @@ public DataResult, T>> decode(DynamicOps ops, T input) { @Override public DataResult encode(List input, DynamicOps ops, T prefix) { final ListBuilder builder = ops.listBuilder(); - DynamicOps rest; - if (ops instanceof AccompaniedOps) { - rest = DelegatingOps.without(CommentOps.TOKEN, ops); - } else { - rest = ops; - } + DynamicOps rest = AccompaniedOps.find(ops).map(o -> DelegatingOps.without(CommentOps.TOKEN, ops)).orElse(ops); boolean isFirst = true; for (A a : input) { if (isFirst) { diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java index e801c2d..cc24d03 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java @@ -96,7 +96,7 @@ public RecordBuilder mapError(UnaryOperator onError) { @Override public DataResult build(T prefix) { DataResult built = builder.build(prefix); - if (this.ops() instanceof AccompaniedOps accompaniedOps) { + return AccompaniedOps.find(this.ops()).map(accompaniedOps -> { Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); if (commentOps.isPresent()) { return built.flatMap(t -> @@ -104,8 +104,8 @@ public DataResult build(T prefix) { ops.createString(e.getKey()), e -> ops.createString(e.getValue())))) ); } - } - return built; + return built; + }).orElse(built); } }; } diff --git a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java index ba28aab..dff07e9 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java @@ -7,4 +7,14 @@ public interface AccompaniedOps extends DynamicOps { default > Optional getCompanion(O token) { return Optional.empty(); } + + static Optional> find(DynamicOps ops) { + for (var retriever : DelegatingOps.ALTERNATE_COMPANION_RETRIEVERS) { + var companion = retriever.locateCompanionDelegate(ops); + if (companion.isPresent()) { + return companion; + } + } + return Optional.empty(); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/companion/AlternateCompanionRetriever.java b/src/main/java/dev/lukebemish/codecextras/companion/AlternateCompanionRetriever.java new file mode 100644 index 0000000..8db9223 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/companion/AlternateCompanionRetriever.java @@ -0,0 +1,10 @@ +package dev.lukebemish.codecextras.companion; + +import com.mojang.serialization.DynamicOps; +import java.util.Optional; + +public interface AlternateCompanionRetriever { + Optional> locateCompanionDelegate(DynamicOps ops); + + DynamicOps delegate(DynamicOps ops, AccompaniedOps delegate); +} diff --git a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java index e34c952..ca095e9 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java @@ -9,10 +9,12 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.ServiceLoader; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -26,31 +28,67 @@ public abstract class DelegatingOps implements AccompaniedOps { @Nullable protected final AccompaniedOps accompanied; public DelegatingOps(DynamicOps delegate) { this.delegate = delegate; - if (delegate instanceof AccompaniedOps accompaniedOps) { - this.accompanied = accompaniedOps; + this.accompanied = AccompaniedOps.find(delegate).orElse(null); + } + + public static DynamicOps of(Q token, Companion companion, DynamicOps delegate) { + var possibleParent = retrieveMapOps(delegate); + if (possibleParent != null) { + if (possibleParent.getSecond() instanceof MapDelegatingOps mapOps) { + Map>> map = new HashMap<>(mapOps.companions); + map.put(token, Optional.of(companion)); + return possibleParent.getFirst().delegate(delegate, new MapDelegatingOps<>(delegate, map)); + } + return possibleParent.getFirst().delegate(delegate, new MapDelegatingOps<>(delegate, Map.of(token, Optional.of(companion)))); } else { - this.accompanied = null; + return new MapDelegatingOps<>(delegate, Map.of(token, Optional.of(companion))); } } - public static AccompaniedOps of(Q token, Companion companion, DynamicOps delegate) { - if (delegate instanceof MapDelegatingOps mapOps) { - Map>> map = new HashMap<>(mapOps.companions); - map.put(token, Optional.of(companion)); - return new MapDelegatingOps<>(delegate, map); + public static DynamicOps without(Q token, DynamicOps delegate) { + var possibleParent = retrieveMapOps(delegate); + if (possibleParent != null) { + if (possibleParent.getSecond() instanceof MapDelegatingOps mapOps) { + Map>> map = new HashMap<>(mapOps.companions); + map.put(token, Optional.empty()); + return possibleParent.getFirst().delegate(delegate, new MapDelegatingOps<>(delegate, map)); + } + return possibleParent.getFirst().delegate(delegate, new MapDelegatingOps<>(delegate, Map.of(token, Optional.empty()))); } else { - return new MapDelegatingOps<>(delegate, Map.of(token, Optional.of(companion))); + return new MapDelegatingOps<>(delegate, Map.of(token, Optional.empty())); } } - public static AccompaniedOps without(Q token, DynamicOps delegate) { - if (delegate instanceof MapDelegatingOps mapOps) { - Map>> map = new HashMap<>(mapOps.companions); - map.put(token, Optional.empty()); - return new MapDelegatingOps<>(delegate, map); - } else { - return new MapDelegatingOps<>(delegate, Map.of(token, Optional.empty())); + static final List ALTERNATE_COMPANION_RETRIEVERS; + + static { + List retrievers = new ArrayList<>(); + retrievers.add(new AlternateCompanionRetriever() { + @Override + public Optional> locateCompanionDelegate(DynamicOps ops) { + if (ops instanceof MapDelegatingOps mapOps) { + return Optional.of(mapOps); + } + return Optional.empty(); + } + + @Override + public AccompaniedOps delegate(DynamicOps ops, AccompaniedOps delegate) { + return delegate; + } + }); + retrievers.addAll(ServiceLoader.load(AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); + ALTERNATE_COMPANION_RETRIEVERS = List.copyOf(retrievers); + } + + private static @Nullable Pair> retrieveMapOps(DynamicOps ops) { + for (AlternateCompanionRetriever retriever : ALTERNATE_COMPANION_RETRIEVERS) { + Optional> companionDelegate = retriever.locateCompanionDelegate(ops); + if (companionDelegate.isPresent()) { + return Pair.of(retriever, companionDelegate.get()); + } } + return null; } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/ExtendedRecordCodecBuilder.java b/src/main/java/dev/lukebemish/codecextras/record/CurriedRecordCodecBuilder.java similarity index 76% rename from src/main/java/dev/lukebemish/codecextras/ExtendedRecordCodecBuilder.java rename to src/main/java/dev/lukebemish/codecextras/record/CurriedRecordCodecBuilder.java index 1a30500..256e56e 100644 --- a/src/main/java/dev/lukebemish/codecextras/ExtendedRecordCodecBuilder.java +++ b/src/main/java/dev/lukebemish/codecextras/record/CurriedRecordCodecBuilder.java @@ -1,4 +1,4 @@ -package dev.lukebemish.codecextras; +package dev.lukebemish.codecextras.record; import com.mojang.serialization.*; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -7,34 +7,33 @@ /** * An equivalent to {@link RecordCodecBuilder} that allows for any number of fields. - * Note: this will be moved to {@link dev.lukebemish.codecextras.record} and potentially renamed in a future version. * @param the type of the object being encoded/decoded * @param the type of the highest level field * @param the type of the final builder function used during decoding */ -public abstract sealed class ExtendedRecordCodecBuilder { +public abstract sealed class CurriedRecordCodecBuilder { /** - * Creates a new {@link ExtendedRecordCodecBuilder} with the given codec and getter as the bottom-most field. + * Creates a new {@link CurriedRecordCodecBuilder} with the given codec and getter as the bottom-most field. * @param codec the codec for the bottom-most field * @param getter the getter for the bottom-most field - * @return a new {@link ExtendedRecordCodecBuilder} + * @return a new {@link CurriedRecordCodecBuilder} * @param the type of the object being encoded/decoded * @param the type of the bottom-most field */ - public static ExtendedRecordCodecBuilder> start(MapCodec codec, Function getter) { + public static CurriedRecordCodecBuilder> start(MapCodec codec, Function getter) { return new Endpoint<>(codec, getter); } /** - * Creates a new {@link ExtendedRecordCodecBuilder} with the given codec and getter as the next field above the + * Creates a new {@link CurriedRecordCodecBuilder} with the given codec and getter as the next field above the * current one. * @param codec the codec for the next field * @param getter the getter for the next field - * @return a new {@link ExtendedRecordCodecBuilder} + * @return a new {@link CurriedRecordCodecBuilder} * @param the type of the next field */ - public ExtendedRecordCodecBuilder> field(MapCodec codec, Function getter) { + public CurriedRecordCodecBuilder> field(MapCodec codec, Function getter) { return new Delegating<>(codec, getter, this); } @@ -74,7 +73,7 @@ public Stream keys(DynamicOps ops) { @Override public String toString() { - return ExtendedRecordCodecBuilder.this.toString(); + return CurriedRecordCodecBuilder.this.toString(); } }; } @@ -92,7 +91,7 @@ public sealed interface AppFunction {} protected final MapCodec codec; protected final Function getter; - private ExtendedRecordCodecBuilder(MapCodec codec, Function getter) { + private CurriedRecordCodecBuilder(MapCodec codec, Function getter) { this.codec = codec; this.getter = getter; } @@ -101,7 +100,7 @@ private ExtendedRecordCodecBuilder(MapCodec codec, Function getter) { protected abstract DataResult decodePartial(DynamicOps ops, MapLike input, B b); protected abstract Stream keysPartial(DynamicOps ops); - private static final class Endpoint> extends ExtendedRecordCodecBuilder { + private static final class Endpoint> extends CurriedRecordCodecBuilder { private Endpoint(MapCodec codec, Function getter) { super(codec, getter); } @@ -130,9 +129,9 @@ public String toString() { } } - private static final class Delegating> extends ExtendedRecordCodecBuilder { - private final ExtendedRecordCodecBuilder delegate; - private Delegating(MapCodec codec, Function getter, ExtendedRecordCodecBuilder delegate) { + private static final class Delegating> extends CurriedRecordCodecBuilder { + private final CurriedRecordCodecBuilder delegate; + private Delegating(MapCodec codec, Function getter, CurriedRecordCodecBuilder delegate) { super(codec, getter); this.delegate = delegate; } @@ -161,7 +160,7 @@ protected Stream keysPartial(DynamicOps ops) { @Override public String toString() { - return "ExtendedRecordCodec[" + codec + "] -> " + delegate.toString(); + return "CurriedRecordCodec[" + codec + "] -> " + delegate.toString(); } } } diff --git a/src/main/java/dev/lukebemish/codecextras/record/KeyedRecordCodecBuilder.java b/src/main/java/dev/lukebemish/codecextras/record/KeyedRecordCodecBuilder.java index a696aae..7e1676c 100644 --- a/src/main/java/dev/lukebemish/codecextras/record/KeyedRecordCodecBuilder.java +++ b/src/main/java/dev/lukebemish/codecextras/record/KeyedRecordCodecBuilder.java @@ -7,7 +7,6 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.lukebemish.codecextras.ExtendedRecordCodecBuilder; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -16,8 +15,8 @@ import org.jetbrains.annotations.ApiStatus; /** - * Similar to {@link ExtendedRecordCodecBuilder}, an alternative to {@link RecordCodecBuilder} that allows for any - * number of fields. Unlike {@link ExtendedRecordCodecBuilder}, this does not require massively curried lambdas and so + * Similar to {@link CurriedRecordCodecBuilder}, an alternative to {@link RecordCodecBuilder} that allows for any + * number of fields. Unlike {@link CurriedRecordCodecBuilder}, this does not require massively curried lambdas and so * is less likely to make IDEs cry, and may be slightly faster in some scenarios. */ @ApiStatus.Experimental diff --git a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingLogOps.java b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingLogOps.java index 38e0ebc..b502d8b 100644 --- a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingLogOps.java +++ b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingLogOps.java @@ -1,14 +1,13 @@ package dev.lukebemish.codecextras.repair; import com.mojang.serialization.DynamicOps; -import dev.lukebemish.codecextras.companion.AccompaniedOps; import dev.lukebemish.codecextras.companion.Companion; import dev.lukebemish.codecextras.companion.DelegatingOps; public interface FillMissingLogOps extends Companion { FillMissingLogOps.RepairLogOpsToken TOKEN = new RepairLogOpsToken(); - static AccompaniedOps of(FillMissingLogOps logOps, DynamicOps delegate) { + static DynamicOps of(FillMissingLogOps logOps, DynamicOps delegate) { return DelegatingOps.of(TOKEN, logOps, delegate); } diff --git a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java index f18e063..7a7c173 100644 --- a/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/repair/FillMissingMapCodec.java @@ -101,12 +101,11 @@ public A repair(DynamicOps ops, MapLike flawed) { if (value == null) { value = ops.empty(); } - if (ops instanceof AccompaniedOps accompaniedOps) { + T finalValue = value; + AccompaniedOps.find(ops).ifPresent(accompaniedOps -> { Optional> fillMissingLogOps = accompaniedOps.getCompanion(FillMissingLogOps.TOKEN); - if (fillMissingLogOps.isPresent()) { - fillMissingLogOps.get().logMissingField(field, value); - } - } + fillMissingLogOps.ifPresent(tFillMissingLogOps -> tFillMissingLogOps.logMissingField(field, finalValue)); + }); return Repair.this.repair(ops, value); } }; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java new file mode 100644 index 0000000..c06ec13 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java @@ -0,0 +1,44 @@ +package dev.lukebemish.codecextras.minecraft.companion; + +import com.google.auto.service.AutoService; +import com.mojang.serialization.DynamicOps; +import dev.lukebemish.codecextras.companion.AccompaniedOps; +import dev.lukebemish.codecextras.companion.AlternateCompanionRetriever; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Optional; +import net.minecraft.resources.DelegatingOps; +import net.minecraft.resources.RegistryOps; + +@AutoService(AlternateCompanionRetriever.class) +public class RegistryOpsCompanionRetriever implements AlternateCompanionRetriever { + static final MethodHandle DELEGATE_FIELD; + + static { + try { + var lookup = MethodHandles.privateLookupIn(DelegatingOps.class, MethodHandles.lookup()); + DELEGATE_FIELD = lookup.unreflectGetter(DelegatingOps.class.getDeclaredField("delegate")); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public Optional> locateCompanionDelegate(DynamicOps ops) { + if (ops instanceof RegistryOps registryOps) { + try { + return Optional.of((AccompaniedOps) DELEGATE_FIELD.invoke(registryOps)); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + return Optional.empty(); + } + + @Override + public DynamicOps delegate(DynamicOps ops, AccompaniedOps delegate) { + var registryOps = (RegistryOps) ops; + return registryOps.withParent(delegate); + } +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/package-info.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/package-info.java new file mode 100644 index 0000000..9fc339b --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.minecraft.companion; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/dev/lukebemish/codecextras/test/record/TestExtendedRecords.java b/src/test/java/dev/lukebemish/codecextras/test/record/TestCurriedRecords.java similarity index 84% rename from src/test/java/dev/lukebemish/codecextras/test/record/TestExtendedRecords.java rename to src/test/java/dev/lukebemish/codecextras/test/record/TestCurriedRecords.java index c634dda..275ad65 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/record/TestExtendedRecords.java +++ b/src/test/java/dev/lukebemish/codecextras/test/record/TestCurriedRecords.java @@ -2,13 +2,13 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; -import dev.lukebemish.codecextras.ExtendedRecordCodecBuilder; +import dev.lukebemish.codecextras.record.CurriedRecordCodecBuilder; import dev.lukebemish.codecextras.test.CodecAssertions; import org.junit.jupiter.api.Test; -class TestExtendedRecords { +class TestCurriedRecords { private record TestRecord(int a, int b, float c) { - public static final Codec CODEC = ExtendedRecordCodecBuilder + public static final Codec CODEC = CurriedRecordCodecBuilder .start(Codec.INT.fieldOf("a"), TestRecord::a) .field(Codec.INT.fieldOf("b"), TestRecord::b) .field(Codec.FLOAT.fieldOf("c"), TestRecord::c) From 49fb1b00bfc46a3eb82196960607ee9c7f01644b Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 15:12:55 -0500 Subject: [PATCH 63/76] Give main codecextras module a proper module-info --- settings.gradle | 10 ++++++ .../codecextras/companion/AccompaniedOps.java | 2 +- .../codecextras/companion/DelegatingOps.java | 36 +++++++++++++++++-- .../structured/StructuredMapCodec.java | 3 +- src/main/java/module-info.java | 35 ++++++++++++++++++ src/main/resources/META-INF/MANIFEST.MF | 1 - 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/main/java/module-info.java diff --git a/settings.gradle b/settings.gradle index 192f03e..76b9389 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,10 +22,20 @@ pluginManagement { } plugins { + id 'org.gradlex.extra-java-module-info' version '1.8' apply false id 'dev.lukebemish.conventions' version '0.1.11' id 'dev.lukebemish.multisource' version '0.1.8' } +gradle.beforeProject { + it.plugins.apply('org.gradlex.extra-java-module-info') + it.extraJavaModuleInfo { + failOnMissingModuleInfo.set(false) + automaticModule('dev.lukebemish.autoextension:autoextension', 'autoextension') + automaticModule('com.mojang:datafixerupper', 'datafixerupper') + } +} + multisource.of(':') { repositories { maven { diff --git a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java index dff07e9..fccffed 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java @@ -9,7 +9,7 @@ default > Optional } static Optional> find(DynamicOps ops) { - for (var retriever : DelegatingOps.ALTERNATE_COMPANION_RETRIEVERS) { + for (var retriever : DelegatingOps.forOps(ops)) { var companion = retriever.locateCompanionDelegate(ops); if (companion.isPresent()) { return companion; diff --git a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java index ca095e9..c02594d 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.companion; +import com.google.common.collect.MapMaker; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.DataResult; import com.mojang.serialization.Decoder; @@ -59,7 +60,8 @@ public static DynamicOps without(Q to } } - static final List ALTERNATE_COMPANION_RETRIEVERS; + private static final List ALTERNATE_COMPANION_RETRIEVERS; + private static final Map> RETRIEVERS = new MapMaker().weakKeys().weakValues().makeMap(); static { List retrievers = new ArrayList<>(); @@ -81,8 +83,38 @@ public AccompaniedOps delegate(DynamicOps ops, AccompaniedOps deleg ALTERNATE_COMPANION_RETRIEVERS = List.copyOf(retrievers); } + static List forOps(DynamicOps ops) { + var clazz = ops.getClass(); + var layer = clazz.getModule().getLayer(); + if (layer == null) { + return ALTERNATE_COMPANION_RETRIEVERS; + } + return RETRIEVERS.computeIfAbsent(layer, k -> { + List retrievers = new ArrayList<>(); + retrievers.add(new AlternateCompanionRetriever() { + @Override + public Optional> locateCompanionDelegate(DynamicOps ops) { + if (ops instanceof MapDelegatingOps mapOps) { + return Optional.of(mapOps); + } + return Optional.empty(); + } + + @Override + public AccompaniedOps delegate(DynamicOps ops, AccompaniedOps delegate) { + return delegate; + } + }); + retrievers.addAll(ServiceLoader.load(layer, AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); + if (layer != DelegatingOps.class.getModule().getLayer()) { + retrievers.addAll(ServiceLoader.load(AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); + } + return List.copyOf(retrievers); + }); + } + private static @Nullable Pair> retrieveMapOps(DynamicOps ops) { - for (AlternateCompanionRetriever retriever : ALTERNATE_COMPANION_RETRIEVERS) { + for (AlternateCompanionRetriever retriever : forOps(ops)) { Optional> companionDelegate = retriever.locateCompanionDelegate(ops); if (companionDelegate.isPresent()) { return Pair.of(retriever, companionDelegate.get()); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index b4f9a74..9b388d4 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -9,7 +9,6 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentMapCodec; -import dev.lukebemish.codecextras.repair.FillMissingMapCodec; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -56,7 +55,7 @@ public static DataResult> of(List MapCodec makeFieldCodec(Codec fieldCodec, RecordStructure.Field field, boolean lenient) { - return field.missingBehavior().map(behavior -> FillMissingMapCodec.fieldOf(fieldCodec.optionalFieldOf(field.name()), FillMissingMapCodec.lazyRepair(Optional::empty).fieldOf(field.name()), lenient).xmap( + return field.missingBehavior().map(behavior -> (lenient ? fieldCodec.lenientOptionalFieldOf(field.name()) : fieldCodec.optionalFieldOf(field.name())).xmap( optional -> optional.orElseGet(behavior.missing()), value -> behavior.predicate().test(value) ? Optional.of(value) : Optional.empty() )).orElseGet(() -> fieldCodec.fieldOf(field.name())); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..edf4411 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,35 @@ +module dev.lukebemish.codecextras { + uses dev.lukebemish.codecextras.companion.AlternateCompanionRetriever; + + requires static autoextension; + requires static com.electronwill.nightconfig.core; + requires static com.electronwill.nightconfig.toml; + requires com.google.common; + requires com.google.gson; + requires datafixerupper; + requires it.unimi.dsi.fastutil; + requires static jankson; + requires static org.jetbrains.annotations; + requires static org.jspecify; + requires static org.objectweb.asm; + requires static org.slf4j; + + exports dev.lukebemish.codecextras; + exports dev.lukebemish.codecextras.comments; + exports dev.lukebemish.codecextras.companion; + + exports dev.lukebemish.codecextras.compat.jankson; + exports dev.lukebemish.codecextras.compat.nightconfig; + + exports dev.lukebemish.codecextras.config; + exports dev.lukebemish.codecextras.extension; + exports dev.lukebemish.codecextras.mutable; + exports dev.lukebemish.codecextras.polymorphic; + exports dev.lukebemish.codecextras.record; + exports dev.lukebemish.codecextras.repair; + + exports dev.lukebemish.codecextras.structured; + exports dev.lukebemish.codecextras.structured.schema; + + exports dev.lukebemish.codecextras.types; +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF index 72758bc..5e57fdd 100644 --- a/src/main/resources/META-INF/MANIFEST.MF +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -1,2 +1 @@ FMLModType: LIBRARY -Automatic-Module-Name: dev.lukebemish.codecextras From f91729fd6c9719c24f77bac9f1f81679c86e4f22 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 20:22:52 -0500 Subject: [PATCH 64/76] More docs work and cleanup --- .../lukebemish/codecextras/XorMapCodec.java | 67 ++++++ .../structured/CodecInterpreter.java | 11 + .../structured/IdentityInterpreter.java | 5 + .../codecextras/structured/Interpreter.java | 2 + .../structured/MapCodecInterpreter.java | 12 + .../codecextras/structured/Range.java | 4 + .../structured/RecordStructure.java | 110 ++++++++- .../codecextras/structured/Structure.java | 217 +++++++++++++++++- .../schema/JsonSchemaInterpreter.java | 26 ++- .../lukebemish/codecextras/types/Flip.java | 7 + .../codecextras/types/Identity.java | 5 + .../config/ConfigScreenBuilder.java | 3 + .../structured/config/ConfigScreenEntry.java | 8 + .../config/ConfigScreenInterpreter.java | 24 ++ .../structured/StreamCodecInterpreter.java | 11 + .../test/structured/TestDispatch.java | 2 +- .../test/structured/TestStructured.java | 2 +- 17 files changed, 505 insertions(+), 11 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/XorMapCodec.java diff --git a/src/main/java/dev/lukebemish/codecextras/XorMapCodec.java b/src/main/java/dev/lukebemish/codecextras/XorMapCodec.java new file mode 100644 index 0000000..debfdbd --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/XorMapCodec.java @@ -0,0 +1,67 @@ +package dev.lukebemish.codecextras; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; +import java.util.Objects; +import java.util.stream.Stream; + +public final class XorMapCodec extends MapCodec> { + private final MapCodec first; + private final MapCodec second; + + private XorMapCodec(MapCodec first, MapCodec second) { + this.first = first; + this.second = second; + } + + public static XorMapCodec of(MapCodec first, MapCodec second) { + return new XorMapCodec<>(first, second); + } + + @Override + public Stream keys(DynamicOps dynamicOps) { + return Stream.concat(this.first.keys(dynamicOps), this.second.keys(dynamicOps)); + } + + @Override + public DataResult> decode(DynamicOps dynamicOps, MapLike mapLike) { + var firstResult = this.first.decode(dynamicOps, mapLike); + if (firstResult.isError()) { + return this.second.decode(dynamicOps, mapLike).map(Either::right); + } + var secondResult = this.second.decode(dynamicOps, mapLike); + if (secondResult.isError()) { + return firstResult.map(Either::left); + } + return DataResult.error(() -> "Both alternatives read successfully, can not pick the correct one; first: " + firstResult.getOrThrow() + ", second: " + secondResult.getOrThrow()); + } + + @Override + public RecordBuilder encode(Either fsEither, DynamicOps dynamicOps, RecordBuilder recordBuilder) { + return fsEither.map( + f -> this.first.encode(f, dynamicOps, recordBuilder), + s -> this.second.encode(s, dynamicOps, recordBuilder) + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof XorMapCodec that)) return false; + return Objects.equals(first, that.first) && Objects.equals(second, that.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + + @Override + public String toString() { + return "XorMapCodec[" + this.first + ", " + this.second + "]"; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index c091f6f..a603907 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -20,6 +20,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; +/** + * Interprets a {@link Structure} into a {@link Codec} for the same type. + * @see #interpret(Structure) + */ public abstract class CodecInterpreter extends KeyStoringInterpreter { public CodecInterpreter(Keys keys, Keys2, K1, K1> parametricKeys) { super(keys.join(Keys.builder() @@ -122,6 +126,13 @@ public DataResult>> either(App return DataResult.success(new Holder<>(Codec.either(leftCodec, rightCodec))); } + @Override + public DataResult>> xor(App left, App right) { + var leftCodec = unbox(left); + var rightCodec = unbox(right); + return DataResult.success(new Holder<>(Codec.xor(leftCodec, rightCodec))); + } + @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { return keyStructure.interpret(this).map(CodecInterpreter::unbox).flatMap(keyCodec -> { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 39fc971..7bdb28e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -114,6 +114,11 @@ public DataResult>> either(App "No default value available for an either"); } + @Override + public DataResult>> xor(App left, App right) { + return DataResult.error(() -> "No default value available for an xor"); + } + public DataResult interpret(Structure structure) { return structure.interpret(this).map(i -> Identity.unbox(i).value()); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index f29b1a8..928e90b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -75,5 +75,7 @@ default DataResult> bounded(Structure input, Supplier> v DataResult>> either(App left, App right); + DataResult>> xor(App left, App right); + DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index 224178b..cb46dac 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -10,6 +10,7 @@ import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; import com.mojang.serialization.codecs.KeyDispatchCodec; +import dev.lukebemish.codecextras.XorMapCodec; import dev.lukebemish.codecextras.comments.CommentMapCodec; import dev.lukebemish.codecextras.types.Identity; import java.util.List; @@ -20,6 +21,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; +/** + * Interprets a {@link Structure} into a {@link MapCodec} for the same type. + * @see #interpret(Structure) + */ public abstract class MapCodecInterpreter extends KeyStoringInterpreter { public MapCodecInterpreter( Keys keys, @@ -132,6 +137,13 @@ public DataResult>> either(App return DataResult.success(new Holder<>(Codec.mapEither(leftCodec, rightCodec))); } + @Override + public DataResult>> xor(App left, App right) { + var leftCodec = unbox(left); + var rightCodec = unbox(right); + return DataResult.success(new Holder<>(XorMapCodec.of(leftCodec, rightCodec))); + } + @Override public MapCodecInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new CodecAndMapInterpreters(codecInterpreter().keys(), keys().join(keys), codecInterpreter().parametricKeys(), parametricKeys().join(parametricKeys)).mapCodecInterpreter(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Range.java b/src/main/java/dev/lukebemish/codecextras/structured/Range.java index 78015c5..b38b030 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Range.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Range.java @@ -17,6 +17,10 @@ public record Range>(N min, N max) implements A } } + /** + * {@return the value, or the closest endpoint if the value is outside the range} + * @param value the value to clamp + */ public N clamp(N value) { if (value.compareTo(min) < 0) { return min; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index 462e198..f4177e2 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -12,11 +12,20 @@ import java.util.function.Predicate; import java.util.function.Supplier; +/** + * Used to assemble a set of key-structure pairs, potentially optionally present, into a structure. Most often you will + * create a {@link RecordStructure.Builder} and pass it to {@link Structure#record(Builder)}. + * @param The type of the record represented. + */ public class RecordStructure { private final List> fields = new ArrayList<>(); private final Set fieldNames = new HashSet<>(); private int count = 0; + /** + * When assembling an object from the layout defined in a record structure, values that have been read in for known + * keys are available in a {@link Container}. + */ public static final class Container { private final Key[] keys; private final Object[] array; @@ -34,6 +43,10 @@ private T get(Key key) { return (T) array[key.count]; } + /** + * {@return a container builder that has not read any keys yet} + * {@link Interpreter}s may need to assemble a container to use with a record structure. + */ public static Builder builder() { return new Builder(); } @@ -44,17 +57,30 @@ public static final class Builder { private Builder() {} + /** + * Add a key-value pair to the container. + * @param key the key + * @param value the value + * @param the type of the value + */ public void add(Key key, T value) { keys.add(key); values.add(value); } + /** + * {@return a new container with the keys and values added so far} + */ public Container build() { - return new Container(keys.toArray(new Key[0]), values.toArray()); + return new Container(keys.toArray(Key[]::new), values.toArray()); } } } + /** + * A handle to the value of a single field as read into a {@link Container} + * @param the type of the field + */ public static final class Key implements Function { private final int count; @@ -68,19 +94,50 @@ public T apply(Container container) { } } + /** + * A single field in a record structure. + * @param the type of the complete type + * @param the type of the field + */ public interface Field { + /** + * {@return the name of the field} + */ String name(); + /** + * {@return the structure for the field's contents} + */ Structure structure(); + /** + * {@return a function to extract the field's value from the full data type} + */ Function getter(); + /** + * {@return how the field should behave if it is missing from data} + */ Optional> missingBehavior(); + /** + * {@return the key to access the field's value once it is read in} + */ Key key(); + /** + * Represents how a field should behave if it is missing from data. + * @param the type of the field + */ interface MissingBehavior { + /** + * {@return the default value to use if the field is missing} + */ Supplier missing(); + + /** + * {@return whether a given field value should be re-encoded or just left missing} + */ Predicate predicate(); } } @@ -89,6 +146,14 @@ private record MissingBehaviorImpl(Supplier missing, Predicate predicat private record FieldImpl(String name, Structure structure, Function getter, Optional> missingBehavior, Key key) implements Field {} + /** + * Add a field to the record structure. + * @param name the name of the field + * @param structure the structure of the field's contents + * @param getter a function to extract the field's value from the full data type + * @return a key to access the field's value once it is read in to a {@link Container} + * @param the type of the field + */ public Key add(String name, Structure structure, Function getter) { var key = new Key(count); count++; @@ -97,6 +162,14 @@ public Key add(String name, Structure structure, Function getter return key; } + /** + * Add an optional field to the record structure. + * @param name the name of the field + * @param structure the structure of the field's contents + * @param getter a function to extract the field's value from the full data type + * @return a key to access the field's value once it is read in to a {@link Container} + * @param the type of the field + */ public Key> addOptional(String name, Structure structure, Function> getter) { var key = new Key>(count); count++; @@ -114,6 +187,15 @@ public Key> addOptional(String name, Structure structure, Fun return key; } + /** + * Add a field to the record structure with a default value. The field will not be encoded if equal to its default value. + * @param name the name of the field + * @param structure the structure of the field's contents + * @param getter a function to extract the field's value from the full data type + * @param defaultValue the default value to use if the field is missing + * @return a key to access the field's value once it is read in to a {@link Container} + * @param the type of the field + */ public Key addOptional(String name, Structure structure, Function getter, Supplier defaultValue) { var key = new Key(count); count++; @@ -122,6 +204,14 @@ public Key addOptional(String name, Structure structure, Function the type of the sub-record + */ public Function add(RecordStructure.Builder part, Function getter) { RecordStructure partial = new RecordStructure<>(); partial.count = this.count; @@ -141,6 +231,12 @@ private void partialField(Function getter, Field field) { fieldNames.add(field.name()); } + /** + * Turn a record structure builder into a {@link Structure}. + * @param builder the record structure builder + * @return a new structure + * @param the type of the data represented + */ static Structure create(RecordStructure.Builder builder) { RecordStructure instance = new RecordStructure<>(); var creator = builder.build(instance); @@ -152,8 +248,20 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * A builder for a record structure. This is the fundamental unit used to assemble record structures, and much as + * {@link com.mojang.serialization.MapCodec}s are reusable, record structure builders can be combined and reused via + * {@link #add(Builder, Function)}. + * @param the type of the record represented + */ @FunctionalInterface public interface Builder { + /** + * Assemble a record structure for the given type. Should collect {@link Key}s for every field needed and return + * a function that uses those keys to assemble the final type from a {@link Container}. + * @param builder a blank record structure to add fields to + * @return a function to assemble the final type from a {@link Container} + */ Function build(RecordStructure builder); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 7fe0e57..d60f122 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -95,6 +95,7 @@ public Keys annotations() { /** * {@return a new structure representing a list of the current structure} + * Analogous to {@link Codec#listOf()}. */ default Structure> listOf() { var outer = this; @@ -106,6 +107,15 @@ public DataResult>> interpret(Interpreter in }; } + /** + * {@return a structure representing key-value pairs of the provided types, with no bounds on the possible key values) + * Unlike a structure created with {@link #record(RecordStructure.Builder)}, there is no set of potential keys known + * ahead of time. Analogous to {@link Codec#unboundedMap(Codec, Codec)}. + * @param key the structure representing the key type + * @param value the structure representing the value type + * @param the represented key type + * @param the represented value type + */ static Structure> unboundedMap(Structure key, Structure value) { return new Structure<>() { @Override @@ -115,6 +125,15 @@ public DataResult>> interpret(Interpreter }; } + /** + * {@return a structure representing data matching at least one of two possible structures, with the left structure preferred} + * Analogous to {@link Codec#either(Codec, Codec)}. + * @param left the preferred structure + * @param right the fallback structure + * @param the type of data the first structure represents + * @param the type of data the second structure represents + * @see #xor(Structure, Structure) + */ static Structure> either(Structure left, Structure right) { return new Structure<>() { @Override @@ -128,21 +147,61 @@ public DataResult>> interpret(Interpreter the type of data the first structure represents + * @param the type of data the second structure represents + * @see #either(Structure, Structure) + */ + static Structure> xor(Structure left, Structure right) { + return new Structure<>() { + @Override + public DataResult>> interpret(Interpreter interpreter) { + var leftResult = left.interpret(interpreter); + var rightResult = right.interpret(interpreter); + return leftResult + .mapError(s -> rightResult.error().map(e -> s + "; " + e.message()).orElse(s)) + .flatMap(leftApp -> rightResult.flatMap(rightApp -> interpreter.xor(leftApp, rightApp))); + } + }; + } + + /** + * {@return a partial record structure containing the current structure as a field} + * Analogous to {@link Codec#fieldOf(String)}. + * @param name the name of the field + * @see #record(RecordStructure.Builder) + */ default RecordStructure.Builder fieldOf(String name) { return builder -> builder.add(name, this, Function.identity()); } + /** + * {@return a record structure optionally containing the current structure as a field} + * Analogous t= {@link Codec#optionalFieldOf(String)}. + * @param name the name of the field + * @see #fieldOf(String) + */ default RecordStructure.Builder> optionalFieldOf(String name) { return builder -> builder.addOptional(name, this, Function.identity()); } + /** + * {@return a record structure containing the current structure as a field, with a default value if it is not present} + * Analogous t= {@link Codec#optionalFieldOf(String, Object)}. + * @param name the name of the field + * @see #optionalFieldOf(String) + */ default RecordStructure.Builder optionalFieldOf(String name, Supplier defaultValue) { return builder -> builder.addOptional(name, this, Function.identity(), defaultValue); } /** * Like codecs, the type a structure represents can be changed without changing the actual underlying data structure, - * by providing conversion functions to and from the new type. + * by providing conversion functions to and from the new type. Analogous to {@link Codec#flatXmap(Function, Function)}. * @param to converts the old type to the new type, if possible * @param from converts the new type to the old type, if possible * @return a new structure representing the new type @@ -158,7 +217,8 @@ public DataResult> interpret(Interpreter interpre } /** - * Similar to {@link #flatXmap(Function, Function)} (Function, Function)}, except that the conversion functions are not allowed to fail. + * Similar to {@link #flatXmap(Function, Function)}, except that the conversion functions are not allowed to fail. + * Analogous to {@link Codec#xmap(Function, Function)}. * @param to converts the old type to the new type * @param from converts the new type to the old type * @return a new structure representing the new type @@ -168,18 +228,55 @@ default Structure xmap(Function to, Function from) { return flatXmap(a -> DataResult.success(to.apply(a)), b -> DataResult.success(from.apply(b))); } + /** + * Similar to {@link #flatXmap(Function, Function)}, except that the second conversion function is not allowed to fail. + * Analogous to {@link Codec#comapFlatMap(Function, Function)}. + * @param to converts the old type to the new type + * @param from converts the new type to the old type + * @return a new structure representing the new type + * @param the new type to represent + */ default Structure comapFlatMap(Function> to, Function from) { return flatXmap(to, b -> DataResult.success(from.apply(b))); } + /** + * Similar to {@link #flatXmap(Function, Function)}, except that the first conversion function is not allowed to fail. + * Analogous to {@link Codec#flatComapMap(Function, Function)}. + * @param to converts the old type to the new type + * @param from converts the new type to the old type + * @return a new structure representing the new type + * @param the new type to represent + */ default Structure flatComapMap(Function to, Function> from) { return flatXmap(a -> DataResult.success(to.apply(a)), from); } + /** + * Creates a structure such that the structure of the data is dependent on the value for a given key. The key must + * have a finite set of possible values. The current structure becomes the structure of the key field. + * Analogous to {@link Codec#dispatch(String, Function, Function)}. + * @param key the key to dispatch on + * @param function retrieve a key from the final data type + * @param keys the set of possible key values + * @param structures converts keys into structures + * @return a new structure representing the data type + * @param the type of data the structure represents + */ default Structure dispatch(String key, Function> function, Supplier> keys, Function>> structures) { return dispatch(key, function, keys, structures, true); } + /** + * Similar to {@link #dispatch(String, Function, Supplier, Function)}, except that while the set of keys is still finite, + * and keys are still checked as belonging to the set, the resulting structure does not expose information about those bounds. + * @param key the key to dispatch on + * @param function retrieve a key from the final data type + * @param keys the set of possible key values + * @param structures converts keys into structures + * @return a new structure representing the data type + * @param the type of data the structure represents + */ default Structure dispatchUnbounded(String key, Function> function, Supplier> keys, Function>> structures) { return dispatch(key, function, keys, structures, false); } @@ -194,10 +291,26 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Creates a structure representing a map where the structure of a value is dependent on the key. The key must have a + * finite set of possible values. The current structure becomes the structure of the key field. Analogous to {@link Codec#dispatchedMap(Codec, Function)}. + * @param keys the set of possible key values + * @param structures converts keys into structures + * @return a new structure representing the map type + * @param the type of data the map values represent + */ default Structure> dispatchedMap(Supplier> keys, Function>> structures) { return dispatchedMap(keys, structures, true); } + /** + * Similar to {@link #dispatchedMap(Supplier, Function)}, except that while the set of keys is still finite, and keys + * are still checked as belonging to the set, the resulting structure does not expose information about those bounds. + * @param keys the set of possible key values + * @param structures converts keys into structures + * @return a new structure representing the map type + * @param the type of data the map values represent + */ default Structure> dispatchedUnboundedMap(Supplier> keys, Function>> structures) { return dispatchedMap(keys, structures, false); } @@ -213,7 +326,8 @@ public DataResult>> interpret(Interpreter } /** - * It might be necessary to lazily-initialize a structure to avoid circular static field dependencies. + * It might be necessary to lazily-initialize a structure to avoid circular static field dependencies. Analogous to + * {@link Codec#lazyInitialized(Supplier)}. * @param supplier the structure to lazily initialize * @return a new structure that will initialize the wrapped structure when interpreted * @param the type of data the structure represents @@ -250,6 +364,7 @@ public DataResult> interpret(Interpreter interpre * @param fallback the structure to interpret if the key cannot be resolved * @return a new structure * @param the type of data the structure represents + * @see #keyed(Key) */ static Structure keyed(Key key, Structure fallback) { return new Structure<>() { @@ -274,6 +389,7 @@ public DataResult> interpret(Interpreter interpre * @return a new structure * @param the type of data the structure represents * @see Interpreter#keyConsumers() + * @see #keyed(Key) */ static Structure keyed(Key key, Keys, K1> keys) { return new Structure<>() { @@ -288,6 +404,18 @@ public DataResult> interpret(Interpreter interpre } + /** + * Similar to {@link #keyed(Key)}, providing both a fallback structure and a set of interpreter-specific + * representations. + * @param key the key which will be matched to a specific representation + * @param keys the set of specific representations to match against + * @param fallback the structure to interpret if the key cannot be resolved + * @return a new structure + * @param the type of data the structure represents + * @see #keyed(Key) + * @see #keyed(Key, Keys) + * @see #keyed(Key, Structure) + */ static Structure keyed(Key key, Keys, K1> keys, Structure fallback) { return new Structure<>() { @Override @@ -304,6 +432,20 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Similar to a {@link #keyed(Key)}, a parametrically keyed structure provides a key that interpreters can resolve to + * obtain a specific representation; however, the key is parameterized by another type, which the structure also contains + * an instance of. The structure will resolve a {@link ParametricKeyedValue} for the key, representing a conversion + * of the parameter type into the structure type for arbitrary parameterizations of the parameter type. + * @param key the key which will be matched to a specific representation + * @param parameter the parameter to convert into a specific representation + * @param unboxer unboxes the structure type from its {@link App} representation used by the key + * @return a new structure + * @param the type function of the data type represented by the created structure + * @param the type function of the parameter type associated with the key + * @param the type to parameterize both {@link MuO} and {@link MuP} with + * @param the type of data the structure represents + */ static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer) { return new Structure<>() { @Override @@ -315,6 +457,20 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Similar to {@link #parametricallyKeyed(Key2, App, Function)}, except a fallback structure is provided in case the + * interpreter cannot resolve the key. + * @param key the key which will be matched to a specific representation + * @param parameter the parameter to convert into a specific representation + * @param unboxer unboxes the structure type from its {@link App} representation used by the key + * @param fallback the structure to interpret if the key cannot be resolved + * @return a new structure + * @param the type function of the data type represented by the created structure + * @param the type function of the parameter type associated with the key + * @param the type to parameterize both {@link MuO} and {@link MuP} with + * @param the type of data the structure represents + * @see #parametricallyKeyed(Key2, App, Function) + */ static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Structure fallback) { return new Structure<>() { @Override @@ -330,6 +486,22 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * Similar to {@link #parametricallyKeyed(Key2, App, Function)}, except that specific representations may also be + * stored on the structure, by resolved interpreter-specific keys. The key set used is {@link Flip}ed so that the + * type parameterizing the key can be the type function of the interpreter. Interpreter keys are matched against the + * provided key set, and if missing the provided key is resolved by the interpreter. + * @param key the key which will be matched to a specific representation + * @param parameter the parameter to convert into a specific representation + * @param unboxer unboxes the structure type from its {@link App} representation used by the key + * @param keys the set of specific representations to match against + * @return a new structure + * @param the type function of the data type represented by the created structure + * @param the type function of the parameter type associated with the key + * @param the type to parameterize both {@link MuO} and {@link MuP} with + * @param the type of data the structure represents + * @see #parametricallyKeyed(Key2, App, Function) + */ static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys) { return new Structure<>() { @Override @@ -350,6 +522,23 @@ private static Optional> converted .map(c::convert); } + /** + * Similar to {@link #parametricallyKeyed(Key2, App, Function)}, providing both a fallback structure and a set of + * interpreter-specific representations. + * @param key the key which will be matched to a specific representation + * @param parameter the parameter to convert into a specific representation + * @param unboxer unboxes the structure type from its {@link App} representation used by the key + * @param keys the set of specific representations to match against + * @param fallback the structure to interpret if the key cannot be resolved + * @return a new structure + * @param the type function of the data type represented by the created structure + * @param the type function of the parameter type associated with the key + * @param the type to parameterize both {@link MuO} and {@link MuP} with + * @param the type of data the structure represents + * @see #parametricallyKeyed(Key2, App, Function) + * @see #parametricallyKeyed(Key2, App, Function, Keys) + * @see #parametricallyKeyed(Key2, App, Function, Structure) + */ static > Structure parametricallyKeyed(Key2 key, App parameter, Function, A> unboxer, Keys, K1> keys, Structure fallback) { return new Structure<>() { @Override @@ -368,6 +557,10 @@ public DataResult> interpret(Interpreter interpre }; } + /** + * {@return a structure that represents only the provided values} + * @param available the set of values to represent + */ default Structure bounded(Supplier> available) { final class BoundedStructure implements Structure { private final Structure outer; @@ -392,10 +585,21 @@ public DataResult> interpret(Interpreter interpre return annotatedDelegatingStructure(BoundedStructure::new, this, this.annotations()); } + /** + * {@return a structure that represents only data passing the provided validation function} + * @param verifier the validation function + */ default Structure validate(Function> verifier) { return this.flatXmap(verifier, verifier); } + /** + * {@return a structure representing a collection of key-value pairs with defined structures, which may be optionally present} + * Analogous to the use of {@link com.mojang.serialization.codecs.RecordCodecBuilder}. + * @param builder the builder to use to create the record structure + * @param the type of data the structure represents + * @see RecordStructure + */ static Structure record(RecordStructure.Builder builder) { return RecordStructure.create(builder); } @@ -447,11 +651,18 @@ static Structure record(RecordStructure.Builder builder) { .build() ); + /** + * Represents a {@link Unit} value, but only as an empty map. + */ Structure EMPTY_MAP = keyed( Interpreter.EMPTY_MAP, unboundedMap(STRING, PASSTHROUGH) .comapFlatMap(map -> map.isEmpty() ? DataResult.success(Unit.INSTANCE) : DataResult.error(() -> "Expected an empty map"), u -> Map.of()) ); + + /** + * Represents a {@link Unit} value, but only as an empty list. + */ Structure EMPTY_LIST = keyed( Interpreter.EMPTY_LIST, PASSTHROUGH.listOf() diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index ee5098c..249e75d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -31,6 +31,11 @@ import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +/** + * Creates a JSON schema for a given {@link Structure}. Note that interpreting a structure with this interpreter will + * require resolving any lazy aspects of the structure, such as bounds, so should not be done until that is safe + * @see #interpret(Structure) + */ public class JsonSchemaInterpreter extends KeyStoringInterpreter { private final CodecInterpreter codecInterpreter; private final DynamicOps ops; @@ -300,6 +305,21 @@ public DataResult>> either(App return DataResult.success(new Holder<>(schema, definitions)); } + @Override + public DataResult>> xor(App left, App right) { + var schema = new JsonObject(); + var oneOf = new JsonArray(); + var definitions = new HashMap>(); + var leftSchema = schemaValue(left); + var rightSchema = schemaValue(right); + definitions.putAll(definitions(left)); + definitions.putAll(definitions(right)); + oneOf.add(leftSchema); + oneOf.add(rightSchema); + schema.add("oneOf", oneOf); + return DataResult.success(new Holder<>(schema, definitions)); + } + @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { var definitions = new HashMap>(); @@ -329,11 +349,7 @@ private static Map> definitions(App box) { return Holder.unbox(box).definition; } - public DataResult nestedSchema(Structure structure) { - return structure.interpret(this).map(JsonSchemaInterpreter::schemaValue); - } - - public DataResult rootSchema(Structure structure) { + public DataResult interpret(Structure structure) { return structure.interpret(this).flatMap(holder -> { var object = copy(schemaValue(holder)); var definitions = definitions(holder); diff --git a/src/main/java/dev/lukebemish/codecextras/types/Flip.java b/src/main/java/dev/lukebemish/codecextras/types/Flip.java index 3ab4daa..c7f3a8e 100644 --- a/src/main/java/dev/lukebemish/codecextras/types/Flip.java +++ b/src/main/java/dev/lukebemish/codecextras/types/Flip.java @@ -3,6 +3,13 @@ import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; +/** + * Allows the order of arguments to {@link App} to be "flipped", where {@code App F T = F[T]} becomes {@code App (Flip T) F = F[T]} + * when boxed. + * @param value the boxed value + * @param the type function to flip + * @param the type parameter for {@link F} + */ public record Flip(App value) implements App, F> { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/main/java/dev/lukebemish/codecextras/types/Identity.java b/src/main/java/dev/lukebemish/codecextras/types/Identity.java index 8eb0e48..fc3f7cb 100644 --- a/src/main/java/dev/lukebemish/codecextras/types/Identity.java +++ b/src/main/java/dev/lukebemish/codecextras/types/Identity.java @@ -5,6 +5,11 @@ import com.mojang.datafixers.kinds.K1; import java.util.function.Function; +/** + * Represents the type function {@code T -> T} as an {@link App} type. + * @param value the boxed value + * @param the type represented + */ public record Identity(T value) implements App { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java index 11aee57..c07a098 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java @@ -14,6 +14,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Turns a list of {@link ConfigScreenEntry}s into a screen that can be opened by the user. + */ public class ConfigScreenBuilder { private record SingleScreen(ConfigScreenEntry screenEntry, Consumer onClose, Supplier context, Supplier initialData) {} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index 1393ec3..c5236c1 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -10,6 +10,14 @@ import net.minecraft.client.gui.screens.Screen; import org.slf4j.Logger; +/** + * Represents both a single level of a configuration screen, and the entry it would become when nested in another config + * screen. To turn into a screen, use {@link ConfigScreenBuilder}. + * @param layout prevides the layout used when this data is nested in another config screen + * @param screenEntryProvider provides the entries present on this screen + * @param entryCreationInfo the information needed to create this entry + * @param the type of data this entry represents + */ public record ConfigScreenEntry(LayoutFactory layout, ScreenEntryFactory screenEntryProvider, EntryCreationInfo entryCreationInfo) implements App { public static final class Mu implements K1 { private Mu() {} } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 8be4622..7b3131d 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -70,6 +70,13 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; +/** + * Interprets a {@link Structure} into a {@link ConfigScreenEntry} for the same type. Note that interpreting a structure + * with this interpreter will require resolving any lazy aspects of the structure, such as bounds, so should not be done + * until that is safe. + * @see #interpret(Structure) + * @see ConfigScreenEntry + */ public class ConfigScreenInterpreter extends KeyStoringInterpreter { private static final Logger LOGGER = LogUtils.getLogger(); @@ -754,6 +761,23 @@ public DataResult>> either(App DataResult>> xor(App left, App right) { + var codecLeft = ConfigScreenEntry.unbox(left).entryCreationInfo().codec(); + var codecRight = ConfigScreenEntry.unbox(right).entryCreationInfo().codec(); + var codecResult = codecInterpreter.xor(new CodecInterpreter.Holder<>(codecLeft), new CodecInterpreter.Holder<>(codecRight)).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(codecResult.error().orElseThrow().messageSupplier()); + } + return DataResult.success(ConfigScreenEntry.single( + Widgets.either( + ConfigScreenEntry.unbox(left).layout(), + ConfigScreenEntry.unbox(right).layout() + ), + new EntryCreationInfo<>(codecResult.getOrThrow(), ComponentInfo.empty()) + )); + } + @Override public DataResult> bounded(Structure inputSupplier, Supplier> values) { return interpret(inputSupplier).flatMap(input -> { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 6a4df78..79adda3 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -40,6 +40,11 @@ import net.minecraft.network.codec.StreamCodec; import org.jspecify.annotations.Nullable; +/** + * Interprets a {@link Structure} into a {@link StreamCodec} for the same type. + * @param the type of the {@link ByteBuf} to encode and decode + * @see #interpret(Structure) + */ public class StreamCodecInterpreter extends KeyStoringInterpreter, StreamCodecInterpreter> { private final Key> key; private final List>> parentConsumers; @@ -420,6 +425,12 @@ public DataResult, Either>> either(App(ByteBufCodecs.either(leftCodec, rightCodec))); } + @Override + public DataResult, Either>> xor(App, L> left, App, R> right) { + // For stream codecs, xor is just either + return either(left, right); + } + public record Holder(StreamCodec streamCodec) implements App, T> { public static final class Mu implements K1 {} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java index e411474..8c7c02d 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestDispatch.java @@ -173,6 +173,6 @@ void testEncoding() { @Test void testJsonSchema() { - CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().rootSchema(Dispatches.STRUCTURE).getOrThrow().toString()); + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(Dispatches.STRUCTURE).getOrThrow().toString()); } } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java index 31c7ab8..18149b2 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestStructured.java @@ -76,6 +76,6 @@ void testEncodingCodec() { @Test void testJsonSchema() { - CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().rootSchema(TestRecord.STRUCTURE).getOrThrow().toString()); + CodecAssertions.assertJsonEquals(schema, new JsonSchemaInterpreter().interpret(TestRecord.STRUCTURE).getOrThrow().toString()); } } From 05c66b6889bce1b68614b94f92ae2ae2bc440744 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 22:40:23 -0500 Subject: [PATCH 65/76] Fix tabs --- build.gradle | 298 +++++++++++++++++++++--------------------- buildSrc/build.gradle | 2 +- settings.gradle | 60 ++++----- 3 files changed, 180 insertions(+), 180 deletions(-) diff --git a/build.gradle b/build.gradle index 39a5ab9..24aeca7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,94 +1,94 @@ import dev.lukebemish.codecextras.gradle.FormatJmhOutput plugins { - alias cLibs.plugins.conventions.java - alias cLibs.plugins.managedversioning + alias cLibs.plugins.conventions.java + alias cLibs.plugins.managedversioning } group = 'dev.lukebemish' managedVersioning { - versionFile.set project.file('version.properties') - versionPRs() - versionSnapshots() - - gitHubActions { - snapshot { - prettyName.set 'Snapshot' - workflowDispatch.set(true) - onBranches.add 'main' - gradleJob { - buildCache() - cacheReadOnly.set false - javaVersion.set '21' - name.set 'build' - gradlew 'Build', 'build' - gradlew 'Publish', 'publish' - mavenSnapshot('github') - } - } - benchmark { - prettyName.set 'Benchmark' - workflowDispatch.set(true) - gradleJob { - buildCache() - javaVersion.set '21' - name.set 'build' - gradlew 'JMH', 'jmhResults' - step { - name.set 'Record JMH Output' - run.set 'cat build/reports/jmh/results.md >> $GITHUB_STEP_SUMMARY' - } - } - } - release { - prettyName.set 'Release' - workflowDispatch.set(true) - gradleJob { - buildCache() - javaVersion.set '21' - name.set 'build' - step { - setupGitUser() - } - readOnly.set false - gradlew 'Tag Release', 'tagRelease' - gradlew 'Build', 'build' - step { - run.set 'git push && git push --tags' - } - recordVersion 'Record Version', 'version' - } - gradleJob { - buildCache() - javaVersion.set '21' - name.set 'publish' - needs.add('build') - tag.set('${{needs.build.outputs.version}}') - gradlew 'Publish', 'publish' - mavenRelease('github') - } - } - build_pr { - prettyName.set 'Build PR' - pullRequest.set(true) - gradleJob { - javaVersion.set '21' - name.set 'build' - gradlew 'Build', 'build' - gradlew 'Publish', 'publish' - pullRequestArtifact() - } - } - publish_pr { - prettyName.set 'Publish PR' - publishPullRequestAction( - 'github', - "${project.group.replace('.', '/')}/${project.name}", - 'Build PR' - ) - } - } + versionFile.set project.file('version.properties') + versionPRs() + versionSnapshots() + + gitHubActions { + snapshot { + prettyName.set 'Snapshot' + workflowDispatch.set(true) + onBranches.add 'main' + gradleJob { + buildCache() + cacheReadOnly.set false + javaVersion.set '21' + name.set 'build' + gradlew 'Build', 'build' + gradlew 'Publish', 'publish' + mavenSnapshot('github') + } + } + benchmark { + prettyName.set 'Benchmark' + workflowDispatch.set(true) + gradleJob { + buildCache() + javaVersion.set '21' + name.set 'build' + gradlew 'JMH', 'jmhResults' + step { + name.set 'Record JMH Output' + run.set 'cat build/reports/jmh/results.md >> $GITHUB_STEP_SUMMARY' + } + } + } + release { + prettyName.set 'Release' + workflowDispatch.set(true) + gradleJob { + buildCache() + javaVersion.set '21' + name.set 'build' + step { + setupGitUser() + } + readOnly.set false + gradlew 'Tag Release', 'tagRelease' + gradlew 'Build', 'build' + step { + run.set 'git push && git push --tags' + } + recordVersion 'Record Version', 'version' + } + gradleJob { + buildCache() + javaVersion.set '21' + name.set 'publish' + needs.add('build') + tag.set('${{needs.build.outputs.version}}') + gradlew 'Publish', 'publish' + mavenRelease('github') + } + } + build_pr { + prettyName.set 'Build PR' + pullRequest.set(true) + gradleJob { + javaVersion.set '21' + name.set 'build' + gradlew 'Build', 'build' + gradlew 'Publish', 'publish' + pullRequestArtifact() + } + } + publish_pr { + prettyName.set 'Publish PR' + publishPullRequestAction( + 'github', + "${project.group.replace('.', '/')}/${project.name}", + 'Build PR' + ) + } + } } managedVersioning.apply() @@ -98,7 +98,7 @@ println "Building: $version" sourceSets { minecraft {} minecraftFabric {} - jmh {} + jmh {} } configurations { @@ -128,18 +128,18 @@ artifacts { } java { - toolchain.languageVersion.set(JavaLanguageVersion.of(21)) - withSourcesJar() - withJavadocJar() - registerFeature("minecraft") { - usingSourceSet sourceSets.minecraft - withSourcesJar() - withJavadocJar() + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + withSourcesJar() + withJavadocJar() + registerFeature("minecraft") { + usingSourceSet sourceSets.minecraft + withSourcesJar() + withJavadocJar() capability(project.group as String, "$project.name-minecraft", project.version as String) capability(project.group as String, "$project.name-minecraft-common", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) - } + } registerFeature("minecraftFabric") { usingSourceSet sourceSets.minecraftFabric capability(project.group as String, "$project.name-minecraft", project.version as String) @@ -158,44 +158,44 @@ java { } repositories { - mavenCentral() - maven { - name = 'Minecraft Libraries' - url = 'https://libraries.minecraft.net/' - } + mavenCentral() + maven { + name = 'Minecraft Libraries' + url = 'https://libraries.minecraft.net/' + } } dependencies { - api 'com.mojang:datafixerupper:8.0.16' - api 'org.slf4j:slf4j-api:2.0.1' + api 'com.mojang:datafixerupper:8.0.16' + api 'org.slf4j:slf4j-api:2.0.1' - jmhCompileOnly cLibs.bundles.compileonly - jmhImplementation project(':') - jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' - jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + jmhCompileOnly cLibs.bundles.compileonly + jmhImplementation project(':') + jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' - jmhRuntimeOnly 'org.ow2.asm:asm:9.5' + jmhRuntimeOnly 'org.ow2.asm:asm:9.5' - testCompileOnly cLibs.bundles.compileonly + testCompileOnly cLibs.bundles.compileonly - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-params:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-params:5.9.2' - compileOnly 'com.electronwill.night-config:core:3.6.4' - compileOnly 'com.electronwill.night-config:toml:3.6.4' - compileOnly 'blue.endless:jankson:1.2.2' - compileOnly 'org.ow2.asm:asm:9.5' + compileOnly 'com.electronwill.night-config:core:3.6.4' + compileOnly 'com.electronwill.night-config:toml:3.6.4' + compileOnly 'blue.endless:jankson:1.2.2' + compileOnly 'org.ow2.asm:asm:9.5' - testImplementation 'com.electronwill.night-config:core:3.6.4' - testImplementation 'com.electronwill.night-config:toml:3.6.4' - testImplementation 'blue.endless:jankson:1.2.2' - testImplementation 'org.ow2.asm:asm:9.5' + testImplementation 'com.electronwill.night-config:core:3.6.4' + testImplementation 'com.electronwill.night-config:toml:3.6.4' + testImplementation 'blue.endless:jankson:1.2.2' + testImplementation 'org.ow2.asm:asm:9.5' - annotationProcessor 'dev.lukebemish.autoextension:autoextension:0.1.1' - compileOnly 'dev.lukebemish.autoextension:autoextension:0.1.1' + annotationProcessor 'dev.lukebemish.autoextension:autoextension:0.1.1' + compileOnly 'dev.lukebemish.autoextension:autoextension:0.1.1' - minecraftApi project(':') + minecraftApi project(':') minecraftCompileOnly cLibs.bundles.compileonly minecraftAnnotationProcessor cLibs.bundles.annotationprocessor minecraftFabricCompileOnly cLibs.bundles.compileonly @@ -258,24 +258,24 @@ tasks.named('jar', Jar) { } tasks.register('jmh', JavaExec) { - group = 'benchmark' - dependsOn sourceSets.jmh.output - mainClass = 'org.openjdk.jmh.Main' - systemProperty 'jmh.executor', 'VIRTUAL' - systemProperty 'jmh.blackhole.mode', 'COMPILER' - args '-rf', 'json', '-rff', 'build/reports/jmh/results.json' - classpath = sourceSets.jmh.runtimeClasspath - doFirst { - mkdir('build/reports/jmh') - } - outputs.file('build/reports/jmh/results.json') + group = 'benchmark' + dependsOn sourceSets.jmh.output + mainClass = 'org.openjdk.jmh.Main' + systemProperty 'jmh.executor', 'VIRTUAL' + systemProperty 'jmh.blackhole.mode', 'COMPILER' + args '-rf', 'json', '-rff', 'build/reports/jmh/results.json' + classpath = sourceSets.jmh.runtimeClasspath + doFirst { + mkdir('build/reports/jmh') + } + outputs.file('build/reports/jmh/results.json') } tasks.register('jmhResults', FormatJmhOutput) { - group = 'benchmark' - dependsOn tasks.jmh - jmhResults.set project.file('build/reports/jmh/results.json') - formattedResults.set project.file('build/reports/jmh/results.md') + group = 'benchmark' + dependsOn tasks.jmh + jmhResults.set project.file('build/reports/jmh/results.json') + formattedResults.set project.file('build/reports/jmh/results.md') } tasks.named('remapMinecraftFabricJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> @@ -287,10 +287,10 @@ tasks.named('minecraftNeoforgeJar', Jar) { task -> } tasks.compileJava { - options.compilerArgs += [ - '-Aautoextension.name=CodecExtras', - "-Aautoextension.version=${version}".toString() - ] + options.compilerArgs += [ + '-Aautoextension.name=CodecExtras', + "-Aautoextension.version=${version}".toString() + ] } ['processResources', 'processMinecraftResources', 'processMinecraftFabricResources', 'processMinecraftNeoforgeResources'].each { @@ -311,20 +311,20 @@ tasks.compileJava { } test { - useJUnitPlatform() - testLogging { - showStandardStreams = true - exceptionFormat = 'full' - events = ['passed', 'failed', 'skipped'] - } + useJUnitPlatform() + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + events = ['passed', 'failed', 'skipped'] + } } publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } + publications { + mavenJava(MavenPublication) { + from components.java + } + } } managedVersioning.publishing.mavenRelease(publishing) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3ac72e1..a43f332 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,3 +1,3 @@ plugins { - id 'groovy' + id 'groovy' } diff --git a/settings.gradle b/settings.gradle index 76b9389..6f0a6c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,30 +1,30 @@ pluginManagement { - repositories { - maven { - name = "Luke's Maven" - url = 'https://maven.lukebemish.dev/releases' - } - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - maven { - name = 'NeoForged' - url = 'https://maven.neoforged.net/' - } - maven { - name = 'Architectury' - url "https://maven.architectury.dev/" - } - mavenCentral() - gradlePluginPortal() - } + repositories { + maven { + name = "Luke's Maven" + url = 'https://maven.lukebemish.dev/releases' + } + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/' + } + maven { + name = 'Architectury' + url "https://maven.architectury.dev/" + } + mavenCentral() + gradlePluginPortal() + } } plugins { id 'org.gradlex.extra-java-module-info' version '1.8' apply false - id 'dev.lukebemish.conventions' version '0.1.11' - id 'dev.lukebemish.multisource' version '0.1.8' + id 'dev.lukebemish.conventions' version '0.1.11' + id 'dev.lukebemish.multisource' version '0.1.8' } gradle.beforeProject { @@ -46,11 +46,11 @@ multisource.of(':') { } } } - configureEach { - minecraft.add project.libs.minecraft - mappings.add loom.officialMojangMappings() - } - common('minecraft', []) {} + configureEach { + minecraft.add project.libs.minecraft + mappings.add loom.officialMojangMappings() + } + common('minecraft', []) {} fabric('minecraftFabric', ['minecraft']) {} neoforge('minecraftNeoforge', ['minecraft']) { neoForge.add project.libs.neoforge @@ -60,9 +60,9 @@ multisource.of(':') { neoForge.add project.libs.neoforge } fabric('testFabric', ['testCommon']) {} - repositories { - it.removeIf { it.name == 'Forge' } - } + repositories { + it.removeIf { it.name == 'Forge' } + } } rootProject.name = 'codecextras' From 20f7b98ce9a00deac6c54cb050edee7f18ad2c6c Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 10 Sep 2024 22:42:54 -0500 Subject: [PATCH 66/76] Fix javadoc --- .../java/dev/lukebemish/codecextras/structured/Structure.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index d60f122..a4bc40f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -108,7 +108,7 @@ public DataResult>> interpret(Interpreter in } /** - * {@return a structure representing key-value pairs of the provided types, with no bounds on the possible key values) + * {@return a structure representing key-value pairs of the provided types, with no bounds on the possible key values} * Unlike a structure created with {@link #record(RecordStructure.Builder)}, there is no set of potential keys known * ahead of time. Analogous to {@link Codec#unboundedMap(Codec, Codec)}. * @param key the structure representing the key type From 54fad2e1d4cbfeb775a25370ec14b86dde3e119f Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 11 Sep 2024 20:01:16 -0500 Subject: [PATCH 67/76] Bump multisource --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 6f0a6c1..2714d91 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,7 +24,7 @@ pluginManagement { plugins { id 'org.gradlex.extra-java-module-info' version '1.8' apply false id 'dev.lukebemish.conventions' version '0.1.11' - id 'dev.lukebemish.multisource' version '0.1.8' + id 'dev.lukebemish.multisource' version '0.2.2' } gradle.beforeProject { From 742c60c27c62e86e49a00c04315bfb8252d17671 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 15 Sep 2024 11:04:35 -0500 Subject: [PATCH 68/76] Make config type test more explicit --- .../lukebemish/codecextras/test/config/ConfigTypeTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/lukebemish/codecextras/test/config/ConfigTypeTest.java b/src/test/java/dev/lukebemish/codecextras/test/config/ConfigTypeTest.java index c053407..7ccdfd3 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/config/ConfigTypeTest.java +++ b/src/test/java/dev/lukebemish/codecextras/test/config/ConfigTypeTest.java @@ -92,9 +92,9 @@ protected TypeRewriteRule makeRule() { var a = dynamic.get("e").asInt(TestRecord.DEFAULT.a()); var b = dynamic.get("f").asInt(TestRecord.DEFAULT.b()); var c = dynamic.get("g").asFloat(TestRecord.DEFAULT.c()); - dynamic.remove("e"); - dynamic.remove("f"); - dynamic.remove("g"); + dynamic = dynamic.remove("e"); + dynamic = dynamic.remove("f"); + dynamic = dynamic.remove("g"); return dynamic .set("a", dynamic.createInt(a)) .set("b", dynamic.createInt(b)) From 76f3e1ecb088e73145b4a18b2f2cfa812b5036a4 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 6 Nov 2024 12:46:25 -0600 Subject: [PATCH 69/76] Update PR workflow --- .github/workflows/publish_pr.yml | 7 ++++--- build.gradle | 2 +- settings.gradle | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_pr.yml b/.github/workflows/publish_pr.yml index e3ceeae..b19b7dc 100644 --- a/.github/workflows/publish_pr.yml +++ b/.github/workflows/publish_pr.yml @@ -17,10 +17,11 @@ }, { "with": { - "script": "const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};\nif (!pull_requests.length) {\n return core.error(\"This workflow doesn't match any pull requests!\");\n}\nlet allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({\n owner: context.repo.owner,\n repo: context.repo.repo,\n run_id: context.payload.workflow_run.id,\n});\nlet matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {\n return artifact.name == \"artifacts\"\n})[0];\nlet download = await github.rest.actions.downloadArtifact({\n owner: context.repo.owner,\n repo: context.repo.repo,\n artifact_id: matchArtifact.id,\n archive_format: 'zip',\n});\nlet fs = require('fs');\nfs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/repo.zip`, Buffer.from(download.data));" + "script": "const response = await github.rest.search.issuesAndPullRequests({\n q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',\n per_page: 1,\n})\nconst items = response.data.items\nif (items.length < 1) {\n console.error('No PRs found')\n return\n}\nconst pullRequestNumber = items[0].number\nlet allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({\n owner: context.repo.owner,\n repo: context.repo.repo,\n run_id: context.payload.workflow_run.id,\n});\nlet matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {\n return artifact.name == \"artifacts\"\n})[0];\nlet download = await github.rest.actions.downloadArtifact({\n owner: context.repo.owner,\n repo: context.repo.repo,\n artifact_id: matchArtifact.id,\n archive_format: 'zip',\n});\nlet fs = require('fs');\nfs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/repo.zip`, Buffer.from(download.data));\nreturn pullRequestNumber;" }, "name": "Download Artifacts", - "uses": "actions/github-script@v7" + "uses": "actions/github-script@v7", + "id": "download_artifacts" }, { "name": "Unpack Artifacts", @@ -33,7 +34,7 @@ "MAVEN_USER": "github", "MAVEN_PASSWORD": "${{ secrets.PR_MAVEN_PASSWORD }}", "MAVEN_URL": "https://maven.lukebemish.dev/pullrequests/", - "ALLOWED_VERSION": "*-pr${{ github.event.workflow_run.pull_requests[0].number }}", + "ALLOWED_VERSION": "*-pr${{ steps.download_artifacts.outputs.result }}", "ALLOWED_PATHS": "dev/lukebemish/codecextras" } } diff --git a/build.gradle b/build.gradle index 24aeca7..d7c5267 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import dev.lukebemish.codecextras.gradle.FormatJmhOutput plugins { alias cLibs.plugins.conventions.java - alias cLibs.plugins.managedversioning + id 'dev.lukebemish.managedversioning' } group = 'dev.lukebemish' diff --git a/settings.gradle b/settings.gradle index 2714d91..89ea5b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ pluginManagement { plugins { id 'org.gradlex.extra-java-module-info' version '1.8' apply false + id 'dev.lukebemish.managedversioning' version '1.2.25' apply false id 'dev.lukebemish.conventions' version '0.1.11' id 'dev.lukebemish.multisource' version '0.2.2' } From 7207d172fa6b73ef3e85404d91dcef5cf123d7c6 Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Wed, 6 Nov 2024 13:13:03 -0600 Subject: [PATCH 70/76] Improve structures (#2) * Prevent annotation leaks * Add support to JsonSchemaInterpreter for _IN_RANGE keys * Give deterministic ordering to json schema definitions * Give Key and Key2 toString * Make Key2 implement App2 to match Key * Fix formatting * Fix some accidental mutation * Prevent recursive structures from bricking JsonSchemaInterpreter * Poke CI --------- Co-authored-by: Luke Bemish --- .../codecextras/structured/Key.java | 5 ++ .../codecextras/structured/Key2.java | 19 ++++- .../schema/JsonSchemaInterpreter.java | 83 ++++++++++++------- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key.java b/src/main/java/dev/lukebemish/codecextras/structured/Key.java index 0485869..57517e0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Key.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key.java @@ -43,4 +43,9 @@ public static Key create(String name) { public String name() { return name; } + + @Override + public String toString() { + return "Key[" + name + "]"; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Key2.java b/src/main/java/dev/lukebemish/codecextras/structured/Key2.java index 6c99f4f..0ef47ae 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Key2.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Key2.java @@ -1,6 +1,18 @@ package dev.lukebemish.codecextras.structured; -public final class Key2 { +import com.mojang.datafixers.kinds.App2; +import com.mojang.datafixers.kinds.K2; + +public final class Key2 implements App2 { + public static final class Mu implements K2 { + private Mu() { + } + } + + public static Key2 unbox(App2 box) { + return (Key2) box; + } + private final String name; private Key2(String name) { @@ -15,4 +27,9 @@ public static Key2 create(String name) { public String name() { return name; } + + @Override + public String toString() { + return "Key2[" + name + "]"; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 249e75d..511cbc1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -5,6 +5,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; import com.mojang.serialization.DataResult; @@ -19,12 +20,15 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Keys2; import dev.lukebemish.codecextras.structured.ParametricKeyedValue; +import dev.lukebemish.codecextras.structured.Range; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.SequencedMap; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -69,6 +73,12 @@ public JsonSchemaInterpreter( })) .build() ), parametricKeys.join(Keys2., K1, K1>builder() + .add(Interpreter.INT_IN_RANGE, numberInRange(INTEGER)) + .add(Interpreter.BYTE_IN_RANGE, numberInRange(INTEGER)) + .add(Interpreter.SHORT_IN_RANGE, numberInRange(INTEGER)) + .add(Interpreter.LONG_IN_RANGE, numberInRange(INTEGER)) + .add(Interpreter.FLOAT_IN_RANGE, numberInRange(NUMBER)) + .add(Interpreter.DOUBLE_IN_RANGE, numberInRange(NUMBER)) .add(Interpreter.STRING_REPRESENTABLE, new ParametricKeyedValue<>() { @Override public App> convert(App parameter) { @@ -90,6 +100,19 @@ public App> convert(App> ParametricKeyedValue>, Const.Mu> numberInRange(Supplier base) { + return new ParametricKeyedValue<>() { + @Override + public App, T>> convert(App>, T> parameter) { + final var range = Const.unbox(parameter); + final var result = base.get(); + result.addProperty("minimum", range.min()); + result.addProperty("maximum", range.max()); + return new JsonSchemaInterpreter.Holder<>(result); + } + }; + } + @Override public JsonSchemaInterpreter with(Keys keys, Keys2, K1, K1> parametricKeys) { return new JsonSchemaInterpreter( @@ -120,7 +143,7 @@ public DataResult> record(List var object = OBJECT.get(); var properties = new JsonObject(); var required = new JsonArray(); - Map> definitions = new HashMap<>(); + var definitions = new LinkedHashMap>(); for (RecordStructure.Field field : fields) { Supplier error = singleField(field, properties, required, definitions); if (error != null) { @@ -175,21 +198,21 @@ public DataResult> flatXmap(App input, Fu @Override public DataResult> annotate(Structure input, Keys annotations) { JsonObject schema; - Map> definitions; + SequencedMap> definitions; var refName = Annotation.get(annotations, SchemaAnnotations.REUSE_KEY); if (refName.isPresent()) { schema = new JsonObject(); var ref = refName.get(); schema.addProperty("$ref", "#/$defs/"+ref); - definitions = new HashMap<>(); + definitions = new LinkedHashMap<>(); definitions.put(ref, input); } else { var result = input.interpret(this); if (result.error().isPresent()) { return DataResult.error(result.error().get().messageSupplier()); } - schema = schemaValue(result.result().orElseThrow()); - definitions = new HashMap<>(definitions(result.result().orElseThrow())); + schema = copy(schemaValue(result.result().orElseThrow())); + definitions = new LinkedHashMap<>(definitions(result.result().orElseThrow())); } Annotation.get(annotations, Annotation.PATTERN).ifPresent(pattern -> { @@ -207,7 +230,7 @@ public DataResult> annotate(Structure input, Keys DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keySchemaApp -> { - var definitions = new HashMap<>(definitions(keySchemaApp)); + var definitions = new LinkedHashMap<>(definitions(keySchemaApp)); var keySchema = schemaValue(keySchemaApp); JsonObject out = new JsonObject(); JsonObject properties = new JsonObject(); @@ -275,7 +298,7 @@ public DataResult> bounded(Structure input, Supplier DataResult>> unboundedMap(App key, App value) { var schema = OBJECT.get(); - var definitions = new HashMap>(); + var definitions = new LinkedHashMap>(); var keyJson = schemaValue(key); if (keyJson.has("pattern") && keyJson.get("pattern").isJsonPrimitive()) { // if the key has a pattern, we use "patternProperties" @@ -294,7 +317,7 @@ public DataResult>> unboundedMap(App DataResult>> either(App left, App right) { var schema = new JsonObject(); var anyOf = new JsonArray(); - var definitions = new HashMap>(); + var definitions = new LinkedHashMap>(); var leftSchema = schemaValue(left); var rightSchema = schemaValue(right); definitions.putAll(definitions(left)); @@ -309,7 +332,7 @@ public DataResult>> either(App public DataResult>> xor(App left, App right) { var schema = new JsonObject(); var oneOf = new JsonArray(); - var definitions = new HashMap>(); + var definitions = new LinkedHashMap>(); var leftSchema = schemaValue(left); var rightSchema = schemaValue(right); definitions.putAll(definitions(left)); @@ -322,7 +345,7 @@ public DataResult>> xor(App lef @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { - var definitions = new HashMap>(); + var definitions = new LinkedHashMap>(); return codecInterpreter.interpret(keyStructure).flatMap(keyCodec -> { var schema = OBJECT.get(); for (var key : keys.get()) { @@ -345,30 +368,30 @@ private static JsonObject schemaValue(App box) { return Holder.unbox(box).jsonObject; } - private static Map> definitions(App box) { + private static SequencedMap> definitions(App box) { return Holder.unbox(box).definition; } public DataResult interpret(Structure structure) { return structure.interpret(this).flatMap(holder -> { var object = copy(schemaValue(holder)); - var definitions = definitions(holder); + var definitions = new LinkedHashMap<>(definitions(holder)); var defsObject = new JsonObject(); - while (!definitions.isEmpty()) { - var newDefs = new HashMap>(); - for (Map.Entry> entry : definitions.entrySet()) { - if (defsObject.has(entry.getKey())) { - continue; - } - var result = entry.getValue().interpret(this); - if (result.error().isPresent()) { - return DataResult.error(result.error().get().messageSupplier()); - } - var schema = schemaValue(result.result().orElseThrow()); - defsObject.add(entry.getKey(), schema); - newDefs.putAll(definitions(result.result().orElseThrow())); + while (true) { + final var entry = definitions.pollFirstEntry(); + if (entry == null) break; + if (defsObject.has(entry.getKey())) { + continue; } - definitions = newDefs; + var result = entry.getValue().interpret(this); + if (result.error().isPresent()) { + return DataResult.error(result.error().get().messageSupplier()); + } + var schema = schemaValue(result.result().orElseThrow()); + defsObject.add(entry.getKey(), schema); + definitions.putAll(definitions(result.result().orElseThrow())); + // Remove already interpreted definitions to prevent infinite re-interpretation + definitions.keySet().removeAll(defsObject.keySet()); } if (!defsObject.isEmpty()) { object.add("$defs", defsObject); @@ -396,9 +419,11 @@ public App convert(App input) { ); } - public record Holder(JsonObject jsonObject, Map> definition) implements App { + public record Holder(JsonObject jsonObject, SequencedMap> definition) implements App { + private static final SequencedMap> NO_DEFINITIONS = Collections.unmodifiableSequencedMap(new LinkedHashMap<>()); + public Holder(JsonObject object) { - this(object, Map.of()); + this(object, NO_DEFINITIONS); } public Holder(Supplier objectCreator) { From fe005100be188c7c6ab7f568f423adabccaee6cc Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 6 Nov 2024 19:32:07 -0600 Subject: [PATCH 71/76] Update JsonSchemaInterpreter.java --- .../codecextras/structured/schema/JsonSchemaInterpreter.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 511cbc1..3039004 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -390,8 +390,6 @@ public DataResult interpret(Structure structure) { var schema = schemaValue(result.result().orElseThrow()); defsObject.add(entry.getKey(), schema); definitions.putAll(definitions(result.result().orElseThrow())); - // Remove already interpreted definitions to prevent infinite re-interpretation - definitions.keySet().removeAll(defsObject.keySet()); } if (!defsObject.isEmpty()) { object.add("$defs", defsObject); From 0874c02eb376c2f73ffc8f0d5c293df5f866773c Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 18 Nov 2024 00:31:40 -0600 Subject: [PATCH 72/76] Support for partial results --- gradle/libs.versions.toml | 10 ++-- settings.gradle | 2 + .../structured/StructuredMapCodec.java | 51 +++++++++++++++---- .../codecextras/test/CodecAssertions.java | 12 +++++ .../test/structured/TestPartialResults.java | 43 ++++++++++++++++ 5 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/TestPartialResults.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4a41b5..e2b5641 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -minecraft = "1.21.1" -neoforge = "21.1.31" -fabric_loader = "0.16.3" -fabric_api = "0.103.0+1.21.1" -modmenu = "11.0.2" +minecraft = "1.21.3" +neoforge = "21.3.31-beta" +fabric_loader = "0.16.9" +fabric_api = "0.108.0+1.21.3" +modmenu = "12.0.0-beta.1" [libraries] diff --git a/settings.gradle b/settings.gradle index 89ea5b0..2edd6f8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,6 +24,8 @@ pluginManagement { plugins { id 'org.gradlex.extra-java-module-info' version '1.8' apply false id 'dev.lukebemish.managedversioning' version '1.2.25' apply false + // TODO: switch this over to crochet as soon as possible + id 'dev.architectury.loom' version '1.7.414' apply false id 'dev.lukebemish.conventions' version '0.1.11' id 'dev.lukebemish.multisource' version '0.2.2' } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 9b388d4..666ab0a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -5,6 +5,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.Lifecycle; import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; @@ -13,11 +14,12 @@ import java.util.List; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; class StructuredMapCodec extends MapCodec { - private record Field(MapCodec codec, RecordStructure.Key key, Function getter) {} + private record Field(String name, MapCodec codec, RecordStructure.Key key, Function getter) {} private final List> fields; private final Function creator; @@ -50,7 +52,7 @@ public static DataResult> of(List fieldMapCodec = Annotation.get(field.structure().annotations(), Annotation.COMMENT) .map(comment -> CommentMapCodec.of(makeFieldCodec(fieldCodec, field, lenient), comment)) .orElseGet(() -> makeFieldCodec(fieldCodec, field, lenient)); - mapCodecFields.add(new StructuredMapCodec.Field<>(fieldMapCodec, field.key(), field.getter())); + mapCodecFields.add(new StructuredMapCodec.Field<>(field.name(), fieldMapCodec, field.key(), field.getter())); return null; } @@ -69,22 +71,45 @@ public Stream keys(DynamicOps ops) { @Override public DataResult decode(DynamicOps ops, MapLike input) { var builder = RecordStructure.Container.builder(); + boolean isPartial = false; + boolean isError = false; + Lifecycle errorLifecycle = Lifecycle.stable(); + Supplier errorMessage = null; for (var field : fields) { - DataResult result = singleField(ops, input, field, builder); - if (result != null) return result; + DataResult result = singleField(ops, input, field, builder); + if (result.isError()) { + if (result.hasResultOrPartial()) { + isPartial = true; + } + isError = true; + errorLifecycle = errorLifecycle.add(result.lifecycle()); + if (errorMessage == null) { + errorMessage = result.error().orElseThrow().messageSupplier(); + } else { + var oldMessage = errorMessage; + errorMessage = () -> oldMessage.get() + ": " + result.error().orElseThrow().messageSupplier().get(); + } + } + } + if (isError) { + if (isPartial) { + return DataResult.error(errorMessage, creator.apply(builder.build()), errorLifecycle); + } else { + return DataResult.error(errorMessage, errorLifecycle); + } + } else { + return DataResult.success(creator.apply(builder.build())); } - return DataResult.success(creator.apply(builder.build())); } - private static @Nullable DataResult singleField(DynamicOps ops, MapLike input, Field field, RecordStructure.Container.Builder builder) { + private static DataResult singleField(DynamicOps ops, MapLike input, Field field, RecordStructure.Container.Builder builder) { var key = field.key(); var codec = field.codec(); var result = codec.decode(ops, input); - if (result.error().isPresent()) { - return DataResult.error(result.error().orElseThrow().messageSupplier()); + if (result.hasResultOrPartial()) { + builder.add(key, result.resultOrPartial().orElseThrow()); } - builder.add(key, result.result().orElseThrow()); - return null; + return result; } @Override @@ -100,4 +125,10 @@ private RecordBuilder encodeSingleField(A input, DynamicOps ops, Re var value = field.getter().apply(input); return codec.encode(value, ops, prefix); } + + @Override + public String toString() { + var fields = this.fields.stream().map(f -> f.codec().toString()).reduce((a, b) -> a + ", " + b).orElse(""); + return "StructuredMapCodec[" + fields + "]"; + } } diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index 695ef0e..781e12f 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java +++ b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java @@ -23,6 +23,18 @@ public static void assertDecodes(DynamicOps ops, T data, O expected, C Assertions.assertEquals(expected, dataResult.result().get()); } + public static void assertDecodesOrPartial(DynamicOps jsonOps, String json, O expected, Codec codec) { + Gson gson = new GsonBuilder().create(); + JsonElement jsonElement = gson.fromJson(json, JsonElement.class); + assertDecodesOrPartial(jsonOps, jsonElement, expected, codec); + } + + public static void assertDecodesOrPartial(DynamicOps ops, T data, O expected, Codec codec) { + DataResult dataResult = codec.parse(ops, data); + Assertions.assertTrue(dataResult.resultOrPartial().isPresent(), () -> dataResult.error().orElseThrow().message()); + Assertions.assertEquals(expected, dataResult.resultOrPartial().get()); + } + public static void assertEncodes(DynamicOps jsonOps, O value, String json, Codec codec) { Gson gson = new GsonBuilder().create(); JsonElement jsonElement = gson.fromJson(json, JsonElement.class); diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/TestPartialResults.java b/src/test/java/dev/lukebemish/codecextras/test/structured/TestPartialResults.java new file mode 100644 index 0000000..37f179f --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/TestPartialResults.java @@ -0,0 +1,43 @@ +package dev.lukebemish.codecextras.test.structured; + +import static dev.lukebemish.codecextras.test.CodecAssertions.*; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +public class TestPartialResults { + @Test + void testPartialResults() { + final var codec = RecordCodecBuilder.>create( + instance -> instance.group( + Codec.STRING.listOf().fieldOf("example").forGetter(Function.identity()) + ).apply(instance, Function.identity()) + ); + + final var structure = Structure.>record(i -> + i.add("example", Structure.STRING.listOf(),Function.identity()) + ); + + final var json = """ + { + "example": [ + "abc", + 123, + "def" + ] + }"""; + final var expected = List.of("abc", "def"); + + assertDecodesOrPartial(JsonOps.INSTANCE, json, expected, codec); + assertDecodesOrPartial(JsonOps.INSTANCE, json, expected, CodecInterpreter.create() + .interpret(structure) + .getOrThrow() + ); + } +} From 58d99b6de04383df00616a6bff9bcf6decc46409 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 18 Nov 2024 00:42:35 -0600 Subject: [PATCH 73/76] Fix ColorPickScreen issue --- .../minecraft/structured/config/ColorPickScreen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java index 307c11f..7496419 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickScreen.java @@ -4,7 +4,6 @@ import com.mojang.logging.LogUtils; import java.util.Locale; import java.util.function.Consumer; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; @@ -86,7 +85,7 @@ protected void repositionElements() { public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { this.backgroundScreen.render(guiGraphics, -1, -1, f); guiGraphics.flush(); - RenderSystem.clear(256, Minecraft.ON_OSX); + RenderSystem.clear(256); this.renderTransparentBackground(guiGraphics); } From 6d418cb0ee603206d0219aa36dcd3be32633349e Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 18 Nov 2024 20:28:26 -0600 Subject: [PATCH 74/76] Various changes for MC 1.21.3 --- .../codecextras/config/ConfigType.java | 9 +++ .../structured/CodecExtrasRegistries.java | 2 +- .../structured/MinecraftInterpreters.java | 2 +- .../minecraft/structured/MinecraftKeys.java | 10 ++-- .../structured/MinecraftStructures.java | 60 +++++++++++++------ .../structured/config/ColorPickWidget.java | 17 +++--- .../config/ConfigScreenInterpreter.java | 55 ++++++++--------- .../minecraft/structured/config/Widgets.java | 14 +---- 8 files changed, 98 insertions(+), 71 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java b/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java index b737396..3eee0e4 100644 --- a/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java +++ b/src/main/java/dev/lukebemish/codecextras/config/ConfigType.java @@ -154,6 +154,15 @@ public O load(Path location, OpsIo opsIo, Logger logger) { try (var is = Files.newInputStream(location)) { var out = decode(location.toString(), opsIo.ops(), opsIo.read(is), logger); if (out.error().isPresent()) { + if (out.hasResultOrPartial()) { + var orPartial = out.resultOrPartial().orElseThrow(); + var reEncoded = codec().encodeStart(opsIo.ops(), orPartial).flatMap(t -> codec().decode(opsIo.ops(), t)); + if (reEncoded.isSuccess()) { + logger.warn("Could not load config {}; attempting to fix by writing partial config. Error was {}", location, out.error().get().message()); + save(location, opsIo, logger, out.resultOrPartial().orElseThrow()); + return orPartial; + } + } logger.error("Could not load config {}; attempting to fix by writing default config. Error was {}", location, out.error().get().message()); save(location, opsIo, logger, defaultConfig()); return defaultConfig(); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java index a64d5d7..af206c4 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/CodecExtrasRegistries.java @@ -41,7 +41,7 @@ public PreparableView>>> dataC @SuppressWarnings("unchecked") public final Preparable>>> dataComponentStructures = Preparable.memoize(() -> - (Registry>>) Objects.requireNonNull(BuiltInRegistries.REGISTRY.get(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES.location()), "Registry does not exist")); + (Registry>>) Objects.requireNonNull(BuiltInRegistries.REGISTRY.getValue(CodecExtrasRegistries.DATA_COMPONENT_STRUCTURES.location()), "Registry does not exist")); } void setup(RegistriesImpl registries); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java index 5bb3c1a..8fbfb83 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftInterpreters.java @@ -69,7 +69,7 @@ private MinecraftInterpreters() {} public static final Keys JSON_SCHEMA_KEYS = Keys.builder() .build(); - // TODO: Add regex fo schemas + // TODO: Add regex for schemas public static final Keys2, K1, K1> JSON_SCHEMA_PARAMETRIC_KEYS = Keys2., K1, K1>builder() .build(); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java index 00ad089..a00b8cb 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftKeys.java @@ -6,7 +6,6 @@ import dev.lukebemish.codecextras.structured.Key2; import dev.lukebemish.codecextras.types.Identity; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; @@ -20,7 +19,6 @@ import net.minecraft.world.item.ItemStack; public final class MinecraftKeys { - private static final Map>, Key> DATA_COMPONENT_TYPE_KEYS = new ConcurrentHashMap<>(); public static Key, Object>> VALUE_MAP = Key.create("value_map"); public static Key DATA_COMPONENT_MAP = Key.create("data_component_map"); public static Key DATA_COMPONENT_PATCH = Key.create("data_component_patch"); @@ -33,10 +31,12 @@ private MinecraftKeys() { public static final Key ARGB_COLOR = Key.create("argb_color"); public static final Key RGB_COLOR = Key.create("rgb_color"); - public static final Key> ITEM_NON_AIR = Key.create("item_non_air"); + public static final Key> ITEM = Key.create("item_non_air"); public static final Key OPTIONAL_ITEM_STACK = Key.create("optional_item_stack"); - public static final Key NON_EMPTY_ITEM_STACK = Key.create("non_empty_item_stack"); - public static final Key STRICT_NON_EMPTY_ITEM_STACK = Key.create("strict_non_empty_item_stack"); + public static final Key ITEM_STACK = Key.create("item_stack"); + public static final Key STRICT_ITEM_STACK = Key.create("strict_item_stack"); + public static final Key SINGLE_ITEM_STACK = Key.create("single_item_item_stack"); + public static final Key STRICT_SINGLE_ITEM_STACK = Key.create("strict_single_item_item_stack"); public record DataComponentTypeHolder(DataComponentType value) implements App { public static final class Mu implements K1 { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java index 40eeab1..fac8b4f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftStructures.java @@ -67,7 +67,7 @@ private MinecraftStructures() { private static final Structure, Object>> DATA_COMPONENT_VALUE_MAP_FALLBACK = resourceKey(Registries.DATA_COMPONENT_TYPE) .>flatXmap(key -> { - var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(key); + var type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(key); if (type == null) { return DataResult.error(() -> "Unknown data component type: " + key); } @@ -109,7 +109,7 @@ private MinecraftStructures() { boolean removes = string.startsWith("!"); string = removes ? string.substring(1) : string; return ResourceLocation.read(string).flatMap(rl -> { - var type = BuiltInRegistries.DATA_COMPONENT_TYPE.get(rl); + var type = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(rl); if (type == null) { return DataResult.error(() -> "Unknown data component type: " + rl); } @@ -158,23 +158,23 @@ private MinecraftStructures() { ); @SuppressWarnings("deprecation") - public static final Structure> ITEM_NON_AIR = Structure.keyed( - MinecraftKeys.ITEM_NON_AIR, + public static final Structure> ITEM = Structure.keyed( + MinecraftKeys.ITEM, Keys.>, K1>builder() - .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.ITEM_NON_AIR_CODEC))) + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(Item.CODEC))) .build(), registryOrderedHolder(BuiltInRegistries.ITEM) .validate(holder -> holder.is(Items.AIR.builtInRegistryHolder()) ? DataResult.error(() -> "Item must not be minecraft:air") : DataResult.success(holder)) ); - public static final Structure NON_EMPTY_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( - MinecraftKeys.NON_EMPTY_ITEM_STACK, + public static final Structure ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.ITEM_STACK, Keys., K1>builder() .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.CODEC))) .add(StreamCodecInterpreter.REGISTRY_FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>(ItemStack.STREAM_CODEC))) .build(), Structure.record(builder -> { - var item = builder.add("id", ITEM_NON_AIR, ItemStack::getItemHolder); + var item = builder.add("id", ITEM, ItemStack::getItemHolder); var count = builder.addOptional("count", Structure.intInRange(1, 99), ItemStack::getCount, () -> 1); var patch = builder.addOptional("components", DATA_COMPONENT_PATCH, ItemStack::getComponentsPatch, () -> DataComponentPatch.EMPTY); return container -> new ItemStack( @@ -185,6 +185,22 @@ private MinecraftStructures() { }) )); + public static final Structure SINGLE_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.SINGLE_ITEM_STACK, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.SINGLE_ITEM_CODEC))) + .build(), + Structure.record(builder -> { + var item = builder.add("id", ITEM, ItemStack::getItemHolder); + var patch = builder.addOptional("components", DATA_COMPONENT_PATCH, ItemStack::getComponentsPatch, () -> DataComponentPatch.EMPTY); + return container -> new ItemStack( + item.apply(container), + 1, + patch.apply(container) + ); + }) + )); + public static final Structure OPTIONAL_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( MinecraftKeys.OPTIONAL_ITEM_STACK, Keys., K1>builder() @@ -193,24 +209,32 @@ private MinecraftStructures() { .build(), Structure.either( Structure.EMPTY_MAP, - NON_EMPTY_ITEM_STACK + ITEM_STACK ) .xmap(e -> e.map(u -> ItemStack.EMPTY, Function.identity()), itemStack -> itemStack.isEmpty() ? Either.left(Unit.INSTANCE) : Either.right(itemStack)) )); - public static final Structure STRICT_NON_EMPTY_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( - MinecraftKeys.STRICT_NON_EMPTY_ITEM_STACK, + public static final Structure STRICT_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.STRICT_ITEM_STACK, Keys., K1>builder() .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.STRICT_CODEC))) .build(), - NON_EMPTY_ITEM_STACK.validate(MinecraftStructures::validateItemStackStrict) + ITEM_STACK.validate(MinecraftStructures::validateItemStackStrict) + )); + + public static final Structure STRICT_SINGLE_ITEM_STACK = Structure.lazyInitialized(() -> Structure.keyed( + MinecraftKeys.STRICT_SINGLE_ITEM_STACK, + Keys., K1>builder() + .add(CodecInterpreter.KEY, new Flip<>(new CodecInterpreter.Holder<>(ItemStack.STRICT_SINGLE_ITEM_CODEC))) + .build(), + SINGLE_ITEM_STACK.validate(MinecraftStructures::validateItemStackStrict) )); private static DataResult validateItemStackStrict(ItemStack itemStack) { return ItemStack.validateComponents(itemStack.getComponents()) .flatMap( u -> itemStack.getCount() > itemStack.getMaxStackSize() ? - DataResult.error(() -> "Item stack with stack size of " + itemStack.getCount() + " was larger than maximum: " + itemStack.getMaxStackSize()) : + DataResult.error(() -> "Item stack with stack size of " + itemStack.getCount() + " was larger than maximum: " + itemStack.getMaxStackSize(), itemStack) : DataResult.success(itemStack) ); } @@ -260,7 +284,7 @@ public static Structure> registryOrderedHolder(Registry registr .build(), resourceKey(registry.key()) .bounded(registry::registryKeySet) - .flatXmap(key -> registry.getHolder(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> + .flatXmap(key -> registry.get(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> keyForEntry(holder, registry).map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + holder))) .xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); @@ -282,7 +306,7 @@ public static Structure> registryUnorderedHolder(Registry regis StreamCodecInterpreter.FRIENDLY_BYTE_BUF_KEY, new Flip<>(new StreamCodecInterpreter.Holder<>( ResourceLocation.STREAM_CODEC.>map( - rl -> registry.getHolder(rl).orElseThrow(() -> new DecoderException("Unknown registry entry: " + rl)), + rl -> registry.get(rl).orElseThrow(() -> new DecoderException("Unknown registry entry: " + rl)), holder -> keyForEntry(holder, registry).orElseThrow(() -> new EncoderException("Unknown registry entry: " + holder)).location() ).cast().map(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) )) @@ -290,7 +314,7 @@ public static Structure> registryUnorderedHolder(Registry regis .build(), resourceKey(registry.key()) .bounded(registry::registryKeySet) - .flatXmap(key -> registry.getHolder(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> + .flatXmap(key -> registry.get(key).>>map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + key)), holder -> keyForEntry(holder, registry).map(DataResult::success).orElse(DataResult.error(() -> "Unknown registry entry: " + holder))) .xmap(MinecraftKeys.HolderHolder::new, MinecraftKeys.HolderHolder::value) ).xmap(MinecraftKeys.HolderHolder::value, MinecraftKeys.HolderHolder::new); @@ -345,7 +369,7 @@ public static Structure> tagKey(ResourceKey> public static Structure registryDispatch(String keyField, Function>>> structureFunction, Registry> registry) { var keyStructure = resourceKey(registry.key()); return keyStructure.dispatch(keyField, structureFunction, registry::registryKeySet, k -> - DataResult.success(registry.getOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry()) + "::" + toDefsKey(k.location()))) + DataResult.success(registry.getValueOrThrow(k).annotate(SchemaAnnotations.REUSE_KEY, toDefsKey(k.registry()) + "::" + toDefsKey(k.location()))) ); } @@ -361,7 +385,7 @@ private static DataResult> dataComponentTypeStructure(DataComponent if (!CodecExtrasRegistries.REGISTRIES.dataComponentStructures().isReady()) { return DataResult.error(() -> "Data component structures registry is not frozen"); } - var structure = CodecExtrasRegistries.REGISTRIES.dataComponentStructures().get().get(resourceKey.orElseThrow().location()); + var structure = CodecExtrasRegistries.REGISTRIES.dataComponentStructures().get().getValue(resourceKey.orElseThrow().location()); return fallbackDataComponentTypeStructure(type, structure); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java index 382370a..2f4eed6 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ColorPickWidget.java @@ -138,12 +138,13 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { guiGraphics.renderOutline(getX() + 128 + 8 * 3 + 2 * 2, getY(), 8 + 2, 128 + 2, BORDER_COLOR); } Matrix4f matrix4f = guiGraphics.pose().last().pose(); - VertexConsumer vertexConsumer = guiGraphics.bufferSource().getBuffer(RenderType.gui()); - vertexConsumer.addVertex(matrix4f, x1, y1, 0).setColor(0xFFFFFFFF); - vertexConsumer.addVertex(matrix4f, x1, y2, 0).setColor(0xFFFFFFFF); - vertexConsumer.addVertex(matrix4f, x2, y2, 0).setColor(fullySaturated); - vertexConsumer.addVertex(matrix4f, x2, y1, 0).setColor(fullySaturated); - guiGraphics.flush(); + guiGraphics.drawSpecial(bufferSource -> { + VertexConsumer vertexConsumer = bufferSource.getBuffer(RenderType.gui()); + vertexConsumer.addVertex(matrix4f, x1, y1, 0).setColor(0xFFFFFFFF); + vertexConsumer.addVertex(matrix4f, x1, y2, 0).setColor(0xFFFFFFFF); + vertexConsumer.addVertex(matrix4f, x2, y2, 0).setColor(fullySaturated); + vertexConsumer.addVertex(matrix4f, x2, y1, 0).setColor(fullySaturated); + }); guiGraphics.fillGradient(x1, y1, x2, y2, 0, 0xFF000000); guiGraphics.enableScissor(x1, y1, x2, y2); @@ -153,14 +154,14 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { guiGraphics.fill(xCenter, yCenter-2, xCenter+1, yCenter+3, inverted); guiGraphics.disableScissor(); - guiGraphics.blitSprite(HUE, x1+128+8+2, y1, 8, 128); + guiGraphics.blitSprite(RenderType::guiTextured, HUE, x1+128+8+2, y1, 8, 128); guiGraphics.fill(x1+128+8+2, y1+(int)(hue*127), x1+128+8*2+2, y1+(int)(hue*127)+1, 0xFF000000); var ax1 = x1 + 128 + 8 * 3 + 2 * 2; var ax2 = ax1 + 8; if (this.hasAlpha) { - guiGraphics.blitSprite(TRANSPARENT, ax1, y1, 8, 128); + guiGraphics.blitSprite(RenderType::guiTextured, TRANSPARENT, ax1, y1, 8, 128); guiGraphics.fillGradient(ax1, y1, ax2, y2, 0x00000000, color | 0xFF000000); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 7b3131d..a73ec23 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -36,19 +36,6 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; @@ -66,10 +53,24 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * Interprets a {@link Structure} into a {@link ConfigScreenEntry} for the same type. Note that interpreting a structure * with this interpreter will require resolving any lazy aspects of the structure, such as bounds, so should not be done @@ -173,9 +174,9 @@ public ConfigScreenInterpreter( Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), new EntryCreationInfo<>(Codec.PASSTHROUGH, ComponentInfo.empty()) )) - .add(MinecraftKeys.ITEM_NON_AIR, ConfigScreenEntry.single( + .add(MinecraftKeys.ITEM, ConfigScreenEntry.single( Widgets.pickWidget(new StringRepresentation<>( - () -> BuiltInRegistries.ITEM.holders().>map(Function.identity()).toList(), + () -> BuiltInRegistries.ITEM.listElements().filter(e -> e.value() != Items.AIR).>map(Function.identity()).toList(), holder -> { if (holder instanceof Holder.Reference reference) { return reference.key().location().toString(); @@ -190,11 +191,11 @@ public ConfigScreenInterpreter( return null; } var key = rlResult.getOrThrow(); - return BuiltInRegistries.ITEM.getHolder(key).orElse(null); + return BuiltInRegistries.ITEM.get(key).orElse(null); }, false )), - new EntryCreationInfo<>(ItemStack.ITEM_NON_AIR_CODEC, ComponentInfo.empty()) + new EntryCreationInfo<>(Item.CODEC, ComponentInfo.empty()) )) .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("^([a-z0-9._-]+:)?[a-z0-9/._-]*$"), false)), @@ -436,7 +437,7 @@ public App> var holderCodec = codec.>xmap(MinecraftKeys.ResourceKeyHolder::new, app -> MinecraftKeys.ResourceKeyHolder.unbox(app).value()); return ConfigScreenEntry.single( (parent, width, context, original, update, creationInfo, handleOptional) -> { - var registry = context.registryAccess().registry(registryKey); + var registry = context.registryAccess().lookup(registryKey); LayoutFactory> wrapped; Function, String> mapper = key -> key.location().toString(); if (registry.isPresent()) { @@ -521,7 +522,7 @@ private static LayoutElement byJson(Screen parentOuter, int widthOuter, Entr ComponentInfo.empty() ); final EntryCreationInfo numberInfo = new EntryCreationInfo<>( - BIG_DECIMAL_CODEC.xmap(Function.identity(), number -> number instanceof BigDecimal bigDecimal ? bigDecimal : new BigDecimal(number.toString())), + BIG_DECIMAL_CODEC.xmap(Function.identity(), number -> number instanceof BigDecimal bigDecimal ? bigDecimal : new BigDecimal(number.toString())), ComponentInfo.empty() ); final EntryCreationInfo booleanInfo = new EntryCreationInfo<>( @@ -644,7 +645,7 @@ enum JsonType { }; private final EntryCreationContext.ProblemMarker[] problems = new EntryCreationContext.ProblemMarker[1]; private final Consumer checkedUpdate = newJsonValue -> creationInfo.codec().parse(context.ops(), newJsonValue).ifError(error -> { - problems[0] = context.problem(problems[0], "Coult not encode: "+error.message()); + problems[0] = context.problem(problems[0], "Could not encode: "+error.message()); }).ifSuccess(json -> { context.resolve(problems[0]); update.accept(json); @@ -661,14 +662,14 @@ enum JsonType { private final Map layouts = new EnumMap<>(JsonType.class); { - layouts.put(JsonType.OBJECT, Button.builder(Component.translatable("codecextras.config.configurerecord"), b -> { + layouts.put(JsonType.OBJECT, VisibilityWrapperElement.ofDirect(Button.builder(Component.translatable("codecextras.config.configurerecord"), b -> { Minecraft.getInstance().setScreen(ScreenEntryProvider.create( new UnboundedMapScreenEntryProvider<>(stringEntry, jsonEntry, context, elements.get(JsonType.OBJECT), newJsonValue -> { elements.put(JsonType.OBJECT, newJsonValue); checkedUpdate.accept(newJsonValue); }), parent, context, creationInfo.componentInfo() )); - }).width(remainingWidth).build()); + }).width(remainingWidth).build())); layouts.put(JsonType.ARRAY, Button.builder(Component.translatable("codecextras.config.configurelist"), b -> { Minecraft.getInstance().setScreen(ScreenEntryProvider.create( new ListScreenEntryProvider<>(jsonEntry, context, elements.get(JsonType.ARRAY), newJsonValue -> { @@ -680,19 +681,19 @@ enum JsonType { layouts.put(JsonType.STRING, stringEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.STRING), newJsonValue -> { elements.put(JsonType.STRING, newJsonValue); checkedUpdate.accept(newJsonValue); - }, outerEntryHolder.stringInfo, handleOptional)); + }, outerEntryHolder.stringInfo, false)); layouts.put(JsonType.NUMBER, numberEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.NUMBER), newJsonValue -> { elements.put(JsonType.NUMBER, newJsonValue); checkedUpdate.accept(newJsonValue); - }, outerEntryHolder.numberInfo, handleOptional)); + }, outerEntryHolder.numberInfo, false)); layouts.put(JsonType.BOOLEAN, booleanEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.BOOLEAN), newJsonValue -> { elements.put(JsonType.BOOLEAN, newJsonValue); checkedUpdate.accept(newJsonValue); - }, outerEntryHolder.booleanInfo, handleOptional)); + }, outerEntryHolder.booleanInfo, false)); layouts.put(JsonType.RAW, outerEntryHolder.rawJsonEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.RAW), newJsonValue -> { elements.put(JsonType.RAW, newJsonValue); checkedUpdate.accept(newJsonValue); - }, outerEntryHolder.jsonInfo, handleOptional)); + }, outerEntryHolder.jsonInfo, false)); onTypeUpdate.run(); } }; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 08f714d..82718a5 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -28,6 +28,7 @@ import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.LayoutSettings; import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.RenderType; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; @@ -250,7 +251,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int i, int j, float f) { int endY = getY() + getHeight()/2 + rectangleHeight/2; int endX = getX() + getWidth() - (startX - getX()); if (includeAlpha) { - guiGraphics.blitSprite(TRANSPARENT, startX, startY, endX - startX, endY - startY); + guiGraphics.blitSprite(RenderType::guiTextured, TRANSPARENT, startX, startY, endX - startX, endY - startY); } guiGraphics.fill(startX, startY, endX, endY, includeAlpha ? value[0] : value[0] | 0xFF000000); } @@ -468,15 +469,6 @@ public static LayoutFactory bool() { w.setMessage(creationInfo.componentInfo().title()); return w; }; - return (parent, width, context, original, update, entry, handleOptional) -> { - if (handleOptional) { - return widget.create(parent, width, context, original, update, entry, true); - } - var button = Button.builder(Component.translatable("codecextras.config.unit"), b -> {}) - .width(width) - .build(); - button.active = false; - return VisibilityWrapperElement.ofInactive(button); - }; + return wrapWithOptionalHandling(widget); } } From b650f3eb669e93774444ae7ebc581f7409efb598 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 18 Nov 2024 20:41:03 -0600 Subject: [PATCH 75/76] Style fixes --- .../config/ConfigScreenInterpreter.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index a73ec23..6872a69 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -36,6 +36,19 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; @@ -57,20 +70,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - /** * Interprets a {@link Structure} into a {@link ConfigScreenEntry} for the same type. Note that interpreting a structure * with this interpreter will require resolving any lazy aspects of the structure, such as bounds, so should not be done From 4bdbd5958367e685f657f75fca8c7fcfda0acbe6 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 18 Nov 2024 20:55:49 -0600 Subject: [PATCH 76/76] Updates to build --- .github/workflows/benchmark.yml | 4 ++-- .github/workflows/build_pr.yml | 4 ++-- .github/workflows/release.yml | 29 +++++++++++++++++++++-------- .github/workflows/snapshot.yml | 4 ++-- build.gradle | 27 ++++++++++++++++----------- settings.gradle | 2 +- 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 4fef386..6e1a308 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,7 @@ }, { "name": "Validate Gradle Wrapper", - "uses": "gradle/actions/wrapper-validation@v3" + "uses": "gradle/actions/wrapper-validation@v4" }, { "with": { @@ -34,7 +34,7 @@ "gradle-home-cache-cleanup": true }, "name": "Setup Gradle", - "uses": "gradle/actions/setup-gradle@v3" + "uses": "gradle/actions/setup-gradle@v4" }, { "name": "JMH", diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 7d73a7f..26f789e 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -17,7 +17,7 @@ }, { "name": "Validate Gradle Wrapper", - "uses": "gradle/actions/wrapper-validation@v3" + "uses": "gradle/actions/wrapper-validation@v4" }, { "with": { @@ -34,7 +34,7 @@ "gradle-home-cache-cleanup": true }, "name": "Setup Gradle", - "uses": "gradle/actions/setup-gradle@v3" + "uses": "gradle/actions/setup-gradle@v4" }, { "name": "Build", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be1e132..24d8eca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ }, { "name": "Validate Gradle Wrapper", - "uses": "gradle/actions/wrapper-validation@v3" + "uses": "gradle/actions/wrapper-validation@v4" }, { "with": { @@ -38,7 +38,7 @@ "gradle-home-cache-cleanup": true }, "name": "Setup Gradle", - "uses": "gradle/actions/setup-gradle@v3" + "uses": "gradle/actions/setup-gradle@v4" }, { "uses": "fregante/setup-git-user@v2" @@ -80,6 +80,15 @@ "name": "Capture Recorded Version", "run": "echo version=$(cat build/recordVersion.txt) >> \"$GITHUB_OUTPUT\"", "id": "record_version_capture_version" + }, + { + "name": "Submit Dependencies", + "uses": "gradle/actions/dependency-submission@v4", + "env": { + "BUILD_CACHE_PASSWORD": "${{ secrets.BUILD_CACHE_PASSWORD }}", + "BUILD_CACHE_USER": "${{ secrets.BUILD_CACHE_USER }}", + "BUILD_CACHE_URL": "${{ secrets.BUILD_CACHE_URL }}" + } } ] }, @@ -104,7 +113,7 @@ }, { "name": "Validate Gradle Wrapper", - "uses": "gradle/actions/wrapper-validation@v3" + "uses": "gradle/actions/wrapper-validation@v4" }, { "with": { @@ -121,19 +130,23 @@ "gradle-home-cache-cleanup": true }, "name": "Setup Gradle", - "uses": "gradle/actions/setup-gradle@v3" + "uses": "gradle/actions/setup-gradle@v4" }, { "name": "Publish", - "run": "./gradlew publish", + "run": "./gradlew publish closeAndReleaseSonatypeStagingRepository", "id": "publish", "env": { "BUILD_CACHE_PASSWORD": "${{ secrets.BUILD_CACHE_PASSWORD }}", "BUILD_CACHE_USER": "${{ secrets.BUILD_CACHE_USER }}", "BUILD_CACHE_URL": "${{ secrets.BUILD_CACHE_URL }}", - "RELEASE_MAVEN_PASSWORD": "${{ secrets.RELEASE_MAVEN_PASSWORD }}", - "RELEASE_MAVEN_USER": "github", - "RELEASE_MAVEN_URL": "https://maven.lukebemish.dev/releases/" + "GPG_KEY": "${{ secrets.GPG_KEY }}", + "GPG_PASSWORD": "${{ secrets.GPG_PASSWORD }}", + "SONATYPE_PASSWORD": "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USER": "${{ secrets.SONATYPE_USER }}", + "STAGING_MAVEN_PASSWORD": "${{ secrets.STAGING_MAVEN_PASSWORD }}", + "STAGING_MAVEN_USER": "github", + "STAGING_MAVEN_URL": "https://maven.lukebemish.dev/staging/" } } ] diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 863f3ca..5bd718a 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -17,7 +17,7 @@ }, { "name": "Validate Gradle Wrapper", - "uses": "gradle/actions/wrapper-validation@v3" + "uses": "gradle/actions/wrapper-validation@v4" }, { "with": { @@ -33,7 +33,7 @@ "gradle-home-cache-cleanup": true }, "name": "Setup Gradle", - "uses": "gradle/actions/setup-gradle@v3" + "uses": "gradle/actions/setup-gradle@v4" }, { "name": "Build", diff --git a/build.gradle b/build.gradle index d7c5267..848594f 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ managedVersioning { run.set 'git push && git push --tags' } recordVersion 'Record Version', 'version' + dependencySubmission() } gradleJob { buildCache() @@ -65,8 +66,10 @@ managedVersioning { name.set 'publish' needs.add('build') tag.set('${{needs.build.outputs.version}}') - gradlew 'Publish', 'publish' - mavenRelease('github') + gradlew 'Publish', 'publish', 'closeAndReleaseSonatypeStagingRepository' + sign() + mavenCentral() + mavenStaging('github') } } build_pr { @@ -139,6 +142,9 @@ java { capability(project.group as String, "$project.name-minecraft-common", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) + + withJavadocJar() + withSourcesJar() } registerFeature("minecraftFabric") { usingSourceSet sourceSets.minecraftFabric @@ -147,6 +153,9 @@ java { // Old name capability(project.group as String, "$project.name-stream", project.version as String) capability(project.group as String, "$project.name-stream-intermediary", project.version as String) + + withJavadocJar() + withSourcesJar() } registerFeature("minecraftNeoforge") { usingSourceSet sourceSets.minecraftNeoforge @@ -154,6 +163,9 @@ java { capability(project.group as String, "$project.name-minecraft-neoforge", project.version as String) // Old name capability(project.group as String, "$project.name-stream", project.version as String) + + withJavadocJar() + withSourcesJar() } } @@ -278,14 +290,6 @@ tasks.register('jmhResults', FormatJmhOutput) { formattedResults.set project.file('build/reports/jmh/results.md') } -tasks.named('remapMinecraftFabricJar', dev.lukebemish.multisource.CopyArchiveFileTask) { task -> - task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-intermediary.jar") -} - -tasks.named('minecraftNeoforgeJar', Jar) { task -> - task.archiveFile.set project.layout.buildDirectory.file("libs/${project.name}-${project.version}-minecraft-neoforge.jar") -} - tasks.compileJava { options.compilerArgs += [ '-Aautoextension.name=CodecExtras', @@ -327,6 +331,7 @@ publishing { } } -managedVersioning.publishing.mavenRelease(publishing) +managedVersioning.publishing.mavenStaging(publishing) +managedVersioning.publishing.mavenCentral() managedVersioning.publishing.mavenPullRequest(publishing) managedVersioning.publishing.mavenSnapshot(publishing) diff --git a/settings.gradle b/settings.gradle index 2edd6f8..daafd2f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id 'org.gradlex.extra-java-module-info' version '1.8' apply false - id 'dev.lukebemish.managedversioning' version '1.2.25' apply false + id 'dev.lukebemish.managedversioning' version '1.2.26' apply false // TODO: switch this over to crochet as soon as possible id 'dev.architectury.loom' version '1.7.414' apply false id 'dev.lukebemish.conventions' version '0.1.11'

parent) { + builder.join(parent.keys().map(new Keys.Converter<>() { + @Override + public App, A> convert(App, A> input) { + return new Holder<>(unbox(input).cast()); + } + })); + } + + private static