diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index bbb39ccb6c..c0c55b29cc 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -109,6 +109,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.DifferenceIngredient; import net.neoforged.neoforge.common.crafting.IngredientType; @@ -203,7 +204,7 @@ public class NeoForgeMod { /** * Reach Distance represents the distance at which a player may interact with the world. The default is 4.5 blocks. Players in creative mode have an additional 0.5 blocks of block reach. - * + * * @see IPlayerExtension#getBlockReach() * @see IPlayerExtension#canReach(BlockPos, double) */ @@ -231,7 +232,7 @@ public class NeoForgeMod { /** * Attack Range represents the distance at which a player may attack an entity. The default is 3 blocks. Players in creative mode have an additional 2 blocks of entity reach. * The default of 3.0 is technically considered a bug by Mojang - see MC-172289 and MC-92484. However, updating this value would allow for longer-range attacks on vanilla servers, which makes some people mad. - * + * * @see IPlayerExtension#getEntityReach() * @see IPlayerExtension#canReach(Entity, double) * @see IPlayerExtension#canReach(Vec3, double) @@ -240,7 +241,7 @@ public class NeoForgeMod { /** * Step Height Addition modifies the amount of blocks an entity may walk up without jumping. - * + * * @see IEntityExtension#getStepHeight() */ public static final Holder STEP_HEIGHT = ATTRIBUTES.register("step_height", () -> new RangedAttribute("neoforge.step_height", 0.0D, -512.0D, 512.0D).setSyncable(true)); @@ -399,6 +400,7 @@ public class NeoForgeMod { public static final DeferredHolder, IngredientType> NBT_INGREDIENT_TYPE = INGREDIENT_TYPES.register("nbt", () -> new IngredientType<>(NBTIngredient.CODEC, NBTIngredient.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> BLOCK_TAG_INGREDIENT = INGREDIENT_TYPES.register("block_tag", () -> new IngredientType<>(BlockTagIngredient.CODEC)); private static final DeferredRegister> CONDITION_CODECS = DeferredRegister.create(NeoForgeRegistries.Keys.CONDITION_CODECS, NeoForgeVersion.MOD_ID); public static final DeferredHolder, Codec> AND_CONDITION = CONDITION_CODECS.register("and", () -> AndCondition.CODEC); @@ -567,7 +569,7 @@ public ResourceLocation getFlowingTexture() { * Used in place of {@link DamageSources#magic()} for damage dealt by {@link MobEffects#POISON}. *

* May also be used by mods providing poison-like effects. - * + * * @see {@link Tags.DamageTypes#IS_POISON} */ public static final ResourceKey POISON_DAMAGE = ResourceKey.create(Registries.DAMAGE_TYPE, new ResourceLocation(NeoForgeVersion.MOD_ID, "poison")); 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..60b1cd0683 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/crafting/BlockTagIngredient.java @@ -0,0 +1,101 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import net.minecraft.core.Holder; +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; + +/** + * {@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 extends Ingredient { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i + .group(TagKey.codec(Registries.BLOCK).fieldOf("tag").forGetter(BlockTagIngredient::getTag)) + .apply(i, BlockTagIngredient::new)); + + protected final TagKey tag; + @Nullable + protected ItemStack[] itemStacks; + + public BlockTagIngredient(TagKey tag) { + super(Stream.of(new BlockTagValue(tag)), NeoForgeMod.BLOCK_TAG_INGREDIENT); + this.tag = tag; + } + + @Override + public ItemStack[] getItems() { + List list = new ArrayList<>(); + + for (Value value : values) { + list.addAll(value.getItems()); + } + + return itemStacks = list.toArray(ItemStack[]::new); + } + + @Override + public boolean test(@Nullable ItemStack stack) { + if (stack == null) { + return false; + } + + this.getItems(); + for (ItemStack itemStack : itemStacks) { + if (itemStack.is(stack.getItem())) { + return true; + } + } + + return false; + } + + public TagKey getTag() { + return tag; + } + + @Override + public IngredientType getType() { + return NeoForgeMod.BLOCK_TAG_INGREDIENT.get(); + } + + public record BlockTagValue(TagKey tag) implements Ingredient.Value { + @Override + public Collection getItems() { + List list = new ArrayList<>(); + + for (Holder holder : BuiltInRegistries.BLOCK.getTagOrEmpty(this.tag)) { + ItemStack stack = new ItemStack(holder.value()); + if (!stack.isEmpty()) { + list.add(stack); + } + } + + if (list.isEmpty()) + list.add(new ItemStack(Blocks.BARRIER).setHoverName(Component.literal("Empty Tag: " + this.tag.location()))); + + return list; + } + } +} 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..3cddc927da --- /dev/null +++ b/tests/src/generated/resources/data/neotests_block_tag_ingredient/advancements/recipes/misc/block_tag.json @@ -0,0 +1,34 @@ +{ + "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..3ce66bdff5 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_block_tag_ingredient/recipes/block_tag.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + { + "type": "neoforge:block_tag", + "tag": "minecraft:convertable_to_mud" + }, + { + "item": "minecraft:water_bucket" + } + ], + "result": { + "item": "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 0d8e19062c..90a9e52d13 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 @@ -16,6 +16,7 @@ import net.minecraft.gametest.framework.GameTest; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.BlockTags; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Blocks; @@ -23,6 +24,7 @@ import net.minecraft.world.level.block.entity.CrafterBlockEntity; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.common.crafting.BlockTagIngredient; import net.neoforged.neoforge.common.crafting.NBTIngredient; import net.neoforged.neoforge.registries.NeoForgeRegistries; import net.neoforged.testframework.DynamicTest; @@ -34,6 +36,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()) { + @Override + protected void buildRecipes(RecipeOutput output) { + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.MUD) + .requires(new TestEnabledIngredient(new BlockTagIngredient(BlockTags.CONVERTABLE_TO_MUD), test.framework(), test.id())) + .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")