diff --git a/settings.gradle b/settings.gradle
index 31144b8..83a52d0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -53,7 +53,7 @@ dependencyResolutionManagement {
mavenCentral()
maven {
name = "NeoForge"
- url = "https://maven.neoforged.net"
+ url = "https://maven.neoforged.net/releases/"
}
}
}
diff --git a/src/main/java/net/neoforged/installertools/EnumExtension.java b/src/main/java/net/neoforged/installertools/EnumExtension.java
new file mode 100644
index 0000000..1554741
--- /dev/null
+++ b/src/main/java/net/neoforged/installertools/EnumExtension.java
@@ -0,0 +1,104 @@
+/*
+ * InstallerTools
+ * Copyright (c) 2019-2025.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation version 2.1
+ * of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package net.neoforged.installertools;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.AnnotationNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.FieldNode;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * This class applies enum extension data files,
+ * and marks enums extended in such a way with an annotation.
+ *
+ * Note that only a subset of FML's RuntimeEnumExtender's functionality is re-implemented here.
+ * Thus, the resulting class files will not work at runtime, unless FML is present to replace the injected enum entries
+ * in a way that gives the proper ordering guarantees.
+ */
+public class EnumExtension {
+ private final String annotationMarker;
+
+ // Enum class -> entries to add
+ private final Map> extensions;
+
+ public EnumExtension(List extensionDataFiles, String annotationMarker) {
+ this.annotationMarker = annotationMarker;
+
+ extensions = new HashMap<>();
+ Gson gson = new Gson();
+ for (File file : extensionDataFiles) {
+ try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
+ JsonObject json = gson.fromJson(new BufferedReader(reader), JsonObject.class);
+ if (json.has("entries")) {
+ JsonArray entries = json.getAsJsonArray("entries");
+ for (JsonElement entry : entries) {
+ JsonObject entryObj = entry.getAsJsonObject();
+ String enumName = entryObj.getAsJsonPrimitive("enum").getAsString();
+ String entryName = entryObj.getAsJsonPrimitive("name").getAsString();
+ extensions.computeIfAbsent(enumName, unused -> new LinkedHashSet<>()).add(entryName);
+ }
+ }
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Failed to read interface injection data file " + file, exception);
+ }
+ }
+ }
+
+ public boolean containsClassTarget(Type classType) {
+ return extensions.containsKey(classType.getInternalName());
+ }
+
+ public void transform(ClassNode cn, Type type) {
+ Set entries = extensions.get(type.getInternalName());
+ if (entries == null || entries.isEmpty()) {
+ return;
+ }
+ List sortedEntries = entries.stream().sorted().collect(Collectors.toList());
+ for (String entry : sortedEntries) {
+ FieldNode field = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM, entry, type.getDescriptor(), null, null);
+ List invisibleAnnotations = new ArrayList<>();
+ invisibleAnnotations.add(new AnnotationNode(
+ Type.getObjectType(annotationMarker).getDescriptor()
+ ));
+ field.invisibleAnnotations = invisibleAnnotations;
+ cn.fields.add(field);
+ }
+ }
+}
diff --git a/src/main/java/net/neoforged/installertools/ProcessMinecraftJar.java b/src/main/java/net/neoforged/installertools/ProcessMinecraftJar.java
index ebb645b..227d04d 100644
--- a/src/main/java/net/neoforged/installertools/ProcessMinecraftJar.java
+++ b/src/main/java/net/neoforged/installertools/ProcessMinecraftJar.java
@@ -118,6 +118,8 @@ public void process(String[] args) throws IOException {
OptionSpec accessTransformerArg = parser.accepts("access-transformer", "Apply an access transformer.").withOptionalArg().ofType(File.class);
OptionSpec iiAnnotationMarkerArg = parser.accepts("interface-injection-marker", "The name (binary representation) of an annotation to use as a marker for injected interfaces.").withOptionalArg().ofType(String.class);
OptionSpec iiDataFilesArg = parser.accepts("interface-injection-data", "The paths to read interface injection JSON files from.").withOptionalArg().ofType(File.class);
+ OptionSpec eeAnnotationMarkerArg = parser.accepts("enum-extensions-marker", "The name (binary representation) of an annotation to use as a marker for extended enum entries.").withOptionalArg().ofType(String.class);
+ OptionSpec eeDataFilesArg = parser.accepts("enum-extensions-data", "The paths to read enum extension JSON files from.").withOptionalArg().ofType(File.class);
OptionSet options;
try {
@@ -145,6 +147,9 @@ public void process(String[] args) throws IOException {
List accessTransformerFiles = options.valuesOf(accessTransformerArg);
String iiAnnotationMarker = options.valueOf(iiAnnotationMarkerArg);
List iiDataFiles = options.valuesOf(iiDataFilesArg);
+
+ String eeAnnotationMarker = options.valueOf(eeAnnotationMarkerArg);
+ List eeDataFiles = options.valuesOf(eeDataFilesArg);
AccessTransformerEngine accessTransformers = null;
if (!accessTransformerFiles.isEmpty()) {
@@ -156,6 +161,12 @@ public void process(String[] args) throws IOException {
interfaceInjection = new InterfaceInjection(iiDataFiles, iiAnnotationMarker);
logElapsed("load interface injection data", iiStart);
}
+ EnumExtension enumExtension = null;
+ if (!eeDataFiles.isEmpty()) {
+ long eeStart = System.nanoTime();
+ enumExtension = new EnumExtension(eeDataFiles, eeAnnotationMarker);
+ logElapsed("load enum extension data", eeStart);
+ }
boolean addModManifest = !options.has(noModManifest);
boolean addDistAnnotations = !options.has(noDistAnnotations);
@@ -178,7 +189,7 @@ public void process(String[] args) throws IOException {
}
try {
- processZip(inputFile, inputMappingsFile, mergeInputFile, outputFile, librariesFolder, neoformDataFile, patchBundleFile, addModManifest, accessTransformers, interfaceInjection, addDistAnnotations);
+ processZip(inputFile, inputMappingsFile, mergeInputFile, outputFile, librariesFolder, neoformDataFile, patchBundleFile, addModManifest, accessTransformers, interfaceInjection, enumExtension, addDistAnnotations);
} finally {
if (ownedExecutor != null) {
ownedExecutor.shutdownNow();
@@ -202,6 +213,8 @@ private void processZip(File inputFile,
AccessTransformerEngine accessTransformers,
@Nullable
InterfaceInjection interfaceInjection,
+ @Nullable
+ EnumExtension enumExtension,
boolean addDistAnnotations) {
CompletableFuture