diff --git a/src/main/java/net/neoforged/neoform/runtime/actions/CreateMinecraftModJarAction.java b/src/main/java/net/neoforged/neoform/runtime/actions/CreateMinecraftModJarAction.java new file mode 100644 index 0000000..d98707a --- /dev/null +++ b/src/main/java/net/neoforged/neoform/runtime/actions/CreateMinecraftModJarAction.java @@ -0,0 +1,90 @@ +package net.neoforged.neoform.runtime.actions; + +import net.neoforged.neoform.runtime.engine.ProcessingEnvironment; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * In older NeoForm processes, the output of the process is not a usable Minecraft mod jar. + *

+ * The old process strips resources early (in the strip/stripClient/stripServer step), and then only deals with + * classes. In the new process, resources are never stripped, and the Jar pre-processor will also add both the + * Side-Manifest entries to the Jar Manifest, and add a neoforge.mods.toml to the Jar. + */ +public class CreateMinecraftModJarAction extends BuiltInAction { + private static final String MOD_MANIFEST_NAME = "META-INF/neoforge.mods.toml"; + + static final long STABLE_TIMESTAMP = 0x386D4380; //01/01/2000 00:00:00 java 8 breaks when using 0. + + private final String minecraftVersion; + + public CreateMinecraftModJarAction(String minecraftVersion) { + this.minecraftVersion = minecraftVersion; + } + + @Override + public void run(ProcessingEnvironment environment) throws IOException, InterruptedException { + + var classesFile = environment.getRequiredInputPath("classes"); + var resourcesFile = environment.getInputPath("resources"); + var output = environment.getOutputPath("output"); + + try (var os = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(output)))) { + boolean modManifestCopied = false; + + try (var in = new ZipInputStream(new BufferedInputStream(Files.newInputStream(classesFile)))) { + for (var entry = in.getNextEntry(); entry != null; entry = in.getNextEntry()) { + os.putNextEntry(entry); + in.transferTo(os); + os.closeEntry(); + if (entry.getName().equals(MOD_MANIFEST_NAME)) { + modManifestCopied = true; + } + } + } + + if (resourcesFile != null) { + try (var in = new ZipInputStream(new BufferedInputStream(Files.newInputStream(resourcesFile)))) { + for (var entry = in.getNextEntry(); entry != null; entry = in.getNextEntry()) { + os.putNextEntry(entry); + in.transferTo(os); + os.closeEntry(); + if (entry.getName().equals(MOD_MANIFEST_NAME)) { + modManifestCopied = true; + } + } + } + } + + // If no META-INF/neoforge.mods.toml was copied over, we now inject a new one. + if (!modManifestCopied) { + appendModManifest(os); + } + } + } + + // This is based on installertools. + private void appendModManifest(ZipOutputStream os) throws IOException { + String modManifest = "modLoader=\"minecraft\"\n" + + "license=\"Minecraft EULA\"\n" + + "[[mods]]\n" + + "modId=\"minecraft\"\n" + + "version=\"" + minecraftVersion + "\"\n" + + "displayName=\"Minecraft\"\n" + + "authors=\"Mojang Studios\"\n" + + "description=\"\"\n"; + + var entry = new ZipEntry(MOD_MANIFEST_NAME); + entry.setTime(STABLE_TIMESTAMP); + os.putNextEntry(entry); + os.write(modManifest.getBytes(StandardCharsets.UTF_8)); + os.closeEntry(); + } +} diff --git a/src/main/java/net/neoforged/neoform/runtime/engine/NeoFormEngine.java b/src/main/java/net/neoforged/neoform/runtime/engine/NeoFormEngine.java index 48762ad..71a00b6 100644 --- a/src/main/java/net/neoforged/neoform/runtime/engine/NeoFormEngine.java +++ b/src/main/java/net/neoforged/neoform/runtime/engine/NeoFormEngine.java @@ -8,6 +8,7 @@ import net.neoforged.neoform.runtime.actions.ExternalJavaToolAction; import net.neoforged.neoform.runtime.actions.InjectFromZipFileSource; import net.neoforged.neoform.runtime.actions.InjectZipContentAction; +import net.neoforged.neoform.runtime.actions.CreateMinecraftModJarAction; import net.neoforged.neoform.runtime.actions.MergeWithSourcesAction; import net.neoforged.neoform.runtime.actions.PatchActionFactory; import net.neoforged.neoform.runtime.actions.RecompileSourcesAction; @@ -202,7 +203,17 @@ public void loadNeoFormProcess(NeoFormDistConfig distConfig) { // The split-off resources must also be made available. The steps are not consistently named across dists if (graph.hasOutput("stripClient", "resourcesOutput")) { - graph.setResult("clientResources", graph.getRequiredOutput("stripClient", "resourcesOutput")); + var resourceOutput = graph.getRequiredOutput("stripClient", "resourcesOutput"); + graph.setResult("clientResources", resourceOutput); + + // In NeoForm versions that use the old process, we manually created nodes that merge the resources back into the jar, and create the neoforge.mods.toml + createMinecraftModJar(distConfig.minecraftVersion(), "minecraftModJar", compiledOutput.asInput(), resourceOutput.asInput()); + createMinecraftModJar(distConfig.minecraftVersion(), "minecraftModJarWithSources", sourcesAndCompiledOutput.asInput(), resourceOutput.asInput()); + } else { + // In newer versions of the process where resources aren't stripped, we're likely to also have a neoforge.mods.toml already + // Then we just alias the outputs + graph.setResult("minecraftModJar", compiledOutput); + graph.setResult("minecraftModJarWithSources", sourcesAndCompiledOutput); } if (graph.hasOutput("stripServer", "resourcesOutput")) { graph.setResult("serverResources", graph.getRequiredOutput("stripServer", "resourcesOutput")); @@ -486,6 +497,19 @@ private void createDownloadFromVersionManifest(ExecutionNodeBuilder builder, Str builder.action(new DownloadFromVersionManifestAction(artifactManager, manifestEntry)); } + private void createMinecraftModJar(String minecraftVersion, String withResourcesId, NodeInput classesInput, @Nullable NodeInput resourceInput) { + var builder = graph.nodeBuilder(withResourcesId); + builder.input("classes", classesInput); + if (resourceInput != null) { + builder.input("resources", resourceInput); + } + builder.action(new CreateMinecraftModJarAction(minecraftVersion)); + var output = builder.output("output", NodeOutputType.JAR, "Result of " + classesInput.getId() + " with Minecraft resources added into the same jar."); + builder.build(); + + graph.setResult(withResourcesId, output); + } + private void triggerAndWait(Collection nodes) throws InterruptedException { record Pair(ExecutionNode node, CompletableFuture future) { } diff --git a/src/main/java/net/neoforged/neoform/runtime/graph/ExecutionGraph.java b/src/main/java/net/neoforged/neoform/runtime/graph/ExecutionGraph.java index 6383148..f4018d6 100644 --- a/src/main/java/net/neoforged/neoform/runtime/graph/ExecutionGraph.java +++ b/src/main/java/net/neoforged/neoform/runtime/graph/ExecutionGraph.java @@ -55,7 +55,7 @@ public NodeOutput getResult(String id) { var outputId = matcher.group(2); return getRequiredOutput(step, outputId); } - return null; + throw new IllegalArgumentException("Unknown result: " + id + ". Available results: " + results.keySet()); } public Map getResults() {