diff --git a/patches/net/minecraft/world/item/crafting/Ingredient.java.patch b/patches/net/minecraft/world/item/crafting/Ingredient.java.patch index 25e66a11cb..6c1acda91c 100644 --- a/patches/net/minecraft/world/item/crafting/Ingredient.java.patch +++ b/patches/net/minecraft/world/item/crafting/Ingredient.java.patch @@ -1,21 +1,22 @@ --- a/net/minecraft/world/item/crafting/Ingredient.java +++ b/net/minecraft/world/item/crafting/Ingredient.java -@@ -29,22 +_,63 @@ +@@ -29,15 +_,54 @@ - public class Ingredient implements Predicate { + public final class Ingredient implements Predicate { public static final Ingredient EMPTY = new Ingredient(Stream.empty()); - public static final StreamCodec CONTENTS_STREAM_CODEC = ItemStack.LIST_STREAM_CODEC - .map(p_319730_ -> fromValues(p_319730_.stream().map(Ingredient.ItemValue::new)), p_319731_ -> Arrays.asList(p_319731_.getItems())); + public static final StreamCodec CONTENTS_STREAM_CODEC = new StreamCodec<>() { -+ private static final StreamCodec CODEC_STREAM_CODEC = net.neoforged.neoforge.network.codec.NeoForgeStreamCodecs.lazy(() -> net.minecraft.network.codec.ByteBufCodecs.fromCodecWithRegistries(CODEC)); ++ private static final StreamCodec CUSTOM_INGREDIENT_CODEC = net.minecraft.network.codec.ByteBufCodecs.registry(net.neoforged.neoforge.registries.NeoForgeRegistries.Keys.INGREDIENT_TYPES) ++ .dispatch(c -> c.getType(), t -> t.streamCodec()); + + @Override + public void encode(RegistryFriendlyByteBuf buf, Ingredient ingredient) { -+ if (ingredient.synchronizeWithContents()) { ++ if (ingredient.isSimple()) { + ItemStack.LIST_STREAM_CODEC.encode(buf, Arrays.asList(ingredient.getItems())); + } else { + buf.writeVarInt(-1); -+ CODEC_STREAM_CODEC.encode(buf, ingredient); ++ CUSTOM_INGREDIENT_CODEC.encode(buf, ingredient.customIngredient); + } + } + @@ -23,149 +24,173 @@ + public Ingredient decode(RegistryFriendlyByteBuf buf) { + var size = buf.readVarInt(); + if (size == -1) { -+ return CODEC_STREAM_CODEC.decode(buf); ++ return new Ingredient(CUSTOM_INGREDIENT_CODEC.decode(buf)); + } + return fromValues(Stream.generate(() -> ItemStack.STREAM_CODEC.decode(buf)).limit(size).map(Ingredient.ItemValue::new)); + } + }; - public final Ingredient.Value[] values; + private final Ingredient.Value[] values; @Nullable private ItemStack[] itemStacks; @Nullable private IntList stackingIds; - public static final Codec CODEC = codec(true); - public static final Codec CODEC_NONEMPTY = codec(false); -+ private final java.util.function.Supplier> type; -+ @Nullable private Boolean areAllStacksEmpty; ++ @Nullable ++ private net.neoforged.neoforge.common.crafting.ICustomIngredient customIngredient = null; + -+ public static final Codec VANILLA_CODEC = codec(true); -+ public static final Codec VANILLA_CODEC_NONEMPTY = codec(false); -+ public static final Codec CODEC = net.neoforged.neoforge.common.crafting.CraftingHelper.makeIngredientCodec(true, VANILLA_CODEC); -+ public static final Codec CODEC_NONEMPTY = net.neoforged.neoforge.common.crafting.CraftingHelper.makeIngredientCodec(false, VANILLA_CODEC_NONEMPTY); -+ public static final Codec> LIST_CODEC = CODEC.listOf(); -+ public static final Codec> LIST_CODEC_NONEMPTY = CODEC_NONEMPTY.listOf(); ++ /** ++ * This codec allows both the {@code {...}} and {@code [{...}, {...}, ...]} syntax. ++ * {@code []} is allowed for empty ingredients, and will only match empty stacks. ++ */ ++ public static final Codec CODEC = net.neoforged.neoforge.common.crafting.CraftingHelper.makeIngredientCodec(true); ++ /** ++ * Same as {@link #CODEC} except that empty ingredients ({@code []}) are not allowed. ++ */ ++ public static final Codec CODEC_NONEMPTY = net.neoforged.neoforge.common.crafting.CraftingHelper.makeIngredientCodec(false); ++ /** ++ * This is a codec that only allows the {@code {...}} syntax. ++ * Array ingredients are serialized using the CompoundIngredient custom ingredient type: ++ * {@code { "type": "neoforge:compound", "ingredients": [{...}, {...}, ...] }}. ++ */ ++ public static final com.mojang.serialization.MapCodec MAP_CODEC_NONEMPTY = net.neoforged.neoforge.common.crafting.CraftingHelper.makeIngredientMapCodec(); ++ public static final Codec> LIST_CODEC = MAP_CODEC_NONEMPTY.codec().listOf(); ++ public static final Codec> LIST_CODEC_NONEMPTY = LIST_CODEC.validate(list -> list.isEmpty() ? DataResult.error(() -> "Item array cannot be empty, at least one item must be defined") : DataResult.success(list)); - protected Ingredient(Stream p_43907_) { -- this.values = p_43907_.toArray(Ingredient.Value[]::new); -+ this(p_43907_, net.neoforged.neoforge.common.NeoForgeMod.VANILLA_INGREDIENT_TYPE); + private Ingredient(Stream p_43907_) { + this.values = p_43907_.toArray(Ingredient.Value[]::new); +@@ -47,9 +_,20 @@ + this.values = p_301044_; } - private Ingredient(Ingredient.Value[] p_301044_) { -+ this(p_301044_, net.neoforged.neoforge.common.NeoForgeMod.VANILLA_INGREDIENT_TYPE); -+ } -+ -+ protected Ingredient(Stream p_43907_, java.util.function.Supplier> type) { -+ this.values = p_43907_.toArray(Value[]::new); -+ this.type = type; -+ } -+ -+ private Ingredient(Ingredient.Value[] p_301044_, java.util.function.Supplier> type) { - this.values = p_301044_; -+ this.type = type; ++ public Ingredient(net.neoforged.neoforge.common.crafting.ICustomIngredient customIngredient) { ++ this(new Value[0]); ++ this.customIngredient = customIngredient; + } + -+ public net.neoforged.neoforge.common.crafting.IngredientType getType() { -+ return type.get(); - } - public ItemStack[] getItems() { -@@ -62,7 +_,7 @@ + if (this.itemStacks == null) { ++ if (this.customIngredient != null) { ++ this.itemStacks = this.customIngredient.getItems() ++ .distinct()//Mimic vanilla that calls distinct on the stacks ++ .toArray(ItemStack[]::new); ++ } else { + this.itemStacks = Arrays.stream(this.values).flatMap(p_43916_ -> p_43916_.getItems().stream()).distinct().toArray(ItemStack[]::new); ++ } + } + + return this.itemStacks; +@@ -58,6 +_,8 @@ + public boolean test(@Nullable ItemStack p_43914_) { + if (p_43914_ == null) { + return false; ++ } else if (this.customIngredient != null) { ++ return this.customIngredient.test(p_43914_); + } else if (this.isEmpty()) { return p_43914_.isEmpty(); } else { - for (ItemStack itemstack : this.getItems()) { -- if (itemstack.is(p_43914_.getItem())) { -+ if (areStacksEqual(itemstack, p_43914_)) { - return true; - } - } -@@ -71,6 +_,10 @@ - } +@@ -86,13 +_,65 @@ + return this.stackingIds; } -+ protected boolean areStacksEqual(ItemStack left, ItemStack right) { -+ return left.is(right.getItem()); -+ } -+ - public IntList getStackingIds() { - if (this.stackingIds == null) { - ItemStack[] aitemstack = this.getItems(); -@@ -86,8 +_,23 @@ - return this.stackingIds; ++ /** ++ * Returns {@code true} if this ingredient is explicitly chosen to be empty, i.e. using {@code []}. ++ */ + public boolean isEmpty() { + return this.values.length == 0; } -+ private boolean areAllStacksEmpty() { -+ Boolean empty = this.areAllStacksEmpty; -+ if (empty == null) { -+ boolean allEmpty = true; -+ for (ItemStack stack : this.getItems()) { -+ if (!stack.isEmpty()) { -+ allEmpty = false; -+ break; -+ } -+ } -+ this.areAllStacksEmpty = empty = allEmpty; ++ /** ++ * Returns {@code true} if this ingredient has an empty stack list. ++ * Unlike {@link #isEmpty()}, this will catch "accidentally empty" ingredients, ++ * for example a tag ingredient that has an empty tag. ++ */ ++ public boolean hasNoItems() { ++ ItemStack[] items = getItems(); ++ if (items.length == 0) ++ return true; ++ if (items.length == 1) { ++ // If we potentially added a barrier due to the ingredient being an empty tag, try and check if it is the stack we added ++ ItemStack item = items[0]; ++ return item.getItem() == net.minecraft.world.item.Items.BARRIER && item.getHoverName() instanceof net.minecraft.network.chat.MutableComponent hoverName && hoverName.getString().startsWith("Empty Tag: "); + } -+ return empty; ++ return false; + } + - public boolean isEmpty() { -- return this.values.length == 0; -+ return this.values.length == 0 || this.areAllStacksEmpty(); - } - @Override -@@ -95,6 +_,17 @@ - return p_301003_ instanceof Ingredient ingredient ? Arrays.equals((Object[])this.values, (Object[])ingredient.values) : false; - } - -+ public boolean isSimple() { -+ return true; + public boolean equals(Object p_301003_) { +- return p_301003_ instanceof Ingredient ingredient ? Arrays.equals((Object[])this.values, (Object[])ingredient.values) : false; ++ return p_301003_ instanceof Ingredient ingredient ? java.util.Objects.equals(this.customIngredient, ingredient.customIngredient) && Arrays.equals((Object[])this.values, (Object[])ingredient.values) : false; ++ } ++ ++ @Override ++ public int hashCode() { ++ if (this.customIngredient != null) { ++ return this.customIngredient.hashCode(); ++ } ++ return Arrays.hashCode(this.values); + } + + /** -+ * {@return if {@code true}, this ingredient will be synchronized using its contents, as in vanilla, otherwise it will be synchronized via the {@link #codec(boolean) codec}} ++ * Retrieves the underlying values of this ingredient. ++ * If this is a {@linkplain #isCustom custom ingredient}, an exception is thrown. + */ -+ public boolean synchronizeWithContents() { -+ return true; ++ public Value[] getValues() { ++ if (isCustom()) { ++ throw new IllegalStateException("Cannot retrieve values from custom ingredient!"); ++ } ++ return this.values; ++ } ++ ++ public boolean isSimple() { ++ return this.customIngredient == null || this.customIngredient.isSimple(); + } + ++ @Nullable ++ public net.neoforged.neoforge.common.crafting.ICustomIngredient getCustomIngredient() { ++ return this.customIngredient; ++ } ++ ++ public boolean isCustom() { ++ return this.customIngredient != null; + } + public static Ingredient fromValues(Stream p_43939_) { - Ingredient ingredient = new Ingredient(p_43939_); - return ingredient.isEmpty() ? EMPTY : ingredient; -@@ -143,7 +_,11 @@ - ); +@@ -120,6 +_,7 @@ + return fromValues(Stream.of(new Ingredient.TagValue(p_204133_))); } -- public static record ItemValue(ItemStack item) implements Ingredient.Value { -+ public static record ItemValue(ItemStack item, java.util.function.BiFunction comparator) implements Ingredient.Value { -+ public ItemValue(ItemStack item) { -+ this(item, ItemValue::areStacksEqual); -+ } -+ - static final Codec CODEC = RecordCodecBuilder.create( ++ @Deprecated // Neo: We take over the codec creation entirely to support custom ingredients - see CraftingHelper + private static Codec codec(boolean p_301074_) { + Codec codec = Codec.list(Ingredient.Value.CODEC) + .comapFlatMap( +@@ -144,10 +_,11 @@ + } + + public static record ItemValue(ItemStack item) implements Ingredient.Value { +- static final Codec CODEC = RecordCodecBuilder.create( ++ static final com.mojang.serialization.MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( p_330109_ -> p_330109_.group(ItemStack.SIMPLE_ITEM_CODEC.fieldOf("item").forGetter(p_300919_ -> p_300919_.item)) .apply(p_330109_, Ingredient.ItemValue::new) -@@ -153,13 +_,18 @@ - public boolean equals(Object p_301316_) { - return !(p_301316_ instanceof Ingredient.ItemValue ingredient$itemvalue) - ? false -- : ingredient$itemvalue.item.getItem().equals(this.item.getItem()) && ingredient$itemvalue.item.getCount() == this.item.getCount(); -+ : comparator().apply(item(), ingredient$itemvalue.item()); - } + ); ++ static final Codec CODEC = MAP_CODEC.codec(); @Override - public Collection getItems() { - return Collections.singleton(this.item); - } -+ -+ private static boolean areStacksEqual(ItemStack left, ItemStack right) { -+ return left.getItem().equals(right.getItem()) -+ && left.getCount() == right.getCount(); -+ } + public boolean equals(Object p_301316_) { +@@ -163,10 +_,11 @@ } public static record TagValue(TagKey tag) implements Ingredient.Value { -@@ -181,6 +_,11 @@ +- static final Codec CODEC = RecordCodecBuilder.create( ++ static final com.mojang.serialization.MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( + p_301118_ -> p_301118_.group(TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter(p_301154_ -> p_301154_.tag)) + .apply(p_301118_, Ingredient.TagValue::new) + ); ++ static final Codec CODEC = MAP_CODEC.codec(); + + @Override + public boolean equals(Object p_301162_) { +@@ -181,12 +_,18 @@ list.add(new ItemStack(holder)); } @@ -177,13 +202,19 @@ return list; } } -@@ -198,5 +_,9 @@ + ++ // Neo: Do not extend this interface. For custom ingredient behaviors see ICustomIngredient. + public interface Value { +- Codec CODEC = Codec.xor(Ingredient.ItemValue.CODEC, Ingredient.TagValue.CODEC) ++ com.mojang.serialization.MapCodec MAP_CODEC = net.neoforged.neoforge.common.util.NeoForgeExtraCodecs.xor(Ingredient.ItemValue.MAP_CODEC, Ingredient.TagValue.MAP_CODEC) + .xmap(p_300956_ -> p_300956_.map(p_300932_ -> p_300932_, p_301313_ -> p_301313_), p_301304_ -> { + if (p_301304_ instanceof Ingredient.TagValue ingredient$tagvalue) { + return Either.right(ingredient$tagvalue); +@@ -196,6 +_,7 @@ + throw new UnsupportedOperationException("This is neither an item value nor a tag value."); + } }); ++ Codec CODEC = MAP_CODEC.codec(); Collection getItems(); -+ } -+ -+ public final Value[] getValues() { -+ return values; } - } diff --git a/patches/net/minecraft/world/item/crafting/Recipe.java.patch b/patches/net/minecraft/world/item/crafting/Recipe.java.patch index f082a364c4..a0782ea1c0 100644 --- a/patches/net/minecraft/world/item/crafting/Recipe.java.patch +++ b/patches/net/minecraft/world/item/crafting/Recipe.java.patch @@ -25,6 +25,6 @@ default boolean isIncomplete() { NonNullList nonnulllist = this.getIngredients(); - return nonnulllist.isEmpty() || nonnulllist.stream().anyMatch(p_151268_ -> p_151268_.getItems().length == 0); -+ return nonnulllist.isEmpty() || nonnulllist.stream().anyMatch(net.neoforged.neoforge.common.CommonHooks::hasNoElements); ++ return nonnulllist.isEmpty() || nonnulllist.stream().anyMatch(Ingredient::hasNoItems); } } diff --git a/patches/net/minecraft/world/item/crafting/ShapedRecipe.java.patch b/patches/net/minecraft/world/item/crafting/ShapedRecipe.java.patch index 8188e0dd08..87b636751b 100644 --- a/patches/net/minecraft/world/item/crafting/ShapedRecipe.java.patch +++ b/patches/net/minecraft/world/item/crafting/ShapedRecipe.java.patch @@ -14,7 +14,7 @@ public boolean isIncomplete() { NonNullList nonnulllist = this.getIngredients(); - return nonnulllist.isEmpty() || nonnulllist.stream().filter(p_151277_ -> !p_151277_.isEmpty()).anyMatch(p_151273_ -> p_151273_.getItems().length == 0); -+ return nonnulllist.isEmpty() || nonnulllist.stream().filter(p_151277_ -> !p_151277_.isEmpty()).anyMatch(net.neoforged.neoforge.common.CommonHooks::hasNoElements); ++ return nonnulllist.isEmpty() || nonnulllist.stream().filter(p_151277_ -> !p_151277_.isEmpty()).anyMatch(Ingredient::hasNoItems); } public static class Serializer implements RecipeSerializer { diff --git a/patches/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch b/patches/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch index dfbc2f3586..7d94f428dd 100644 --- a/patches/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch +++ b/patches/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch @@ -5,7 +5,7 @@ @Override public boolean isIncomplete() { - return Stream.of(this.template, this.base, this.addition).anyMatch(Ingredient::isEmpty); -+ return Stream.of(this.template, this.base, this.addition).anyMatch(net.neoforged.neoforge.common.CommonHooks::hasNoElements); ++ return Stream.of(this.template, this.base, this.addition).anyMatch(Ingredient::hasNoItems); } public static class Serializer implements RecipeSerializer { diff --git a/patches/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch b/patches/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch index e0ecd7a810..91cdb963c3 100644 --- a/patches/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch +++ b/patches/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch @@ -5,7 +5,7 @@ @Override public boolean isIncomplete() { - return Stream.of(this.template, this.base, this.addition).anyMatch(Ingredient::isEmpty); -+ return Stream.of(this.template, this.base, this.addition).anyMatch(net.neoforged.neoforge.common.CommonHooks::hasNoElements); ++ return Stream.of(this.template, this.base, this.addition).anyMatch(Ingredient::hasNoItems); } public static class Serializer implements RecipeSerializer { diff --git a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java index 108f93646d..ce5d91c41a 100644 --- a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java +++ b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java @@ -100,7 +100,6 @@ import net.minecraft.world.item.EnchantedBookItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; import net.minecraft.world.item.PotionItem; import net.minecraft.world.item.SpawnEggItem; import net.minecraft.world.item.Tiers; @@ -108,7 +107,6 @@ import net.minecraft.world.item.alchemy.Potion; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.item.enchantment.EnchantmentHelper; @@ -897,18 +895,6 @@ public static int onNoteChange(Level level, BlockPos pos, BlockState state, int return event.getVanillaNoteId(); } - public static boolean hasNoElements(Ingredient ingredient) { - ItemStack[] items = ingredient.getItems(); - if (items.length == 0) - return true; - if (items.length == 1) { - // If we potentially added a barrier due to the ingredient being an empty tag, try and check if it is the stack we added - ItemStack item = items[0]; - return item.getItem() == Items.BARRIER && item.getHoverName() instanceof MutableComponent hoverName && hoverName.getString().startsWith("Empty Tag: "); - } - return false; - } - @Deprecated(forRemoval = true, since = "1.20.1") // Tags use a codec now This was never used in 1.20 public static void deserializeTagAdditions(List list, JsonObject json, List allList) {} diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 7d9e256112..779a90d403 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -58,7 +58,6 @@ import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.Items; -import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.GameRules; @@ -367,10 +366,10 @@ public class NeoForgeMod { private static final DeferredRegister> INGREDIENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.INGREDIENT_TYPES, NeoForgeVersion.MOD_ID); - public static final DeferredHolder, IngredientType> COMPOUND_INGREDIENT_TYPE = INGREDIENT_TYPES.register("compound", () -> new IngredientType<>(CompoundIngredient.CODEC, CompoundIngredient.CODEC_NONEMPTY)); - public static final DeferredHolder, IngredientType> NBT_INGREDIENT_TYPE = INGREDIENT_TYPES.register("components", () -> new IngredientType<>(DataComponentIngredient.CODEC, DataComponentIngredient.CODEC_NONEMPTY)); - public static final DeferredHolder, IngredientType> DIFFERENCE_INGREDIENT_TYPE = INGREDIENT_TYPES.register("difference", () -> new IngredientType<>(DifferenceIngredient.CODEC, DifferenceIngredient.CODEC_NONEMPTY)); - public static final DeferredHolder, IngredientType> INTERSECTION_INGREDIENT_TYPE = INGREDIENT_TYPES.register("intersection", () -> new IngredientType<>(IntersectionIngredient.CODEC, IntersectionIngredient.CODEC_NONEMPTY)); + public static final DeferredHolder, IngredientType> COMPOUND_INGREDIENT_TYPE = INGREDIENT_TYPES.register("compound", () -> new IngredientType<>(CompoundIngredient.CODEC)); + public static final DeferredHolder, IngredientType> DATA_COMPONENT_INGREDIENT_TYPE = INGREDIENT_TYPES.register("components", () -> new IngredientType<>(DataComponentIngredient.CODEC)); + public static final DeferredHolder, IngredientType> DIFFERENCE_INGREDIENT_TYPE = INGREDIENT_TYPES.register("difference", () -> new IngredientType<>(DifferenceIngredient.CODEC)); + public static final DeferredHolder, IngredientType> INTERSECTION_INGREDIENT_TYPE = INGREDIENT_TYPES.register("intersection", () -> new IngredientType<>(IntersectionIngredient.CODEC)); private static final DeferredRegister> CONDITION_CODECS = DeferredRegister.create(NeoForgeRegistries.Keys.CONDITION_CODECS, NeoForgeVersion.MOD_ID); public static final DeferredHolder, MapCodec> AND_CONDITION = CONDITION_CODECS.register("and", () -> AndCondition.CODEC); @@ -381,10 +380,6 @@ public class NeoForgeMod { public static final DeferredHolder, MapCodec> OR_CONDITION = CONDITION_CODECS.register("or", () -> OrCondition.CODEC); public static final DeferredHolder, MapCodec> TAG_EMPTY_CONDITION = CONDITION_CODECS.register("tag_empty", () -> TagEmptyCondition.CODEC); public static final DeferredHolder, MapCodec> TRUE_CONDITION = CONDITION_CODECS.register("true", () -> TrueCondition.CODEC); - private static final DeferredRegister> VANILLA_INGREDIENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.INGREDIENT_TYPES, "minecraft"); - - // TODO 1.20.5: this will be gone with the custom ingredient cleanup - public static final DeferredHolder, IngredientType> VANILLA_INGREDIENT_TYPE = VANILLA_INGREDIENT_TYPES.register("item", () -> new IngredientType<>(Ingredient.VANILLA_CODEC.fieldOf("value"), Ingredient.VANILLA_CODEC_NONEMPTY.fieldOf("value"))); private static final DeferredRegister> ENTITY_PREDICATE_CODECS = DeferredRegister.create(Registries.ENTITY_SUB_PREDICATE_TYPE, NeoForgeVersion.MOD_ID); public static final DeferredHolder, MapCodec> PIGLIN_NEUTRAL_ARMOR_PREDICATE = ENTITY_PREDICATE_CODECS.register("piglin_neutral_armor", () -> PiglinNeutralArmorEntityPredicate.CODEC); @@ -583,7 +578,6 @@ public NeoForgeMod(IEventBus modEventBus, Dist dist, ModContainer container) { STRUCTURE_MODIFIER_SERIALIZERS.register(modEventBus); HOLDER_SET_TYPES.register(modEventBus); VANILLA_FLUID_TYPES.register(modEventBus); - VANILLA_INGREDIENT_TYPES.register(modEventBus); ENTITY_PREDICATE_CODECS.register(modEventBus); ITEM_SUB_PREDICATES.register(modEventBus); INGREDIENT_TYPES.register(modEventBus); diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/ChildBasedIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/ChildBasedIngredient.java deleted file mode 100644 index 39e85f43cd..0000000000 --- a/src/main/java/net/neoforged/neoforge/common/crafting/ChildBasedIngredient.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.common.crafting; - -import java.util.Collections; -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Stream; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import org.jetbrains.annotations.Nullable; - -/** Intermediary class for easing handling of ingredients that make use of multiple children */ -public abstract class ChildBasedIngredient extends Ingredient { - protected final List children; - private final boolean isSimple; - private final boolean synchronizeWithContents; - - @Nullable - private ItemStack[] filteredMatchingStacks; - - protected ChildBasedIngredient(Stream values, Supplier> type, List children) { - super(values, type); - this.children = Collections.unmodifiableList(children); - this.isSimple = children.stream().allMatch(Ingredient::isSimple); - this.synchronizeWithContents = children.stream().anyMatch(Ingredient::synchronizeWithContents); - } - - protected abstract Stream generateMatchingStacks(); - - protected abstract boolean testComplex(@Nullable ItemStack stack); - - @Override - public final ItemStack[] getItems() { - if (synchronizeWithContents() && isSimple()) { - return super.getItems(); - } - - if (this.filteredMatchingStacks == null) { - this.filteredMatchingStacks = generateMatchingStacks() - .distinct()//Mimic super that calls distinct on the stacks - .toArray(ItemStack[]::new); - } - return this.filteredMatchingStacks; - } - - @Override - public final boolean test(@Nullable ItemStack stack) { - return synchronizeWithContents() && isSimple() ? super.test(stack) : testComplex(stack); - } - - @Override - public final boolean isSimple() { - return isSimple; - } - - @Override - public final boolean synchronizeWithContents() { - return synchronizeWithContents; - } - - public final List getChildren() { - return this.children; - } -} diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/CompoundIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/CompoundIngredient.java index f23aebcae1..1327ce7470 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/CompoundIngredient.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/CompoundIngredient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) Forge Development LLC and contributors + * Copyright (c) NeoForged and contributors * SPDX-License-Identifier: LGPL-2.1-only */ @@ -8,50 +8,56 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.stream.Stream; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; -import org.jetbrains.annotations.Nullable; /** Ingredient that matches if any of the child ingredients match */ -public class CompoundIngredient extends ChildBasedIngredient { - public static final MapCodec CODEC = NeoForgeExtraCodecs.aliasedFieldOf(Ingredient.LIST_CODEC, "children", "ingredients").xmap(CompoundIngredient::new, ChildBasedIngredient::getChildren); - public static final Codec DIRECT_CODEC = Ingredient.LIST_CODEC.xmap(CompoundIngredient::new, ChildBasedIngredient::getChildren); - public static final MapCodec CODEC_NONEMPTY = NeoForgeExtraCodecs.aliasedFieldOf(Ingredient.LIST_CODEC_NONEMPTY, "children", "ingredients").xmap(CompoundIngredient::new, ChildBasedIngredient::getChildren); - public static final Codec DIRECT_CODEC_NONEMPTY = Ingredient.LIST_CODEC_NONEMPTY.xmap(CompoundIngredient::new, ChildBasedIngredient::getChildren); - - protected CompoundIngredient(List children) { - super(children.stream().map(Value::new), NeoForgeMod.COMPOUND_INGREDIENT_TYPE, children); - } +public record CompoundIngredient(List children) implements ICustomIngredient { + public static final MapCodec CODEC = NeoForgeExtraCodecs.aliasedFieldOf(Ingredient.LIST_CODEC_NONEMPTY, "children", "ingredients").xmap(CompoundIngredient::new, CompoundIngredient::children); + public static final Codec DIRECT_CODEC = Ingredient.LIST_CODEC.xmap(CompoundIngredient::new, CompoundIngredient::children); + public static final Codec DIRECT_CODEC_NONEMPTY = Ingredient.LIST_CODEC_NONEMPTY.xmap(CompoundIngredient::new, CompoundIngredient::children); /** Creates a compound ingredient from the given list of ingredients */ public static Ingredient of(Ingredient... children) { if (children.length == 0) - return of(); + return Ingredient.EMPTY; if (children.length == 1) return children[0]; - return new CompoundIngredient(List.of(children)); + return new CompoundIngredient(List.of(children)).toVanilla(); } @Override - protected Stream generateMatchingStacks() { + public Stream getItems() { return children.stream().flatMap(child -> Arrays.stream(child.getItems())); } @Override - protected boolean testComplex(@Nullable ItemStack stack) { - return children.stream().anyMatch(i -> i.test(stack)); + public boolean test(ItemStack stack) { + for (var child : children) { + if (child.test(stack)) { + return true; + } + } + return false; } - private record Value(Ingredient inner) implements Ingredient.Value { - @Override - public Collection getItems() { - return List.of(inner.getItems()); + @Override + public boolean isSimple() { + for (var child : children) { + if (!child.isSimple()) { + return false; + } } + return true; + } + + @Override + public IngredientType getType() { + return NeoForgeMod.COMPOUND_INGREDIENT_TYPE.get(); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/CraftingHelper.java b/src/main/java/net/neoforged/neoforge/common/crafting/CraftingHelper.java index 0dc13003c3..f19e510c37 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/CraftingHelper.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/CraftingHelper.java @@ -8,42 +8,60 @@ import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; -import java.util.function.Function; +import com.mojang.serialization.MapCodec; +import java.util.stream.Stream; import net.minecraft.world.item.crafting.Ingredient; -import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; import net.neoforged.neoforge.registries.NeoForgeRegistries; import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public class CraftingHelper { - @ApiStatus.Internal - public static Codec makeIngredientCodec(boolean allowEmpty, Codec vanillaCodec) { + public static Codec makeIngredientCodec(boolean allowEmpty) { var compoundIngredientCodec = Codec.lazyInitialized(() -> allowEmpty ? CompoundIngredient.DIRECT_CODEC : CompoundIngredient.DIRECT_CODEC_NONEMPTY); - return NeoForgeExtraCodecs.withAlternative( - // Compound ingredient handling - compoundIngredientCodec.flatComapMap( - Function.identity(), - i -> i instanceof CompoundIngredient c ? DataResult.success(c) : DataResult.error(() -> "Not a compound ingredient")), - // Otherwise choose between custom and vanilla - makeIngredientCodec0(allowEmpty, vanillaCodec)); + return Codec.either(compoundIngredientCodec, makeIngredientMapCodec().codec()) + .xmap(either -> either.map(ICustomIngredient::toVanilla, i -> i), ingredient -> { + if (convertToCompoundIngredient(ingredient).getCustomIngredient() instanceof CompoundIngredient compound) { + // Use [] syntax for vanilla array ingredients and CompoundIngredients. + return Either.left(compound); + } else { + // Else use {} syntax. + return Either.right(ingredient); + } + }); } - // Choose between dispatch codec for custom ingredients and vanilla codec - private static Codec makeIngredientCodec0(boolean allowEmpty, Codec vanillaCodec) { - // Dispatch codec for custom ingredient types: - Codec dispatchCodec = NeoForgeRegistries.INGREDIENT_TYPES.byNameCodec() - .dispatch(Ingredient::getType, ingredientType -> ingredientType.codec(allowEmpty)); - // Either codec to combine with the vanilla ingredient codec: - Codec> eitherCodec = Codec.either( - dispatchCodec, - vanillaCodec); - return eitherCodec.xmap(either -> either.map(i -> i, i -> i), ingredient -> { - // Prefer writing without the "type" field if possible: - if (ingredient.getType() == NeoForgeMod.VANILLA_INGREDIENT_TYPE.get()) { - return Either.right(ingredient); - } else { - return Either.left(ingredient); - } - }); + /** + * Converts vanilla "array ingredients" to {@link CompoundIngredient}s. + */ + private static Ingredient convertToCompoundIngredient(Ingredient ingredient) { + if (!ingredient.isCustom() && ingredient.getValues().length != 1) { + // Convert vanilla ingredient to custom CompoundIngredient + return CompoundIngredient.of(Stream.of(ingredient.getValues()).map(v -> Ingredient.fromValues(Stream.of(v))).toArray(Ingredient[]::new)); + } + return ingredient; + } + + public static MapCodec makeIngredientMapCodec() { + // Dispatch codec for custom ingredient types, else fallback to vanilla ingredient codec. + return NeoForgeExtraCodecs., ICustomIngredient, Ingredient.Value>dispatchMapOrElse( + NeoForgeRegistries.INGREDIENT_TYPES.byNameCodec(), + ICustomIngredient::getType, + IngredientType::codec, + Ingredient.Value.MAP_CODEC) + .xmap(either -> either.map(ICustomIngredient::toVanilla, v -> Ingredient.fromValues(Stream.of(v))), ingredient -> { + var customIngredient = convertToCompoundIngredient(ingredient).getCustomIngredient(); + if (customIngredient == null) { + return Either.right(ingredient.getValues()[0]); + } else { + return Either.left(customIngredient); + } + }) + .validate(ingredient -> { + if (!ingredient.isCustom() && ingredient.getValues().length == 0) { + return DataResult.error(() -> "Cannot serialize empty ingredient using the map codec"); + } + return DataResult.success(ingredient); + }); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/DataComponentIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/DataComponentIngredient.java index 3c0e59fa2c..c3565ff734 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/DataComponentIngredient.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/DataComponentIngredient.java @@ -10,6 +10,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.Arrays; import java.util.function.Supplier; +import java.util.stream.Stream; import net.minecraft.core.Holder; import net.minecraft.core.HolderSet; import net.minecraft.core.component.DataComponentMap; @@ -23,7 +24,6 @@ import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.ItemLike; import net.neoforged.neoforge.common.NeoForgeMod; -import org.jetbrains.annotations.Nullable; /** * Ingredient that matches the given items, performing either a {@link DataComponentIngredient#isStrict() strict} or a partial NBT test. @@ -31,7 +31,7 @@ * Strict NBT ingredients will only match items that have exactly the provided tag, while partial ones will * match if the item's tags contain all of the elements of the provided one, while allowing for additional elements to exist. */ -public class DataComponentIngredient extends Ingredient { +public class DataComponentIngredient implements ICustomIngredient { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( @@ -39,34 +39,25 @@ public class DataComponentIngredient extends Ingredient { DataComponentPredicate.CODEC.fieldOf("components").forGetter(DataComponentIngredient::components), Codec.BOOL.optionalFieldOf("strict", false).forGetter(DataComponentIngredient::isStrict)) .apply(builder, DataComponentIngredient::new)); - public static final MapCodec CODEC_NONEMPTY = RecordCodecBuilder.mapCodec( - builder -> builder - .group( - HolderSetCodec.create(Registries.ITEM, BuiltInRegistries.ITEM.holderByNameCodec(), false).fieldOf("items").forGetter(DataComponentIngredient::items), - DataComponentPredicate.CODEC.fieldOf("components").forGetter(DataComponentIngredient::components), - Codec.BOOL.optionalFieldOf("strict", false).forGetter(DataComponentIngredient::isStrict)) - .apply(builder, DataComponentIngredient::new)); private final HolderSet items; private final DataComponentPredicate components; private final boolean strict; + private final ItemStack[] stacks; - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - protected DataComponentIngredient(HolderSet items, DataComponentPredicate components, boolean strict) { - super(items.stream().map(item -> { - ItemStack stack = new ItemStack(item, 1, components.asPatch()); - return new Ingredient.ItemValue(stack, ItemStack::matches); - }), NeoForgeMod.NBT_INGREDIENT_TYPE); + public DataComponentIngredient(HolderSet items, DataComponentPredicate components, boolean strict) { this.items = items; this.components = components; this.strict = strict; + this.stacks = items.stream() + .map(i -> new ItemStack(i, 1, components.asPatch())) + .toArray(ItemStack[]::new); } @Override - public boolean test(@Nullable ItemStack stack) { - if (stack == null) return false; + public boolean test(ItemStack stack) { if (strict) { - for (ItemStack stack2 : getItems()) { + for (ItemStack stack2 : this.stacks) { if (ItemStack.matches(stack, stack2)) return true; } return false; @@ -76,8 +67,8 @@ public boolean test(@Nullable ItemStack stack) { } @Override - public boolean synchronizeWithContents() { - return false; + public Stream getItems() { + return Stream.of(stacks); } @Override @@ -85,6 +76,11 @@ public boolean isSimple() { return false; } + @Override + public IngredientType getType() { + return NeoForgeMod.DATA_COMPONENT_INGREDIENT_TYPE.get(); + } + public HolderSet items() { return items; } @@ -100,28 +96,28 @@ public boolean isStrict() { /** * Creates a new ingredient matching the given item, containing the given components */ - public static DataComponentIngredient of(boolean strict, ItemStack stack) { + public static Ingredient of(boolean strict, ItemStack stack) { return of(strict, stack.getComponents(), stack.getItem()); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, DataComponentType type, T value, ItemLike... items) { + public static Ingredient of(boolean strict, DataComponentType type, T value, ItemLike... items) { return of(strict, DataComponentPredicate.builder().expect(type, value).build(), items); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, Supplier> type, T value, ItemLike... items) { + public static Ingredient of(boolean strict, Supplier> type, T value, ItemLike... items) { return of(strict, type.get(), value, items); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, DataComponentMap map, ItemLike... items) { + public static Ingredient of(boolean strict, DataComponentMap map, ItemLike... items) { return of(strict, DataComponentPredicate.allOf(map), items); } @@ -129,14 +125,14 @@ public static DataComponentIngredient of(boolean strict, DataComponentMap map, I * Creates a new ingredient matching any item from the list, containing the given components */ @SafeVarargs - public static DataComponentIngredient of(boolean strict, DataComponentMap map, Holder... items) { + public static Ingredient of(boolean strict, DataComponentMap map, Holder... items) { return of(strict, DataComponentPredicate.allOf(map), items); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, DataComponentMap map, HolderSet items) { + public static Ingredient of(boolean strict, DataComponentMap map, HolderSet items) { return of(strict, DataComponentPredicate.allOf(map), items); } @@ -144,21 +140,21 @@ public static DataComponentIngredient of(boolean strict, DataComponentMap map, H * Creates a new ingredient matching any item from the list, containing the given components */ @SafeVarargs - public static DataComponentIngredient of(boolean strict, DataComponentPredicate predicate, Holder... items) { + public static Ingredient of(boolean strict, DataComponentPredicate predicate, Holder... items) { return of(strict, predicate, HolderSet.direct(items)); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, DataComponentPredicate predicate, ItemLike... items) { + public static Ingredient of(boolean strict, DataComponentPredicate predicate, ItemLike... items) { return of(strict, predicate, HolderSet.direct(Arrays.stream(items).map(ItemLike::asItem).map(Item::builtInRegistryHolder).toList())); } /** * Creates a new ingredient matching any item from the list, containing the given components */ - public static DataComponentIngredient of(boolean strict, DataComponentPredicate predicate, HolderSet items) { - return new DataComponentIngredient(items, predicate, strict); + public static Ingredient of(boolean strict, DataComponentPredicate predicate, HolderSet items) { + return new DataComponentIngredient(items, predicate, strict).toVanilla(); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/DifferenceIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/DifferenceIngredient.java index 1c315ac1c4..2fed470d18 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/DifferenceIngredient.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/DifferenceIngredient.java @@ -7,57 +7,38 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; import java.util.stream.Stream; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.neoforged.neoforge.common.NeoForgeMod; -import org.jetbrains.annotations.Nullable; /** Ingredient that matches everything from the first ingredient that is not included in the second ingredient */ -public class DifferenceIngredient extends ChildBasedIngredient { +public record DifferenceIngredient(Ingredient base, Ingredient subtracted) implements ICustomIngredient { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( - Ingredient.CODEC.fieldOf("base").forGetter(DifferenceIngredient::getBase), - Ingredient.CODEC.fieldOf("subtracted").forGetter(DifferenceIngredient::getSubtracted)) + Ingredient.CODEC.fieldOf("base").forGetter(DifferenceIngredient::base), + Ingredient.CODEC.fieldOf("subtracted").forGetter(DifferenceIngredient::subtracted)) .apply(builder, DifferenceIngredient::new)); - public static final MapCodec CODEC_NONEMPTY = RecordCodecBuilder.mapCodec( - builder -> builder - .group( - Ingredient.CODEC_NONEMPTY.fieldOf("base").forGetter(DifferenceIngredient::getBase), - Ingredient.CODEC_NONEMPTY.fieldOf("subtracted").forGetter(DifferenceIngredient::getSubtracted)) - .apply(builder, DifferenceIngredient::new)); - - private final Ingredient base; - private final Ingredient subtracted; - - protected DifferenceIngredient(Ingredient base, Ingredient subtracted) { - super(Arrays.stream(base.getValues()).map(value -> new SubtractingValue(value, subtracted)), NeoForgeMod.DIFFERENCE_INGREDIENT_TYPE, List.of(base, subtracted)); - - this.base = base; - this.subtracted = subtracted; - } - - public Ingredient getBase() { - return base; + @Override + public Stream getItems() { + return Stream.of(base.getItems()).filter(subtracted.negate()); } - public Ingredient getSubtracted() { - return subtracted; + @Override + public boolean test(ItemStack stack) { + return base.test(stack) && !subtracted.test(stack); } @Override - protected Stream generateMatchingStacks() { - return Arrays.stream(base.getItems()).filter(subtracted.negate()); + public boolean isSimple() { + return base.isSimple() && subtracted.isSimple(); } @Override - protected boolean testComplex(@Nullable ItemStack stack) { - return base.test(stack) && !subtracted.test(stack); + public IngredientType getType() { + return NeoForgeMod.DIFFERENCE_INGREDIENT_TYPE.get(); } /** @@ -67,14 +48,7 @@ protected boolean testComplex(@Nullable ItemStack stack) { * @param subtracted Ingredient the item must not match * @return Ingredient that {@code base} anything in base that is not in {@code subtracted} */ - public static DifferenceIngredient of(Ingredient base, Ingredient subtracted) { - return new DifferenceIngredient(base, subtracted); - } - - private record SubtractingValue(Value inner, Ingredient subtracted) implements Ingredient.Value { - @Override - public Collection getItems() { - return inner().getItems().stream().filter(subtracted.negate()).toList(); - } + public static Ingredient of(Ingredient base, Ingredient subtracted) { + return new DifferenceIngredient(base, subtracted).toVanilla(); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/ICustomIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/ICustomIngredient.java new file mode 100644 index 0000000000..dc213aee67 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/crafting/ICustomIngredient.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.crafting; + +import java.util.stream.Stream; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import org.jetbrains.annotations.ApiStatus; + +/** + * Interface that modders can implement to create new behaviors for {@link Ingredient}s. + * + *

This is not directly implemented on vanilla {@link Ingredient}s, but conversions are possible: + *

    + *
  • {@link #toVanilla()} converts a custom ingredient to a vanilla {@link Ingredient}.
  • + *
  • {@link Ingredient#getCustomIngredient()} retrieves the custom ingredient inside a vanilla {@link Ingredient}.
  • + *
+ * + *

Implementations of this interface must implement {@link Object#equals} and {@link Object#hashCode} + * to ensure that the ingredient also supports them. + */ +public interface ICustomIngredient { + /** + * Checks if a stack matches this ingredient. + * The stack must not be modified in any way. + * + * @param stack the stack to test + * @return {@code true} if the stack matches this ingredient, {@code false} otherwise + */ + boolean test(ItemStack stack); + + /** + * {@return the list of stacks that this ingredient accepts} + * + *

The following guidelines should be followed for good compatibility: + *

    + *
  • These stacks are generally used for display purposes, and need not be exhaustive or perfectly accurate.
  • + *
  • An exception is ingredients that {@linkplain #isSimple() are simple}, + * for which it is important that the returned stacks correspond exactly to all the accepted {@link Item}s.
  • + *
  • At least one stack must be returned for the ingredient not to be considered {@linkplain Ingredient#hasNoItems() accidentally empty}.
  • + *
  • The ingredient should try to return at least one stack with each accepted {@link Item}. + * This allows mods that inspect the ingredient to figure out which stacks it might accept.
  • + *
+ * + *

Note: no caching needs to be done by the implementation, this is already handled by the ingredient itself. + */ + Stream getItems(); + + /** + * Returns whether this ingredient always requires {@linkplain #test direct stack testing}. + * + * @return {@code true} if this ingredient ignores NBT data when matching stacks, {@code false} otherwise + * @see Ingredient#isSimple() + */ + boolean isSimple(); + + /** + * {@return the type of this ingredient} + * + *

The type must be registered to {@link NeoForgeRegistries#INGREDIENT_TYPES}. + */ + IngredientType getType(); + + /** + * {@return a new {@link Ingredient} behaving as defined by this custom ingredient} + */ + @ApiStatus.NonExtendable + default Ingredient toVanilla() { + return new Ingredient(this); + } +} diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/IngredientType.java b/src/main/java/net/neoforged/neoforge/common/crafting/IngredientType.java index 2d7d8a8696..c8b21c56d4 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/IngredientType.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/IngredientType.java @@ -6,26 +6,20 @@ package net.neoforged.neoforge.common.crafting; import com.mojang.serialization.MapCodec; -import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; /** - * An ingredient type encapsulates the codecs to serialize and deserialize an ingredient. - *

- * {@code codec} allows ingredients that are known to be empty at deserialization time, - * whereas {@code nonEmptyCodec} does not. + * An ingredient type encapsulates the codecs to serialize and deserialize a custom ingredient. + * + *

Note that the {@link #streamCodec()} is only used if {@link ICustomIngredient#isSimple()} returns {@code false}. */ -public record IngredientType(MapCodec codec, MapCodec nonEmptyCodec) { +public record IngredientType(MapCodec codec, StreamCodec streamCodec) { /** - * Constructor for ingredient types that have the same codec for empty and non-empty serialization. + * Constructor for ingredient types that use a regular codec for network syncing. */ - public IngredientType(MapCodec nonEmptyCodec) { - this(nonEmptyCodec, nonEmptyCodec); - } - - /** - * Returns the right codec for this ingredient type based on {@code allowEmpty}. - */ - public MapCodec codec(boolean allowEmpty) { - return allowEmpty ? codec : nonEmptyCodec; + public IngredientType(MapCodec codec) { + this(codec, ByteBufCodecs.fromCodecWithRegistries(codec.codec())); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/IntersectionIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/IntersectionIngredient.java index 3ae0a22ceb..7f3678822b 100644 --- a/src/main/java/net/neoforged/neoforge/common/crafting/IntersectionIngredient.java +++ b/src/main/java/net/neoforged/neoforge/common/crafting/IntersectionIngredient.java @@ -7,39 +7,27 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.stream.Stream; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.neoforged.neoforge.common.NeoForgeMod; -import org.jetbrains.annotations.Nullable; /** Ingredient that matches if all child ingredients match */ -public class IntersectionIngredient extends ChildBasedIngredient { - public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( - builder -> builder - .group( - Ingredient.LIST_CODEC.fieldOf("children").forGetter(ChildBasedIngredient::getChildren)) - .apply(builder, IntersectionIngredient::new)); +public record IntersectionIngredient(List children) implements ICustomIngredient { + public IntersectionIngredient { + if (children.isEmpty()) { + throw new IllegalArgumentException("Cannot create an IntersectionIngredient with no children, use Ingredient.of() to create an empty ingredient"); + } + } - public static final MapCodec CODEC_NONEMPTY = RecordCodecBuilder.mapCodec( + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( - Ingredient.LIST_CODEC_NONEMPTY.fieldOf("children").forGetter(ChildBasedIngredient::getChildren)) + Ingredient.LIST_CODEC_NONEMPTY.fieldOf("children").forGetter(IntersectionIngredient::children)) .apply(builder, IntersectionIngredient::new)); - protected IntersectionIngredient(List children) { - super(children.stream().flatMap(ingredient -> Arrays.stream(ingredient.getValues()).map(value -> { - final List matchers = new ArrayList<>(children); - matchers.remove(ingredient); - - return new IntersectionValue(value, matchers); - })), NeoForgeMod.INTERSECTION_INGREDIENT_TYPE, children); - } - /** * Gets an intersection ingredient * @@ -52,27 +40,38 @@ public static Ingredient of(Ingredient... ingredients) { if (ingredients.length == 1) return ingredients[0]; - return new IntersectionIngredient(Arrays.asList(ingredients)); + return new IntersectionIngredient(Arrays.asList(ingredients)).toVanilla(); } @Override - protected Stream generateMatchingStacks() { + public boolean test(ItemStack stack) { + for (var child : children) { + if (!child.test(stack)) { + return false; + } + } + return true; + } + + @Override + public Stream getItems() { return children.stream() .flatMap(child -> Arrays.stream(child.getItems())) - .filter(this::testComplex); + .filter(this::test); } @Override - protected boolean testComplex(@Nullable ItemStack stack) { - return children.stream().allMatch(c -> c.test(stack)); + public boolean isSimple() { + for (var child : children) { + if (!child.isSimple()) { + return false; + } + } + return true; } - public record IntersectionValue(Value inner, List other) implements Ingredient.Value { - @Override - public Collection getItems() { - return inner().getItems().stream() - .filter(stack -> other().stream().allMatch(ingredient -> ingredient.test(stack))) - .toList(); - } + @Override + public IngredientType getType() { + return NeoForgeMod.INTERSECTION_INGREDIENT_TYPE.get(); } } diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/SizedIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/SizedIngredient.java new file mode 100644 index 0000000000..f55e959612 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/crafting/SizedIngredient.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.crafting; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.stream.Stream; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.tags.TagKey; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; +import org.jetbrains.annotations.Nullable; + +/** + * Standard implementation for an ingredient and a count. + * + *

{@link Ingredient} does not perform count checks, so this class is used to wrap an ingredient with a count, + * and provide a standard serialization format. + */ +public final class SizedIngredient { + /** + * The "flat" codec for {@link SizedIngredient}. + * + *

The count is serialized inline with the rest of the ingredient, for example: + * + *

{@code
+     * {
+     *     "item": "minecraft:apple",
+     *     "count": 3
+     * }
+     * }
+ * + * Array ingredients are serialized using the compound ingredient type: + * + *
{@code
+     * {
+     *     "type": "neoforge:compound",
+     *     "ingredients": [
+     *         { "item": "minecraft:coal" },
+     *         { "item": "minecraft:charcoal" }
+     *     ],
+     *     "count": 2
+     * }
+     * }
+ * + * See {@link Ingredient#MAP_CODEC_NONEMPTY} for details of the ingredient serialization. + */ + public static final Codec FLAT_CODEC = RecordCodecBuilder.create(instance -> instance.group( + Ingredient.MAP_CODEC_NONEMPTY.forGetter(SizedIngredient::ingredient), + NeoForgeExtraCodecs.optionalFieldAlwaysWrite(ExtraCodecs.POSITIVE_INT, "count", 1).forGetter(SizedIngredient::count)) + .apply(instance, SizedIngredient::new)); + + /** + * The "nested" codec for {@link SizedIngredient}. + * + *

The count is serialized separately from the rest of the ingredient, for example: + * + *

{@code
+     * {
+     *     "ingredient": {
+     *         "item": "minecraft:apple"
+     *     },
+     *     "count": 3
+     * }
+     * }
+ */ + public static final Codec NESTED_CODEC = RecordCodecBuilder.create(instance -> instance.group( + Ingredient.CODEC_NONEMPTY.fieldOf("ingredient").forGetter(SizedIngredient::ingredient), + NeoForgeExtraCodecs.optionalFieldAlwaysWrite(ExtraCodecs.POSITIVE_INT, "count", 1).forGetter(SizedIngredient::count)) + .apply(instance, SizedIngredient::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + Ingredient.CONTENTS_STREAM_CODEC, + SizedIngredient::ingredient, + ByteBufCodecs.VAR_INT, + SizedIngredient::count, + SizedIngredient::new); + + /** + * Helper method to create a simple sized ingredient that matches a single item. + */ + public static SizedIngredient of(ItemLike item, int count) { + return new SizedIngredient(Ingredient.of(item), count); + } + + /** + * Helper method to create a simple sized ingredient that matches items in a tag. + */ + public static SizedIngredient of(TagKey tag, int count) { + return new SizedIngredient(Ingredient.of(tag), count); + } + + private final Ingredient ingredient; + private final int count; + @Nullable + private ItemStack[] cachedStacks; + + public SizedIngredient(Ingredient ingredient, int count) { + if (count <= 0) { + throw new IllegalArgumentException("Size must be positive"); + } + this.ingredient = ingredient; + this.count = count; + } + + public Ingredient ingredient() { + return ingredient; + } + + public int count() { + return count; + } + + /** + * Performs a size-sensitive test on the given stack. + * + * @return {@code true} if the stack matches the ingredient and has at least the required count. + */ + public boolean test(ItemStack stack) { + return ingredient.test(stack) && stack.getCount() >= count; + } + + /** + * Returns a list of the stacks from this {@link #ingredient}, with an updated {@link #count}. + * + * @implNote the array is cached and should not be modified, just like {@link Ingredient#getItems()}. + */ + public ItemStack[] getItems() { + if (cachedStacks == null) { + cachedStacks = Stream.of(ingredient.getItems()) + .map(s -> s.copyWithCount(count)) + .toArray(ItemStack[]::new); + } + return cachedStacks; + } + + @Override + public String toString() { + return count + "x " + ingredient; + } +} diff --git a/src/main/java/net/neoforged/neoforge/common/util/NeoForgeExtraCodecs.java b/src/main/java/net/neoforged/neoforge/common/util/NeoForgeExtraCodecs.java index c97f9f5a86..bf166467cd 100644 --- a/src/main/java/net/neoforged/neoforge/common/util/NeoForgeExtraCodecs.java +++ b/src/main/java/net/neoforged/neoforge/common/util/NeoForgeExtraCodecs.java @@ -38,6 +38,13 @@ public static MapCodec aliasedFieldOf(final Codec codec, final String. return mapCodec; } + /** + * Similar to {@link Codec#optionalFieldOf(String, Object)}, except that the default value is always written. + */ + public static MapCodec optionalFieldAlwaysWrite(Codec codec, String name, T defaultValue) { + return codec.optionalFieldOf(name).xmap(o -> o.orElse(defaultValue), Optional::of); + } + public static MapCodec mapWithAlternative(final MapCodec mapCodec, final MapCodec alternative) { return Codec.mapEither(mapCodec, alternative).xmap(either -> either.map(Function.identity(), Function.identity()), Either::left); } @@ -193,4 +200,101 @@ public String toString() { return "AlternativeMapCodec[" + codec + ", " + alternative + "]"; } } + + /** + * Map dispatch codec with an alternative. + * + *

The alternative will only be used if there is no {@code "type"} key in the serialized object. + * + * @param typeCodec codec for the dispatch type + * @param type function to retrieve the dispatch type from the dispatched type + * @param codec function to retrieve the dispatched type map codec from the dispatch type + * @param fallbackCodec fallback to use when the deserialized object does not have a {@code "type"} key + * @param dispatch type + * @param dispatched type + * @param fallback type + */ + public static MapCodec> dispatchMapOrElse(Codec typeCodec, Function type, Function> codec, MapCodec fallbackCodec) { + var dispatchCodec = typeCodec.dispatchMap(type, codec); + return new MapCodec<>() { + @Override + public Stream keys(DynamicOps ops) { + return Stream.concat(dispatchCodec.keys(ops), fallbackCodec.keys(ops)).distinct(); + } + + @Override + public DataResult> decode(DynamicOps ops, MapLike input) { + if (input.get("type") != null) { + return dispatchCodec.decode(ops, input).map(Either::left); + } else { + return fallbackCodec.decode(ops, input).map(Either::right); + } + } + + @Override + public RecordBuilder encode(Either input, DynamicOps ops, RecordBuilder prefix) { + return input.map( + dispatched -> dispatchCodec.encode(dispatched, ops, prefix), + fallback -> fallbackCodec.encode(fallback, ops, prefix)); + } + + @Override + public String toString() { + return "DispatchOrElse[" + dispatchCodec + ", " + fallbackCodec + "]"; + } + }; + } + + /** + * Codec that matches exactly one out of two map codecs. + * Same as {@link Codec#xor} but for {@link MapCodec}s. + */ + public static MapCodec> xor(MapCodec first, MapCodec second) { + return new XorMapCodec<>(first, second); + } + + private static 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; + } + + @Override + public Stream keys(DynamicOps ops) { + return Stream.concat(first.keys(ops), second.keys(ops)).distinct(); + } + + @Override + public DataResult> decode(DynamicOps ops, MapLike input) { + DataResult> firstResult = first.decode(ops, input).map(Either::left); + DataResult> secondResult = second.decode(ops, input).map(Either::right); + var firstValue = firstResult.result(); + var secondValue = secondResult.result(); + if (firstValue.isPresent() && secondValue.isPresent()) { + return DataResult.error( + () -> "Both alternatives read successfully, cannot pick the correct one; first: " + firstValue.get() + " second: " + + secondValue.get(), + firstValue.get()); + } else if (firstValue.isPresent()) { + return firstResult; + } else if (secondValue.isPresent()) { + return secondResult; + } else { + return firstResult.apply2((x, y) -> y, secondResult); + } + } + + @Override + public RecordBuilder encode(Either input, DynamicOps ops, RecordBuilder prefix) { + return input.map(x -> first.encode(x, ops, prefix), x -> second.encode(x, ops, prefix)); + } + + @Override + public String toString() { + return "XorMapCodec[" + first + ", " + second + "]"; + } + } } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index c44c149243..a552331bd1 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -341,10 +341,6 @@ public net.minecraft.world.item.alchemy.PotionBrewing$Mix from # from public net.minecraft.world.item.alchemy.PotionBrewing$Mix ingredient # ingredient public net.minecraft.world.item.context.BlockPlaceContext (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/phys/BlockHitResult;)V # constructor public net.minecraft.world.item.context.UseOnContext (Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/phys/BlockHitResult;)V # constructor -public-f net.minecraft.world.item.crafting.Ingredient -protected net.minecraft.world.item.crafting.Ingredient (Ljava/util/stream/Stream;)V # constructor -public net.minecraft.world.item.crafting.Ingredient values # values -public+f net.minecraft.world.item.crafting.Ingredient toNetwork(Lnet/minecraft/network/FriendlyByteBuf;)V # toNetwork public net.minecraft.world.item.crafting.Ingredient fromValues(Ljava/util/stream/Stream;)Lnet/minecraft/world/item/crafting/Ingredient; # fromValues public net.minecraft.world.item.crafting.Ingredient$ItemValue public net.minecraft.world.item.crafting.Ingredient$ItemValue (Lnet/minecraft/world/item/ItemStack;)V # constructor diff --git a/testframework/src/main/java/net/neoforged/testframework/condition/TestEnabledIngredient.java b/testframework/src/main/java/net/neoforged/testframework/condition/TestEnabledIngredient.java index 11785142dc..b322b3dec2 100644 --- a/testframework/src/main/java/net/neoforged/testframework/condition/TestEnabledIngredient.java +++ b/testframework/src/main/java/net/neoforged/testframework/condition/TestEnabledIngredient.java @@ -8,53 +8,41 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import java.util.Arrays; +import java.util.stream.Stream; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; import net.neoforged.testframework.TestFramework; import net.neoforged.testframework.impl.MutableTestFramework; import net.neoforged.testframework.impl.TestFrameworkMod; -import org.jetbrains.annotations.Nullable; -public final class TestEnabledIngredient extends Ingredient { +public record TestEnabledIngredient(Ingredient base, TestFramework framework, String testId) implements ICustomIngredient { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( - builder -> builder - .group( - Ingredient.CODEC.fieldOf("base").forGetter(i -> i.base), - MutableTestFramework.REFERENCE_CODEC.fieldOf("framework").forGetter(i -> i.framework), - Codec.STRING.fieldOf("testId").forGetter(i -> i.testId)) - .apply(builder, TestEnabledIngredient::new)); - public static final MapCodec CODEC_NONEMPTY = RecordCodecBuilder.mapCodec( builder -> builder .group( Ingredient.CODEC_NONEMPTY.fieldOf("base").forGetter(i -> i.base), MutableTestFramework.REFERENCE_CODEC.fieldOf("framework").forGetter(i -> i.framework), Codec.STRING.fieldOf("testId").forGetter(i -> i.testId)) .apply(builder, TestEnabledIngredient::new)); - - private final Ingredient base; - private final TestFramework framework; - private final String testId; - - public TestEnabledIngredient(Ingredient base, TestFramework framework, String testId) { - super(Arrays.stream(base.getValues()), TestFrameworkMod.TEST_ENABLED_INGREDIENT); - this.base = base; - this.framework = framework; - this.testId = testId; - } - @Override - public boolean test(@Nullable ItemStack stack) { + public boolean test(ItemStack stack) { return base.test(stack) && framework.tests().isEnabled(testId); } @Override - public boolean synchronizeWithContents() { - return false; + public Stream getItems() { + return Stream.of(base.getItems()); } @Override public boolean isSimple() { return false; } + + @Override + public IngredientType getType() { + return TestFrameworkMod.TEST_ENABLED_INGREDIENT.get(); + } } diff --git a/testframework/src/main/java/net/neoforged/testframework/impl/TestFrameworkMod.java b/testframework/src/main/java/net/neoforged/testframework/impl/TestFrameworkMod.java index 5f07246b0b..a5dab4b927 100644 --- a/testframework/src/main/java/net/neoforged/testframework/impl/TestFrameworkMod.java +++ b/testframework/src/main/java/net/neoforged/testframework/impl/TestFrameworkMod.java @@ -22,7 +22,7 @@ public class TestFrameworkMod { public static final DeferredHolder TEST_ENABLED = LOOT_CONDITIONS.register("test_enabled", () -> new LootItemConditionType(TestEnabledLootCondition.CODEC)); public static final DeferredRegister> INGREDIENTS = DeferredRegister.create(NeoForgeRegistries.INGREDIENT_TYPES, "testframework"); - public static final DeferredHolder, IngredientType> TEST_ENABLED_INGREDIENT = INGREDIENTS.register("test_enabled", () -> new IngredientType<>(TestEnabledIngredient.CODEC, TestEnabledIngredient.CODEC_NONEMPTY)); + public static final DeferredHolder, IngredientType> TEST_ENABLED_INGREDIENT = INGREDIENTS.register("test_enabled", () -> new IngredientType<>(TestEnabledIngredient.CODEC)); public TestFrameworkMod(IEventBus bus) { LOOT_CONDITIONS.register(bus); diff --git a/tests/src/generated/resources/data/neotests_test_sized_ingredient/advancements/recipes/misc/sized_ingredient_1.json b/tests/src/generated/resources/data/neotests_test_sized_ingredient/advancements/recipes/misc/sized_ingredient_1.json new file mode 100644 index 0000000000..28c3c0e09f --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_sized_ingredient/advancements/recipes/misc/sized_ingredient_1.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_pick": { + "conditions": { + "items": [ + { + "items": "minecraft:diamond_pickaxe" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "neotests_test_sized_ingredient:sized_ingredient_1" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_pick" + ] + ], + "rewards": { + "recipes": [ + "neotests_test_sized_ingredient:sized_ingredient_1" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_test_sized_ingredient/recipes/sized_ingredient_1.json b/tests/src/generated/resources/data/neotests_test_sized_ingredient/recipes/sized_ingredient_1.json new file mode 100644 index 0000000000..df41607ceb --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_sized_ingredient/recipes/sized_ingredient_1.json @@ -0,0 +1,31 @@ +{ + "type": "neotests_ingredients:compressed_shapeless", + "category": "misc", + "ingredients": [ + { + "type": "testframework:test_enabled", + "base": { + "item": "minecraft:diamond_pickaxe" + }, + "count": 2, + "framework": "neotests:tests", + "testId": "testSizedIngredient" + }, + { + "type": "neoforge:compound", + "children": [ + { + "item": "minecraft:coal" + }, + { + "item": "minecraft:charcoal" + } + ], + "count": 2 + } + ], + "result": { + "count": 1, + "id": "minecraft:cherry_fence" + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/pack.mcmeta b/tests/src/generated/resources/pack.mcmeta index 6a4e7b5809..a4eaeef2f9 100644 --- a/tests/src/generated/resources/pack.mcmeta +++ b/tests/src/generated/resources/pack.mcmeta @@ -38,7 +38,7 @@ }, "pack": { "description": "NeoForge tests resource pack", - "pack_format": 31, + "pack_format": 32, "supported_formats": [ 0, 2147483647 diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/crafting/IngredientTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/crafting/IngredientTests.java index 6e1bf3dfdc..cb4383a44e 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/crafting/IngredientTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/crafting/IngredientTests.java @@ -5,29 +5,56 @@ package net.neoforged.neoforge.debug.crafting; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; import net.minecraft.core.FrontAndTop; +import net.minecraft.core.NonNullList; import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; import net.minecraft.data.recipes.RecipeCategory; import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.data.recipes.RecipeProvider; import net.minecraft.data.recipes.ShapedRecipeBuilder; import net.minecraft.data.recipes.ShapelessRecipeBuilder; import net.minecraft.gametest.framework.GameTest; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.crafting.CraftingBookCategory; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.ShapedRecipePattern; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.CrafterBlock; import net.minecraft.world.level.block.entity.CrafterBlockEntity; import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.neoforged.neoforge.common.conditions.ICondition; import net.neoforged.neoforge.common.crafting.DataComponentIngredient; +import net.neoforged.neoforge.common.crafting.SizedIngredient; +import net.neoforged.neoforge.registries.DeferredHolder; import net.neoforged.testframework.DynamicTest; +import net.neoforged.testframework.TestFramework; import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.OnInit; import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.condition.TestEnabledIngredient; import net.neoforged.testframework.gametest.EmptyTemplate; import net.neoforged.testframework.registration.RegistrationHelper; +import org.jetbrains.annotations.Nullable; @ForEachTest(groups = "crafting.ingredient") public class IngredientTests { @@ -42,7 +69,7 @@ protected void buildRecipes(RecipeOutput output) { .pattern("IDE") .define('I', new TestEnabledIngredient( DataComponentIngredient.of(false, DataComponents.DAMAGE, 2, Items.IRON_AXE), - test.framework(), test.id())) + test.framework(), test.id()).toVanilla()) .define('D', Items.DIAMOND) .define('E', Items.EMERALD) .unlockedBy("has_axe", has(Items.IRON_AXE)) @@ -87,7 +114,7 @@ protected void buildRecipes(RecipeOutput output) { ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.ACACIA_BOAT) .requires(new TestEnabledIngredient( DataComponentIngredient.of(true, DataComponents.DAMAGE, 4, Items.DIAMOND_PICKAXE), - test.framework(), test.id())) + test.framework(), test.id()).toVanilla()) .requires(Items.ACACIA_PLANKS) .unlockedBy("has_pick", has(Items.DIAMOND_PICKAXE)) .save(output, new ResourceLocation(reg.modId(), "strict_nbt")); @@ -126,6 +153,154 @@ protected void buildRecipes(RecipeOutput output) { .thenSucceed()); } + + private static final RegistrationHelper REG_HELPER = RegistrationHelper.create("neotests_ingredients"); + + @OnInit + static void register(final TestFramework framework) { + REG_HELPER.register(framework.modEventBus()); + } + + private static final DeferredHolder, CompressedShapelessRecipeSerializer> COMPRESSED_SHAPELESS_SERIALIZER = REG_HELPER + .registrar(Registries.RECIPE_SERIALIZER) + .register("compressed_shapeless", CompressedShapelessRecipeSerializer::new); + + static class CompressedShapelessRecipe extends ShapelessRecipe { + public CompressedShapelessRecipe(String group, CraftingBookCategory category, ItemStack result, List ingredients) { + super(group, category, result, decompressList(ingredients)); + } + + public CompressedShapelessRecipe(ShapelessRecipe uncompressed) { + this(uncompressed.getGroup(), uncompressed.category(), uncompressed.getResultItem(null), compressIngredients(uncompressed.getIngredients())); + } + + private static NonNullList decompressList(List ingredients) { + var list = new ArrayList(); + for (var ingredient : ingredients) { + for (int i = 0; i < ingredient.count(); ++i) { + list.add(ingredient.ingredient()); + } + } + return NonNullList.copyOf(list); + } + + private static List compressIngredients(NonNullList ingredients) { + Map ingredientsMap = new LinkedHashMap<>(); + for (var ingredient : ingredients) { + ingredientsMap.merge(ingredient, 1, Integer::sum); + } + return ingredientsMap.entrySet().stream() + .map(entry -> new SizedIngredient(entry.getKey(), entry.getValue())) + .toList(); + } + + @Override + public RecipeSerializer getSerializer() { + return COMPRESSED_SHAPELESS_SERIALIZER.get(); + } + } + + static class CompressedShapelessRecipeSerializer implements RecipeSerializer { + private static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + p_337958_ -> p_337958_.group( + Codec.STRING.optionalFieldOf("group", "").forGetter(ShapelessRecipe::getGroup), + CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter(p_301133_ -> p_301133_.category()), + ItemStack.CODEC.fieldOf("result").forGetter(p_301142_ -> p_301142_.getResultItem(null)), + SizedIngredient.FLAT_CODEC + .listOf() + .fieldOf("ingredients") + .flatXmap( + ingredients -> { + if (ingredients.isEmpty()) { + return DataResult.error(() -> "No ingredients for shapeless recipe"); + } else { + return ingredients.size() > ShapedRecipePattern.getMaxHeight() * ShapedRecipePattern.getMaxWidth() + ? DataResult.error(() -> "Too many ingredients for shapeless recipe. The maximum is: %s".formatted(ShapedRecipePattern.getMaxHeight() * ShapedRecipePattern.getMaxWidth())) + : DataResult.success(ingredients); + } + }, + DataResult::success) + .forGetter(r -> CompressedShapelessRecipe.compressIngredients(r.getIngredients()))) + .apply(p_337958_, CompressedShapelessRecipe::new)); + + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public StreamCodec streamCodec() { + // very ugly, don't look too much at this + return (StreamCodec) ShapelessRecipe.Serializer.STREAM_CODEC; + } + } + + static class CompressedShapelessRecipeBuilder extends ShapelessRecipeBuilder { + public CompressedShapelessRecipeBuilder(RecipeCategory category, ItemStack result) { + super(category, result); + } + + @Override + public void save(RecipeOutput recipeOutput, ResourceLocation location) { + super.save(new RecipeOutput() { + @Override + public Advancement.Builder advancement() { + return recipeOutput.advancement(); + } + + @Override + public void accept(ResourceLocation id, Recipe recipe, @Nullable AdvancementHolder advancement, ICondition... conditions) { + recipeOutput.accept(id, new CompressedShapelessRecipe((ShapelessRecipe) recipe), advancement, conditions); + } + }, location); + } + + public static ShapelessRecipeBuilder shapeless(RecipeCategory category, ItemLike result) { + return new CompressedShapelessRecipeBuilder(category, new ItemStack(result, 1)); + } + } + + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests if sized ingredients serialize and deserialize correctly") + static void testSizedIngredient(final DynamicTest test, final RegistrationHelper reg) { + reg.addProvider(event -> new RecipeProvider(event.getGenerator().getPackOutput(), event.getLookupProvider()) { + @Override + protected void buildRecipes(RecipeOutput output) { + CompressedShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.CHERRY_FENCE) + .requires(new TestEnabledIngredient( + Ingredient.of(Items.DIAMOND_PICKAXE), + test.framework(), test.id()).toVanilla(), 2) + .requires(Ingredient.of(Items.COAL, Items.CHARCOAL), 2) + .unlockedBy("has_pick", has(Items.DIAMOND_PICKAXE)) + .save(output, new ResourceLocation(reg.modId(), "sized_ingredient_1")); + } + }); + + test.onGameTest(helper -> helper + .startSequence() + .thenExecute(() -> helper.setBlock(1, 1, 1, Blocks.CRAFTER.defaultBlockState().setValue(BlockStateProperties.ORIENTATION, FrontAndTop.UP_NORTH).setValue(CrafterBlock.CRAFTING, true))) + .thenExecute(() -> helper.setBlock(1, 2, 1, Blocks.CHEST)) + + .thenMap(() -> helper.requireBlockEntity(1, 1, 1, CrafterBlockEntity.class)) + .thenExecute(crafter -> crafter.setItem(1, Items.DIAMOND_PICKAXE.getDefaultInstance())) + .thenExecute(crafter -> crafter.setItem(2, Items.DIAMOND_PICKAXE.getDefaultInstance())) + .thenExecute(crafter -> crafter.setItem(0, Items.COAL.getDefaultInstance())) + .thenIdle(3) + + // Missing an item, the recipe shouldn't work + .thenExecute(() -> helper.pulseRedstone(1, 1, 2, 2)) + .thenExecuteAfter(7, () -> helper.assertContainerEmpty(1, 2, 1)) + + .thenIdle(5) // Crafter cooldown + + // Add the missing item, the recipe should work + .thenExecute(crafter -> crafter.setItem(3, Items.CHARCOAL.getDefaultInstance())) + .thenExecute(() -> helper.pulseRedstone(1, 1, 2, 2)) + .thenExecuteAfter(7, () -> helper.assertContainerContains(1, 2, 1, Items.CHERRY_FENCE)) + + .thenSucceed()); + } /* @GameTest @EmptyTemplate diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/recipebook/RecipeBookTestRecipe.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/recipebook/RecipeBookTestRecipe.java index 3774e7897b..1cc2600882 100644 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/recipebook/RecipeBookTestRecipe.java +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/recipebook/RecipeBookTestRecipe.java @@ -28,7 +28,6 @@ import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.item.crafting.ShapedRecipe; import net.minecraft.world.level.Level; -import net.neoforged.neoforge.common.CommonHooks; public class RecipeBookTestRecipe implements Recipe { public final Ingredients ingredients; @@ -125,7 +124,7 @@ public boolean isIncomplete() { return this.getIngredients().isEmpty() || this.getIngredients().stream() .filter((ingredient) -> !ingredient.isEmpty()) - .anyMatch(CommonHooks::hasNoElements); + .anyMatch(Ingredient::hasNoItems); } @Override diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/world/item/IngredientInvalidationTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/world/item/IngredientInvalidationTest.java index 3e2dff360a..16261a3d5c 100644 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/world/item/IngredientInvalidationTest.java +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/world/item/IngredientInvalidationTest.java @@ -5,9 +5,6 @@ package net.neoforged.neoforge.oldtest.world.item; -import java.util.stream.Stream; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.Ingredient; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.common.NeoForge; @@ -27,15 +24,15 @@ public class IngredientInvalidationTest { private static boolean invalidateExpected = false; private static boolean gotInvalidate = false; - private static final Ingredient TEST_INGREDIENT = new Ingredient(Stream.of(new Ingredient.ItemValue(new ItemStack(Items.WHEAT)))) { - // TODO: - /*@Override - protected void invalidate() - { - super.invalidate(); - gotInvalidate = true; - }*/ - }; +// private static final Ingredient TEST_INGREDIENT = new Ingredient(Stream.of(new Ingredient.ItemValue(new ItemStack(Items.WHEAT)))) { +// // TODO: +// /*@Override +// protected void invalidate() +// { +// super.invalidate(); +// gotInvalidate = true; +// }*/ +// }; public IngredientInvalidationTest() { if (!ENABLED)