From f628d01844ee9068513ca7bd5693b8af9d471683 Mon Sep 17 00:00:00 2001 From: apple502j <33279053+apple502j@users.noreply.github.com> Date: Tue, 12 Mar 2024 19:31:21 +0900 Subject: [PATCH] [1.20.5] Add back custom data ingredient (#3642) * [1.20.5] Add back custom data ingredient --------- Co-authored-by: modmuss50 --- .../ingredient/DefaultCustomIngredients.java | 36 +++++ .../ingredient/CustomIngredientInit.java | 2 + .../builtin/CustomDataIngredient.java | 141 ++++++++++++++++++ .../ingredient/IngredientMatchTests.java | 39 +++++ 4 files changed, 218 insertions(+) create mode 100644 fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/builtin/CustomDataIngredient.java diff --git a/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/api/recipe/v1/ingredient/DefaultCustomIngredients.java b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/api/recipe/v1/ingredient/DefaultCustomIngredients.java index e348ad24db..e890b1fe41 100644 --- a/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/api/recipe/v1/ingredient/DefaultCustomIngredients.java +++ b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/api/recipe/v1/ingredient/DefaultCustomIngredients.java @@ -22,11 +22,14 @@ import net.minecraft.component.ComponentChanges; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtHelper; import net.minecraft.recipe.Ingredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AllIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AnyIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.ComponentsIngredient; +import net.fabricmc.fabric.impl.recipe.ingredient.builtin.CustomDataIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.DifferenceIngredient; /** @@ -151,6 +154,39 @@ public static Ingredient components(ItemStack stack) { return components(Ingredient.ofItems(stack.getItem()), stack.getComponentChanges()); } + /** + * Creates an ingredient that wraps another ingredient to also check for stack's {@linkplain + * net.minecraft.component.DataComponentTypes#CUSTOM_DATA custom data}. + * This check is non-strict; the ingredient custom data must be a subset of the stack custom data. + * This is useful for mods that still rely on NBT-based custom data instead of custom components, + * such as those requiring vanilla compatibility or interacting with another data packs. + * + *

Passing a {@code null} or empty {@code nbt} is not allowed, as it would always match. + * For strict matching, use {@link #components(Ingredient, UnaryOperator)} like this instead: + * + *

{@code
+	 * components(base, builder -> builder.add(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(nbt)));
+	 * // or, to check for absence of custom data:
+	 * components(base, builder -> builder.remove(DataComponentTypes.CUSTOM_DATA));
+	 * }
+ * + *

See {@link NbtHelper#matches} for how matching works. + * + *

The JSON format is as follows: + *

{@code
+	 * {
+	 *    "fabric:type": "fabric:custom_data",
+	 *    "base": // base ingredient,
+	 *    "nbt": // NBT tag to match, either in JSON directly or a string representation
+	 * }
+	 * }
+ * + * @throws IllegalArgumentException if {@code nbt} is {@code null} or empty + */ + public static Ingredient customData(Ingredient base, NbtCompound nbt) { + return new CustomDataIngredient(base, nbt).toVanilla(); + } + private DefaultCustomIngredients() { } } diff --git a/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/CustomIngredientInit.java b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/CustomIngredientInit.java index 56230ed1c5..2c6e39832b 100644 --- a/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/CustomIngredientInit.java +++ b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/CustomIngredientInit.java @@ -21,6 +21,7 @@ import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AllIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AnyIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.ComponentsIngredient; +import net.fabricmc.fabric.impl.recipe.ingredient.builtin.CustomDataIngredient; import net.fabricmc.fabric.impl.recipe.ingredient.builtin.DifferenceIngredient; /** @@ -33,5 +34,6 @@ public void onInitialize() { CustomIngredientSerializer.register(AnyIngredient.SERIALIZER); CustomIngredientSerializer.register(DifferenceIngredient.SERIALIZER); CustomIngredientSerializer.register(ComponentsIngredient.SERIALIZER); + CustomIngredientSerializer.register(CustomDataIngredient.SERIALIZER); } } diff --git a/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/builtin/CustomDataIngredient.java b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/builtin/CustomDataIngredient.java new file mode 100644 index 0000000000..acca30f52e --- /dev/null +++ b/fabric-recipe-api-v1/src/main/java/net/fabricmc/fabric/impl/recipe/ingredient/builtin/CustomDataIngredient.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.recipe.ingredient.builtin; + +import java.util.ArrayList; +import java.util.List; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.StringNbtReader; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.recipe.Ingredient; +import net.minecraft.util.Identifier; +import net.minecraft.util.dynamic.Codecs; + +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; + +public class CustomDataIngredient implements CustomIngredient { + public static final CustomIngredientSerializer SERIALIZER = new Serializer(); + private final Ingredient base; + private final NbtCompound nbt; + + public CustomDataIngredient(Ingredient base, NbtCompound nbt) { + if (nbt == null || nbt.isEmpty()) throw new IllegalArgumentException("NBT cannot be null; use components ingredient for strict matching"); + + this.base = base; + this.nbt = nbt; + } + + @Override + public boolean test(ItemStack stack) { + if (!base.test(stack)) return false; + + NbtComponent nbt = stack.get(DataComponentTypes.CUSTOM_DATA); + + return nbt != null && nbt.matches(this.nbt); + } + + @Override + public List getMatchingStacks() { + List stacks = new ArrayList<>(List.of(base.getMatchingStacks())); + stacks.replaceAll(stack -> { + ItemStack copy = stack.copy(); + copy.apply(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT, existingNbt -> NbtComponent.of(existingNbt.copyNbt().copyFrom(this.nbt))); + return copy; + }); + stacks.removeIf(stack -> !base.test(stack)); + return stacks; + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + private Ingredient getBase() { + return base; + } + + private NbtCompound getNbt() { + return nbt; + } + + private static class Serializer implements CustomIngredientSerializer { + private static final Identifier ID = new Identifier("fabric", "custom_data"); + + // Supports decoding the NBT as a string as well as the object. + private static final Codec NBT_CODEC = Codecs.xor( + Codec.STRING, NbtCompound.CODEC + ).flatXmap(either -> either.map(s -> { + try { + return DataResult.success(StringNbtReader.parse(s)); + } catch (CommandSyntaxException e) { + return DataResult.error(e::getMessage); + } + }, DataResult::success), nbtCompound -> DataResult.success(Either.left(nbtCompound.asString()))); + + private static final Codec ALLOW_EMPTY_CODEC = createCodec(Ingredient.ALLOW_EMPTY_CODEC); + private static final Codec DISALLOW_EMPTY_CODEC = createCodec(Ingredient.DISALLOW_EMPTY_CODEC); + + private static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + Ingredient.PACKET_CODEC, CustomDataIngredient::getBase, + PacketCodecs.NBT_COMPOUND, CustomDataIngredient::getNbt, + CustomDataIngredient::new + ); + + private static Codec createCodec(Codec ingredientCodec) { + return RecordCodecBuilder.create(instance -> + instance.group( + ingredientCodec.fieldOf("base").forGetter(CustomDataIngredient::getBase), + NBT_CODEC.fieldOf("nbt").forGetter(CustomDataIngredient::getNbt) + ).apply(instance, CustomDataIngredient::new) + ); + } + + @Override + public Identifier getIdentifier() { + return ID; + } + + @Override + public Codec getCodec(boolean allowEmpty) { + return allowEmpty ? ALLOW_EMPTY_CODEC : DISALLOW_EMPTY_CODEC; + } + + @Override + public PacketCodec getPacketCodec() { + return PACKET_CODEC; + } + } +} diff --git a/fabric-recipe-api-v1/src/testmod/java/net/fabricmc/fabric/test/recipe/ingredient/IngredientMatchTests.java b/fabric-recipe-api-v1/src/testmod/java/net/fabricmc/fabric/test/recipe/ingredient/IngredientMatchTests.java index 2b86a73794..b4b7c13bef 100644 --- a/fabric-recipe-api-v1/src/testmod/java/net/fabricmc/fabric/test/recipe/ingredient/IngredientMatchTests.java +++ b/fabric-recipe-api-v1/src/testmod/java/net/fabricmc/fabric/test/recipe/ingredient/IngredientMatchTests.java @@ -29,6 +29,7 @@ import net.minecraft.test.GameTestException; import net.minecraft.test.TestContext; import net.minecraft.text.Text; +import net.minecraft.util.Util; import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; import net.fabricmc.fabric.api.recipe.v1.ingredient.DefaultCustomIngredients; @@ -158,6 +159,44 @@ public void testComponentIngredient(TestContext context) { context.complete(); } + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testCustomDataIngredient(TestContext context) { + final NbtCompound requiredNbt = Util.make(new NbtCompound(), nbt -> { + nbt.putInt("keyA", 1); + }); + final NbtCompound acceptedNbt = Util.make(requiredNbt.copy(), nbt -> { + nbt.putInt("keyB", 2); + }); + final NbtCompound rejectedNbt1 = Util.make(new NbtCompound(), nbt -> { + nbt.putInt("keyA", -1); + }); + final NbtCompound rejectedNbt2 = Util.make(new NbtCompound(), nbt -> { + nbt.putInt("keyB", 2); + }); + + final Ingredient baseIngredient = Ingredient.ofItems(Items.STICK); + final Ingredient customDataIngredient = DefaultCustomIngredients.customData(baseIngredient, requiredNbt); + + ItemStack stack = new ItemStack(Items.STICK); + assertEquals(false, customDataIngredient.test(stack)); + stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(requiredNbt)); + assertEquals(true, customDataIngredient.test(stack)); + // This is a non-strict matching + stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(acceptedNbt)); + assertEquals(true, customDataIngredient.test(stack)); + stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(rejectedNbt1)); + assertEquals(false, customDataIngredient.test(stack)); + stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(rejectedNbt2)); + assertEquals(false, customDataIngredient.test(stack)); + + ItemStack[] matchingStacks = customDataIngredient.getMatchingStacks(); + assertEquals(1, matchingStacks.length); + assertEquals(Items.STICK, matchingStacks[0].getItem()); + assertEquals(NbtComponent.of(requiredNbt), matchingStacks[0].get(DataComponentTypes.CUSTOM_DATA)); + + context.complete(); + } + private static void assertEquals(T expected, T actual) { if (!Objects.equals(expected, actual)) { throw new GameTestException(String.format("assertEquals failed%nexpected: %s%n but was: %s", expected, actual));