diff --git a/patches/net/minecraft/data/DataGenerator.java.patch b/patches/net/minecraft/data/DataGenerator.java.patch index b1a6783a18..58bccf1011 100644 --- a/patches/net/minecraft/data/DataGenerator.java.patch +++ b/patches/net/minecraft/data/DataGenerator.java.patch @@ -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 getProvidersView() { + return this.providersView; + } @@ -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 { diff --git a/patches/net/minecraft/world/flag/FeatureFlags.java.patch b/patches/net/minecraft/world/flag/FeatureFlags.java.patch new file mode 100644 index 0000000000..eaf12e2c5e --- /dev/null +++ b/patches/net/minecraft/world/flag/FeatureFlags.java.patch @@ -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 CODEC; + public static final FeatureFlagSet VANILLA_SET; + public static final FeatureFlagSet DEFAULT_FLAGS; ++ /** ++ * A feature flag for use with experimental features that may introduce unexpected or potentially bug-inducing behaviors.
++ * Unlike the standard set of flags, which can change frequently, this flag remains consistent across major version updates.
++ *

++ * Modders can reference this flag during built-in feature registration.
++ * However, they must provide their own flagged datapacks to associate datapack features (such as recipes and enchantments) with this flag.
++ * These datapacks can be provided either as optional files or via the {@linkplain net.neoforged.neoforge.event.AddPackFindersEvent} event. ++ *

++ *

++ * It is highly recommended that modders document which features are experimental and which ones are not.
++ * Due to the nature of this flag being a ‘catch-all’, it enables any and all modded experiments that may exist. ++ *

++ */ ++ public static final FeatureFlag MOD_EXPERIMENTAL; + + 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); diff --git a/src/generated/resources/data/neoforge/datapacks/mod_experimental/pack.mcmeta b/src/generated/resources/data/neoforge/datapacks/mod_experimental/pack.mcmeta new file mode 100644 index 0000000000..032418bba8 --- /dev/null +++ b/src/generated/resources/data/neoforge/datapacks/mod_experimental/pack.mcmeta @@ -0,0 +1,17 @@ +{ + "features": { + "enabled": [ + "neoforge:mod_experimental" + ] + }, + "pack": { + "description": { + "translate": "pack.neoforge.experimental.description" + }, + "pack_format": 48, + "supported_formats": [ + 0, + 2147483647 + ] + } +} \ No newline at end of file diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 71861b234a..32e3e1afc7 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -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; @@ -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; @@ -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; @@ -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())); @@ -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 diff --git a/src/main/java/net/neoforged/neoforge/data/event/GatherDataEvent.java b/src/main/java/net/neoforged/neoforge/data/event/GatherDataEvent.java index 7ae69bf146..64bb7a0b75 100644 --- a/src/main/java/net/neoforged/neoforge/data/event/GatherDataEvent.java +++ b/src/main/java/net/neoforged/neoforge/data/event/GatherDataEvent.java @@ -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) { diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index cc81b475d8..b3b439aa90 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -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", diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/advancement/recipes/misc/diamond_from_dirt.json b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/advancement/recipes/misc/diamond_from_dirt.json new file mode 100644 index 0000000000..199ed1078c --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/advancement/recipes/misc/diamond_from_dirt.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/recipe/diamond_from_dirt.json b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/recipe/diamond_from_dirt.json new file mode 100644 index 0000000000..cd3f3ace93 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/data/neotests_experimental_tests_moda/recipe/diamond_from_dirt.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "group": "experimental", + "ingredients": [ + { + "item": "minecraft:dirt" + } + ], + "result": { + "count": 1, + "id": "minecraft:diamond" + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/pack.mcmeta b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/pack.mcmeta new file mode 100644 index 0000000000..a36adc101b --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_moda/datapacks/experimental_features/pack.mcmeta @@ -0,0 +1,11 @@ +{ + "features": { + "enabled": [ + "neoforge:mod_experimental" + ] + }, + "pack": { + "description": "Enables experimental features (neotests_experimental_tests_moda)", + "pack_format": 48 + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/advancement/recipes/misc/dirt_from_diamond.json b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/advancement/recipes/misc/dirt_from_diamond.json new file mode 100644 index 0000000000..df42f33abd --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/advancement/recipes/misc/dirt_from_diamond.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/recipe/dirt_from_diamond.json b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/recipe/dirt_from_diamond.json new file mode 100644 index 0000000000..5865837baa --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/data/neotests_experimental_tests_modb/recipe/dirt_from_diamond.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "group": "experimental", + "ingredients": [ + { + "tag": "c:gems/diamond" + } + ], + "result": { + "count": 1, + "id": "minecraft:dirt" + } +} \ No newline at end of file diff --git a/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/pack.mcmeta b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/pack.mcmeta new file mode 100644 index 0000000000..1bd211dae7 --- /dev/null +++ b/tests/src/generated/resources/data/neotests_experimental_tests_modb/datapacks/experimental_features/pack.mcmeta @@ -0,0 +1,11 @@ +{ + "features": { + "enabled": [ + "neoforge:mod_experimental" + ] + }, + "pack": { + "description": "Enables experimental features (neotests_experimental_tests_modb)", + "pack_format": 48 + } +} \ No newline at end of file diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/resources/ExperimentalTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/resources/ExperimentalTests.java new file mode 100644 index 0000000000..d64a3bd794 --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/resources/ExperimentalTests.java @@ -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> 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()); + }); + } +}