diff --git a/src/main/java/io/wispforest/owo/serialization/Deserializer.java b/src/main/java/io/wispforest/owo/serialization/Deserializer.java index 656f65e4..73308a4b 100644 --- a/src/main/java/io/wispforest/owo/serialization/Deserializer.java +++ b/src/main/java/io/wispforest/owo/serialization/Deserializer.java @@ -1,9 +1,12 @@ package io.wispforest.owo.serialization; import io.wispforest.owo.serialization.impl.SerializationAttribute; +import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; public interface Deserializer { @@ -33,6 +36,8 @@ public interface Deserializer { long readVarLong(); + V tryRead(Function, V> func); + SequenceDeserializer sequence(Endec elementEndec); MapDeserializer map(Endec valueEndec); diff --git a/src/main/java/io/wispforest/owo/serialization/Endec.java b/src/main/java/io/wispforest/owo/serialization/Endec.java index 354dde11..52793a46 100644 --- a/src/main/java/io/wispforest/owo/serialization/Endec.java +++ b/src/main/java/io/wispforest/owo/serialization/Endec.java @@ -2,6 +2,7 @@ import com.google.gson.*; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import io.wispforest.owo.serialization.impl.*; import io.wispforest.owo.serialization.impl.nbt.NbtEndec; import io.wispforest.owo.serialization.impl.json.JsonEndec; @@ -10,8 +11,11 @@ import net.minecraft.nbt.*; import net.minecraft.network.PacketByteBuf; import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.tag.TagKey; import net.minecraft.text.Text; import net.minecraft.util.Identifier; +import net.minecraft.util.InvalidIdentifierException; import net.minecraft.util.Uuids; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.ChunkPos; @@ -139,6 +143,31 @@ static Endec ofRegistry(Registry registry) { return Endec.IDENTIFIER.then(registry::get, registry::getId); } + static Endec> unprefixedTagKey(RegistryKey> registry) { + return IDENTIFIER.then(id -> TagKey.of(registry, id), TagKey::id); + } + + static Endec> tagKey(RegistryKey> registry) { + return Endec.STRING + .validate(s -> { + if(!s.startsWith("#")) throw new IllegalStateException("Not a tag id"); + + var id = s.substring(1); + + try { + if(!Identifier.isValid(id)) throw new IllegalStateException("Not a valid resource location: " + id); + } catch (InvalidIdentifierException var2) { + throw new IllegalStateException("Not a valid resource location: " + id + " " + var2.getMessage()); + } + + return s; + }) + .then( + s -> TagKey.of(registry, new Identifier(s.substring(1))), + tag -> "#" + tag.id() + ); + } + static Endec dispatchedOf(Function> keyToEndec, Function keyGetter, Endec keyEndec) { return new StructEndec() { @Override @@ -204,6 +233,10 @@ public R decode(Deserializer deserializer) { }; } + default StructField field(String name, Function getter){ + return StructField.of(name, this, getter); + } + default Endec> ofOptional(){ return new Endec<>() { @Override @@ -222,6 +255,20 @@ public Optional decode(Deserializer deserializer) { return ofOptional().then(o -> o.orElse(null), Optional::ofNullable); } + default Endec validate(Function validator){ + return new Endec() { + @Override + public void encode(Serializer serializer, T value) { + Endec.this.encode(serializer, value); + } + + @Override + public T decode(Deserializer deserializer) { + return validator.apply(Endec.this.decode(deserializer)); + } + }; + } + default Endec onError(TriConsumer encode, BiFunction decode){ return new Endec<>() { @Override @@ -236,7 +283,7 @@ public void encode(Serializer serializer, T value) { @Override public T decode(Deserializer deserializer) { try { - return Endec.this.decode(deserializer); + return deserializer.tryRead(Endec.this::decode); } catch (Exception e) { return decode.apply(deserializer, e); } diff --git a/src/main/java/io/wispforest/owo/serialization/endecs/EitherEndec.java b/src/main/java/io/wispforest/owo/serialization/endecs/EitherEndec.java new file mode 100644 index 00000000..be8387fa --- /dev/null +++ b/src/main/java/io/wispforest/owo/serialization/endecs/EitherEndec.java @@ -0,0 +1,84 @@ +package io.wispforest.owo.serialization.endecs; + +import com.mojang.datafixers.util.Either; +import io.wispforest.owo.serialization.Deserializer; +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.Serializer; +import io.wispforest.owo.serialization.impl.SerializationAttribute; + +import java.util.Objects; +import java.util.Optional; + +public record EitherEndec(Endec first, Endec second) implements Endec> { + + @Override + public void encode(Serializer serializer, Either either) { + boolean selfDescribing = serializer.attributes().contains(SerializationAttribute.SELF_DESCRIBING); + + if(!selfDescribing){ + either.ifLeft(left -> { + try(var struct = serializer.struct()) { + struct.field("side", Endec.VAR_INT, 0) + .field("value", first, left); + } + }).ifRight(right -> { + try(var struct = serializer.struct()) { + struct.field("side", Endec.VAR_INT, 1) + .field("value", second, right); + } + }); + + return; + } + + either.ifLeft(left -> first.encode(serializer, left)) + .ifRight(right -> second.encode(serializer, right)); + } + + @Override + public Either decode(Deserializer deserializer) { + boolean selfDescribing = deserializer.attributes().contains(SerializationAttribute.SELF_DESCRIBING); + + if(!selfDescribing){ + var struct = deserializer.struct(); + + return switch (struct.field("side", Endec.VAR_INT)){ + case 0 -> Either.left(struct.field("value", first)); + case 1 -> Either.right(struct.field("value", second)); + default -> throw new IllegalStateException("Unknown Int value for given Either Endec"); + }; + } + + Optional> result1 = Optional.empty(); + + try { + result1 = Optional.of(Either.left(deserializer.tryRead(first::decode))); + } catch (Exception ignore) {} + + Optional> result2 = Optional.empty(); + + try { + result2 = Optional.of(Either.right(deserializer.tryRead(second::decode))); + } catch (Exception ignore) {} + + if (result1.isPresent()) return result1.get(); + if (result2.isPresent()) return result2.get(); + + throw new IllegalStateException("Neither alternatives read successfully!"); + } + + public boolean equals(Object o) { + if (this == o) return true; + + if (o != null && this.getClass() == o.getClass()) { + EitherEndec either = (EitherEndec) o; + return Objects.equals(this.first, either.first) && Objects.equals(this.second, either.second); + } + + return false; + } + + public String toString() { + return "EitherCodec[" + this.first + ", " + this.second + "]"; + } +} diff --git a/src/main/java/io/wispforest/owo/serialization/endecs/ExtraEndecs.java b/src/main/java/io/wispforest/owo/serialization/endecs/ExtraEndecs.java new file mode 100644 index 00000000..e4eb1e8d --- /dev/null +++ b/src/main/java/io/wispforest/owo/serialization/endecs/ExtraEndecs.java @@ -0,0 +1,22 @@ +package io.wispforest.owo.serialization.endecs; + +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.impl.ReflectionEndecBuilder; +import net.minecraft.recipe.book.CraftingRecipeCategory; + +import java.util.function.Function; + +public class ExtraEndecs { + + public static final Endec NONNEGATIVE_INT = rangedInt(0, Integer.MAX_VALUE, v -> "Value must be non-negative: " + v); + public static final Endec POSITIVE_INT = rangedInt(1, Integer.MAX_VALUE, v -> "Value must be positive: " + v); + + + private static Endec rangedInt(int min, int max, Function messageFactory) { + return Endec.INT.validate(value -> { + if(value.compareTo(min) >= 0 && value.compareTo(max) <= 0) return value; + + throw new IllegalStateException(messageFactory.apply(value)); + }); + } +} diff --git a/src/main/java/io/wispforest/owo/serialization/endecs/IngredientEndec.java b/src/main/java/io/wispforest/owo/serialization/endecs/IngredientEndec.java new file mode 100644 index 00000000..f83086f2 --- /dev/null +++ b/src/main/java/io/wispforest/owo/serialization/endecs/IngredientEndec.java @@ -0,0 +1,70 @@ +package io.wispforest.owo.serialization.endecs; + +import com.mojang.datafixers.util.Either; +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.impl.StructEndecBuilder; +import net.minecraft.recipe.Ingredient; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.util.collection.DefaultedList; + +import java.util.Arrays; +import java.util.List; + +public class IngredientEndec { + + private static final Endec STACK_ENTRY_ENDEC = StructEndecBuilder.of( + RecipeEndecs.INGREDIENT.field("item", e -> e.stack), + Ingredient.StackEntry::new + ); + + private static final Endec TAG_ENTRY_ENDEC = StructEndecBuilder.of( + Endec.unprefixedTagKey(RegistryKeys.ITEM).field("tag", e -> e.tag), + Ingredient.TagEntry::new + ); + + private static final Endec INGREDIENT_ENTRY_ENDEC = new XorEndec<>(STACK_ENTRY_ENDEC, TAG_ENTRY_ENDEC) + .then( + either -> either.map(stackEntry -> stackEntry, tagEntry -> tagEntry), + entry -> { + if (entry instanceof Ingredient.TagEntry tagEntry) return Either.right(tagEntry); + if (entry instanceof Ingredient.StackEntry stackEntry) return Either.left(stackEntry); + + throw new UnsupportedOperationException("This is neither an item value nor a tag value."); + } + ); + + public static final Endec ALLOW_EMPTY_CODEC = createEndec(true); + public static final Endec DISALLOW_EMPTY_CODEC = createEndec(false); + +// public static final Endec RAW_DATA = Endec.ITEM_STACK.list() +// .then( +// stackList -> Ingredient.ofEntries(stackList.stream().map(Ingredient.StackEntry::new)), +// ingredient -> Arrays.stream(ingredient.getMatchingStacks()).toList() +// ); + + private static Endec createEndec(boolean allowEmpty) { + Endec endec = INGREDIENT_ENTRY_ENDEC.list() + .validate(entries -> { + if(!allowEmpty && entries.size() < 1){ + throw new IllegalStateException("Item array cannot be empty, at least one item must be defined"); + } + + return entries; + }) + .then(entries -> entries.toArray(new Ingredient.Entry[0]), List::of); + + return new EitherEndec<>(endec, INGREDIENT_ENTRY_ENDEC) + .then( + either -> either.map(Ingredient::new, entry -> new Ingredient(new Ingredient.Entry[]{entry})), + ingredient -> { + if(ingredient.entries.length == 0 && !allowEmpty){ + throw new IllegalStateException("Item array cannot be empty, at least one item must be defined"); + } + + return (ingredient.entries.length == 1) + ? Either.right(ingredient.entries[0]) + : Either.left(ingredient.entries); + } + ); + } +} diff --git a/src/main/java/io/wispforest/owo/serialization/endecs/RecipeEndecs.java b/src/main/java/io/wispforest/owo/serialization/endecs/RecipeEndecs.java new file mode 100644 index 00000000..a2e0f171 --- /dev/null +++ b/src/main/java/io/wispforest/owo/serialization/endecs/RecipeEndecs.java @@ -0,0 +1,37 @@ +package io.wispforest.owo.serialization.endecs; + +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.impl.ReflectionEndecBuilder; +import io.wispforest.owo.serialization.impl.StructEndecBuilder; +import io.wispforest.owo.serialization.impl.StructField; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.registry.Registries; + +public class RecipeEndecs { + private static final Endec CRAFTING_RESULT_ITEM = Endec.ofRegistry(Registries.ITEM) + .validate(item -> { + if(item == Items.AIR) throw new IllegalStateException("Crafting result must not be minecraft:air"); + + return item; + }); + + public static final Endec CRAFTING_RESULT = StructEndecBuilder.of( + CRAFTING_RESULT_ITEM.field("item", ItemStack::getItem), + StructField.defaulted("count", ExtraEndecs.POSITIVE_INT, ItemStack::getCount, 1), + ItemStack::new + ); + + static final Endec INGREDIENT = Endec.ofRegistry(Registries.ITEM) + .validate(item -> { + if(item == Items.AIR) throw new IllegalStateException("Empty ingredient not allowed here"); + + return item; + }) + .then(ItemStack::new, ItemStack::getItem); + + public static final Endec CATEGORY_ENDEC = ReflectionEndecBuilder.createEnumSerializer(CraftingRecipeCategory.class); + +} diff --git a/src/main/java/io/wispforest/owo/serialization/endecs/XorEndec.java b/src/main/java/io/wispforest/owo/serialization/endecs/XorEndec.java new file mode 100644 index 00000000..91372995 --- /dev/null +++ b/src/main/java/io/wispforest/owo/serialization/endecs/XorEndec.java @@ -0,0 +1,76 @@ +package io.wispforest.owo.serialization.endecs; + +import com.mojang.datafixers.util.Either; +import io.wispforest.owo.serialization.Deserializer; +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.Serializer; +import io.wispforest.owo.serialization.impl.SerializationAttribute; + +import java.util.Optional; + +public record XorEndec(Endec first, Endec second) implements Endec> { + + @Override + public void encode(Serializer serializer, Either either) { + boolean selfDescribing = serializer.attributes().contains(SerializationAttribute.SELF_DESCRIBING); + + if(!selfDescribing){ + either.ifLeft(left -> { + try(var struct = serializer.struct()) { + struct.field("side", Endec.VAR_INT, 0) + .field("value", first, left); + } + }).ifRight(right -> { + try(var struct = serializer.struct()) { + struct.field("side", Endec.VAR_INT, 1) + .field("value", second, right); + } + }); + + return; + } + + either.ifLeft(left -> this.first.encode(serializer, (F) left)) + .ifRight(right -> this.second.encode(serializer, (S) right)); + } + + @Override + public Either decode(Deserializer deserializer) { + boolean selfDescribing = deserializer.attributes().contains(SerializationAttribute.SELF_DESCRIBING); + + if(!selfDescribing){ + var struct = deserializer.struct(); + + return switch (struct.field("side", Endec.VAR_INT)){ + case 0 -> Either.left(struct.field("value", first)); + case 1 -> Either.right(struct.field("value", second)); + default -> throw new IllegalStateException("Unknown Int value for given Either Endec"); + }; + } + + Optional> result1 = Optional.empty(); + + try { + result1 = Optional.of(Either.left(deserializer.tryRead(first::decode))); + } catch (Exception ignore){} + + Optional> result2 = Optional.empty(); + + try { + result2 = Optional.of(Either.right(deserializer.tryRead(second::decode))); + } catch (Exception ignore){} + + if (result1.isPresent() && result2.isPresent()) { + throw new IllegalStateException("Both alternatives read successfully, can not pick the correct one; first: " + result1.get() + " second: " + result2.get()); + } + + if (result1.isPresent()) return result1.get(); + if (result2.isPresent()) return result2.get(); + + throw new IllegalStateException("Neither alternatives read successfully!"); + } + + public String toString() { + return "XorCodec[" + this.first + ", " + this.second + "]"; + } +} diff --git a/src/main/java/io/wispforest/owo/serialization/impl/ListEndec.java b/src/main/java/io/wispforest/owo/serialization/impl/ListEndec.java index 48993f81..bd0493a3 100644 --- a/src/main/java/io/wispforest/owo/serialization/impl/ListEndec.java +++ b/src/main/java/io/wispforest/owo/serialization/impl/ListEndec.java @@ -23,6 +23,11 @@ public ListEndec listConstructor(IntFunction> listConstructor){ return this; } + public > Endec conform(IntFunction mapConstructor){ + return this.listConstructor(mapConstructor::apply) + .then(map -> (L) map, map -> map); + } + @Override public void encode(Serializer serializer, List value) { try (var state = serializer.sequence(endec, value.size())) { diff --git a/src/main/java/io/wispforest/owo/serialization/impl/MapEndec.java b/src/main/java/io/wispforest/owo/serialization/impl/MapEndec.java index b10fcfa8..62d2b289 100644 --- a/src/main/java/io/wispforest/owo/serialization/impl/MapEndec.java +++ b/src/main/java/io/wispforest/owo/serialization/impl/MapEndec.java @@ -39,6 +39,15 @@ public MapEndec keyThen(Function toFunc, Function fromFunc return new MapEndec<>(this.endec, fromFunc.andThen(this.fromKey), this.toKey.andThen(toFunc)); } + public MapEndec keyValidator(Function validator) { + return new MapEndec<>(this.endec, this.fromKey, this.toKey.andThen(validator)); + } + + public > Endec conform(IntFunction mapConstructor){ + return this.mapConstructor(mapConstructor::apply) + .then(map -> (M) map, map -> map); + } + @Override public void encode(Serializer serializer, Map value) { try (var state = serializer.map(endec, value.size())) { diff --git a/src/main/java/io/wispforest/owo/serialization/impl/bytebuf/ByteBufDeserializer.java b/src/main/java/io/wispforest/owo/serialization/impl/bytebuf/ByteBufDeserializer.java index 9343b018..958a9878 100644 --- a/src/main/java/io/wispforest/owo/serialization/impl/bytebuf/ByteBufDeserializer.java +++ b/src/main/java/io/wispforest/owo/serialization/impl/bytebuf/ByteBufDeserializer.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; public class ByteBufDeserializer implements Deserializer { @@ -32,7 +33,7 @@ public Set attributes() { @Override public Optional readOptional(Endec endec) { - var bl = buf.readBoolean(); + var bl = readBoolean(); return Optional.ofNullable(bl ? endec.decode(this) : null); } @@ -96,6 +97,19 @@ public long readVarLong() { return VarLongs.read(buf); } + @Override + public V tryRead(Function, V> func) { + var prevReader = buf.readerIndex(); + + try { + return func.apply(this); + } catch (Exception exception){ + this.buf.readerIndex(prevReader); + + throw exception; + } + } + @Override public SequenceDeserializer sequence(Endec elementEndec) { return new ByteBufSequenceDeserializer().valueEndec(elementEndec, readVarInt()); diff --git a/src/main/java/io/wispforest/owo/serialization/impl/json/JsonDeserializer.java b/src/main/java/io/wispforest/owo/serialization/impl/json/JsonDeserializer.java index f47ced42..f2a46beb 100644 --- a/src/main/java/io/wispforest/owo/serialization/impl/json/JsonDeserializer.java +++ b/src/main/java/io/wispforest/owo/serialization/impl/json/JsonDeserializer.java @@ -7,6 +7,7 @@ import java.math.BigDecimal; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; public class JsonDeserializer implements SelfDescribedDeserializer { @@ -180,6 +181,20 @@ public long readVarLong() { return readLong(); } + @Override + public V tryRead(Function, V> func) { + var stackCopy = new ArrayList<>(stack); + + try { + return func.apply(this); + } catch (Exception e){ + stack.clear(); + stack.addAll(stackCopy); + + throw e; + } + } + @Override public SequenceDeserializer sequence(Endec elementEndec) { return new JsonSequenceDeserializer<>(((JsonArray) topElement()).asList(), elementEndec); @@ -223,13 +238,9 @@ public boolean hasNext() { public V next() { JsonDeserializer.this.stack.push(entries::next); - V entry; + V entry = valueEndec.decode(JsonDeserializer.this); - try { - entry = valueEndec.decode(JsonDeserializer.this); - } finally { - JsonDeserializer.this.stack.pop(); - } + JsonDeserializer.this.stack.pop(); return entry; } @@ -265,13 +276,9 @@ public Map.Entry next() { JsonDeserializer.this.stack.push(entry::getValue); - Map.Entry value; + Map.Entry value = Map.entry(entry.getKey(), valueEndec.decode(JsonDeserializer.this)); - try { - value = Map.entry(entry.getKey(), valueEndec.decode(JsonDeserializer.this)); - } finally { - JsonDeserializer.this.stack.pop(); - } + JsonDeserializer.this.stack.pop(); return value; } @@ -291,13 +298,9 @@ public F field(String field, Endec endec, @Nullable F defaultValue) { JsonDeserializer.this.stack.push(() -> map.get(field)); - F value; + F value = endec.decode(JsonDeserializer.this); - try { - value = endec.decode(JsonDeserializer.this); - } finally { - JsonDeserializer.this.stack.pop(); - } + JsonDeserializer.this.stack.pop(); return value; } diff --git a/src/main/java/io/wispforest/owo/serialization/impl/nbt/NbtDeserializer.java b/src/main/java/io/wispforest/owo/serialization/impl/nbt/NbtDeserializer.java index 7aaa4f21..c426da52 100644 --- a/src/main/java/io/wispforest/owo/serialization/impl/nbt/NbtDeserializer.java +++ b/src/main/java/io/wispforest/owo/serialization/impl/nbt/NbtDeserializer.java @@ -1,11 +1,13 @@ package io.wispforest.owo.serialization.impl.nbt; +import com.google.gson.JsonElement; import io.wispforest.owo.serialization.*; import io.wispforest.owo.serialization.impl.SerializationAttribute; import net.minecraft.nbt.*; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; public class NbtDeserializer implements SelfDescribedDeserializer { @@ -211,6 +213,20 @@ public long readVarLong() { throw new RuntimeException("[NbtFormat] input was not AbstractNbtNumber"); } + @Override + public V tryRead(Function, V> func) { + var stackCopy = new ArrayList<>(stack); + + try { + return func.apply(this); + } catch (Exception e){ + stack.clear(); + stack.addAll(stackCopy); + + throw e; + } + } + @Override public SequenceDeserializer sequence(Endec elementEndec) { return new NbtSequenceDeserializer<>(((AbstractNbtList) topElement()), elementEndec); @@ -292,13 +308,9 @@ public Map.Entry next() { NbtDeserializer.this.stack.push(entry::getValue); - Map.Entry newEntry; + Map.Entry newEntry = Map.entry(entry.getKey(), valueEndec.decode(NbtDeserializer.this)); - try { - newEntry = Map.entry(entry.getKey(), valueEndec.decode(NbtDeserializer.this)); - } finally { - NbtDeserializer.this.stack.pop(); - } + NbtDeserializer.this.stack.pop(); return newEntry; } @@ -318,13 +330,9 @@ public F field(String field, Endec endec, @Nullable F defaultValue) { NbtDeserializer.this.stack.push(() -> map.get(field)); - F value; + F value = endec.decode(NbtDeserializer.this); - try { - value = endec.decode(NbtDeserializer.this); - } finally { - NbtDeserializer.this.stack.pop(); - } + NbtDeserializer.this.stack.pop(); return value; } diff --git a/src/main/resources/owo.accesswidener b/src/main/resources/owo.accesswidener index 20a9ef88..7124807b 100644 --- a/src/main/resources/owo.accesswidener +++ b/src/main/resources/owo.accesswidener @@ -10,3 +10,23 @@ transitive-accessible class net/minecraft/client/gui/DrawContext$ScissorStack accessible field net/minecraft/util/dynamic/ForwardingDynamicOps delegate Lcom/mojang/serialization/DynamicOps; accessible method net/minecraft/nbt/NbtCompound toMap ()Ljava/util/Map; + +accessible class net/minecraft/recipe/Ingredient$Entry + +accessible method net/minecraft/recipe/Ingredient$StackEntry (Lnet/minecraft/item/ItemStack;)V + +accessible class net/minecraft/recipe/Ingredient$StackEntry +accessible field net/minecraft/recipe/Ingredient$StackEntry stack Lnet/minecraft/item/ItemStack; + +accessible method net/minecraft/recipe/Ingredient$TagEntry (Lnet/minecraft/registry/tag/TagKey;)V + +accessible class net/minecraft/recipe/Ingredient$TagEntry +accessible field net/minecraft/recipe/Ingredient$TagEntry tag Lnet/minecraft/registry/tag/TagKey; + +accessible method net/minecraft/recipe/Ingredient ([Lnet/minecraft/recipe/Ingredient$Entry;)V + +accessible field net/minecraft/recipe/Ingredient entries [Lnet/minecraft/recipe/Ingredient$Entry; +accessible method net/minecraft/recipe/Ingredient ofEntries (Ljava/util/stream/Stream;)Lnet/minecraft/recipe/Ingredient; + +accessible method net/minecraft/recipe/ShapedRecipe removePadding (Ljava/util/List;)[Ljava/lang/String; + diff --git a/src/testmod/java/io/wispforest/uwu/Uwu.java b/src/testmod/java/io/wispforest/uwu/Uwu.java index 8aa31a75..b18d77d3 100644 --- a/src/testmod/java/io/wispforest/uwu/Uwu.java +++ b/src/testmod/java/io/wispforest/uwu/Uwu.java @@ -33,6 +33,7 @@ import io.wispforest.uwu.config.UwuConfig; import io.wispforest.uwu.items.UwuItems; import io.wispforest.uwu.network.*; +import io.wispforest.uwu.recipe.UwuShapedRecipe; import io.wispforest.uwu.text.BasedTextContent; import net.fabricmc.api.EnvType; import net.fabricmc.api.ModInitializer; @@ -194,6 +195,8 @@ public void onInitialize() { System.out.println(RegistryAccess.getEntry(Registries.ITEM, Items.ACACIA_BOAT)); System.out.println(RegistryAccess.getEntry(Registries.ITEM, new Identifier("acacia_planks"))); + UwuShapedRecipe.init(); + CommandRegistrationCallback.EVENT.register((dispatcher, access, environment) -> { dispatcher.register( literal("show_nbt") @@ -454,5 +457,4 @@ public record TestMessage(String string, Integer integer, Long along, ItemStack int[] arr1, String[] arr2, short[] arr3, long[] arr4, byte[] arr5, Optional optional1, Optional optional2, List posses, SealedTestClass sealed1, SealedTestClass sealed2) {} - } \ No newline at end of file diff --git a/src/testmod/java/io/wispforest/uwu/recipe/EndecRecipeSerializer.java b/src/testmod/java/io/wispforest/uwu/recipe/EndecRecipeSerializer.java new file mode 100644 index 00000000..566370f8 --- /dev/null +++ b/src/testmod/java/io/wispforest/uwu/recipe/EndecRecipeSerializer.java @@ -0,0 +1,40 @@ +package io.wispforest.uwu.recipe; + +import com.mojang.serialization.Codec; +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.impl.bytebuf.ByteBufDeserializer; +import io.wispforest.owo.serialization.impl.bytebuf.ByteBufSerializer; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.recipe.Recipe; +import net.minecraft.recipe.RecipeSerializer; + +public class EndecRecipeSerializer> implements RecipeSerializer { + + public final Endec endec; + public final Codec codec; + + public EndecRecipeSerializer(Endec endec){ + this.codec = endec.codec(); + this.endec = endec; + } + + public EndecRecipeSerializer(Endec endecCodec, Endec packetEndec){ + this.codec = endecCodec.codec(); + this.endec = packetEndec; + } + + @Override + public Codec codec() { + return this.codec; + } + + @Override + public void write(PacketByteBuf buf, T recipe) { + this.endec.encode(new ByteBufSerializer<>(buf), recipe); + } + + @Override + public T read(PacketByteBuf buf) { + return this.endec.decode(new ByteBufDeserializer(buf)); + } +} diff --git a/src/testmod/java/io/wispforest/uwu/recipe/UwuShapedRecipe.java b/src/testmod/java/io/wispforest/uwu/recipe/UwuShapedRecipe.java new file mode 100644 index 00000000..bdf0c160 --- /dev/null +++ b/src/testmod/java/io/wispforest/uwu/recipe/UwuShapedRecipe.java @@ -0,0 +1,134 @@ +package io.wispforest.uwu.recipe; + +import com.google.common.collect.Sets; +import io.wispforest.owo.serialization.Endec; +import io.wispforest.owo.serialization.endecs.ExtraEndecs; +import io.wispforest.owo.serialization.endecs.IngredientEndec; +import io.wispforest.owo.serialization.endecs.RecipeEndecs; +import io.wispforest.owo.serialization.impl.AttributeEndecBuilder; +import io.wispforest.owo.serialization.impl.SerializationAttribute; +import io.wispforest.owo.serialization.impl.StructEndecBuilder; +import io.wispforest.owo.serialization.impl.StructField; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.ShapedRecipe; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class UwuShapedRecipe extends ShapedRecipe { + + public static RecipeSerializer RECIPE_SERIALIZER; + + public UwuShapedRecipe(String group, CraftingRecipeCategory category, int width, int height, DefaultedList ingredients, ItemStack result, boolean showNotification) { + super(group, category, width, height, ingredients, result, showNotification); + } + + @Override + public RecipeSerializer getSerializer() { + return RECIPE_SERIALIZER; + } + + public static void init(){ + RECIPE_SERIALIZER = Registry.register( + Registries.RECIPE_SERIALIZER, + new Identifier("uwu:crafting_shaped"), + new EndecRecipeSerializer<>(ENDEC) + ); + } + + //-- + + private static final Endec FROM_RAW_RECIPE = RawShapedRecipe.ENDEC.then(recipe -> { + String[] strings = ShapedRecipe.removePadding(recipe.pattern); + int i = strings[0].length(); + int j = strings.length; + DefaultedList defaultedList = DefaultedList.ofSize(i * j, Ingredient.EMPTY); + Set set = Sets.newHashSet(recipe.key.keySet()); + + for(int k = 0; k < strings.length; ++k) { + String string = strings[k]; + + for(int l = 0; l < string.length(); ++l) { + String string2 = string.substring(l, l + 1); + Ingredient ingredient = string2.equals(" ") ? Ingredient.EMPTY : (Ingredient)recipe.key.get(string2); + + if (ingredient == null) { + throw new IllegalStateException("Pattern references symbol '" + string2 + "' but it's not defined in the key"); + } + + set.remove(string2); + defaultedList.set(l + i * k, ingredient); + } + } + + if (!set.isEmpty()) throw new IllegalStateException("Key defines symbols that aren't used in pattern: " + set); + + return new UwuShapedRecipe(recipe.group, recipe.category, i, j, defaultedList, recipe.result, recipe.showNotification); + }, recipe -> { + throw new NotImplementedException("Serializing ShapedRecipe is not implemented yet."); + }); + + private static final Endec> INGREDIENTS = IngredientEndec.ALLOW_EMPTY_CODEC.list() + .conform(size -> DefaultedList.ofSize(size, Ingredient.EMPTY)); + + private static final Endec FROM_INSTANCE = StructEndecBuilder.of( + Endec.STRING.field("group", ShapedRecipe::getGroup), + RecipeEndecs.CATEGORY_ENDEC.field("category", ShapedRecipe::getCategory), + Endec.VAR_INT.field("width", ShapedRecipe::getWidth), + Endec.VAR_INT.field("height", ShapedRecipe::getHeight), + INGREDIENTS.field("ingredients", ShapedRecipe::getIngredients), + RecipeEndecs.CRAFTING_RESULT.field("result", recipe -> recipe.getResult(null)), + Endec.BOOLEAN.field("show_notification", ShapedRecipe::showNotification), + UwuShapedRecipe::new + ); + + private static final Endec ENDEC = new AttributeEndecBuilder<>(FROM_RAW_RECIPE, SerializationAttribute.HUMAN_READABLE) + .orElse(FROM_INSTANCE); + + //-- + + private static final Endec> PATTERN_ENDEC = Endec.STRING.list().validate(rows -> { + if (rows.size() > 3) throw new IllegalStateException("Invalid pattern: too many rows, 3 is maximum"); + if (rows.isEmpty()) throw new IllegalStateException("Invalid pattern: empty pattern not allowed"); + + int i = rows.get(0).length(); + + for(String string : rows) { + if (string.length() > 3) throw new IllegalStateException("Invalid pattern: too many columns, 3 is maximum"); + if (i != string.length()) throw new IllegalStateException("Invalid pattern: each row must be the same width"); + } + + return rows; + }); + + private record RawShapedRecipe(String group, CraftingRecipeCategory category, Map key, List pattern, ItemStack result, boolean showNotification) { + public static final Endec ENDEC = StructEndecBuilder.of( + StructField.defaulted("group", Endec.STRING, recipe -> recipe.group, ""), + StructField.defaulted("category", RecipeEndecs.CATEGORY_ENDEC, recipe -> recipe.category, CraftingRecipeCategory.MISC), + IngredientEndec.DISALLOW_EMPTY_CODEC.map().keyValidator(UwuShapedRecipe::keyEntryValidator).field("key", recipe -> recipe.key), + PATTERN_ENDEC.field("pattern", recipe -> recipe.pattern), + RecipeEndecs.CRAFTING_RESULT.field("result", recipe -> recipe.result), + StructField.defaulted("show_notification", Endec.BOOLEAN, recipe -> recipe.showNotification, true), + RawShapedRecipe::new + ); + } + + private static String keyEntryValidator(String key){ + if (key.length() != 1) { + throw new IllegalStateException("Invalid key entry: '" + key + "' is an invalid symbol (must be 1 character only)."); + } else if(" ".equals(key)){ + throw new IllegalStateException("Invalid key entry: ' ' is a reserved symbol."); + } + + return key; + } +} \ No newline at end of file diff --git a/src/testmod/resources/data/uwu/recipes/uwu_shaped_recipe.json b/src/testmod/resources/data/uwu/recipes/uwu_shaped_recipe.json new file mode 100644 index 00000000..131e1438 --- /dev/null +++ b/src/testmod/resources/data/uwu/recipes/uwu_shaped_recipe.json @@ -0,0 +1,19 @@ +{ + "type": "uwu:crafting_shaped", + "key": { + "#": { + "item": "minecraft:cobblestone" + }, + "X": { + "item": "minecraft:sand" + } + }, + "pattern": [ + "XXX", + "X#X", + "XXX" + ], + "result": { + "item": "minecraft:stone" + } +} \ No newline at end of file