Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.21.3] Add condition to validate feature flags enabled state #1706

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -385,6 +386,7 @@ public class NeoForgeMod {
public static final DeferredHolder<MapCodec<? extends ICondition>, MapCodec<OrCondition>> OR_CONDITION = CONDITION_CODECS.register("or", () -> OrCondition.CODEC);
public static final DeferredHolder<MapCodec<? extends ICondition>, MapCodec<TagEmptyCondition>> TAG_EMPTY_CONDITION = CONDITION_CODECS.register("tag_empty", () -> TagEmptyCondition.CODEC);
public static final DeferredHolder<MapCodec<? extends ICondition>, MapCodec<TrueCondition>> TRUE_CONDITION = CONDITION_CODECS.register("true", () -> TrueCondition.CODEC);
public static final DeferredHolder<MapCodec<? extends ICondition>, MapCodec<FlagCondition>> FEATURE_FLAG_CONDITION = CONDITION_CODECS.register("feature_flags", () -> FlagCondition.CODEC);

private static final DeferredRegister<MapCodec<? extends EntitySubPredicate>> ENTITY_PREDICATE_CODECS = DeferredRegister.create(Registries.ENTITY_SUB_PREDICATE_TYPE, NeoForgeVersion.MOD_ID);
public static final DeferredHolder<MapCodec<? extends EntitySubPredicate>, MapCodec<PiglinNeutralArmorEntityPredicate>> PIGLIN_NEUTRAL_ARMOR_PREDICATE = ENTITY_PREDICATE_CODECS.register("piglin_neutral_armor", () -> PiglinNeutralArmorEntityPredicate.CODEC);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceKey<? extends Registry<?>>, HolderLookup.RegistryLookup<?>> pendingTags;
private final FeatureFlagSet enabledFeatures;

public ConditionContext(List<Registry.PendingTags<?>> pendingTags) {
public ConditionContext(List<Registry.PendingTags<?>> 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<Registry.PendingTags<?>> pendingTags) {
this(pendingTags, FeatureFlags.VANILLA_SET);
}

public void clear() {
this.pendingTags.clear();
}
Expand All @@ -33,4 +46,9 @@ public <T> boolean isTagLoaded(TagKey<T> key) {
var lookup = pendingTags.get(key.registry());
return lookup != null && lookup.get((TagKey) key).isPresent();
}

@Override
public FeatureFlagSet enabledFeatures() {
return enabledFeatures;
}
}
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* {@code requiredFeatures} - {@link FeatureFlagSet} containing all {@link FeatureFlag feature flags} to be validated.
* {@code checkEnabled} - 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<FlagCondition> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
FeatureFlags.CODEC.fieldOf("flags").forGetter(condition -> condition.requiredFeatures),
Codec.BOOL.lenientOptionalFieldOf("check_enabled", true).forGetter(condition -> condition.checkEnabled)).apply(instance, FlagCondition::new));

private final FeatureFlagSet requiredFeatures;
private final boolean checkEnabled;

private FlagCondition(FeatureFlagSet requiredFeatures, boolean checkEnabled) {
this.requiredFeatures = requiredFeatures;
this.checkEnabled = checkEnabled;
}

@Override
public boolean test(IContext context) {
var flagsEnabled = requiredFeatures.isSubsetOf(context.enabledFeatures());
// true if: 'checkEnabled' is true nd all given flags are enabled
// false if: `enabledEnabled' is false and all given flags are disabled
return flagsEnabled == checkEnabled;
}

@Override
public MapCodec<? extends ICondition> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICondition> CODEC = NeoForgeRegistries.CONDITION_SERIALIZERS.byNameCodec()
Expand Down Expand Up @@ -91,5 +94,15 @@ public <T> boolean isTagLoaded(TagKey<T> key) {
* Returns {@code true} if the requested tag is available.
*/
<T> boolean isTagLoaded(TagKey<T> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -41,4 +43,28 @@ default ICondition modLoaded(String modid) {
default ICondition tagEmpty(TagKey<Item> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@

package net.neoforged.neoforge.debug.data;

import net.minecraft.core.HolderLookup;
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.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.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 {
Expand Down Expand Up @@ -89,4 +97,31 @@ static void testFeatureGating(final DynamicTest test) {
}
});
}

@TestHolder(description = "Tests that elements can be toggled via conditions using the flag condition")
static void testFlagCondition(DynamicTest test, RegistrationHelper reg) {
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() {
// custom flag provided by our tests
// and enabled via our `custom featureflag test pack`
var flag = FeatureFlags.REGISTRY.getFlag(ResourceLocation.fromNamespaceAndPath("custom_feature_flags_pack_test", "test_flag"));

shapeless(RecipeCategory.MISC, Items.DIAMOND)
.requires(ItemTags.DIRT)
.unlockedBy("has_dirt", has(ItemTags.DIRT))
.save(output.withConditions(FlagCondition.isEnabled(flag)), reg.modId() + ":diamonds_from_dirt");
}
};
}

@Override
public String getName() {
return "conditional_flag_recipes";
}
});
}
}