diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 517265f790..e06c0db61a 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -104,6 +104,7 @@ import net.neoforged.neoforge.common.conditions.OrCondition; import net.neoforged.neoforge.common.conditions.TagEmptyCondition; import net.neoforged.neoforge.common.conditions.TrueCondition; +import net.neoforged.neoforge.common.crafting.BlockTagIngredient; import net.neoforged.neoforge.common.crafting.CompoundIngredient; import net.neoforged.neoforge.common.crafting.DataComponentIngredient; import net.neoforged.neoforge.common.crafting.DifferenceIngredient; @@ -376,6 +377,7 @@ public class NeoForgeMod { 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)); + public static final DeferredHolder, IngredientType> BLOCK_TAG_INGREDIENT = INGREDIENT_TYPES.register("block_tag", () -> new IngredientType<>(BlockTagIngredient.CODEC)); private static final DeferredRegister> FLUID_INGREDIENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPES, NeoForgeVersion.MOD_ID); public static final DeferredHolder, FluidIngredientType> SINGLE_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("single", () -> new FluidIngredientType<>(SingleFluidIngredient.CODEC)); diff --git a/src/main/java/net/neoforged/neoforge/common/crafting/BlockTagIngredient.java b/src/main/java/net/neoforged/neoforge/common/crafting/BlockTagIngredient.java new file mode 100644 index 0000000000..4994e80542 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/crafting/BlockTagIngredient.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.crafting; + +import com.mojang.serialization.MapCodec; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.neoforge.common.NeoForgeMod; +import org.jetbrains.annotations.Nullable; + +/** + * {@link Ingredient} that matches {@link ItemStack}s of {@link Block}s from a {@link TagKey}, useful in cases + * like {@code "minecraft:convertable_to_mud"} where there isn't an accompanying item tag + *

+ * Notice: This should not be used as a replacement for the normal {@link Ingredient#of(TagKey)}, + * This should only be used when there is no way an item tag can be used in your use case + */ +public class BlockTagIngredient implements ICustomIngredient { + public static final MapCodec CODEC = TagKey.codec(Registries.BLOCK) + .xmap(BlockTagIngredient::new, BlockTagIngredient::getTag).fieldOf("tag"); + + protected final TagKey tag; + + @Nullable + protected ItemStack[] itemStacks; + + public BlockTagIngredient(TagKey tag) { + this.tag = tag; + } + + protected void dissolve() { + if (itemStacks == null) { + List list = new ArrayList<>(); + for (Holder block : BuiltInRegistries.BLOCK.getTagOrEmpty(tag)) { + ItemStack stack = new ItemStack(block.value()); + if (!stack.isEmpty()) { + list.add(stack); + } + } + + if (list.isEmpty()) { + ItemStack itemStack = new ItemStack(Blocks.BARRIER); + itemStack.set(DataComponents.CUSTOM_NAME, Component.literal("Empty Tag: " + this.tag.location())); + list.add(itemStack); + } + + itemStacks = list.toArray(ItemStack[]::new); + } + } + + @Override + public Stream getItems() { + dissolve(); + return Stream.of(itemStacks); + } + + @Override + public boolean test(@Nullable ItemStack stack) { + if (stack == null) + return false; + + dissolve(); + for (ItemStack itemStack : itemStacks) { + if (itemStack.is(stack.getItem())) { + return true; + } + } + + return false; + } + + public TagKey getTag() { + return tag; + } + + @Override + public boolean isSimple() { + return true; + } + + @Override + public IngredientType getType() { + return NeoForgeMod.BLOCK_TAG_INGREDIENT.get(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BlockTagIngredient that)) return false; + return tag.equals(that.tag); + } + + @Override + public int hashCode() { + return tag.hashCode(); + } +} diff --git a/tests/src/generated/resources/data/neotests_block_tag_ingredient/advancements/recipes/misc/block_tag.json b/tests/src/generated/resources/data/neotests_block_tag_ingredient/advancements/recipes/misc/block_tag.json new file mode 100644 index 0000000000..dccac5b93a --- /dev/null +++ b/tests/src/generated/resources/data/neotests_block_tag_ingredient/advancements/recipes/misc/block_tag.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "minecraft:water_bucket" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "neotests_block_tag_ingredient:block_tag" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_item" + ] + ], + "rewards": { + "recipes": [ + "neotests_block_tag_ingredient:block_tag" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_block_tag_ingredient/recipes/block_tag.json b/tests/src/generated/resources/data/neotests_block_tag_ingredient/recipes/block_tag.json new file mode 100644 index 0000000000..3638a4dce6 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_block_tag_ingredient/recipes/block_tag.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + { + "type": "neoforge:block_tag", + "tag": "minecraft:convertable_to_mud" + }, + { + "item": "minecraft:water_bucket" + } + ], + "result": { + "count": 1, + "id": "minecraft:mud" + } +} \ No newline at end of file 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 0e458098fe..57c8739b28 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 @@ -31,6 +31,7 @@ import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.BlockTags; import net.minecraft.tags.ItemTags; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -47,6 +48,7 @@ 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.BlockTagIngredient; import net.neoforged.neoforge.common.crafting.CompoundIngredient; import net.neoforged.neoforge.common.crafting.DataComponentIngredient; import net.neoforged.neoforge.common.crafting.DifferenceIngredient; @@ -66,6 +68,37 @@ @ForEachTest(groups = "crafting.ingredient") public class IngredientTests { + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests if BlockTagIngredient works") + static void blockTagIngredient(final DynamicTest test, final RegistrationHelper reg) { + reg.addProvider(event -> new RecipeProvider(event.getGenerator().getPackOutput(), event.getLookupProvider()) { + @Override + protected void buildRecipes(RecipeOutput output) { + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.MUD) + .requires(new TestEnabledIngredient(new BlockTagIngredient(BlockTags.CONVERTABLE_TO_MUD).toVanilla(), test.framework(), test.id()).toVanilla()) + .requires(Items.WATER_BUCKET) + .unlockedBy("has_item", has(Items.WATER_BUCKET)) + .save(output, new ResourceLocation(reg.modId(), "block_tag")); + } + }); + + 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(0, Items.DIRT.getDefaultInstance())) + .thenExecute(crafter -> crafter.setItem(1, Items.WATER_BUCKET.getDefaultInstance())) + .thenIdle(3) + + .thenExecute(() -> helper.pulseRedstone(1, 1, 2, 2)) + .thenExecuteAfter(7, () -> helper.assertContainerContains(1, 2, 1, Items.MUD)) + + .thenSucceed()); + } + @GameTest @EmptyTemplate @TestHolder(description = "Tests if partial NBT ingredients match the correct stacks")