diff --git a/patches/net/minecraft/server/ReloadableServerResources.java.patch b/patches/net/minecraft/server/ReloadableServerResources.java.patch index 4af3bf71c6..fae77e4d24 100644 --- a/patches/net/minecraft/server/ReloadableServerResources.java.patch +++ b/patches/net/minecraft/server/ReloadableServerResources.java.patch @@ -6,7 +6,7 @@ this.functionLibrary = new ServerFunctionLibrary(p_206859_, this.commands.getDispatcher()); + // Neo: Store registries and create context object + this.registryLookup = p_361583_; -+ this.context = new net.neoforged.neoforge.common.conditions.ConditionContext(this.postponedTags); ++ this.context = new net.neoforged.neoforge.common.conditions.ConditionContext(this.postponedTags, p_250695_); } public ServerFunctionLibrary getFunctionLibrary() { diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 4f7ae5a757..bbdffc2b55 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -87,6 +87,7 @@ import net.neoforged.neoforge.common.advancements.critereon.SnowBootsEntityPredicate; import net.neoforged.neoforge.common.conditions.AndCondition; import net.neoforged.neoforge.common.conditions.FalseCondition; +import net.neoforged.neoforge.common.conditions.FlagCondition; import net.neoforged.neoforge.common.conditions.ICondition; import net.neoforged.neoforge.common.conditions.ItemExistsCondition; import net.neoforged.neoforge.common.conditions.ModLoadedCondition; @@ -385,6 +386,7 @@ 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); + public static final DeferredHolder, MapCodec> FEATURE_FLAG_CONDITION = CONDITION_CODECS.register("feature_flags", () -> FlagCondition.CODEC); 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); diff --git a/src/main/java/net/neoforged/neoforge/common/conditions/ConditionContext.java b/src/main/java/net/neoforged/neoforge/common/conditions/ConditionContext.java index de5b9b5d36..cffc6d0106 100644 --- a/src/main/java/net/neoforged/neoforge/common/conditions/ConditionContext.java +++ b/src/main/java/net/neoforged/neoforge/common/conditions/ConditionContext.java @@ -12,17 +12,30 @@ import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.tags.TagKey; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; +import org.jetbrains.annotations.ApiStatus; public class ConditionContext implements ICondition.IContext { private final Map>, HolderLookup.RegistryLookup> pendingTags; + private final FeatureFlagSet enabledFeatures; - public ConditionContext(List> pendingTags) { + public ConditionContext(List> pendingTags, FeatureFlagSet enabledFeatures) { this.pendingTags = new IdentityHashMap<>(); + this.enabledFeatures = enabledFeatures; + for (var tags : pendingTags) { this.pendingTags.put(tags.key(), tags.lookup()); } } + // Use FeatureFlagSet sensitive constructor + @ApiStatus.ScheduledForRemoval(inVersion = "1.21.4") + @Deprecated(forRemoval = true, since = "1.21.3") + public ConditionContext(List> pendingTags) { + this(pendingTags, FeatureFlags.VANILLA_SET); + } + public void clear() { this.pendingTags.clear(); } @@ -33,4 +46,9 @@ public boolean isTagLoaded(TagKey key) { var lookup = pendingTags.get(key.registry()); return lookup != null && lookup.get((TagKey) key).isPresent(); } + + @Override + public FeatureFlagSet enabledFeatures() { + return enabledFeatures; + } } diff --git a/src/main/java/net/neoforged/neoforge/common/conditions/FlagCondition.java b/src/main/java/net/neoforged/neoforge/common/conditions/FlagCondition.java new file mode 100644 index 0000000000..2b59ed1584 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/conditions/FlagCondition.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.conditions; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.flag.FeatureFlag; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; + +/** + * Condition checking for the enabled state of a given {@link FeatureFlagSet}. + *

