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] Add Experimental FeatureFlag #1167

Open
wants to merge 10 commits into
base: 1.21.x
Choose a base branch
from
Open
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
19 changes: 18 additions & 1 deletion patches/net/minecraft/data/DataGenerator.java.patch
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@
stopwatch1.start();
hashcache.applyUpdate(hashcache.generateUpdate(p_254418_, p_253750_::run).join());
stopwatch1.stop();
@@ -56,6 +_,34 @@
@@ -56,6 +_,51 @@
public DataGenerator.PackGenerator getBuiltinDatapack(boolean p_253826_, String p_254134_) {
Path path = this.vanillaPackOutput.getOutputFolder(PackOutput.Target.DATA_PACK).resolve("minecraft").resolve("datapacks").resolve(p_254134_);
return new DataGenerator.PackGenerator(p_253826_, p_254134_, new PackOutput(path));
+ }
+
+ public PackGenerator getBuiltinDatapack(boolean run, String namespace, String path) {
+ var packPath = vanillaPackOutput.getOutputFolder(PackOutput.Target.DATA_PACK).resolve(namespace).resolve("datapacks").resolve(path);
+ return new PackGenerator(run, namespace + '_' + path, new PackOutput(packPath));
+ }
+
+ public Map<String, DataProvider> getProvidersView() {
+ return this.providersView;
+ }
Expand All @@ -48,6 +53,18 @@
+ DataGenerator.this.providersToRun.put(id, provider);
+
+ return provider;
+ }
+
+ public void merge(DataGenerator other) {
+ other.providersToRun.forEach((id, provider) -> {
+ if(!allProviderIds.add(id))
+ throw new IllegalStateException("Duplicate provider: " + id);
+
+ providersToRun.put(id, provider);
+ });
+
+ other.providersToRun.clear();
+ other.allProviderIds.clear();
}

