diff --git a/README.md b/README.md
index e77d5b85b..3c7c669f9 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,34 @@ dependencies {
}
```
+#### Enum Extensions
+The userdev plugin provides a way to configure enum extensions for your mod.
+This allows you to reference runtime enum extensions at compile time, so you can include them in switch statements or the like.
+Note that you still need to provide the extensions to FML at runtime in your `neoforge.mods.toml`.
+```groovy
+enumExtensions {
+ file 'src/main/resources/META-INF/enumextensions.json'
+}
+```
+You can find more information on the format of the file [here](https://docs.neoforged.net/docs/advanced/extensibleenums/).
+
+##### From Dependencies
+When you want to include enum extension entries from a dependency you can do so by using the `consume` and `consumeApi` dependency collector:
+```groovy
+enumExtensions {
+ consume 'net.something.group:module:1.0.0-version' //Use the enum extensions published by this dependency.
+ consumeApi 'net.something.group:module:1.0.0-version' //Use the enum extensions published by this dependency, and expose it for your consumers as a dependency as well.
+}
+```
+
+Alternatively you can use the configurations, `enumExtensions` or `enumExtensionsApi` respectively, to handle this:
+```groovy
+dependencies {
+ enumExtensions 'net.something.group:module:1.0.0-version' //Use the enum extensions published by this dependency.
+ enumExtensionsApi 'net.something.group:module:1.0.0-version' //Use the enum extensions published by this dependency, and expose it for your consumers as a dependency as well.
+}
+```
+
#### Dependency management by the userdev plugin
When this plugin detects a dependency on NeoForge, it will spring into action and create the necessary NeoForm runtime tasks to build a usable Minecraft JAR-file that contains the requested NeoForge version.
It additionally (if configured to do so via conventions, which is the default) will create runs for your project, and add the necessary dependencies to the classpath of the run.
diff --git a/common/src/main/java/net/neoforged/gradle/common/CommonProjectPlugin.java b/common/src/main/java/net/neoforged/gradle/common/CommonProjectPlugin.java
index 6cdc2272b..5d5b12827 100644
--- a/common/src/main/java/net/neoforged/gradle/common/CommonProjectPlugin.java
+++ b/common/src/main/java/net/neoforged/gradle/common/CommonProjectPlugin.java
@@ -3,6 +3,7 @@
import net.neoforged.gradle.common.accesstransformers.AccessTransformerPublishing;
import net.neoforged.gradle.common.conventions.ConventionConfigurator;
import net.neoforged.gradle.common.dependency.ExtraJarDependencyManager;
+import net.neoforged.gradle.common.enumextensions.EnumExtensionsPublishing;
import net.neoforged.gradle.common.extensions.*;
import net.neoforged.gradle.common.extensions.dependency.replacement.ReplacementLogic;
import net.neoforged.gradle.common.extensions.problems.IProblemReporter;
@@ -92,6 +93,7 @@ public void apply(Project project) {
project.getExtensions().create(DependencyReplacement.class, "dependencyReplacements", ReplacementLogic.class, project);
project.getExtensions().create(AccessTransformers.class, "accessTransformers", AccessTransformersExtension.class, project);
project.getExtensions().create(InterfaceInjections.class, "interfaceInjections", InterfaceInjectionsExtension.class, project);
+ project.getExtensions().create(EnumExtensions.class, "enumExtensions", EnumExtensionsExtension.class, project);
project.getExtensions().create(Minecraft.class, "minecraft", MinecraftExtension.class, project);
project.getExtensions().create(Mappings.class,"mappings", MappingsExtension.class, project);
@@ -143,6 +145,9 @@ public void apply(Project project) {
//Set up publishing for interface injection elements
InterfaceInjectionPublishing.setup(project);
+
+ // Set up publishing for enum extensions
+ EnumExtensionsPublishing.setup(project);
//Set up the IDE run integration manager
IdeRunIntegrationManager.getInstance().setup(project);
diff --git a/common/src/main/java/net/neoforged/gradle/common/enumextensions/EnumExtensionsPublishing.java b/common/src/main/java/net/neoforged/gradle/common/enumextensions/EnumExtensionsPublishing.java
new file mode 100644
index 000000000..8244ca2c4
--- /dev/null
+++ b/common/src/main/java/net/neoforged/gradle/common/enumextensions/EnumExtensionsPublishing.java
@@ -0,0 +1,84 @@
+package net.neoforged.gradle.common.enumextensions;
+
+import net.neoforged.gradle.common.util.ProjectUtils;
+import net.neoforged.gradle.dsl.common.extensions.EnumExtensions;
+import org.gradle.api.Action;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.attributes.AttributeContainer;
+import org.gradle.api.attributes.Category;
+import org.gradle.api.component.AdhocComponentWithVariants;
+
+import java.io.File;
+import java.util.Comparator;
+import java.util.List;
+
+public class EnumExtensionsPublishing {
+
+ public static final String ENUM_EXTENSIONS_ELEMENTS_CONFIGURATION = "enumExtensionsElements";
+ public static final String ENUM_EXTENSIONS_API_CONFIGURATION = "enumExtensionsApi";
+ public static final String ENUM_EXTENSIONS_CONFIGURATION = "enumExtensions";
+ public static final String ENUM_EXTENSIONS_CATEGORY = "enumExtensions";
+
+ @SuppressWarnings("UnstableApiUsage")
+ public static void setup(Project project) {
+ EnumExtensions extension = project.getExtensions().getByType(EnumExtensions.class);
+
+ Configuration elementsConfig = project.getConfigurations().maybeCreate(ENUM_EXTENSIONS_ELEMENTS_CONFIGURATION);
+ Configuration apiConfig = project.getConfigurations().maybeCreate(ENUM_EXTENSIONS_API_CONFIGURATION);
+ Configuration implementationConfig = project.getConfigurations().maybeCreate(ENUM_EXTENSIONS_CONFIGURATION);
+
+ apiConfig.setCanBeConsumed(false);
+ apiConfig.setCanBeResolved(false);
+
+ implementationConfig.setCanBeConsumed(false);
+ implementationConfig.setCanBeResolved(true);
+
+ elementsConfig.setCanBeConsumed(true);
+ elementsConfig.setCanBeResolved(false);
+ elementsConfig.setCanBeDeclared(false);
+
+ Action action = attributes -> {
+ attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, ENUM_EXTENSIONS_CATEGORY));
+ };
+
+ elementsConfig.attributes(action);
+ implementationConfig.attributes(action);
+
+ implementationConfig.extendsFrom(apiConfig);
+ elementsConfig.extendsFrom(apiConfig);
+
+ // Now we set up the component, conditionally
+ AdhocComponentWithVariants java = (AdhocComponentWithVariants) project.getComponents().getByName("java");
+ Runnable enable = () -> java.addVariantsFromConfiguration(elementsConfig, variant -> {
+ });
+
+ elementsConfig.getAllDependencies().configureEach(dep -> {
+ enable.run();
+ });
+ elementsConfig.getArtifacts().configureEach(artifact -> enable.run());
+
+ // And add resolved iis to the extension
+ extension.getFiles().from(implementationConfig);
+
+ //When the user has configured the dependency collectors add the relevant files.
+ ProjectUtils.afterEvaluate(project, () -> {
+ apiConfig.fromDependencyCollector(extension.getConsumeApi());
+ implementationConfig.fromDependencyCollector(extension.getConsume());
+
+ final List files = extension.getFiles().getFiles().stream()
+ .sorted(Comparator.comparing(File::getName))
+ .toList();
+ for (int i = 0; i < files.size(); i++)
+ {
+ final var file = files.get(i);
+ if (files.size() == 1) {
+ extension.expose(file, artifact -> artifact.setClassifier("enum-extensions"));
+ } else {
+ final int index = i;
+ extension.expose(file, artifact -> artifact.setClassifier("enum-extensions-%d".formatted(index)));
+ }
+ }
+ });
+ }
+}
diff --git a/common/src/main/java/net/neoforged/gradle/common/extensions/EnumExtensionsExtension.java b/common/src/main/java/net/neoforged/gradle/common/extensions/EnumExtensionsExtension.java
new file mode 100644
index 000000000..0c0c14f23
--- /dev/null
+++ b/common/src/main/java/net/neoforged/gradle/common/extensions/EnumExtensionsExtension.java
@@ -0,0 +1,61 @@
+package net.neoforged.gradle.common.extensions;
+
+import net.neoforged.gradle.common.enumextensions.EnumExtensionsPublishing;
+import net.neoforged.gradle.common.extensions.problems.IProblemReporter;
+import net.neoforged.gradle.dsl.common.extensions.EnumExtensions;
+import org.gradle.api.Action;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.ConfigurablePublishArtifact;
+import org.gradle.api.artifacts.Dependency;
+import org.gradle.api.artifacts.dsl.ArtifactHandler;
+import org.gradle.api.artifacts.dsl.DependencyHandler;
+
+import javax.inject.Inject;
+
+public abstract class EnumExtensionsExtension implements EnumExtensions {
+ private transient final DependencyHandler projectDependencies;
+ private transient final ArtifactHandler projectArtifacts;
+
+ private final Project project;
+
+ @Inject
+ public EnumExtensionsExtension(final Project project) {
+ this.project = project;
+
+ this.projectDependencies = project.getDependencies();
+ this.projectArtifacts = project.getArtifacts();
+ }
+
+ @Override
+ public Project getProject() {
+ return project;
+ }
+
+ @Override
+ public void expose(Object path, Action action) {
+ getFiles().from(path);
+ projectArtifacts.add(EnumExtensionsPublishing.ENUM_EXTENSIONS_ELEMENTS_CONFIGURATION, path, action);
+ }
+
+ @Override
+ public void expose(Object path) {
+ expose(path, artifacts -> {
+ });
+ }
+
+ @SuppressWarnings("removal")
+ @Override
+ public void expose(Dependency dependency) {
+ project.getExtensions().getByType(IProblemReporter.class)
+ .reporting(
+ spec -> spec.id("enum-extensions", "expose.deprecated")
+ .details("Using the expose(Dependency) method is deprecated.")
+ .solution("Use the dependency collectors: 'consume' and 'consumeApi' for adding enum extensions from your dependencies.")
+ .section("userdev-enum-extensions-from-dependencies")
+ .contextualLabel("Deprecations"),
+ project.getLogger()
+ );
+
+ projectDependencies.add(EnumExtensionsPublishing.ENUM_EXTENSIONS_API_CONFIGURATION, dependency);
+ }
+}
diff --git a/common/src/main/java/net/neoforged/gradle/common/extensions/MinecraftExtension.java b/common/src/main/java/net/neoforged/gradle/common/extensions/MinecraftExtension.java
index c28bc9cf1..84d9b628f 100644
--- a/common/src/main/java/net/neoforged/gradle/common/extensions/MinecraftExtension.java
+++ b/common/src/main/java/net/neoforged/gradle/common/extensions/MinecraftExtension.java
@@ -23,6 +23,7 @@
import net.neoforged.gdi.ConfigurableDSLElement;
import net.neoforged.gradle.common.runtime.naming.NamingChannelProvider;
import net.neoforged.gradle.dsl.common.extensions.AccessTransformers;
+import net.neoforged.gradle.dsl.common.extensions.EnumExtensions;
import net.neoforged.gradle.dsl.common.extensions.InterfaceInjections;
import net.neoforged.gradle.dsl.common.extensions.Mappings;
import net.neoforged.gradle.dsl.common.extensions.Minecraft;
@@ -38,6 +39,7 @@ public abstract class MinecraftExtension implements ConfigurableDSLElement namingChannelProviders;
@Inject
@@ -45,6 +47,7 @@ public MinecraftExtension(final Project project) {
this.project = project;
this.accessTransformers = project.getExtensions().getByType(AccessTransformers.class);
this.interfaceInjections = project.getExtensions().getByType(InterfaceInjections.class);
+ this.enumExtensions = project.getExtensions().getByType(EnumExtensions.class);
this.namingChannelProviders = project.getObjects().domainObjectContainer(NamingChannel.class, name -> project.getObjects().newInstance(NamingChannelProvider.class, project, name));
final String baseName = project.getName().replace(":", "_");
@@ -85,4 +88,10 @@ public AccessTransformers getAccessTransformers() {
public InterfaceInjections getInterfaceInjections() {
return interfaceInjections;
}
+
+ @NotNull
+ @Override
+ public EnumExtensions getEnumExtensions() {
+ return enumExtensions;
+ }
}
diff --git a/common/src/main/java/net/neoforged/gradle/common/runtime/tasks/JavaSourceTransformer.java b/common/src/main/java/net/neoforged/gradle/common/runtime/tasks/JavaSourceTransformer.java
index 87c2c570c..6fbd14a77 100644
--- a/common/src/main/java/net/neoforged/gradle/common/runtime/tasks/JavaSourceTransformer.java
+++ b/common/src/main/java/net/neoforged/gradle/common/runtime/tasks/JavaSourceTransformer.java
@@ -2,6 +2,7 @@
import com.google.common.collect.Lists;
import net.neoforged.gradle.common.services.caching.jobs.ICacheableJob;
+import net.neoforged.gradle.common.util.EnumExtensionUtils;
import net.neoforged.gradle.common.util.ToolUtilities;
import net.neoforged.gradle.dsl.common.extensions.subsystems.Subsystems;
import net.neoforged.gradle.util.CopyingFileTreeVisitor;
@@ -66,6 +67,28 @@ public JavaSourceTransformer() {
args.add("--interface-injection-stubs");
args.add(stubsFile.getAbsolutePath());
}
+
+ if (!getEnumExtensions().isEmpty()) {
+ final File stubsFile = ensureFileWorkspaceReady(getStubs());
+
+ args.add("--enable-enum-extensions");
+ getEnumExtensions().forEach(f -> {
+ args.add("--enum-extension-data");
+ args.add(f.getAbsolutePath());
+ });
+
+ args.add("--enum-extension-stubs");
+ args.add(stubsFile.getAbsolutePath());
+
+ args.add("--enum-extensions-required-interface");
+ args.add(EnumExtensionUtils.REQUIRED_INTERFACE);
+ args.add("--enum-extensions-indexed-enum-annotation");
+ args.add(EnumExtensionUtils.INDEXED_ENUM);
+ args.add("--enum-extensions-marker");
+ args.add(EnumExtensionUtils.MARKER_ANNOTATION);
+ args.add("--enum-extensions-reserved-constructor-annotation");
+ args.add(EnumExtensionUtils.RESERVED_CONSTRUCTOR);
+ }
if (!getParchmentMappings().isEmpty()) {
final File parchment = getParchmentMappings().getSingleFile();
@@ -133,6 +156,7 @@ public void doExecute() throws Exception {
//We need a separate check here that skips the execute call if there are no transformers.
if (getTransformers().isEmpty() &&
getInterfaceInjections().isEmpty() &&
+ getEnumExtensions().isEmpty() &&
getParchmentMappings().isEmpty()) {
//Unpack the input zip into the outputs:
@@ -196,6 +220,11 @@ private void pack() throws IOException
@Optional
@PathSensitive(PathSensitivity.NONE)
public abstract ConfigurableFileCollection getInterfaceInjections();
+
+ @InputFiles
+ @Optional
+ @PathSensitive(PathSensitivity.NONE)
+ public abstract ConfigurableFileCollection getEnumExtensions();
@InputFiles
@Optional
diff --git a/common/src/main/java/net/neoforged/gradle/common/tasks/ExtendEnumsTask.java b/common/src/main/java/net/neoforged/gradle/common/tasks/ExtendEnumsTask.java
new file mode 100644
index 000000000..7e8c040d0
--- /dev/null
+++ b/common/src/main/java/net/neoforged/gradle/common/tasks/ExtendEnumsTask.java
@@ -0,0 +1,124 @@
+package net.neoforged.gradle.common.tasks;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import net.neoforged.gradle.common.runtime.tasks.DefaultRuntime;
+import net.neoforged.gradle.common.services.caching.CachedExecutionService;
+import net.neoforged.gradle.common.services.caching.jobs.ICacheableJob;
+import net.neoforged.gradle.common.util.EnumExtensionUtils;
+import net.neoforged.gradle.dsl.common.tasks.WithOperations;
+import net.neoforged.gradle.dsl.common.tasks.WithOutput;
+import net.neoforged.gradle.dsl.common.tasks.WithWorkspace;
+import net.neoforged.gradle.dsl.common.tasks.specifications.InputFileSpecification;
+import net.neoforged.gradle.util.ClassVisitingFileTreeVisitor;
+import org.gradle.api.file.ConfigurableFileCollection;
+import org.gradle.api.provider.Property;
+import org.gradle.api.services.ServiceReference;
+import org.gradle.api.tasks.CacheableTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
+import org.gradle.api.tasks.TaskAction;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipOutputStream;
+
+@CacheableTask
+public abstract class ExtendEnumsTask extends DefaultRuntime implements WithOutput, InputFileSpecification, WithOperations, WithWorkspace {
+ public ExtendEnumsTask() {
+ super();
+ }
+
+ @ServiceReference(CachedExecutionService.NAME)
+ public abstract Property getCacheService();
+
+ @InputFiles
+ @PathSensitive(PathSensitivity.NONE)
+ public abstract ConfigurableFileCollection getEnumExtensionsFiles();
+
+ @TaskAction
+ public void extendEnums() throws IOException {
+ getCacheService().get()
+ .cached(this,
+ ICacheableJob.Default.file(this::doExtendEnums, getOutput())
+ )
+ .execute();
+ }
+
+ private void doExtendEnums() throws IOException {
+ Map> extensionMap = parseExtensionFiles();
+ var input = getArchiveOperations().zipTree(getInput());
+ try (final FileOutputStream fos = new FileOutputStream(ensureFileWorkspaceReady(getOutput()));
+ final ZipOutputStream zos = new ZipOutputStream(fos)) {
+ final ClassVisitingFileTreeVisitor visitor = new ClassVisitingFileTreeVisitor(zos, (api, cv) -> new ExtendEnumsClassVisitor(api, cv, extensionMap));
+ input.visit(visitor);
+ }
+ }
+
+ private Map> parseExtensionFiles() throws IOException {
+ var extensions = new HashMap>();
+ ObjectMapper mapper = new ObjectMapper();
+ for (var file : getEnumExtensionsFiles().getFiles()) {
+ Map map = mapper.readValue(file, new TypeReference<>() {});
+ var entries = map.get("entries");
+ if (entries instanceof List> entryList) {
+ for (var entry : entryList) {
+ if (entry instanceof Map, ?> entryMap) {
+ var enumName = entryMap.get("enum");
+ var entryName = entryMap.get("name");
+ if (!(enumName instanceof String enumNameString) || !(entryName instanceof String entryNameString)) {
+ throw new IllegalArgumentException("Invalid enum extension entry format in " + file + ": " + entry);
+ }
+ extensions.computeIfAbsent(enumNameString, unused -> new LinkedHashSet<>()).add(entryNameString);
+ } else {
+ throw new IllegalArgumentException("Invalid enum extension entry format in " + file + ": " + entry);
+ }
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid enum extension format in " + file + ": " + entries);
+ }
+ }
+ return extensions;
+ }
+
+ @VisibleForTesting
+ static class ExtendEnumsClassVisitor extends ClassVisitor {
+ private final Map> extensionMap;
+ private List entries;
+ private Type type;
+
+ public ExtendEnumsClassVisitor(int api, ClassVisitor classVisitor, Map> extensionMap) {
+ super(api, classVisitor);
+ this.extensionMap = extensionMap;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, @Nullable String signature, String superName, String[] interfaces) {
+ this.entries = extensionMap.getOrDefault(name, Collections.emptySet()).stream().sorted().toList();
+ this.type = Type.getObjectType(name);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ for (String entry : entries) {
+ var fieldVisitor = visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM, entry, type.getDescriptor(), null, null);
+ fieldVisitor.visitAnnotation(Type.getObjectType(EnumExtensionUtils.MARKER_ANNOTATION).getDescriptor(), false).visitEnd();
+ fieldVisitor.visitEnd();
+ }
+ super.visitEnd();
+ }
+ }
+}
diff --git a/common/src/main/java/net/neoforged/gradle/common/util/CommonRuntimeTaskUtils.java b/common/src/main/java/net/neoforged/gradle/common/util/CommonRuntimeTaskUtils.java
index 65b2fb13b..9ade45bec 100644
--- a/common/src/main/java/net/neoforged/gradle/common/util/CommonRuntimeTaskUtils.java
+++ b/common/src/main/java/net/neoforged/gradle/common/util/CommonRuntimeTaskUtils.java
@@ -1,6 +1,7 @@
package net.neoforged.gradle.common.util;
import net.neoforged.gradle.common.runtime.tasks.*;
+import net.neoforged.gradle.common.tasks.ExtendEnumsTask;
import net.neoforged.gradle.common.tasks.InjectInterfacesTask;
import net.neoforged.gradle.dsl.common.runtime.definition.Definition;
import net.neoforged.gradle.dsl.common.tasks.WithOutput;
@@ -45,4 +46,10 @@ public static TaskProvider extends InjectInterfacesTask> createBinaryInterface
task.getInterfaceInjectionFiles().from(files);
});
}
+
+ public static TaskProvider extends ExtendEnumsTask> createBinaryEnumExtender(Definition> definition, String namePreFix, FileTree files) {
+ return definition.getSpecification().getProject().getTasks().register(CommonRuntimeUtils.buildTaskName(definition.getSpecification(), String.format("apply%sEnumExtensions", StringCapitalizationUtils.capitalize(namePreFix))), ExtendEnumsTask.class, task -> {
+ task.getEnumExtensionsFiles().from(files);
+ });
+ }
}
diff --git a/common/src/main/java/net/neoforged/gradle/common/util/EnumExtensionUtils.java b/common/src/main/java/net/neoforged/gradle/common/util/EnumExtensionUtils.java
new file mode 100644
index 000000000..a083372dd
--- /dev/null
+++ b/common/src/main/java/net/neoforged/gradle/common/util/EnumExtensionUtils.java
@@ -0,0 +1,10 @@
+package net.neoforged.gradle.common.util;
+
+public final class EnumExtensionUtils {
+ private EnumExtensionUtils() {}
+
+ public static final String REQUIRED_INTERFACE = "net/neoforged/fml/common/asm/enumextension/IExtensibleEnum";
+ public static final String INDEXED_ENUM = "net/neoforged/fml/common/asm/enumextension/IndexedEnum";
+ public static final String MARKER_ANNOTATION = "net/neoforged/fml/common/asm/enumextension/ExtensionEnumEntry";
+ public static final String RESERVED_CONSTRUCTOR = "net/neoforged/fml/common/asm/enumextension/ReservedConstructor";
+}
diff --git a/common/src/main/java/net/neoforged/gradle/common/util/JavaSourceTransformAdapterUtils.java b/common/src/main/java/net/neoforged/gradle/common/util/JavaSourceTransformAdapterUtils.java
index fa596060f..41620e0e6 100644
--- a/common/src/main/java/net/neoforged/gradle/common/util/JavaSourceTransformAdapterUtils.java
+++ b/common/src/main/java/net/neoforged/gradle/common/util/JavaSourceTransformAdapterUtils.java
@@ -4,6 +4,7 @@
import net.neoforged.gradle.common.runtime.tasks.JavaSourceTransformer;
import net.neoforged.gradle.common.runtime.tasks.ListLibraries;
import net.neoforged.gradle.dsl.common.extensions.AccessTransformers;
+import net.neoforged.gradle.dsl.common.extensions.EnumExtensions;
import net.neoforged.gradle.dsl.common.extensions.InterfaceInjections;
import net.neoforged.gradle.dsl.common.extensions.Minecraft;
import net.neoforged.gradle.dsl.common.extensions.subsystems.Subsystems;
@@ -33,6 +34,7 @@ public static TaskTreeAdapter createCustomizationsAdapter(final Project project,
final Minecraft minecraftExtension = project.getExtensions().getByType(Minecraft.class);
final AccessTransformers accessTransformers = minecraftExtension.getAccessTransformers();
final InterfaceInjections interfaceInjections = minecraftExtension.getInterfaceInjections();
+ final EnumExtensions enumExtensions = minecraftExtension.getEnumExtensions();
final SubsystemsExtension.ParchmentExtensions parchment = (SubsystemsExtension.ParchmentExtensions) project.getExtensions().getByType(Subsystems.class).getParchment();
return (definition, previousTasksOutput, runtimeWorkspace, gameArtifacts, mappingVersionData, dependentTaskConfigurationHandler) -> {
@@ -40,6 +42,7 @@ public static TaskTreeAdapter createCustomizationsAdapter(final Project project,
var transformer = createJavaSourceTransformerTask(accessTransformerFiles,
interfaceInjections.getFiles(),
+ enumExtensions.getFiles(),
definition,
previousTasksOutput,
dependentTaskConfigurationHandler,
@@ -72,6 +75,7 @@ public static TaskProvider createRecompileLibrariesList(
public static @Nullable TaskProvider createJavaSourceTransformerTask(
final FileCollection accessTransformerFiles,
final FileCollection interfaceInjectionFiles,
+ final FileCollection enumExtensionsFiles,
final Definition> definition,
final Provider extends WithOutput> previousTasksOutput,
final Consumer> dependentTaskConfigurationHandler,
@@ -94,6 +98,7 @@ public static TaskProvider createRecompileLibrariesList(
.register(CommonRuntimeUtils.buildTaskName(definition.getSpecification(), "transformSource"), JavaSourceTransformer.class, task -> {
task.getTransformers().from(accessTransformerFiles);
task.getInterfaceInjections().from(interfaceInjectionFiles);
+ task.getEnumExtensions().from(enumExtensionsFiles);
if (parchmentArtifact.isPresent())
{
diff --git a/common/src/test/java/net/neoforged/gradle/common/tasks/ExtendEnumsTaskTest.java b/common/src/test/java/net/neoforged/gradle/common/tasks/ExtendEnumsTaskTest.java
new file mode 100644
index 000000000..2ba502014
--- /dev/null
+++ b/common/src/test/java/net/neoforged/gradle/common/tasks/ExtendEnumsTaskTest.java
@@ -0,0 +1,48 @@
+package net.neoforged.gradle.common.tasks;
+
+import org.junit.jupiter.api.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class ExtendEnumsTaskTest {
+ private byte[] generateSimpleClass(String name) {
+ ClassWriter cw = new ClassWriter(0);
+ cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM, name, null, "java/lang/Enum", null);
+ cw.visitEnd();
+ return cw.toByteArray();
+ }
+
+ private ClassNode visitWithExtension(byte[] classBytes, Map> injectionMap) {
+ ClassReader cr = new ClassReader(classBytes);
+ ClassNode cn = new ClassNode();
+ ExtendEnumsTask.ExtendEnumsClassVisitor visitor = new ExtendEnumsTask.ExtendEnumsClassVisitor(Opcodes.ASM9, cn, injectionMap);
+ cr.accept(visitor, 0);
+ return cn;
+ }
+
+ @Test
+ void injectMultipleEnumEntries() {
+ String className = "com/example/TestClass";
+ byte[] original = generateSimpleClass(className);
+ Map> map = Map.of(className, Set.of("A", "B"));
+ ClassNode cn = visitWithExtension(original, map);
+ assertEquals(2, cn.fields.size()); // In this case we never generated the backing values field
+ assertEquals("A", cn.fields.get(0).name);
+ assertEquals("B", cn.fields.get(1).name);
+ for (var field : cn.fields) {
+ assertEquals(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM, field.access);
+ assertNotNull(field.invisibleAnnotations);
+ assertEquals(1, field.invisibleAnnotations.size());
+ assertEquals("Lnet/neoforged/fml/common/asm/enumextension/ExtensionEnumEntry;", field.invisibleAnnotations.get(0).desc);
+ }
+ }
+}
+
diff --git a/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/EnumExtensions.groovy b/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/EnumExtensions.groovy
new file mode 100644
index 000000000..a6bfcd3e1
--- /dev/null
+++ b/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/EnumExtensions.groovy
@@ -0,0 +1,57 @@
+package net.neoforged.gradle.dsl.common.extensions
+
+import groovy.transform.CompileStatic
+import net.neoforged.gdi.BaseDSLElement
+import net.neoforged.gdi.annotations.DSLProperty
+import org.gradle.api.Action
+import org.gradle.api.artifacts.ConfigurablePublishArtifact
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.dsl.Dependencies
+import org.gradle.api.artifacts.dsl.DependencyCollector
+import org.gradle.api.file.ConfigurableFileCollection
+
+/**
+ * Defines a DSL extension which allows for the specification of enum extensions.
+ */
+@CompileStatic
+interface EnumExtensions extends BaseDSLElement, Dependencies {
+
+ /**
+ * {@return enum extensions files}
+ */
+ @DSLProperty
+ ConfigurableFileCollection getFiles()
+
+ /**
+ * {@return enum extensions to add as dependencies}
+ */
+ DependencyCollector getConsume()
+
+ /**
+ * {@return enum extensions to add as dependencies and also expose to consumers}
+ */
+ DependencyCollector getConsumeApi()
+
+ /**
+ * Publishes a transitive dependency on the given interface injection in the published enum extensions of this component.
+ *
+ * @param dependency to expose to consumers
+ */
+ @Deprecated(forRemoval = true, since="7.1")
+ void expose(Dependency dependency)
+
+ /**
+ * Publishes a transitive dependency on the given interface injection in the published enum extensions of this component.
+ *
+ * @param path access transformer file to publish
+ */
+ void expose(Object path)
+
+ /**
+ * Publishes a given interface injection in the published enum extensions of this component.
+ *
+ * @param path enum extensions file to publish
+ * @param action configures the published artifact
+ */
+ void expose(Object path, Action action)
+}
\ No newline at end of file
diff --git a/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/Minecraft.groovy b/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/Minecraft.groovy
index 60191f46d..f5cc4926c 100644
--- a/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/Minecraft.groovy
+++ b/dsl/common/src/main/groovy/net/neoforged/gradle/dsl/common/extensions/Minecraft.groovy
@@ -56,4 +56,13 @@ interface Minecraft extends BaseDSLElement {
@NotNull
@DSLProperty
InterfaceInjections getInterfaceInjections();
+
+ /**
+ * Gives access to the enum extensions configuration extension.
+ *
+ * @return The enum extensions configuration extension.
+ */
+ @NotNull
+ @DSLProperty
+ EnumExtensions getEnumExtensions();
}
diff --git a/userdev/src/main/java/net/neoforged/gradle/userdev/runtime/extension/UserDevRuntimeExtension.java b/userdev/src/main/java/net/neoforged/gradle/userdev/runtime/extension/UserDevRuntimeExtension.java
index 9f345399f..3e8067201 100644
--- a/userdev/src/main/java/net/neoforged/gradle/userdev/runtime/extension/UserDevRuntimeExtension.java
+++ b/userdev/src/main/java/net/neoforged/gradle/userdev/runtime/extension/UserDevRuntimeExtension.java
@@ -5,11 +5,13 @@
import net.neoforged.gradle.common.dependency.ExtraJarDependencyManager;
import net.neoforged.gradle.common.runtime.extensions.CommonRuntimeExtension;
import net.neoforged.gradle.common.runtime.tasks.BinaryAccessTransformer;
+import net.neoforged.gradle.common.tasks.ExtendEnumsTask;
import net.neoforged.gradle.common.tasks.InjectInterfacesTask;
import net.neoforged.gradle.common.tasks.StripFinalFromParametersTask;
import net.neoforged.gradle.common.util.*;
import net.neoforged.gradle.common.util.run.TypesUtil;
import net.neoforged.gradle.dsl.common.extensions.AccessTransformers;
+import net.neoforged.gradle.dsl.common.extensions.EnumExtensions;
import net.neoforged.gradle.dsl.common.extensions.InterfaceInjections;
import net.neoforged.gradle.dsl.common.extensions.Minecraft;
import net.neoforged.gradle.dsl.common.extensions.subsystems.Conventions;
@@ -188,9 +190,28 @@ public UserDevRuntimeExtension(Project project)
final InterfaceInjections userIIs = minecraftExtension.getInterfaceInjections();
final TaskProvider extends InjectInterfacesTask> task = CommonRuntimeTaskUtils.createBinaryInterfaceInjector(
- definition,
- "",
- userIIs.getFiles().getAsFileTree()
+ definition,
+ "",
+ userIIs.getFiles().getAsFileTree()
+ );
+
+ task.configure(t -> {
+ t.getInput().set(previousTasksOutput.flatMap(WithOutput::getOutput));
+ t.dependsOn(previousTasksOutput);
+ });
+
+ dependentTaskConfigurationHandler.accept(task);
+
+ return task;
+ });
+
+ builder.withPostTaskAdapter("setup", (definition, previousTasksOutput, runtimeWorkspace, gameArtifacts, mappingVersionData, dependentTaskConfigurationHandler) -> {
+ final EnumExtensions userEEs = minecraftExtension.getEnumExtensions();
+
+ final TaskProvider extends ExtendEnumsTask> task = CommonRuntimeTaskUtils.createBinaryEnumExtender(
+ definition,
+ "",
+ userEEs.getFiles().getAsFileTree()
);
task.configure(t -> {
diff --git a/vanilla/src/main/java/net/neoforged/gradle/vanilla/runtime/steps/ParchmentStep.java b/vanilla/src/main/java/net/neoforged/gradle/vanilla/runtime/steps/ParchmentStep.java
index 80b62c249..5f16c002d 100644
--- a/vanilla/src/main/java/net/neoforged/gradle/vanilla/runtime/steps/ParchmentStep.java
+++ b/vanilla/src/main/java/net/neoforged/gradle/vanilla/runtime/steps/ParchmentStep.java
@@ -30,6 +30,7 @@ public TaskProvider extends Runtime> buildTask(VanillaRuntimeDefinition defini
@Nullable
final TaskProvider extends Runtime> transformerTask = JavaSourceTransformAdapterUtils.createJavaSourceTransformerTask(
+ definition.getSpecification().getProject().files(),
definition.getSpecification().getProject().files(),
definition.getSpecification().getProject().files(),
definition,