+ * {@code requiredFeatures} - {@link FeatureFlagSet} containing all {@link FeatureFlag feature flags} to be validated. + * {@code expectedResult} - Validates that all given {@link FeatureFlag feature flags} are enabled when {@code true} or disabled when {@code false}. + * + * @apiNote Mainly to be used when flagged content is not contained within the same feature pack which also enables said {@link FeatureFlag feature flags}. + */ +public final class FlagCondition implements ICondition { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + FeatureFlags.CODEC.fieldOf("flags").forGetter(condition -> condition.requiredFeatures), + Codec.BOOL.lenientOptionalFieldOf("expected_result", true).forGetter(condition -> condition.expectedResult)).apply(instance, FlagCondition::new)); + + private final FeatureFlagSet requiredFeatures; + private final boolean expectedResult; + + private FlagCondition(FeatureFlagSet requiredFeatures, boolean expectedResult) { + this.requiredFeatures = requiredFeatures; + this.expectedResult = expectedResult; + } + + @Override + public boolean test(IContext context) { + var flagsEnabled = requiredFeatures.isSubsetOf(context.enabledFeatures()); + // true if: 'expectedResult' is true nd all given flags are enabled + // false if: `enabledEnabled' is false and all given flags are disabled + return flagsEnabled == expectedResult; + } + + @Override + public MapCodec codec() { + return CODEC; + } + + public static ICondition isEnabled(FeatureFlagSet requiredFeatures) { + return new FlagCondition(requiredFeatures, true); + } + + public static ICondition isEnabled(FeatureFlag requiredFlag) { + return isEnabled(FeatureFlagSet.of(requiredFlag)); + } + + public static ICondition isEnabled(FeatureFlag requiredFlag, FeatureFlag... requiredFlags) { + return isEnabled(FeatureFlagSet.of(requiredFlag, requiredFlags)); + } + + public static ICondition isDisabled(FeatureFlagSet requiredFeatures) { + return new FlagCondition(requiredFeatures, false); + } + + public static ICondition isDisabled(FeatureFlag requiredFlag) { + return isDisabled(FeatureFlagSet.of(requiredFlag)); + } + + public static ICondition isDisabled(FeatureFlag requiredFlag, FeatureFlag... requiredFlags) { + return isDisabled(FeatureFlagSet.of(requiredFlag, requiredFlags)); + } +} diff --git a/src/main/java/net/neoforged/neoforge/common/conditions/ICondition.java b/src/main/java/net/neoforged/neoforge/common/conditions/ICondition.java index 1be7e0ce94..2c8ca8583d 100644 --- a/src/main/java/net/neoforged/neoforge/common/conditions/ICondition.java +++ b/src/main/java/net/neoforged/neoforge/common/conditions/ICondition.java @@ -19,7 +19,10 @@ import net.minecraft.resources.RegistryOps; import net.minecraft.tags.TagKey; import net.minecraft.util.Unit; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.server.ServerLifecycleHooks; public interface ICondition { Codec CODEC = NeoForgeRegistries.CONDITION_SERIALIZERS.byNameCodec() @@ -91,5 +94,15 @@ public boolean isTagLoaded(TagKey key) { * Returns {@code true} if the requested tag is available. */ boolean isTagLoaded(TagKey key); + + default FeatureFlagSet enabledFeatures() { + // returning the vanilla set causes reports false positives for flags outside of vanilla + // return FeatureFlags.VANILLA_SET; + + // lookup the active enabledFeatures from the current server + // if no server exists, delegating back to 'VANILLA_SET' should be fine (should rarely ever happen) + var server = ServerLifecycleHooks.getCurrentServer(); + return server == null ? FeatureFlags.VANILLA_SET : server.getWorldData().enabledFeatures(); + } } } diff --git a/src/main/java/net/neoforged/neoforge/common/conditions/IConditionBuilder.java b/src/main/java/net/neoforged/neoforge/common/conditions/IConditionBuilder.java index 06836647a8..f0a532ac16 100644 --- a/src/main/java/net/neoforged/neoforge/common/conditions/IConditionBuilder.java +++ b/src/main/java/net/neoforged/neoforge/common/conditions/IConditionBuilder.java @@ -7,6 +7,8 @@ import java.util.List; import net.minecraft.tags.TagKey; +import net.minecraft.world.flag.FeatureFlag; +import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.item.Item; public interface IConditionBuilder { @@ -41,4 +43,28 @@ default ICondition modLoaded(String modid) { default ICondition tagEmpty(TagKey tag) { return new TagEmptyCondition(tag.location()); } + + default ICondition isFeatureEnabled(FeatureFlagSet requiredFeatures) { + return FlagCondition.isEnabled(requiredFeatures); + } + + default ICondition isFeatureEnabled(FeatureFlag requiredFlag) { + return FlagCondition.isEnabled(requiredFlag); + } + + default ICondition isFeatureEnabled(FeatureFlag requiredFlag, FeatureFlag... requiredFlags) { + return FlagCondition.isEnabled(requiredFlag, requiredFlags); + } + + default ICondition isFeatureDisabled(FeatureFlagSet requiredFeatures) { + return FlagCondition.isDisabled(requiredFeatures); + } + + default ICondition isFeatureDisabled(FeatureFlag requiredFlag) { + return FlagCondition.isDisabled(requiredFlag); + } + + default ICondition isFeatureDisabled(FeatureFlag requiredFlag, FeatureFlag... requiredFlags) { + return FlagCondition.isDisabled(requiredFlag, requiredFlags); + } } diff --git a/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/diamonds_from_dirt.json b/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/diamonds_from_dirt.json new file mode 100644 index 0000000000..2228d43a8b --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/diamonds_from_dirt.json @@ -0,0 +1,40 @@ +{ + "neoforge:conditions": [ + { + "type": "neoforge:feature_flags", + "flags": [ + "custom_feature_flags_pack_test:test_flag" + ] + } + ], + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": "#minecraft:dirt" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "neotests_test_flag_condition:diamonds_from_dirt" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "neotests_test_flag_condition:diamonds_from_dirt" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/dirt_from_diamonds.json b/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/dirt_from_diamonds.json new file mode 100644 index 0000000000..bde8245ef5 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_flag_condition/advancement/recipes/misc/dirt_from_diamonds.json @@ -0,0 +1,41 @@ +{ + "neoforge:conditions": [ + { + "type": "neoforge:feature_flags", + "expected_result": false, + "flags": [ + "custom_feature_flags_pack_test:test_flag" + ] + } + ], + "parent": "minecraft:recipes/root", + "criteria": { + "has_diamond": { + "conditions": { + "items": [ + { + "items": "#c:gems/diamond" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "neotests_test_flag_condition:dirt_from_diamonds" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_diamond" + ] + ], + "rewards": { + "recipes": [ + "neotests_test_flag_condition:dirt_from_diamonds" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/diamonds_from_dirt.json b/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/diamonds_from_dirt.json new file mode 100644 index 0000000000..51550166a3 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/diamonds_from_dirt.json @@ -0,0 +1,19 @@ +{ + "neoforge:conditions": [ + { + "type": "neoforge:feature_flags", + "flags": [ + "custom_feature_flags_pack_test:test_flag" + ] + } + ], + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "#minecraft:dirt" + ], + "result": { + "count": 1, + "id": "minecraft:diamond" + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/dirt_from_diamonds.json b/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/dirt_from_diamonds.json new file mode 100644 index 0000000000..d1984cf97c --- /dev/null +++ b/tests/src/generated/resources/data/neotests_test_flag_condition/recipe/dirt_from_diamonds.json @@ -0,0 +1,20 @@ +{ + "neoforge:conditions": [ + { + "type": "neoforge:feature_flags", + "expected_result": false, + "flags": [ + "custom_feature_flags_pack_test:test_flag" + ] + } + ], + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "#c:gems/diamond" + ], + "result": { + "count": 1, + "id": "minecraft:dirt" + } +} \ No newline at end of file diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/data/CustomFeatureFlagsTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/data/CustomFeatureFlagsTests.java index 86f539a15e..0e852b9840 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/data/CustomFeatureFlagsTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/data/CustomFeatureFlagsTests.java @@ -5,22 +5,33 @@ package net.neoforged.neoforge.debug.data; +import net.minecraft.core.HolderLookup; +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.network.chat.Component; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.tags.ItemTags; import net.minecraft.world.flag.FeatureFlag; import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; +import net.neoforged.neoforge.common.Tags; +import net.neoforged.neoforge.common.conditions.FlagCondition; import net.neoforged.neoforge.event.AddPackFindersEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.registries.DeferredItem; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; +import net.neoforged.testframework.registration.RegistrationHelper; @ForEachTest(groups = "data.feature_flags") public class CustomFeatureFlagsTests { @@ -89,4 +100,69 @@ static void testFeatureGating(final DynamicTest test) { } }); } + + @TestHolder(description = "Tests that elements can be toggled via conditions using the flag condition", enabledByDefault = true) + static void testFlagCondition(DynamicTest test, RegistrationHelper reg) { + // custom flag are provided by our other flag tests + // and enabled via our `custom featureflag test pack` + var flagName = ResourceLocation.fromNamespaceAndPath("custom_feature_flags_pack_test", "test_flag"); + var flag = FeatureFlags.REGISTRY.getFlag(flagName); + + var modId = reg.modId(); + var enabledRecipeName = ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath(modId, "diamonds_from_dirt")); + var disabledRecipeName = ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath(modId, "dirt_from_diamonds")); + + reg.addProvider(event -> new RecipeProvider.Runner(event.getGenerator().getPackOutput(), event.getLookupProvider()) { + @Override + protected RecipeProvider createRecipeProvider(HolderLookup.Provider registries, RecipeOutput output) { + return new RecipeProvider(registries, output) { + @Override + protected void buildRecipes() { + // recipe available when above flag is enabled + shapeless(RecipeCategory.MISC, Items.DIAMOND) + .requires(ItemTags.DIRT) + .unlockedBy("has_dirt", has(ItemTags.DIRT)) + .save(output.withConditions(FlagCondition.isEnabled(flag)), enabledRecipeName); + + // recipe available when above flag is disabled + shapeless(RecipeCategory.MISC, Items.DIRT) + .requires(Tags.Items.GEMS_DIAMOND) + .unlockedBy("has_diamond", has(Tags.Items.GEMS_DIAMOND)) + .save(output.withConditions(FlagCondition.isDisabled(flag)), disabledRecipeName); + } + }; + } + + @Override + public String getName() { + return "conditional_flag_recipes"; + } + }); + + test.eventListeners().forge().addListener((ServerStartedEvent event) -> { + var server = event.getServer(); + var isFlagEnabled = server.getWorldData().enabledFeatures().contains(flag); + var recipeMap = server.getRecipeManager().recipeMap(); + var hasEnabledRecipe = recipeMap.byKey(enabledRecipeName) != null; + var hasDisabledRecipe = recipeMap.byKey(disabledRecipeName) != null; + + if (isFlagEnabled) { + if (!hasEnabledRecipe) { + test.fail("Missing recipe '" + enabledRecipeName.location() + "', This should be enabled due to our flag '" + flagName + "' being enabled"); + } + if (hasDisabledRecipe) { + test.fail("Found recipe '" + disabledRecipeName.location() + "', This should be disabled due to our flag '" + flagName + "' being disabled"); + } + } else { + if (hasEnabledRecipe) { + test.fail("Found recipe '" + enabledRecipeName.location() + "', This should be disabled due to our flag '" + flagName + "' being enabled"); + } + if (!hasDisabledRecipe) { + test.fail("Missing recipe '" + disabledRecipeName.location() + "', This should be enabled due to our flag '" + flagName + "' being disabled"); + } + } + + test.pass(); + }); + } }