static {
31 changes: 31 additions & 0 deletions patches/net/minecraft/world/flag/FeatureFlags.java.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--- a/net/minecraft/world/flag/FeatureFlags.java
+++ b/net/minecraft/world/flag/FeatureFlags.java
@@ -13,6 +_,20 @@
public static final Codec<FeatureFlagSet> CODEC;
public static final FeatureFlagSet VANILLA_SET;
public static final FeatureFlagSet DEFAULT_FLAGS;
+ /**
+ * A <b>feature flag</b> for use with experimental features that may introduce unexpected or potentially bug-inducing behaviors.<br>
+ * Unlike the standard set of flags, which can change frequently, this flag remains consistent across major version updates.<br>
+ * <br><p>
+ * Modders can reference this flag during built-in feature registration.<br>
+ * However, they must provide their own flagged datapacks to associate datapack features (such as recipes and enchantments) with this flag.<br>
+ * These datapacks can be provided either as optional files or via the {@linkplain net.neoforged.neoforge.event.AddPackFindersEvent} event.
+ * </p>
+ * <br><p>
+ * It is highly recommended that modders document which features are experimental and which ones are not.<br>
+ * Due to the nature of this flag being a <i>‘catch-all’</i>, it enables any and all modded experiments that may exist.
+ * </p>
+ */
+ public static final FeatureFlag MOD_EXPERIMENTAL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs javadoc on what the flag is for, and how modders should actually use it -- notably, they have to register their own pack using it in AddPackFindersEvent, which isn't necessarily obvious!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java docs have been added stating what this flag is, how and when to use it, and stating that it is recommended that modders document their experimental features somewhere.


public static String printMissingFlags(FeatureFlagSet p_250581_, FeatureFlagSet p_250326_) {
return printMissingFlags(REGISTRY, p_250581_, p_250326_);
@@ -33,6 +_,7 @@
VANILLA = featureflagregistry$builder.createVanilla("vanilla");
BUNDLE = featureflagregistry$builder.createVanilla("bundle");
TRADE_REBALANCE = featureflagregistry$builder.createVanilla("trade_rebalance");
+ MOD_EXPERIMENTAL = featureflagregistry$builder.create(ResourceLocation.fromNamespaceAndPath("neoforge", "mod_experimental"));
REGISTRY = featureflagregistry$builder.build();
CODEC = REGISTRY.codec();
VANILLA_SET = FeatureFlagSet.of(VANILLA);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"features": {
"enabled": [
"neoforge:mod_experimental"
]
},
"pack": {
"description": {
"translate": "pack.neoforge.experimental.description"
},
"pack_format": 48,
"supported_formats": [
0,
2147483647
]
}
}
22 changes: 22 additions & 0 deletions src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.FeatureFlagsMetadataSection;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.metadata.pack.PackMetadataSection;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackSource;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.InclusiveRange;
Expand All @@ -52,6 +55,8 @@
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.BlockGetter;
Expand Down Expand Up @@ -136,6 +141,7 @@
import net.neoforged.neoforge.common.world.StructureModifier;
import net.neoforged.neoforge.common.world.StructureModifiers;
import net.neoforged.neoforge.data.event.GatherDataEvent;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.fluids.BaseFlowingFluid;
import net.neoforged.neoforge.fluids.CauldronFluidContent;
Expand Down Expand Up @@ -630,6 +636,14 @@ public NeoForgeMod(IEventBus modEventBus, Dist dist, ModContainer container) {

modEventBus.register(NeoForgeDataMaps.class);

modEventBus.addListener(AddPackFindersEvent.class, event -> event.addPackFinders(
ResourceLocation.fromNamespaceAndPath("neoforge", "data/neoforge/datapacks/mod_experimental"),
PackType.SERVER_DATA,
Component.translatable("pack.neoforge.experimental.name"),
PackSource.FEATURE,
false,
Pack.Position.TOP));

if (isPRBuild(container.getModInfo().getVersion().toString())) {
isPRBuild = true;
ModLoader.addLoadingIssue(ModLoadingIssue.warning("loadwarning.neoforge.prbuild").withAffectedMod(container.getModInfo()));
Expand Down Expand Up @@ -675,6 +689,14 @@ public void gatherData(GatherDataEvent event) {
gen.addProvider(event.includeClient(), new NeoForgeSpriteSourceProvider(packOutput, lookupProvider, existingFileHelper));
gen.addProvider(event.includeClient(), new VanillaSoundDefinitionsProvider(packOutput, existingFileHelper));
gen.addProvider(event.includeClient(), new NeoForgeLanguageProvider(packOutput));

// mod experimental pack
gen.getBuiltinDatapack(true, NeoForgeVersion.MOD_ID, "mod_experimental").addProvider(output -> new PackMetadataGenerator(output)
.add(PackMetadataSection.TYPE, new PackMetadataSection(
Component.translatable("pack.neoforge.experimental.description"),
DetectedVersion.BUILT_IN.getPackVersion(PackType.SERVER_DATA),
Optional.of(new InclusiveRange<>(0, Integer.MAX_VALUE))))
.add(FeatureFlagsMetadataSection.TYPE, new FeatureFlagsMetadataSection(FeatureFlagSet.of(FeatureFlags.MOD_EXPERIMENTAL))));
}

// done in an event instead of deferred to only enable if a mod requests it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public void runAll() {
paths.values().forEach(lst -> {
DataGenerator parent = lst.get(0);
for (int x = 1; x < lst.size(); x++)
lst.get(x).getProvidersView().forEach((name, provider) -> parent.addProvider(true, provider));
parent.merge(lst.get(x));
try {
parent.run();
} catch (IOException ex) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/assets/neoforge/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@
"neoforge.chatType.system": "%1$s",

"pack.neoforge.description": "NeoForge data/resource pack",
"pack.neoforge.experimental.name": "Experimental mod features",
"pack.neoforge.experimental.description": "Enables mod provided experimental features",
"pack.neoforge.source.child": "child",

"neoforge.network.negotiation.failure.mod": "Channel of mod \"%1$s\" failed to connect: %2$s",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipe": "neotests_experimental_tests_moda:diamond_from_dirt"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"neotests_experimental_tests_moda:diamond_from_dirt"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"group": "experimental",
"ingredients": [
{
"item": "minecraft:dirt"
}
],
"result": {
"count": 1,
"id": "minecraft:diamond"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"features": {
"enabled": [
"neoforge:mod_experimental"
]
},
"pack": {
"description": "Enables experimental features (neotests_experimental_tests_moda)",
"pack_format": 48
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_diamond": {
"conditions": {
"items": [
{
"items": "#c:gems/diamond"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipe": "neotests_experimental_tests_modb:dirt_from_diamond"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_diamond"
]
],
"rewards": {
"recipes": [
"neotests_experimental_tests_modb:dirt_from_diamond"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"group": "experimental",
"ingredients": [
{
"tag": "c:gems/diamond"
}
],
"result": {
"count": 1,
"id": "minecraft:dirt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"features": {
"enabled": [
"neoforge:mod_experimental"
]
},
"pack": {
"description": "Enables experimental features (neotests_experimental_tests_modb)",
"pack_format": 48
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.debug.resources;

import java.util.concurrent.CompletableFuture;
import java.util.function.UnaryOperator;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.DataGenerator;
import net.minecraft.data.metadata.PackMetadataGenerator;
import net.minecraft.data.recipes.RecipeCategory;
import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.data.recipes.RecipeProvider;
import net.minecraft.data.recipes.ShapelessRecipeBuilder;
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.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.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.neoforged.neoforge.common.Tags;
import net.neoforged.neoforge.data.event.GatherDataEvent;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.testframework.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import org.apache.commons.lang3.function.TriConsumer;

@ForEachTest
public final class ExperimentalTests {
@TestHolder(value = "experimental_tests_base", description = "Test providing experimental items and blocks")
private static void baseMod(DynamicTest test) {
var registration = test.registrationHelper();
var items = registration.items();

items.registerSimpleItem("experimental_item", new Item.Properties().requiredFeatures(FeatureFlags.MOD_EXPERIMENTAL));

var block = registration.blocks().registerSimpleBlock("experimental_block", BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).requiredFeatures(FeatureFlags.MOD_EXPERIMENTAL));
items.registerSimpleBlockItem(block);
}

@TestHolder(value = "experimental_tests_moda", description = "Test providing experimental feature pack (dirt -> diamond recipe)")
private static void modA(DynamicTest test) {
commonMod(test, (event, pack, lookupProvider) -> pack.addProvider(output -> new RecipeProvider(output, lookupProvider) {
@Override
protected void buildRecipes(RecipeOutput output) {
// recipe for dirt -> diamond enabled when MOD_EXPERIMENTAL is enabled
ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.DIAMOND)
.requires(Items.DIRT)
.unlockedBy("has_dirt", has(Items.DIRT))
.group("experimental")
.save(output, ResourceLocation.fromNamespaceAndPath(test.createModId(), "diamond_from_dirt"));
}
}));
}

@TestHolder(value = "experimental_tests_modb", description = "Test providing experimental feature pack (diamond -> dirt recipe)")
private static void modB(DynamicTest test) {
commonMod(test, (event, pack, lookupProvider) -> pack.addProvider(output -> new RecipeProvider(output, lookupProvider) {
@Override
protected void buildRecipes(RecipeOutput output) {
// recipe for diamond -> dirt enabled when MOD_EXPERIMENTAL is enabled
ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.DIRT)
.requires(Tags.Items.GEMS_DIAMOND)
.unlockedBy("has_diamond", has(Tags.Items.GEMS_DIAMOND))
.group("experimental")
.save(output, ResourceLocation.fromNamespaceAndPath(test.createModId(), "dirt_from_diamond"));
}
}));
}

private static void commonMod(DynamicTest test, TriConsumer<DataGenerator, DataGenerator.PackGenerator, CompletableFuture<HolderLookup.Provider>> gatherData) {
var packName = "experimental_features";
var modBus = test.framework().modEventBus();
var modId = test.createModId();

// register pack finder for experimental features pack
modBus.addListener(AddPackFindersEvent.class, event -> event.addPackFinders(
ResourceLocation.fromNamespaceAndPath("neotests", "data/" + modId + "/datapacks/" + packName),
PackType.SERVER_DATA,
Component.literal("Experimental Features (" + modId + ")"),
PackSource.create(UnaryOperator.identity(), false),
false,
Pack.Position.BOTTOM));

modBus.addListener(GatherDataEvent.class, event -> {
var generator = event.getGenerator();
var pack = generator.getBuiltinDatapack(event.includeServer(), modId, packName);
// generate pack metadata for experimental features pack
pack.addProvider(output -> PackMetadataGenerator.forFeaturePack(output, Component.literal("Enables experimental features (" + modId + ")"), FeatureFlagSet.of(FeatureFlags.MOD_EXPERIMENTAL)));
// generate any additional data for this pack
gatherData.accept(generator, pack, event.getLookupProvider());
});
}
}
Loading