From b83f66983a536e84ea0a1017efa90c920d1902ef Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Sun, 28 Jun 2026 23:41:34 +0800 Subject: [PATCH 01/11] mcpforge: independent 1.12.2/MCP plugin --- .gitignore | 2 + LEGACY.md | 26 +- build.gradle | 30 +- legacytest/forge1122/build.gradle | 39 + .../src/main/java/mymod1122/MyMod1122.java | 8 + .../forge1122/src/main/resources/mcmod.info | 16 + .../forge1122/src/main/resources/pack.mcmeta | 6 + legacytest/settings.gradle | 1 + .../generated/MojangRepositoryFilter.java | 2 + .../moddevgradle/boot/McpForgePlugin.java | 14 + .../LegacyForgeLibraryMetadataRule.java | 60 + .../tasks/PopulateForgeGradleMcpCache.java | 102 ++ .../internal/CreateLaunchScriptTask.java | 69 +- .../moddevgradle/internal/ExtractNatives.java | 74 ++ .../internal/JarPostProcessor.java | 25 + .../internal/McpToolchainHooks.java | 31 + .../internal/ModDevArtifactsWorkflow.java | 39 + .../internal/ModDevRunWorkflow.java | 107 +- .../moddevgradle/internal/NeoDevFacade.java | 7 +- .../internal/PrepareRunOrTest.java | 84 +- .../moddevgradle/internal/RunGameTask.java | 22 + .../moddevgradle/internal/RunUtils.java | 57 +- .../moddevgradle/internal/UserDevRunType.java | 9 +- .../internal/utils/FileUtils.java | 104 +- .../nfrtgradle/CreateMinecraftArtifacts.java | 107 ++ .../mcpforge/dsl/McpForgeExtension.java | 64 + .../mcpforge/dsl/McpForgeModdingSettings.java | 23 + .../ExtractDependencyAccessTransformers.java | 92 ++ .../internal/LegacyForgeArtifacts.java | 22 + .../internal/LegacyForgeJarProcessor.java | 1085 +++++++++++++++++ .../LegacyForgeMetadataTransform.java | 127 ++ .../internal/LegacyMetadataTransform.java | 63 + .../mcpforge/internal/Lwjgl2Natives.java | 67 + .../internal/MappingsDisambiguationRule.java | 28 + .../internal/McpForgeModDevPlugin.java | 623 ++++++++++ .../internal/McpMetadataTransform.java | 91 ++ .../mcpforge/internal/MixinCompilerArgs.java | 115 ++ .../mcpforge/internal/MixinExtension.java | 122 ++ .../mcpforge/internal/RemappingTransform.java | 81 ++ .../internal/CreateLaunchScriptTaskTest.java | 66 + .../internal/ExtractNativesTest.java | 92 ++ .../moddevgradle/internal/PrepareRunTest.java | 206 ++++ .../moddevgradle/internal/RunUtilsTest.java | 20 + .../internal/utils/FileUtilsTest.java | 74 ++ .../legacyforge/LegacyModDevPluginTest.java | 154 +++ 45 files changed, 4212 insertions(+), 44 deletions(-) create mode 100644 legacytest/forge1122/build.gradle create mode 100644 legacytest/forge1122/src/main/java/mymod1122/MyMod1122.java create mode 100644 legacytest/forge1122/src/main/resources/mcmod.info create mode 100644 legacytest/forge1122/src/main/resources/pack.mcmeta create mode 100644 src/java8/java/net/neoforged/moddevgradle/boot/McpForgePlugin.java create mode 100644 src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java create mode 100644 src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/ExtractNatives.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/ExtractDependencyAccessTransformers.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeArtifacts.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeMetadataTransform.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyMetadataTransform.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/Lwjgl2Natives.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MappingsDisambiguationRule.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpMetadataTransform.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinCompilerArgs.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinExtension.java create mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/RemappingTransform.java create mode 100644 src/test/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTaskTest.java create mode 100644 src/test/java/net/neoforged/moddevgradle/internal/ExtractNativesTest.java create mode 100644 src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java create mode 100644 src/test/java/net/neoforged/moddevgradle/internal/utils/FileUtilsTest.java diff --git a/.gitignore b/.gitignore index 4f41d45d..55d7972b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .gradle +.gradle-user-home/ build/ +run/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/LEGACY.md b/LEGACY.md index e7fab58f..1ae3a470 100644 --- a/LEGACY.md +++ b/LEGACY.md @@ -1,6 +1,6 @@ # ModDevGradle Legacy Forge Plugin ModDevGradle has a secondary plugin (ID: `net.neoforged.moddev.legacyforge`, released alongside the normal plugin with the same version) -that adds support for developing mods against MinecraftForge and Vanilla Minecraft versions 1.17 up to 1.20.1. +that adds support for developing mods against MinecraftForge and Vanilla Minecraft versions 1.12.2 up to 1.20.1. The legacy plugin is an "addon" plugin, meaning it operates on top of the normal plugin. This means that the APIs normally used are also available when using the legacy plugin. @@ -41,6 +41,30 @@ legacyForge { } ``` +For Minecraft 1.12.2, use a Forge version such as: + +```groovy +legacyForge { + version = "1.12.2-14.23.5.2860" +} +``` + +MDG will use Forge's `userdev3` artifact for 1.12.2. This requires an NFRT build with legacy MCP support +(`--mcp-mappings` and the legacy MCP mapping result IDs). Until that NFRT release is the MDG default, set +`neoForge.neoFormRuntime.version` to a compatible published version or substitute a local NFRT legacy build. + +NFRT's 1.12.2 legacy MCP pipeline uses `de.oceanlabs.mcp:mcp_stable:39-1.12@zip` by default; if you need a +different MCP CSV mapping zip, configure it explicitly: + +```groovy +legacyForge { + enable { + forgeVersion = "1.12.2-14.23.5.2860" + mcpMappings = "de.oceanlabs.mcp:mcp_stable:39-1.12@zip" + } +} +``` + ## Reobfuscating artifacts Forge used SRG mappings as intermediary mappings in 1.20.1 and below. While your mod is developed against the mappings provided by Mojang (known as official mappings), you need to reobfuscate it to SRG mappings for it to work in production. diff --git a/build.gradle b/build.gradle index 0ac18c2b..dc3d5c45 100644 --- a/build.gradle +++ b/build.gradle @@ -71,9 +71,12 @@ sourceSets { runtimeClasspath += java8.output } legacy + mcpforge test { compileClasspath += legacy.output runtimeClasspath += legacy.output + compileClasspath += mcpforge.output + runtimeClasspath += mcpforge.output } } @@ -83,6 +86,7 @@ configurations { // Place shaded dependencies into `compileOnly` so that they do not leak into our publications' dependencies. compileOnly.extendsFrom shaded legacyCompileOnly.extendsFrom shaded + mcpforgeCompileOnly.extendsFrom shaded testCompileOnly.extendsFrom shaded testRuntimeOnly.extendsFrom shaded shadowRuntimeElements { @@ -113,6 +117,8 @@ dependencies { compileOnly "com.intellij:annotations:9.0.4" testCompileOnly "com.intellij:annotations:9.0.4" shaded "com.google.code.gson:gson:2.11.0" + shaded "org.ow2.asm:asm-commons:9.7.1" + shaded "org.tukaani:xz:1.10" implementation "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext:1.2" shaded "net.neoforged:EclipseLaunchConfigs:0.1.11" shaded "net.neoforged:VscLaunchConfigs:1.0.8" @@ -134,6 +140,11 @@ dependencies { legacyImplementation(sourceSets.main.output) legacyImplementation(sourceSets.java8.output) legacyImplementation gradleApi() + + mcpforgeImplementation(sourceSets.main.output) + mcpforgeImplementation(sourceSets.legacy.output) + mcpforgeImplementation(sourceSets.java8.output) + mcpforgeImplementation gradleApi() } java { @@ -147,6 +158,7 @@ jar { archiveClassifier = 'slim' from sourceSets.java8.output from sourceSets.legacy.output + from sourceSets.mcpforge.output } shadowJar { @@ -154,6 +166,7 @@ shadowJar { from sourceSets.java8.output from sourceSets.legacy.output + from sourceSets.mcpforge.output configurations = [project.configurations.shaded] enableRelocation = true @@ -164,8 +177,9 @@ assemble.dependsOn shadowJar tasks.named("compileJava8Java").configure { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(17) } + options.release = 8 } javadoc { @@ -205,7 +219,7 @@ gradlePlugin { id = 'net.neoforged.moddev.legacyforge' implementationClass = 'net.neoforged.moddevgradle.boot.LegacyForgeModDevPlugin' displayName = "Mod Development Plugin for Legacy Forge" - description = "This plugin helps you create Minecraft mods using the Forge platform, up to 1.20.1" + description = "This plugin helps you create Minecraft mods using the Forge platform, from 1.12.2 up to 1.20.1" tags = ["minecraft", "neoforge", "forge", "java", "mod"] } repositories { @@ -219,9 +233,16 @@ gradlePlugin { id = 'net.neoforged.moddev.legacyforge.repositories' implementationClass = 'net.neoforged.moddevgradle.boot.LegacyRepositoriesPlugin' displayName = "Mod Development Repositories Plugin for Legacy Forge" - description = "This plugin adds the repositories needed for developing Minecraft mods using the Forge platform, up to 1.20.1. It is applied automatically by the legacyforge plugin, but can be applied manually in settings.gradle to make use of Gradle dependency management." + description = "This plugin adds the repositories needed for developing Minecraft mods using the Forge platform, from 1.12.2 up to 1.20.1. It is applied automatically by the legacyforge plugin, but can be applied manually in settings.gradle to make use of Gradle dependency management." tags = ["minecraft", "neoforge", "forge", "java", "mod"] } + mcpforge { + id = 'net.neoforged.moddev.mcpforge' + implementationClass = 'net.neoforged.moddevgradle.boot.McpForgePlugin' + displayName = "Mod Development Plugin for Legacy Forge 1.12.2 (MCP)" + description = "This plugin helps you create Minecraft mods using the Forge platform for 1.12.2 (MCP mappings). It is the isolated home for the 1.12.2/MCP-specific toolchain, distinct from legacyforge." + tags = ["minecraft", "forge", "java", "mod", "1.12.2", "mcp"] + } } } @@ -337,6 +358,9 @@ abstract class GenerateRepoFilter extends DefaultTask { artifacts.add(new Artifact(location[0], location[1])) } } + // Required by Forge 1.12.2 and still hosted on Mojang's libraries maven, but not + // referenced by Mojang's vanilla version manifests. + artifacts.add(new Artifact('lzma', 'lzma')) final artifactList = artifacts.toList() Collections.sort(artifactList) final clazz = """ diff --git a/legacytest/forge1122/build.gradle b/legacytest/forge1122/build.gradle new file mode 100644 index 00000000..619db5d2 --- /dev/null +++ b/legacytest/forge1122/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'net.neoforged.moddev.legacyforge' +} + +repositories { + // The Forge 1.12.2 smoke can use a locally published NFRT build until a compatible release is default. + mavenLocal() +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} + +neoFormRuntime { + version = '2.0.19-legacy' +} + +legacyForge { + enable { + forgeVersion = '1.12.2-14.23.5.2860' + mcpMappings = 'de.oceanlabs.mcp:mcp_stable:39-1.12@zip' + // Recompilation now works end-to-end: NFRT runs ForgeFlower/MCPCleanup/recompile, with the legacy + // Java-8 toolchain auto-provisioned by Gradle (Azul Zulu on aarch64) and the legacy Maven repositories + // (Maven Central, Mojang libraries, Forge Maven) passed to NFRT via CreateMinecraftArtifacts. + disableRecompilation = false + } + runs { + client { + client() + } + } + mods { + mymod1122 { + sourceSet(sourceSets.main) + } + } +} diff --git a/legacytest/forge1122/src/main/java/mymod1122/MyMod1122.java b/legacytest/forge1122/src/main/java/mymod1122/MyMod1122.java new file mode 100644 index 00000000..5da85bba --- /dev/null +++ b/legacytest/forge1122/src/main/java/mymod1122/MyMod1122.java @@ -0,0 +1,8 @@ +package mymod1122; + +import net.minecraftforge.fml.common.Mod; + +@Mod(modid = MyMod1122.MOD_ID, name = "Legacy 1.12.2 Smoke Test", version = "1.0.0") +public class MyMod1122 { + public static final String MOD_ID = "mymod1122"; +} diff --git a/legacytest/forge1122/src/main/resources/mcmod.info b/legacytest/forge1122/src/main/resources/mcmod.info new file mode 100644 index 00000000..88fa5c4c --- /dev/null +++ b/legacytest/forge1122/src/main/resources/mcmod.info @@ -0,0 +1,16 @@ +[ + { + "modid": "mymod1122", + "name": "Legacy 1.12.2 Smoke Test", + "description": "Minimal Forge 1.12.2 fixture for ModDevGradle.", + "version": "1.0.0", + "mcversion": "1.12.2", + "url": "", + "updateUrl": "", + "authorList": [], + "credits": "", + "logoFile": "", + "screenshots": [], + "dependencies": [] + } +] diff --git a/legacytest/forge1122/src/main/resources/pack.mcmeta b/legacytest/forge1122/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..c7664ada --- /dev/null +++ b/legacytest/forge1122/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 3, + "description": "Legacy 1.12.2 smoke test resources" + } +} diff --git a/legacytest/settings.gradle b/legacytest/settings.gradle index 912cdaf9..a61995d8 100644 --- a/legacytest/settings.gradle +++ b/legacytest/settings.gradle @@ -6,5 +6,6 @@ plugins { includeBuild '..' include 'forge' +include 'forge1122' include 'forgedownstream' include 'nonmc' diff --git a/src/generated/java/net/neoforged/moddevgradle/internal/generated/MojangRepositoryFilter.java b/src/generated/java/net/neoforged/moddevgradle/internal/generated/MojangRepositoryFilter.java index 37aa5259..1d7b031d 100644 --- a/src/generated/java/net/neoforged/moddevgradle/internal/generated/MojangRepositoryFilter.java +++ b/src/generated/java/net/neoforged/moddevgradle/internal/generated/MojangRepositoryFilter.java @@ -58,6 +58,7 @@ public static void filter(org.gradle.api.artifacts.repositories.RepositoryConten filter.includeModule("io.netty", "netty-transport-native-unix-common"); filter.includeModule("it.unimi.dsi", "fastutil"); filter.includeModule("java3d", "vecmath"); + filter.includeModule("lzma", "lzma"); filter.includeModule("net.java.dev.jna", "jna"); filter.includeModule("net.java.dev.jna", "jna-platform"); filter.includeModule("net.java.dev.jna", "platform"); @@ -92,6 +93,7 @@ public static void filter(org.gradle.api.artifacts.repositories.RepositoryConten filter.includeModule("org.lwjgl.lwjgl", "lwjgl"); filter.includeModule("org.lwjgl.lwjgl", "lwjgl-platform"); filter.includeModule("org.lwjgl.lwjgl", "lwjgl_util"); + filter.includeModule("org.lwjgl.lwjgl", "parent"); filter.includeModule("org.lz4", "lz4-java"); filter.includeModule("org.ow2.asm", "asm"); filter.includeModule("org.ow2.asm", "asm-all"); diff --git a/src/java8/java/net/neoforged/moddevgradle/boot/McpForgePlugin.java b/src/java8/java/net/neoforged/moddevgradle/boot/McpForgePlugin.java new file mode 100644 index 00000000..14ddd5bd --- /dev/null +++ b/src/java8/java/net/neoforged/moddevgradle/boot/McpForgePlugin.java @@ -0,0 +1,14 @@ +package net.neoforged.moddevgradle.boot; + +import org.gradle.api.Project; + +/** + * Boot trampoline for the {@code net.neoforged.moddev.mcpforge} plugin (the isolated 1.12.2/MCP toolchain). + * Mirrors {@link LegacyForgeModDevPlugin}. Kept in the java8 source set so it can be loaded by the + * bootstrap classloader before the main plugin classpath is wired up. + */ +public class McpForgePlugin extends TrampolinePlugin { + public McpForgePlugin() { + super("net.neoforged.moddevgradle.mcpforge.internal.McpForgeModDevPlugin"); + } +} diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java new file mode 100644 index 00000000..42251150 --- /dev/null +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java @@ -0,0 +1,60 @@ +package net.neoforged.moddevgradle.legacyforge.internal; + +import java.util.ArrayList; +import org.gradle.api.artifacts.CacheableRule; +import org.gradle.api.artifacts.ComponentMetadataContext; +import org.gradle.api.artifacts.ComponentMetadataRule; +import org.gradle.api.artifacts.DirectDependenciesMetadata; +import org.gradle.api.artifacts.DirectDependencyMetadata; + +/** + * Normalizes old Forge library metadata that predates today's Maven Central coordinates or only + * exists as jar-only artifacts on Forge's Maven. + */ +@CacheableRule +public class LegacyForgeLibraryMetadataRule implements ComponentMetadataRule { + @Override + public void execute(ComponentMetadataContext context) { + var id = context.getDetails().getId(); + if (!id.getVersion().equals("1.12.2-14.23.5.2860")) { + return; + } + + context.getDetails().allVariants(variant -> variant.withDependencies(dependencies -> { + removeMatching(dependencies, dependency -> { + var group = dependency.getGroup(); + var name = dependency.getName(); + return group.equals("org.scala-lang.plugins") + || group.equals("org.scala-lang") && name.equals("scala-actors-migration_2.11"); + }); + + replaceDependency(dependencies, "org.scala-lang:scala-parser-combinators_2.11:1.0.1", + "org.scala-lang.modules:scala-parser-combinators_2.11:1.0.1"); + replaceDependency(dependencies, "org.scala-lang:scala-swing_2.11:1.0.1", + "org.scala-lang.modules:scala-swing_2.11:1.0.1"); + replaceDependency(dependencies, "org.scala-lang:scala-xml_2.11:1.0.2", + "org.scala-lang.modules:scala-xml_2.11:1.0.2"); + })); + } + + private static void replaceDependency(DirectDependenciesMetadata dependencies, + String oldNotation, + String newNotation) { + var oldParts = oldNotation.split(":"); + removeMatching(dependencies, dependency -> dependency.getGroup().equals(oldParts[0]) + && dependency.getName().equals(oldParts[1]) + && dependency.getVersionConstraint().getRequiredVersion().equals(oldParts[2])); + dependencies.add(newNotation); + } + + private static void removeMatching(DirectDependenciesMetadata dependencies, + java.util.function.Predicate predicate) { + var toRemove = new ArrayList(); + for (var dependency : dependencies) { + if (predicate.test(dependency)) { + toRemove.add(dependency); + } + } + dependencies.removeAll(toRemove); + } +} diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java new file mode 100644 index 00000000..27d1b2f1 --- /dev/null +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java @@ -0,0 +1,102 @@ +package net.neoforged.moddevgradle.legacyforge.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.Optional; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Populates the ForgeGradle-2.x MCP cache directory with the MCP data that ModDevGradle produced, so that legacy + * tooling and build scripts that hardcode the ForgeGradle-2 cache layout continue to find the mappings. + *

+ * ForgeGradle-2 stores MCP data under {@code ~/.gradle/caches/minecraft/de/oceanlabs/mcp///} (the CSV + * files) and the SRG mapping files under {@code ...////srgs/}. ModDevGradle instead + * exposes the same data as build artifacts (requested via {@code createMinecraftArtifacts}); this task mirrors them + * into the ForgeGradle-2 locations for compatibility. + *

+ * This only covers the data ModDevGradle has available: the {@code methods.csv}/{@code fields.csv}/{@code params.csv} + * (from the CSV mapping zip), {@code srg-mcp.srg} (= SRG->MCP) and {@code mcp-srg.srg} (= MCP->SRG). The + * notch-targeted SRG files ({@code notch-srg.srg} etc.) that ForgeGradle-2 also generates are intentionally omitted, + * as production-name obfuscation for 1.12.2 targets SRG (not notch) in this toolchain. + */ +public abstract class PopulateForgeGradleMcpCache extends DefaultTask { + + /** Gradle dependency notation of the legacy MCP mapping zip, e.g. {@code de.oceanlabs.mcp:mcp_stable:39-1.12@zip}. */ + @Input + public abstract Property getMcpMappings(); + + /** The Minecraft version, e.g. {@code 1.12.2}. */ + @Input + public abstract Property getMinecraftVersion(); + + /** + * The ForgeGradle-2 cache base directory to populate, i.e. + * {@code /caches/minecraft/de/oceanlabs/mcp//}. Resolved at configuration time so + * the task remains configuration-cache compatible. + */ + @Input + public abstract Property getCacheBaseDirectory(); + + /** The MCP CSV mapping zip (SRG->MCP CSVs), as produced by NFRT's {@code csvMapping} result. */ + @InputFile + public abstract RegularFileProperty getCsvMappings(); + + /** The SRG->MCP SRG mapping file (NFRT {@code intermediaryToNamedMapping}), mirrors {@code srgs/srg-mcp.srg}. */ + @InputFile + public abstract RegularFileProperty getSrgToMcpMappings(); + + /** The MCP->SRG mapping file (NFRT {@code namedToIntermediaryMapping}), mirrors {@code srgs/mcp-srg.srg}. */ + @InputFile + public abstract RegularFileProperty getMcpToSrgMappings(); + + /** The notch->SRG mapping file (NFRT {@code notchToIntermediaryMapping}), mirrors {@code srgs/notch-srg.srg}. */ + @InputFile + @org.gradle.api.tasks.Optional + public abstract RegularFileProperty getNotchToSrgMappings(); + + @TaskAction + public void populate() throws IOException { + var minecraftVersion = getMinecraftVersion().get(); + var cacheBase = Path.of(getCacheBaseDirectory().get()); + + // CSV files live directly under the version directory + extractCsvs(getCsvMappings().get().getAsFile().toPath(), cacheBase); + + // SRG mapping files live under //srgs/ + var srgsDir = cacheBase.resolve(minecraftVersion).resolve("srgs"); + Files.createDirectories(srgsDir); + Files.copy(getSrgToMcpMappings().get().getAsFile().toPath(), srgsDir.resolve("srg-mcp.srg"), StandardCopyOption.REPLACE_EXISTING); + Files.copy(getMcpToSrgMappings().get().getAsFile().toPath(), srgsDir.resolve("mcp-srg.srg"), StandardCopyOption.REPLACE_EXISTING); + if (getNotchToSrgMappings().isPresent()) { + Files.copy(getNotchToSrgMappings().get().getAsFile().toPath(), srgsDir.resolve("notch-srg.srg"), StandardCopyOption.REPLACE_EXISTING); + } + + getLogger().lifecycle("Populated ForgeGradle-2 MCP cache at {}", cacheBase); + } + + private static void extractCsvs(Path mappingsZip, Path targetDir) throws IOException { + Files.createDirectories(targetDir); + try (var zip = new ZipFile(mappingsZip.toFile())) { + for (String csv : new String[]{"methods.csv", "fields.csv", "params.csv"}) { + var entry = zip.getEntry(csv); + if (entry == null) { + continue; + } + try (InputStream in = zip.getInputStream(entry)) { + Files.copy(in, targetDir.resolve(csv), StandardCopyOption.REPLACE_EXISTING); + } + } + } + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java b/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java index 969fe53d..301fe48e 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.inject.Inject; import net.neoforged.moddevgradle.dsl.RunModel; @@ -50,6 +51,9 @@ abstract class CreateLaunchScriptTask extends DefaultTask { @InputFile abstract Property getProgramArgsFile(); + @InputFile + abstract Property getEnvironmentFile(); + /** * This argument file is only used by the launch shell-scripts. */ @@ -103,22 +107,44 @@ public void createScripts() throws IOException { return; } - var javaCommand = new ArrayList(); - javaCommand.add(getJavaExecutable().get()); - javaCommand.add("@" + getClasspathArgsFile().get().getAsFile().getAbsolutePath()); - javaCommand.add("@" + getVmArgsFile().get()); - javaCommand.add(getModFolders().get().getArgument()); - javaCommand.add(RunUtils.DEV_LAUNCH_MAIN_CLASS); - javaCommand.add("@" + getProgramArgsFile().get()); + var javaCommand = createJavaCommand( + getJavaExecutable().get(), + getClasspathArgsFile().get().getAsFile(), + new File(getVmArgsFile().get()), + getModFolders().get().getArgument(), + new File(getProgramArgsFile().get())); var os = OperatingSystem.current(); + var environment = getMergedEnvironment(); if (os == OperatingSystem.WINDOWS) { - writeLaunchScriptForWindows(javaCommand); + writeLaunchScriptForWindows(javaCommand, environment); } else { - writeLaunchScriptForUnix(javaCommand); + writeLaunchScriptForUnix(javaCommand, environment); } } + private Map getMergedEnvironment() throws IOException { + var environment = new java.util.LinkedHashMap<>(RunUtils.loadEnvironmentFile(new File(getEnvironmentFile().get()))); + environment.putAll(getEnvironment().get()); + return environment; + } + + private static List createJavaCommand( + String javaExecutable, + File classpathArgsFile, + File vmArgsFile, + String modFoldersArgument, + File programArgsFile) throws IOException { + var javaCommand = new ArrayList(); + javaCommand.add(javaExecutable); + javaCommand.addAll(RunUtils.readArgFile(classpathArgsFile)); + javaCommand.addAll(RunUtils.readArgFile(vmArgsFile)); + javaCommand.add(modFoldersArgument); + javaCommand.add(RunUtils.DEV_LAUNCH_MAIN_CLASS); + javaCommand.addAll(RunUtils.readArgFile(programArgsFile)); + return javaCommand; + } + /** * Writes a JVM argument file that would launch the JVM with the same classpath that * Gradle would launch it with. @@ -143,7 +169,7 @@ private void writeClasspathArguments() throws IOException { StringUtils.getNativeCharset()); } - private void writeLaunchScriptForWindows(List javaCommand) throws IOException { + private void writeLaunchScriptForWindows(List javaCommand, Map environment) throws IOException { var lines = new ArrayList(); Collections.addAll(lines, "@echo off", @@ -154,8 +180,8 @@ private void writeLaunchScriptForWindows(List javaCommand) throws IOExce // Switch encoding to Unicode, otherwise the next "cd" might not work with special chars "chcp 65001>nul"); - for (var entry : getEnvironment().get().entrySet()) { - lines.add("set " + escapeBatchScriptArg(entry.getKey()) + "=" + escapeBatchScriptArg(entry.getValue())); + for (var entry : environment.entrySet()) { + lines.add(writeWindowsEnvironmentVariable(entry)); } Collections.addAll(lines, @@ -184,10 +210,25 @@ private String escapeBatchScriptArg(String text) { return text; } - private void writeLaunchScriptForUnix(List javaCommand) throws IOException { + private static String writeWindowsEnvironmentVariable(Map.Entry entry) { + return "set \"" + escapeBatchEnvironmentValue(entry.getKey()) + "=" + escapeBatchEnvironmentValue(entry.getValue()) + "\""; + } + + private static String escapeBatchEnvironmentValue(String text) { + return text + .replace("^", "^^") + .replace("&", "^&") + .replace("|", "^|") + .replace("<", "^<") + .replace(">", "^>") + .replace("%", "%%") + .replace("\"", "^\""); + } + + private void writeLaunchScriptForUnix(List javaCommand, Map environment) throws IOException { var lines = new ArrayList(); - for (var entry : getEnvironment().get().entrySet()) { + for (var entry : environment.entrySet()) { lines.add("export " + escapeShellArg(entry.getKey()) + "=" + escapeShellArg(entry.getValue())); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ExtractNatives.java b/src/main/java/net/neoforged/moddevgradle/internal/ExtractNatives.java new file mode 100644 index 00000000..998854b9 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/ExtractNatives.java @@ -0,0 +1,74 @@ +package net.neoforged.moddevgradle.internal; + +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ArchiveOperations; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.TaskAction; + +public abstract class ExtractNatives extends DefaultTask { + private final ArchiveOperations archiveOperations; + private final FileSystemOperations fileSystemOperations; + + @Classpath + @InputFiles + @SkipWhenEmpty + public abstract ConfigurableFileCollection getNativeLibraries(); + + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @Input + public abstract Property getEnabledForRun(); + + @Inject + public ExtractNatives(ArchiveOperations archiveOperations, FileSystemOperations fileSystemOperations) { + this.archiveOperations = archiveOperations; + this.fileSystemOperations = fileSystemOperations; + getEnabledForRun().convention(false); + } + + @TaskAction + public void extract() { + if (!getEnabledForRun().get()) { + return; + } + + fileSystemOperations.delete(spec -> spec.delete(getOutputDirectory())); + fileSystemOperations.copy(spec -> { + spec.into(getOutputDirectory()); + spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + for (var nativeLibrary : getNativeLibraries()) { + if (isLwjgl2Arm64MacosNativePatch(nativeLibrary.getName())) { + spec.from(archiveOperations.zipTree(nativeLibrary), copy -> copy.exclude("liblwjgl.dylib")); + } else { + spec.from(archiveOperations.zipTree(nativeLibrary)); + } + } + }); + fileSystemOperations.copy(spec -> { + spec.into(getOutputDirectory()); + for (var nativeLibrary : getNativeLibraries()) { + if (isLwjgl2Arm64MacosNativePatch(nativeLibrary.getName())) { + spec.from(archiveOperations.zipTree(nativeLibrary), copy -> { + copy.include("liblwjgl.dylib", "openal.dylib"); + copy.rename("liblwjgl\\.dylib", "liblwjgl.jnilib"); + }); + } + } + }); + } + + private static boolean isLwjgl2Arm64MacosNativePatch(String fileName) { + return fileName.equals("lwjgl-platform-2.9.4-nightly-20150209-mmachina.2-natives-osx.jar"); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java b/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java new file mode 100644 index 00000000..e9551ada --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java @@ -0,0 +1,25 @@ +package net.neoforged.moddevgradle.internal; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Post-processes a Minecraft jar after NFRT generation, for legacy versions that need additional transformation + * (e.g. 1.12.2 Forge deobfuscation data remapping). + * + *

Registered via {@code CreateMinecraftArtifacts.getJarPostProcessors()}; the MCP plugin registers its + * implementation, the default is an empty list. + */ +@ApiStatus.Internal +@FunctionalInterface +public interface JarPostProcessor { + /** + * Process the generated jar (may be modified in place). + * + * @param srgToMcpMappings the SRG→MCP mapping file, or null if not applicable + */ + void process(Path jar, @Nullable Path srgToMcpMappings) throws IOException; +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java b/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java new file mode 100644 index 00000000..464e7e07 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java @@ -0,0 +1,31 @@ +package net.neoforged.moddevgradle.internal; + +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.dsl.DependencyFactory; +import org.jetbrains.annotations.ApiStatus; + +/** + * SPI hooks for MCP-based legacy Minecraft versions (1.12.2 up to 1.16.5). + * + *

The main workflow looks up a registered instance via the {@code __mcpHooks} project extension. The + * {@code mcpforge} plugin registers a real implementation (e.g. LWJGL2 Apple Silicon native replacement); when no + * MCP plugin is active, {@link #NOOP} is used and these calls are no-ops. + */ +@ApiStatus.Internal +public interface McpToolchainHooks { + McpToolchainHooks NOOP = new McpToolchainHooks() {}; + + String EXTENSION_NAME = "__mcpHooks"; + + /** Configures runtime native libraries for legacy versions (e.g. LWJGL2 Apple Silicon replacement). */ + default void configureRuntimeNatives(Configuration configuration, DependencyFactory dependencyFactory, String minecraftVersion) {} + + /** Configures native library dependencies for legacy versions (natives extraction). */ + default void configureNativeLibraries(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion) {} + + /** Retrieves the hooks registered on the project, or {@link #NOOP} if none. */ + static McpToolchainHooks get(org.gradle.api.Project project) { + var hooks = project.getExtensions().findByName(EXTENSION_NAME); + return hooks instanceof McpToolchainHooks mcp ? mcp : NOOP; + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java index 40b1723d..b93921b0 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java @@ -69,6 +69,31 @@ public static ModDevArtifactsWorkflow create(Project project, Configuration interfaceInjectionData, VersionCapabilitiesInternal versionCapabilities, boolean disableRecompilation) { + return create( + project, + enabledSourceSets, + branding, + extension, + moddingDependencies, + artifactNamingStrategy, + accessTransformers, + interfaceInjectionData, + versionCapabilities, + disableRecompilation, + null); + } + + public static ModDevArtifactsWorkflow create(Project project, + Collection enabledSourceSets, + Branding branding, + ModDevExtension extension, + ModdingDependencies moddingDependencies, + ArtifactNamingStrategy artifactNamingStrategy, + Configuration accessTransformers, + Configuration interfaceInjectionData, + VersionCapabilitiesInternal versionCapabilities, + boolean disableRecompilation, + @Nullable String legacyMcpMappings) { if (project.getExtensions().findByName(EXTENSION_NAME) != null) { throw new InvalidUserCodeException("You cannot enable modding in the same project twice."); } @@ -150,6 +175,18 @@ public static ModDevArtifactsWorkflow create(Project project, task.getParchmentData().from(parchmentData); task.getParchmentEnabled().set(parchment.getEnabled()); task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); + if (legacyMcpMappings != null) { + task.getLegacyMcpMappings().set(legacyMcpMappings); + // Legacy MCP versions (e.g. 1.12.2) depend on libraries that are not available on the NeoForged Maven + // NFRT consults by default: lzma:lzma only lives on Mojang's library repository, vecmath/trove4j on + // Maven Central, and the Scala/JLine/etc. deps on the Forge Maven. Point NFRT at all of them so the + // recompile compile-classpath can be resolved. + task.getAdditionalRepositories().addAll(java.util.List.of( + "https://repo1.maven.org/maven2/", + "https://libraries.minecraft.net/", + "https://maven.minecraftforge.net/" + )); + } Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); @@ -210,6 +247,7 @@ public static ModDevArtifactsWorkflow create(Project project, // Technically, the Minecraft dependencies do not strictly need to be on the classpath because they are pulled from the legacy class path. // However, we do it anyway because this matches production environments, and allows launch proxies such as DevLogin to use Minecraft's libraries. config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); + McpToolchainHooks.get(project).configureRuntimeNatives(config, dependencyFactory, versionCapabilities.minecraftVersion()); }); // Configuration in which we place the required dependencies to develop mods for use in the compile-classpath. @@ -220,6 +258,7 @@ public static ModDevArtifactsWorkflow create(Project project, config.setCanBeConsumed(false); config.getDependencies().addLater(minecraftClassesDependency); config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); + McpToolchainHooks.get(project).configureRuntimeNatives(config, dependencyFactory, versionCapabilities.minecraftVersion()); if (!versionCapabilities.needsNeoForgeInMinecraftJar() && moddingDependencies.neoForgeDependency() != null) { config.getDependencies().add(moddingDependencies.neoForgeDependency()); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java index d6be0f49..4781bb03 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -1,12 +1,15 @@ package net.neoforged.moddevgradle.internal; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import javax.inject.Inject; import net.neoforged.minecraftdependencies.MinecraftDistribution; import net.neoforged.moddevgradle.dsl.InternalModelHelper; import net.neoforged.moddevgradle.dsl.ModModel; @@ -20,6 +23,7 @@ import org.gradle.api.Named; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.Action; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ModuleDependency; import org.gradle.api.attributes.Attribute; @@ -28,6 +32,7 @@ import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.jvm.JvmTestSuite; @@ -60,6 +65,7 @@ public class ModDevRunWorkflow { private final ModuleDependency testFixturesDependency; private final ModuleDependency gameLibrariesDependency; private final Configuration userDevConfigOnly; + private final Map> runTemplateReplacements; /** * @param gameLibrariesDependency A module dependency that represents the library dependencies of the game. @@ -75,12 +81,14 @@ private ModDevRunWorkflow(Project project, @Nullable ModuleDependency testFixturesDependency, ModuleDependency gameLibrariesDependency, DomainObjectCollection runs, - VersionCapabilitiesInternal versionCapabilities) { + VersionCapabilitiesInternal versionCapabilities, + Map> runTemplateReplacements) { this.project = project; this.branding = branding; this.modulePathDependency = modulePathDependency; this.testFixturesDependency = testFixturesDependency; this.gameLibrariesDependency = gameLibrariesDependency; + this.runTemplateReplacements = runTemplateReplacements; var configurations = project.getConfigurations(); @@ -138,7 +146,8 @@ private ModDevRunWorkflow(Project project, }, configureLegacyClasspath, artifactsWorkflow.downloadAssets().flatMap(DownloadAssets::getAssetPropertiesFile), - versionCapabilities); + versionCapabilities, + runTemplateReplacements); } private static void forbidAdditionalRuntimeDependencies(Configuration configuration, VersionCapabilitiesInternal versionCapabilities) { @@ -154,6 +163,10 @@ private static void forbidAdditionalRuntimeDependencies(Configuration configurat }); } + private static boolean isClientRunType(String runType) { + return runType.equals("client") || runType.equals("data") || runType.equals("clientData"); + } + public static ModDevRunWorkflow get(Project project) { var workflow = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevRunWorkflow.class); if (workflow == null) { @@ -166,6 +179,14 @@ public static ModDevRunWorkflow create(Project project, Branding branding, ModDevArtifactsWorkflow artifactsWorkflow, DomainObjectCollection runs) { + return create(project, branding, artifactsWorkflow, runs, Map.of()); + } + + public static ModDevRunWorkflow create(Project project, + Branding branding, + ModDevArtifactsWorkflow artifactsWorkflow, + DomainObjectCollection runs, + Map> runTemplateReplacements) { var dependencies = artifactsWorkflow.dependencies(); var versionCapabilites = artifactsWorkflow.versionCapabilities(); @@ -178,7 +199,8 @@ public static ModDevRunWorkflow create(Project project, dependencies.testFixturesDependency(), dependencies.gameLibrariesDependency(), runs, - versionCapabilites); + versionCapabilites, + runTemplateReplacements); project.getExtensions().add(EXTENSION_NAME, workflow); @@ -228,7 +250,8 @@ public void configureTesting(Provider testedMod, Provider configureModulePath, Consumer configureLegacyClasspath, Provider assetPropertiesFile, - VersionCapabilitiesInternal versionCapabilities) { + VersionCapabilitiesInternal versionCapabilities, + Map> runTemplateReplacements) { var dependencyFactory = project.getDependencyFactory(); var ideIntegration = IdeIntegration.of(project, branding); @@ -285,6 +309,7 @@ public static void setupRuns( assetPropertiesFile, devLaunchConfig, versionCapabilities, + runTemplateReplacements, createLaunchScriptsTask); prepareRunTasks.put(run, prepareRunTask); }); @@ -308,6 +333,7 @@ private static TaskProvider setupRunInGradle( Provider assetPropertiesFile, Configuration devLaunchConfig, VersionCapabilitiesInternal versionCapabilities, + Map> runTemplateReplacements, TaskProvider createLaunchScriptsTask) { var ideIntegration = IdeIntegration.of(project, branding); var configurations = project.getConfigurations(); @@ -341,13 +367,14 @@ private static TaskProvider setupRunInGradle( spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); spec.attributes(attributes -> { attributes.attributeProvider(MinecraftDistribution.ATTRIBUTE, type.map(t -> { - var name = t.equals("client") || t.equals("data") || t.equals("clientData") ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER; + var name = isClientRunType(t) ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER; return project.getObjects().named(MinecraftDistribution.class, name); })); setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); }); configureLegacyClasspath.accept(spec); spec.extendsFrom(run.getAdditionalRuntimeClasspathConfiguration()); + McpToolchainHooks.get(project).configureRuntimeNatives(spec, project.getDependencyFactory(), versionCapabilities.minecraftVersion()); }); var writeLcpTask = tasks.register(InternalModelHelper.nameOfRun(run, "write", "legacyClasspath"), WriteLegacyClasspath.class, writeLcp -> { @@ -363,6 +390,32 @@ private static TaskProvider setupRunInGradle( legacyClasspathFile = null; } + var nativeLibraries = configurations.create(InternalModelHelper.nameOfRun(run, "", "nativeLibraries"), spec -> { + spec.setDescription("Contains native libraries that should be extracted for run " + run.getName() + "."); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); + spec.attributes(attributes -> { + setNamedAttribute(project, attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT); + setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); + }); + if (versionCapabilities.legacyClasspath()) { + spec.getDependencies().add(project.getDependencyFactory() + .create("net.neoforged:minecraft-dependencies:" + versionCapabilities.minecraftVersion()) + .capabilities(caps -> caps.requireCapability("net.neoforged:minecraft-dependencies-natives"))); + McpToolchainHooks.get(project).configureNativeLibraries(spec, project.getDependencyFactory(), versionCapabilities.minecraftVersion()); + } + }); + var nativesDirectory = run.getGameDirectory().map(dir -> dir.dir("natives")); + var extractNativesTask = tasks.register(InternalModelHelper.nameOfRun(run, "extract", "natives"), ExtractNatives.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Extracts native libraries for the " + run.getName() + " Minecraft run."); + task.getEnabledForRun().set(type.map(ModDevRunWorkflow::isClientRunType)); + task.onlyIf(ignored -> task.getEnabledForRun().get()); + task.getNativeLibraries().from(nativeLibraries); + task.getOutputDirectory().set(nativesDirectory); + }); + var prepareRunTask = tasks.register(InternalModelHelper.nameOfRun(run, "prepare", "run"), PrepareRun.class, task -> { task.setGroup(branding.internalTaskGroup()); task.setDescription("Prepares all files needed to launch the " + run.getName() + " Minecraft run."); @@ -370,6 +423,7 @@ private static TaskProvider setupRunInGradle( task.getGameDirectory().set(run.getGameDirectory()); task.getVmArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.VMARGS)); task.getProgramArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.PROGRAMARGS)); + task.getEnvironmentFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.ENVIRONMENT)); task.getLog4jConfigFileOverride().set(run.getLoggingConfigFile()); task.getLog4jConfigFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.LOG4J_CONFIG)); task.getRunType().set(run.getType()); @@ -383,12 +437,16 @@ private static TaskProvider setupRunInGradle( props = new HashMap<>(props); return props; })); + task.getUserEnvironment().set(run.getEnvironment()); + task.getRunTemplateReplacements().set(project.provider(() -> runTemplateReplacements.entrySet().stream() + .collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get())))); task.getMainClass().set(run.getMainClass()); task.getProgramArguments().set(run.getProgramArguments()); task.getJvmArguments().set(run.getJvmArguments()); task.getGameLogLevel().set(run.getLogLevel()); task.getDevLogin().set(run.getDevLogin()); task.getVersionCapabilities().set(versionCapabilities); + task.dependsOn(extractNativesTask); }); ideIntegration.runTaskOnProjectSync(prepareRunTask); @@ -404,6 +462,7 @@ private static TaskProvider setupRunInGradle( task.getClasspathArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.CLASSPATH)); task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath())); + task.getEnvironmentFile().set(prepareRunTask.get().getEnvironmentFile().map(d -> d.getAsFile().getAbsolutePath())); task.getEnvironment().set(run.getEnvironment()); task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); }); @@ -413,18 +472,20 @@ private static TaskProvider setupRunInGradle( task.setGroup(branding.publicTaskGroup()); task.setDescription("Runs the " + run.getName() + " Minecraft run configuration."); - // Launch with the Java version used in the project + // Launch with the Java version used in the project (as a convention, so plugins like the 1.12.2 + // mcpforge toolchain — which must run launchwrapper on Java 8 — can override it via set()). var toolchainService = ExtensionUtils.findExtension(project, "javaToolchains", JavaToolchainService.class); - task.getJavaLauncher().set(toolchainService.launcherFor(spec -> spec.getLanguageVersion().set(javaExtension.getToolchain().getLanguageVersion()))); + task.getJavaLauncher().convention(toolchainService.launcherFor(spec -> spec.getLanguageVersion().set(javaExtension.getToolchain().getLanguageVersion()))); // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. // This records a dependency on compiling and processing the resources of the source set. task.getClasspathProvider().from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); task.getGameDirectory().set(run.getGameDirectory()); task.getEnvironmentProperty().set(run.getEnvironment()); - task.jvmArgs(RunUtils.getArgFileParameter(prepareRunTask.get().getVmArgsFile().get()).replace("\\", "\\\\")); + task.getEnvironmentFile().set(prepareRunTask.get().getEnvironmentFile()); + task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile()); + task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile()); task.getMainClass().set(RunUtils.DEV_LAUNCH_MAIN_CLASS); - task.args(RunUtils.getArgFileParameter(prepareRunTask.get().getProgramArgsFile().get()).replace("\\", "\\\\")); // Of course we need the arg files to be up-to-date ;) task.dependsOn(prepareRunTask); task.dependsOn(run.getTasksBefore()); @@ -448,7 +509,8 @@ static void setupTestTask(Project project, Consumer configureModulePath, Consumer configureLegacyClasspath, Provider assetPropertiesFile, - VersionCapabilitiesInternal versionCapabilities) { + VersionCapabilitiesInternal versionCapabilities, + Map> runTemplateReplacements) { var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR); var ideIntegration = IdeIntegration.of(project, branding); @@ -496,6 +558,7 @@ static void setupTestTask(Project project, var vmArgsFile = runArgsDir.map(dir -> dir.file("vmArgs.txt")); var programArgsFile = runArgsDir.map(dir -> dir.file("programArgs.txt")); + var environmentFile = runArgsDir.map(dir -> dir.file("environment.properties")); var log4j2ConfigFile = runArgsDir.map(dir -> dir.file("log4j2.xml")); var prepareTask = tasks.register("prepareNeoForgeTestFiles", PrepareTest.class, task -> { task.setGroup(branding.internalTaskGroup()); @@ -503,6 +566,7 @@ static void setupTestTask(Project project, task.getGameDirectory().set(gameDirectory); task.getVmArgsFile().set(vmArgsFile); task.getProgramArgsFile().set(programArgsFile); + task.getEnvironmentFile().set(environmentFile); task.getLog4jConfigFile().set(log4j2ConfigFile); task.getRunTypeTemplatesSource().from(runTemplatesSourceFile); task.getModules().from(neoForgeModDevModules); @@ -510,6 +574,8 @@ static void setupTestTask(Project project, task.getLegacyClasspathFile().set(legacyClasspathFile); } task.getAssetProperties().set(assetPropertiesFile); + task.getRunTemplateReplacements().set(project.provider(() -> runTemplateReplacements.entrySet().stream() + .collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get())))); task.getGameLogLevel().set(Level.INFO); }); @@ -523,6 +589,9 @@ static void setupTestTask(Project project, // file containing the program arguments needed to launch task.systemProperty("fml.junit.argsfile", programArgsFile.get().getAsFile().getAbsolutePath()); task.jvmArgs(RunUtils.getArgFileParameter(vmArgsFile.get())); + var loadEnvironment = project.getObjects().newInstance(LoadPreparedTestEnvironment.class); + loadEnvironment.getEnvironmentFile().set(environmentFile); + task.doFirst("load prepared Minecraft test environment", loadEnvironment); var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod); task.getJvmArgumentProviders().add(modFoldersProvider); @@ -539,4 +608,20 @@ static void setupTestTask(Project project, private static void setNamedAttribute(Project project, AttributeContainer attributes, Attribute attribute, String value) { attributes.attribute(attribute, project.getObjects().named(attribute.getType(), value)); } + + public static abstract class LoadPreparedTestEnvironment implements Action { + @Inject + public LoadPreparedTestEnvironment() {} + + public abstract RegularFileProperty getEnvironmentFile(); + + @Override + public void execute(Task task) { + try { + ((Test) task).environment(RunUtils.loadEnvironmentFile(getEnvironmentFile().get().getAsFile())); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read prepared test environment", e); + } + } + } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java index 9192640d..cb4ccad2 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java @@ -1,5 +1,6 @@ package net.neoforged.moddevgradle.internal; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import net.neoforged.moddevgradle.dsl.ModModel; @@ -41,7 +42,8 @@ public static void setupRuns(Project project, configureModulePath, configureAdditionalClasspath, assetPropertiesFile, - neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest())); + neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest()), + Map.of()); } public static void setupTestTask(Project project, @@ -65,7 +67,8 @@ public static void setupTestTask(Project project, configureModulePath, configureAdditionalClasspath, assetPropertiesFile, - neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest())); + neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest()), + Map.of()); } public static void runTaskOnProjectSync(Project project, Object task) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index 116bb72f..1977a621 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Properties; import java.util.stream.Collectors; import java.util.zip.ZipFile; import net.neoforged.moddevgradle.internal.utils.FileUtils; @@ -56,6 +57,9 @@ abstract class PrepareRunOrTest extends DefaultTask { @OutputFile public abstract RegularFileProperty getProgramArgsFile(); + @OutputFile + public abstract RegularFileProperty getEnvironmentFile(); + /** * A file to use for the {@code log4j2.xml} config file that will be written. * If absent, the standard log4j2.xml file produced by {@link RunUtils#writeLog4j2Configuration} will be used. @@ -99,6 +103,12 @@ abstract class PrepareRunOrTest extends DefaultTask { @Input public abstract MapProperty getSystemProperties(); + @Input + public abstract MapProperty getUserEnvironment(); + + @Input + public abstract MapProperty getRunTemplateReplacements(); + @Input public abstract ListProperty getJvmArguments(); @@ -128,8 +138,11 @@ abstract class PrepareRunOrTest extends DefaultTask { protected PrepareRunOrTest(ProgramArgsFormat programArgsFormat) { this.programArgsFormat = programArgsFormat; + getInputs().property("gameDirectoryPath", getGameDirectory().map(directory -> directory.getAsFile().getAbsolutePath())); getVersionCapabilities().convention(VersionCapabilitiesInternal.latest()); getDevLogin().convention(false); + getUserEnvironment().convention(Map.of()); + getRunTemplateReplacements().convention(Map.of()); } protected abstract UserDevRunType resolveRunType(UserDevConfig userDevConfig); @@ -151,13 +164,20 @@ private List getInterpolatedJvmArgs(UserDevRunType runConfig) { } result.add(RunUtils.escapeJvmArg(arg)); } - if (isClientDistribution() && OperatingSystem.current() == OperatingSystem.MACOS) { + if (isClientDistribution() && OperatingSystem.current() == OperatingSystem.MACOS && !usesLegacyAppleLwjgl2()) { // TODO: it might be more future-proof to source this from the platform args in the MC version json result.add("-XstartOnFirstThread"); } return result; } + private boolean usesLegacyAppleLwjgl2() { + var capabilities = getVersionCapabilities().get(); + var arch = System.getProperty("os.arch"); + return "1.12.2".equals(capabilities.minecraftVersion()) + && ("aarch64".equals(arch) || "arm64".equals(arch)); + } + @TaskAction public void prepareRun() throws IOException { // Make sure the run directory exists @@ -187,6 +207,31 @@ public void prepareRun() throws IOException { writeJvmArguments(runConfig, sysProps); writeProgramArguments(runConfig, mainClass); + writeEnvironment(runConfig); + configureLegacyForgeSplash(runDir); + } + + private void configureLegacyForgeSplash(File runDir) throws IOException { + if (!isClientDistribution() + || OperatingSystem.current() != OperatingSystem.MACOS + || !"1.12.2".equals(getVersionCapabilities().get().minecraftVersion())) { + return; + } + + var splashConfig = runDir.toPath().resolve("config/splash.properties"); + Files.createDirectories(splashConfig.getParent()); + + var properties = new Properties(); + if (Files.isRegularFile(splashConfig)) { + try (var reader = Files.newBufferedReader(splashConfig, StandardCharsets.UTF_8)) { + properties.load(reader); + } + } + properties.setProperty("enabled", "false"); + + try (var writer = Files.newBufferedWriter(splashConfig, StandardCharsets.UTF_8)) { + properties.store(writer, "Splash screen properties"); + } } private UserDevConfig loadUserDevConfig(File userDevFile) { @@ -292,13 +337,9 @@ private void writeProgramArguments(UserDevRunType runConfig, @Nullable String ma } lines.add("# NeoForge Run-Type Program Arguments"); - var assetProperties = DownloadedAssetsReference.loadProperties(getAssetProperties().get().getAsFile()); List args = runConfig.args(); for (String arg : args) { - switch (arg) { - case "{assets_root}" -> arg = Objects.requireNonNull(assetProperties.assetsRoot(), "assets_root"); - case "{asset_index}" -> arg = Objects.requireNonNull(assetProperties.assetIndex(), "asset_index"); - } + arg = interpolateRunTemplateValue(arg); // FML JUnit simply expects one line per argument if (programArgsFormat == ProgramArgsFormat.FML_JUNIT) { @@ -334,6 +375,37 @@ private void writeProgramArguments(UserDevRunType runConfig, @Nullable String ma StandardCharsets.UTF_8); } + private void writeEnvironment(UserDevRunType runConfig) throws IOException { + var environment = new LinkedHashMap(); + for (var entry : runConfig.env().entrySet()) { + environment.put(entry.getKey(), interpolateRunTemplateValue(entry.getValue())); + } + environment.putAll(getUserEnvironment().get()); + + var properties = new Properties(); + properties.putAll(environment); + var destination = getEnvironmentFile().get().getAsFile().toPath(); + Files.createDirectories(destination.getParent()); + try (var out = FileUtils.newSafeFileOutputStream(destination)) { + properties.store(out, "Minecraft run environment"); + } + } + + private String interpolateRunTemplateValue(String value) { + var assetProperties = DownloadedAssetsReference.loadProperties(getAssetProperties().get().getAsFile()); + var replacements = new LinkedHashMap<>(getRunTemplateReplacements().get()); + replacements.put("assets_root", Objects.requireNonNull(assetProperties.assetsRoot(), "assets_root")); + replacements.put("asset_index", Objects.requireNonNull(assetProperties.assetIndex(), "asset_index")); + replacements.put("natives", getGameDirectory().get().dir("natives").getAsFile().getAbsolutePath()); + replacements.put("MC_VERSION", getVersionCapabilities().get().minecraftVersion()); + + for (var entry : replacements.entrySet()) { + value = value.replace("${" + entry.getKey() + "}", entry.getValue()); + value = value.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return value; + } + private static void addSystemProp(String name, String value, List lines) { lines.add(RunUtils.escapeJvmArg("-D" + name + "=" + value)); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java b/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java index 79928fb9..1bc83ba6 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java @@ -6,9 +6,11 @@ import javax.inject.Inject; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.JavaExec; @@ -28,6 +30,15 @@ public abstract class RunGameTask extends JavaExec { @Input public abstract MapProperty getEnvironmentProperty(); + @InputFile + public abstract RegularFileProperty getEnvironmentFile(); + + @InputFile + public abstract RegularFileProperty getVmArgsFile(); + + @InputFile + public abstract RegularFileProperty getProgramArgsFile(); + @Internal public abstract DirectoryProperty getGameDirectory(); @@ -44,10 +55,21 @@ public void exec() { throw new UncheckedIOException("Failed to create run directory", e); } + try { + getEnvironment().putAll(RunUtils.loadEnvironmentFile(getEnvironmentFile().get().getAsFile())); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read prepared run environment", e); + } getEnvironment().putAll(getEnvironmentProperty().get()); classpath(getClasspathProvider()); setWorkingDir(runDir); + try { + setJvmArgs(RunUtils.readArgFile(getVmArgsFile().get().getAsFile())); + setArgs(RunUtils.readArgFile(getProgramArgsFile().get().getAsFile())); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read prepared run argument files", e); + } super.exec(); } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java index c913ce82..c2d9c215 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java @@ -1,13 +1,17 @@ package net.neoforged.moddevgradle.internal; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -157,6 +161,7 @@ public static Provider getLaunchScript(Provider modDevFo public enum RunArgFile { VMARGS("runVmArgs.txt"), PROGRAMARGS("runProgramArgs.txt"), + ENVIRONMENT("runEnvironment.properties"), CLASSPATH("runClasspath.txt"), LOG4J_CONFIG("log4j2.xml"); @@ -171,6 +176,54 @@ public static String getArgFileParameter(RegularFile argFile) { return "@" + argFile.getAsFile().getAbsolutePath(); } + public static Map loadEnvironmentFile(File file) throws IOException { + var properties = new Properties(); + try (var input = new FileInputStream(file)) { + properties.load(input); + } + + var environment = new LinkedHashMap(); + for (var name : properties.stringPropertyNames()) { + environment.put(name, properties.getProperty(name)); + } + return environment; + } + + public static List readArgFile(File file) throws IOException { + var result = new ArrayList(); + for (var line : Files.readAllLines(file.toPath())) { + line = line.strip(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + result.add(unescapeArgFileLine(line)); + } + return result; + } + + private static String unescapeArgFileLine(String line) { + if (line.length() >= 2 && line.startsWith("\"") && line.endsWith("\"")) { + line = line.substring(1, line.length() - 1); + } + var result = new StringBuilder(line.length()); + boolean escaped = false; + for (int i = 0; i < line.length(); i++) { + var c = line.charAt(i); + if (escaped) { + result.append(c); + escaped = false; + } else if (c == '\\') { + escaped = true; + } else { + result.append(c); + } + } + if (escaped) { + result.append('\\'); + } + return result.toString(); + } + public static ModFoldersProvider getGradleModFoldersProvider(Project project, Provider> modsProvider, Provider testedMod) { var modFoldersProvider = project.getObjects().newInstance(ModFoldersProvider.class); modFoldersProvider.getModFolders().set(getModFoldersForGradle(project, modsProvider, testedMod)); @@ -199,9 +252,7 @@ public static Project findSourceSetProject(Project someProject, SourceSet source /** * In the run model, the environment variable "MOD_CLASSES" is set to the gradle output folders by the legacy plugin, - * since MDG itself completely ignores run-type specific environment variables. - * To ensure that in IDE runs, the IDE output folders are used, we replace the MOD_CLASSES environment variable - * explicitly. + * and we replace it with the IDE output folders for IDE runs. */ public static Map replaceModClassesEnv(RunModel model, ModFoldersProvider modFoldersProvider) { var vars = model.getEnvironment().get(); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/UserDevRunType.java b/src/main/java/net/neoforged/moddevgradle/internal/UserDevRunType.java index bb6e54de..8eb67797 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/UserDevRunType.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/UserDevRunType.java @@ -4,4 +4,11 @@ import java.util.Map; public record UserDevRunType(boolean singleInstance, String main, List args, List jvmArgs, - Map env, Map props) {} + Map env, Map props) { + public UserDevRunType { + args = args == null ? List.of() : args; + jvmArgs = jvmArgs == null ? List.of() : jvmArgs; + env = env == null ? Map.of() : env; + props = props == null ? Map.of() : props; + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java index 9f32a71c..021ffda6 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java @@ -14,10 +14,12 @@ import java.nio.file.StandardCopyOption; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.util.HexFormat; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.gradle.api.GradleException; import org.jetbrains.annotations.ApiStatus; @@ -28,6 +30,8 @@ public final class FileUtils { * The maximum number of tries that the system will try to atomically move a file. */ private static final int MAX_TRIES = 2; + /** Package prefix for SpongePowered Mixin/ASM classes that bundler jars (e.g. VoxelMap) ship but shouldn't expose. */ + private static final String SPONGE_PREFIX = "org/spongepowered/asm/"; private FileUtils() {} @@ -71,6 +75,71 @@ public static String hashFile(File file, String algorithm) { } } + public static void stripJarSignatures(Path jar) throws IOException { + rewriteJar(jar, entry -> !entry.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME) && !isJarSignatureFile(entry.getName()), unsignedManifest(readManifest(jar))); + } + + public static void removeJarEntries(Path jar, Set entryNames) throws IOException { + rewriteJar(jar, entry -> !entryNames.contains(entry.getName()), readManifest(jar)); + } + + private static Manifest readManifest(Path jar) throws IOException { + try (var input = new JarFile(jar.toFile(), false)) { + return input.getManifest(); + } + } + + private static void rewriteJar(Path jar, java.util.function.Predicate keepEntry, Manifest manifest) throws IOException { + var tempFile = jar.resolveSibling(jar.getFileName().toString() + ".unsigned.tmp"); + try { + try (var input = new JarFile(jar.toFile(), false); + var output = manifest == null + ? new JarOutputStream(Files.newOutputStream(tempFile)) + : new JarOutputStream(Files.newOutputStream(tempFile), manifest)) { + var entries = input.entries(); + while (entries.hasMoreElements()) { + var entry = entries.nextElement(); + if (entry.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME) || !keepEntry.test(entry)) { + continue; + } + + var newEntry = new JarEntry(entry.getName()); + newEntry.setTime(entry.getTime()); + output.putNextEntry(newEntry); + if (!entry.isDirectory()) { + try (var entryInput = input.getInputStream(entry)) { + entryInput.transferTo(output); + } + } + output.closeEntry(); + } + } + atomicMove(tempFile, jar); + } finally { + Files.deleteIfExists(tempFile); + } + } + + private static Manifest unsignedManifest(Manifest manifest) { + if (manifest == null) { + manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + return manifest; + } + + var result = new Manifest(); + result.getMainAttributes().putAll(manifest.getMainAttributes()); + return result; + } + + private static boolean isJarSignatureFile(String name) { + var upperName = name.toUpperCase(java.util.Locale.ROOT); + if (!upperName.startsWith("META-INF/") || upperName.indexOf('/', "META-INF/".length()) != -1) { + return false; + } + return upperName.endsWith(".SF") || upperName.endsWith(".DSA") || upperName.endsWith(".RSA") || upperName.endsWith(".EC"); + } + public static void writeStringSafe(Path destination, String content, Charset charset) throws IOException { if (!charset.newEncoder().canEncode(content)) { throw new IllegalArgumentException("The given character set " + charset @@ -162,4 +231,33 @@ private static void atomicMoveIfPossible(final Path source, final Path destinati Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING); } } + + /** Rewrites the jar in place, removing every entry under org/spongepowered/asm/. */ + public static void stripSpongePowered(File jar) throws IOException { + var tmp = Path.of(jar.getAbsolutePath() + ".stripped.tmp"); + try (var input = new JarFile(jar); var output = new JarOutputStream(Files.newOutputStream(tmp))) { + Enumeration entries = input.entries(); + while (entries.hasMoreElements()) { + var entry = entries.nextElement(); + if (entry.getName().startsWith(RemappingTransform.SPONGE_PREFIX)) continue; + var copy = new JarEntry(entry.getName()); + copy.setTime(entry.getTime()); + output.putNextEntry(copy); + if (!entry.isDirectory()) { + try (var in = input.getInputStream(entry)) { + in.transferTo(output); + } + } + output.closeEntry(); + } + } + Files.move(tmp, jar.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + /** True if the jar contains any org/spongepowered/asm/ entry. */ + public static boolean containsSpongePowered(File jar) throws IOException { + try (var jf = new JarFile(jar)) { + return jf.stream().map(ZipEntry::getName).anyMatch(n -> n.startsWith(SPONGE_PREFIX)); + } + } } diff --git a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java index d9f9eb54..473a51b7 100644 --- a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java +++ b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java @@ -6,7 +6,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.inject.Inject; +import net.neoforged.moddevgradle.internal.JarPostProcessor; +import net.neoforged.moddevgradle.internal.utils.FileUtils; import net.neoforged.moddevgradle.internal.utils.ProblemReportingUtil; import net.neoforged.problems.FileProblemReporter; import net.neoforged.problems.Problem; @@ -15,6 +18,7 @@ import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.problems.Problems; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; @@ -115,6 +119,25 @@ public CreateMinecraftArtifacts() { @Optional public abstract Property getParchmentConflictResolutionPrefix(); + /** + * Path or Maven coordinates of a legacy MCP CSV mapping zip. + *

+ * This is only used by NFRT processes for Minecraft versions before Mojang official mappings existed. + */ + @Input + @Optional + public abstract Property getLegacyMcpMappings(); + + /** + * Additional Maven repository URLs that NFRT should consult when resolving artifacts it needs to download + * itself (notably the Minecraft and Forge libraries used as the compile classpath during recompilation). + * NFRT's built-in defaults only cover the NeoForged Maven and the local Maven cache, which is insufficient for + * legacy versions whose libraries are spread across Maven Central, Mojang's library repository and the Forge Maven. + * Each entry is passed to NFRT using the {@code --repository} command line option. + */ + @Input + public abstract ListProperty getAdditionalRepositories(); + /** * This property can be used to access additional results of the NeoForm process being run by NFRT. * The map key is the ID of the result while the value is the output file where that result should be written. @@ -254,6 +277,13 @@ public RegularFileProperty getSourcesArtifact() { @ApiStatus.Experimental public abstract Property getIncludeResourcesInGameJar(); + /** + * Jar post-processors for legacy MCP versions (e.g. 1.12.2 Forge deobf data remapping). + * Registered by the mcpforge plugin; empty by default. + */ + @Internal + public abstract ListProperty getJarPostProcessors(); + @Inject protected abstract Problems getProblems(); @@ -312,6 +342,19 @@ public void createArtifacts() { } } + if (getLegacyMcpMappings().isPresent()) { + var legacyMcpMappings = getLegacyMcpMappings().get(); + if (!legacyMcpMappings.isBlank()) { + args.add("--mcp-mappings"); + args.add(legacyMcpMappings); + } + } + + for (var repository : getAdditionalRepositories().get()) { + args.add("--repository"); + args.add(repository); + } + if (!getEnableCache().get()) { args.add("--disable-cache"); } @@ -395,11 +438,75 @@ public void createArtifacts() { try { run(args); + stripJarSignatures(requestedResults); + remapLegacyForgeMinecraftReferences(requestedResults, getJarPostProcessors().get()); + removePreAppliedLegacyForgeRuntimePatches(requestedResults); } finally { reportProblems(problemsReport); } } + private static void removePreAppliedLegacyForgeRuntimePatches(List requestedResults) { + for (var requestedResult : requestedResults) { + var destination = requestedResult.destination(); + if (!destination.isFile() || !destination.getName().endsWith(".jar")) { + continue; + } + + try { + FileUtils.removeJarEntries(destination.toPath(), Set.of("binpatches.pack.lzma")); + } catch (IOException e) { + throw new GradleException("Failed to remove legacy Forge runtime patches from generated jar " + destination, e); + } + } + } + + private static void remapLegacyForgeMinecraftReferences(List requestedResults, List postProcessors) { + if (postProcessors.isEmpty()) { + return; + } + for (var requestedResult : requestedResults) { + var destination = requestedResult.destination(); + if (!destination.isFile() || !destination.getName().endsWith(".jar")) { + continue; + } + + try { + for (var postProcessor : postProcessors) { + postProcessor.process( + destination.toPath(), + findRequestedResult(requestedResults, "intermediaryToNamedMapping")); + } + } catch (IOException e) { + throw new GradleException("Failed to remap legacy Forge Minecraft references in generated jar " + destination, e); + } + } + } + + private static java.nio.file.Path findRequestedResult(List requestedResults, String id) { + for (var requestedResult : requestedResults) { + if (requestedResult.id().equals(id) && requestedResult.destination().isFile()) { + return requestedResult.destination().toPath(); + } + } + return null; + } + + private static void stripJarSignatures(List requestedResults) { + for (var requestedResult : requestedResults) { + var destination = requestedResult.destination(); + if (!destination.isFile() || !destination.getName().endsWith(".jar")) { + continue; + } + + try { + FileUtils.stripJarSignatures(destination.toPath()); + } catch (IOException e) { + throw new GradleException("Failed to strip signature metadata from generated jar " + destination, e); + } + } + } + private void reportProblems(File problemsReport) { if (!problemsReport.exists()) { return; // Not created -> nothing to report diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java new file mode 100644 index 00000000..c7bd0daa --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java @@ -0,0 +1,64 @@ +package net.neoforged.moddevgradle.mcpforge.dsl; + +import javax.inject.Inject; +import net.neoforged.moddevgradle.dsl.DataFileCollection; +import net.neoforged.moddevgradle.dsl.ModDevExtension; +import net.neoforged.moddevgradle.internal.ModDevArtifactsWorkflow; +import net.neoforged.moddevgradle.mcpforge.dsl.McpForgeModdingSettings; +import net.neoforged.moddevgradle.mcpforge.internal.McpForgeModDevPlugin; +import org.gradle.api.Action; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; + +/** + * The {@code legacyForge} extension for the mcpforge plugin, owning its {@link #enable(Action)} wiring. + * {@link LegacyForgeModdingSettings} is reused as-is. + */ +public abstract class McpForgeExtension extends ModDevExtension { + private final Project project; + + @Inject + public McpForgeExtension(Project project, + DataFileCollection accessTransformers, + DataFileCollection interfaceInjectionData) { + super(project, accessTransformers, interfaceInjectionData); + this.project = project; + } + + /** Coordinates of jars that ARE the Mixin provider (exempt from bundled-spongepowered stripping). */ + public abstract ListProperty getMixinProviders(); + + /** Declares a jar as the Mixin provider (e.g. {@code mixinProvider 'zone.rong:mixinbooter:10.7'}). */ + public void mixinProvider(String notation) { + getMixinProviders().add(notation); + } + + /** Shorthand for {@code enable { forgeVersion = '...' }. */ + public void setVersion(String version) { + enable(settings -> settings.setForgeVersion(version)); + } + + /** Shorthand for {@code enable { mcpVersion = '...' }. */ + public void setMcpVersion(String version) { + enable(settings -> settings.setMcpVersion(version)); + } + + /** After enabling, the MCP version picked. Throws if not enabled. */ + public String getMcpVersion() { + var dependencies = ModDevArtifactsWorkflow.get(project).dependencies(); + if (dependencies.neoFormDependency() == null) { + throw new InvalidUserCodeException("You cannot retrieve the MCP version without setting it first."); + } + return dependencies.neoFormDependency().getVersion(); + } + + public void enable(Action customizer) { + var plugin = project.getPlugins().getPlugin(McpForgeModDevPlugin.class); + + var settings = project.getObjects().newInstance(McpForgeModdingSettings.class); + customizer.execute(settings); + + plugin.enable(project, settings, this); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java new file mode 100644 index 00000000..72097eb0 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java @@ -0,0 +1,23 @@ +package net.neoforged.moddevgradle.mcpforge.dsl; + +import javax.inject.Inject; +import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeModdingSettings; +import org.gradle.api.Project; +import org.jetbrains.annotations.Nullable; + +public abstract class McpForgeModdingSettings extends LegacyForgeModdingSettings { + private String mcpMappings; + + @Inject + public McpForgeModdingSettings(Project project) { + super(project); + } + + public @Nullable String getMcpMappings() { + return mcpMappings; + } + + public void setMcpMappings(String mcpMappings) { + this.mcpMappings = mcpMappings; + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/ExtractDependencyAccessTransformers.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/ExtractDependencyAccessTransformers.java new file mode 100644 index 00000000..f094d8a4 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/ExtractDependencyAccessTransformers.java @@ -0,0 +1,92 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +/** + * Extracts Access Transformer {@code .cfg} files declared via the {@code FMLAT} manifest attribute in dependency jars. + * + *

FML's contract: a mod/coremod jar may carry {@code FMLAT: }, each at + * {@code META-INF/} inside the jar. MDG has no runtime AT class-transformer (it runs the recompiled MC jar + * directly), so dependency ATs must be applied at build time — the extracted cfgs are fed into the + * {@code accessTransformers} DataFileCollection so NFRT bakes them in via {@code --access-transformer}. + * + *

AT files use SRG member names and are extracted verbatim (only CRLF→LF normalization); NFRT renames + * SRG→MCP during recompilation. + */ +public abstract class ExtractDependencyAccessTransformers extends DefaultTask { + + private static final Attributes.Name FMLAT = new Attributes.Name("FMLAT"); + + /** Dependency jars to scan for {@code FMLAT} declarations (typically compileClasspath + runtimeClasspath). */ + @InputFiles + public abstract ConfigurableFileCollection getDependencies(); + + /** Directory where extracted AT cfgs are written: one file per (jar, at) pair. */ + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @TaskAction + public void extract() throws IOException { + var outDir = getOutputDirectory().get().getAsFile().toPath(); + Files.createDirectories(outDir); + + for (var file : getDependencies().getFiles()) { + if (file == null || !file.isFile() || !file.getName().toLowerCase().endsWith(".jar")) { + continue; + } + try (var jar = new JarFile(file, false)) { + var manifest = jar.getManifest(); + if (manifest == null) { + continue; + } + var atNames = manifest.getMainAttributes().getValue(FMLAT); + if (atNames == null || atNames.isBlank()) { + continue; + } + var jarBase = stripExtension(file.getName()); + for (var rawName : atNames.split(" ")) { + var atName = rawName.trim(); + if (atName.isEmpty()) { + continue; + } + var entry = jar.getEntry("META-INF/" + atName); + if (entry == null) { + getLogger().warn("Dependency AT '{}!META-INF/{}' not found, skipping.", file.getName(), atName); + continue; + } + String content; + try (InputStream in = jar.getInputStream(entry)) { + content = new String(in.readAllBytes(), StandardCharsets.UTF_8).replaceAll("\r\n", "\n"); + } + var outFile = outDir.resolve(jarBase + "-" + sanitize(atName) + ".cfg"); + Files.writeString(outFile, content, StandardCharsets.UTF_8); + getLogger().lifecycle("Extracted dependency AT '{}' from {}", atName, file.getName()); + } + } catch (IOException ignored) { + // Not a readable jar — skip. + } + } + } + + private static String stripExtension(String name) { + var i = name.lastIndexOf('.'); + return i > 0 ? name.substring(0, i) : name; + } + + private static String sanitize(String s) { + return s.replaceAll("[^A-Za-z0-9._-]", "_"); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeArtifacts.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeArtifacts.java new file mode 100644 index 00000000..e58472ff --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeArtifacts.java @@ -0,0 +1,22 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; + +public final class LegacyForgeArtifacts { + private LegacyForgeArtifacts() {} + + static String userdevClassifier(VersionCapabilitiesInternal versionCapabilities) { + if ("1.12.2".equals(versionCapabilities.minecraftVersion())) { + return "userdev3"; + } + return "userdev"; + } + + static String userdevNotation(String groupId, String version, VersionCapabilitiesInternal versionCapabilities) { + return groupId + ":forge:" + version + ":" + userdevClassifier(versionCapabilities); + } + + static String userdevJarName(String moduleName, String version, VersionCapabilitiesInternal versionCapabilities) { + return moduleName + "-" + version + "-" + userdevClassifier(versionCapabilities) + ".jar"; + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java new file mode 100644 index 00000000..d6071b1f --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java @@ -0,0 +1,1085 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipFile; +import net.neoforged.moddevgradle.internal.utils.FileUtils; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.MethodRemapper; +import org.objectweb.asm.commons.Remapper; +import org.tukaani.xz.LZMAInputStream; + +@ApiStatus.Internal +public final class LegacyForgeJarProcessor { + private static final String FORGE_1_12_DEOBF_DATA = "deobfuscation_data-1.12.2.lzma"; + + private LegacyForgeJarProcessor() {} + + public static void remapMinecraftReferences(Path jar) throws IOException { + remapMinecraftReferences(jar, null); + } + + public static void remapMinecraftReferences(Path jar, Path srgToMcpMappings) throws IOException { + LegacyForgeMappings mappings; + Map classInfos; + try (var input = new JarFile(jar.toFile(), false, ZipFile.OPEN_READ)) { + var mappingsEntry = input.getJarEntry(FORGE_1_12_DEOBF_DATA); + if (mappingsEntry == null) { + return; + } + + mappings = readForgeMappings(input, mappingsEntry); + classInfos = readClassInfos(input); + } + + if (mappings.classMappings().isEmpty()) { + return; + } + + if (srgToMcpMappings != null) { + mappings = mappings.withSrgToMcp(readSrgToMcpMappings(srgToMcpMappings)); + } + mappings = mappings.withInheritedMemberMappings(classInfos); + + var tempFile = jar.resolveSibling(jar.getFileName().toString() + ".remapped.tmp"); + try { + try (var input = new JarFile(jar.toFile(), false, ZipFile.OPEN_READ); + var output = new JarOutputStream(Files.newOutputStream(tempFile))) { + var entries = input.entries(); + var remapper = new LegacyForgeRemapper(mappings); + while (entries.hasMoreElements()) { + var entry = entries.nextElement(); + var newEntry = new JarEntry(entry.getName()); + newEntry.setTime(entry.getTime()); + output.putNextEntry(newEntry); + if (!entry.isDirectory()) { + try (var entryInput = input.getInputStream(entry)) { + if (shouldProcess(entry.getName())) { + output.write(remapClass(entryInput.readAllBytes(), remapper, classInfos)); + } else { + entryInput.transferTo(output); + } + } + } + output.closeEntry(); + } + } + FileUtils.atomicMove(tempFile, jar); + } finally { + Files.deleteIfExists(tempFile); + } + } + + private static LegacyForgeMappings readForgeMappings(JarFile jar, JarEntry mappingsEntry) throws IOException { + var classMappings = new HashMap(); + var fieldMappings = new HashMap(); + var methodMappings = new HashMap(); + try (var input = new LZMAInputStream(jar.getInputStream(mappingsEntry)); + var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + var parts = line.split("\\s+"); + if (parts.length < 3) { + continue; + } + + if ("CL:".equals(parts[0])) { + classMappings.put(parts[1], parts[2]); + } else if ("FD:".equals(parts[0])) { + var source = splitMember(parts[1]); + var target = splitMember(parts[2]); + fieldMappings.put(new MemberKey(source.owner(), source.name(), null), target.name()); + } else if ("MD:".equals(parts[0]) && parts.length >= 5) { + var source = splitMember(parts[1]); + var target = splitMember(parts[3]); + methodMappings.put(new MemberKey(source.owner(), source.name(), parts[2]), target.name()); + } + } + } + return new LegacyForgeMappings(classMappings, fieldMappings, methodMappings); + } + + private static SrgToMcpMappings readSrgToMcpMappings(Path mappingsFile) throws IOException { + var fieldMappings = new HashMap(); + var methodMappings = new HashMap(); + try (var reader = Files.newBufferedReader(mappingsFile, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + var parts = line.split("\\s+"); + if (parts.length < 3) { + continue; + } + + if ("FD:".equals(parts[0])) { + var source = splitMember(parts[1]); + var target = splitMember(parts[2]); + fieldMappings.put(new MemberKey(source.owner(), source.name(), null), target.name()); + } else if ("MD:".equals(parts[0]) && parts.length >= 5) { + var source = splitMember(parts[1]); + var target = splitMember(parts[3]); + methodMappings.put(new MemberKey(source.owner(), source.name(), parts[2]), target.name()); + } + } + } + return new SrgToMcpMappings(fieldMappings, methodMappings); + } + + private static Map readClassInfos(JarFile jar) throws IOException { + var classInfos = new HashMap(); + var entries = jar.entries(); + while (entries.hasMoreElements()) { + var entry = entries.nextElement(); + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + + try (var input = jar.getInputStream(entry)) { + var reader = new ClassReader(input.readAllBytes()); + var methods = new ArrayList(); + var fields = new ArrayList(); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public org.objectweb.asm.FieldVisitor visitField( + int access, + String name, + String descriptor, + String signature, + Object value) { + fields.add(new FieldInfo(access, name, descriptor)); + return null; + } + + @Override + public org.objectweb.asm.MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions) { + methods.add(new MethodInfo(access, name, descriptor)); + return null; + } + }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + classInfos.put(reader.getClassName(), new ClassInfo( + reader.getClassName(), + reader.getSuperName(), + List.of(reader.getInterfaces()), + List.copyOf(fields), + List.copyOf(methods))); + } + } + return classInfos; + } + + private static Member splitMember(String value) { + var separator = value.lastIndexOf('/'); + return new Member(value.substring(0, separator), value.substring(separator + 1)); + } + + private static boolean shouldProcess(String entryName) { + return entryName.endsWith(".class") && shouldProcessClass(entryName.substring(0, entryName.length() - ".class".length())); + } + + private static boolean shouldProcessClass(String internalName) { + return isMinecraftClass(internalName) || isForgeClass(internalName); + } + + private static boolean shouldRemapClass(String internalName) { + return isForgeClass(internalName); + } + + private static boolean isMinecraftClass(String internalName) { + return internalName.startsWith("net/minecraft/"); + } + + private static boolean isForgeClass(String internalName) { + return internalName.startsWith("net/minecraftforge/"); + } + + private static byte[] remapClass(byte[] classBytes, LegacyForgeRemapper remapper, Map classInfos) { + var reader = new ClassReader(classBytes); + var writer = newClassWriter(reader.getClassName()); + if (isMinecraftClass(reader.getClassName())) { + reader.accept(new LegacyMinecraftClassReferenceRemapper(writer, remapper, classInfos), 0); + } else { + reader.accept(new LegacyForgeClassRemapper(writer, remapper), 0); + } + return writer.toByteArray(); + } + + private static ClassWriter newClassWriter(String className) { + if (LegacyMinecraftClassReferenceRemapper.needsComputedFrames(className)) { + return new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { + @Override + protected String getCommonSuperClass(String type1, String type2) { + return "java/lang/Object"; + } + }; + } + return new ClassWriter(0); + } + + private static final class LegacyForgeClassRemapper extends ClassRemapper { + private final LegacyForgeRemapper remapper; + + private LegacyForgeClassRemapper(ClassVisitor classVisitor, LegacyForgeRemapper remapper) { + super(classVisitor, remapper); + this.remapper = remapper; + } + + @Override + protected MethodVisitor createMethodRemapper(MethodVisitor methodVisitor) { + return new LegacyMethodRemapper(methodVisitor, remapper); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + var method = super.visitMethod(access, name, descriptor, signature, exceptions); + if (method == null) { + return null; + } + return new MethodVisitor(Opcodes.ASM9, method) { + @Override + public void visitLdcInsn(Object value) { + if (value instanceof String string) { + super.visitLdcInsn(remapper.mapStringConstant(string)); + } else { + super.visitLdcInsn(value); + } + } + }; + } + } + + private static final class LegacyMinecraftClassReferenceRemapper extends ClassVisitor { + private static final String GL_ALLOCATION = "net/minecraft/client/renderer/GLAllocation"; + private static final String CRASH_REPORT_CATEGORY = "net/minecraft/crash/CrashReportCategory"; + + private final LegacyForgeRemapper remapper; + private final Remapper referenceRemapper; + private String className; + + private LegacyMinecraftClassReferenceRemapper( + ClassVisitor classVisitor, + LegacyForgeRemapper remapper, + Map classInfos) { + super(Opcodes.ASM9, classVisitor); + this.remapper = remapper; + this.referenceRemapper = new LegacyMinecraftReferenceRemapper(remapper, classInfos); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + className = name; + super.visit( + version, + access, + remapper.mapType(name), + remapper.mapSignature(signature, false), + remapper.mapType(superName), + interfaces == null ? null : remapper.mapTypes(interfaces)); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return super.visitField( + access, + name, + remapper.mapDesc(descriptor), + remapper.mapSignature(signature, true), + remapper.mapValue(value)); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (GL_ALLOCATION.equals(className) && name.equals("generateDisplayLists") && descriptor.equals("(I)I")) { + var method = super.visitMethod(access, name, descriptor, signature, exceptions); + if (method != null) { + writeGenerateDisplayLists(method); + } + return null; + } + if (CRASH_REPORT_CATEGORY.equals(className) + && name.equals("firstTwoElementsOfStackTraceMatch") + && descriptor.equals("(Ljava/lang/StackTraceElement;Ljava/lang/StackTraceElement;)Z")) { + var method = super.visitMethod(access, name, descriptor, signature, exceptions); + if (method != null) { + writeFirstTwoElementsOfStackTraceMatch(method); + } + return null; + } + + var method = super.visitMethod( + access, + name, + remapper.mapMethodDesc(descriptor), + remapper.mapSignature(signature, false), + exceptions == null ? null : remapper.mapTypes(exceptions)); + return method == null ? null : new LegacyMethodRemapper(method, referenceRemapper); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + super.visitInnerClass( + remapper.mapType(name), + outerName == null ? null : remapper.mapType(outerName), + innerName, + access); + } + + @Override + public void visitOuterClass(String owner, String name, String descriptor) { + super.visitOuterClass( + remapper.mapType(owner), + name == null ? null : referenceRemapper.mapMethodName(owner, name, descriptor), + descriptor == null ? null : remapper.mapMethodDesc(descriptor)); + } + + private void writeGenerateDisplayLists(MethodVisitor method) { + method.visitCode(); + + var success = new org.objectweb.asm.Label(); + var throwFailure = new org.objectweb.asm.Label(); + var retry = new org.objectweb.asm.Label(); + var retryHandler = new org.objectweb.asm.Label(); + var retryEnd = new org.objectweb.asm.Label(); + var retryDone = new org.objectweb.asm.Label(); + + method.visitTryCatchBlock(retry, retryEnd, retryHandler, "org/lwjgl/LWJGLException"); + + method.visitVarInsn(Opcodes.ILOAD, 0); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "net/minecraft/client/renderer/GlStateManager", + "glGenLists", + "(I)I", + false); + method.visitVarInsn(Opcodes.ISTORE, 1); + method.visitVarInsn(Opcodes.ILOAD, 1); + method.visitJumpInsn(Opcodes.IFNE, success); + + method.visitLabel(retry); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/lwjgl/opengl/Display", + "isCreated", + "()Z", + false); + method.visitJumpInsn(Opcodes.IFEQ, retryEnd); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/lwjgl/opengl/Display", + "getDrawable", + "()Lorg/lwjgl/opengl/Drawable;", + false); + method.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + "org/lwjgl/opengl/Drawable", + "makeCurrent", + "()V", + true); + method.visitLabel(retryEnd); + method.visitJumpInsn(Opcodes.GOTO, retryDone); + + method.visitLabel(retryHandler); + method.visitInsn(Opcodes.POP); + + method.visitLabel(retryDone); + method.visitVarInsn(Opcodes.ILOAD, 0); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "net/minecraft/client/renderer/GlStateManager", + "glGenLists", + "(I)I", + false); + method.visitVarInsn(Opcodes.ISTORE, 1); + method.visitVarInsn(Opcodes.ILOAD, 1); + method.visitJumpInsn(Opcodes.IFEQ, throwFailure); + + method.visitLabel(success); + method.visitVarInsn(Opcodes.ILOAD, 1); + method.visitInsn(Opcodes.IRETURN); + + method.visitLabel(throwFailure); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "net/minecraft/client/renderer/GlStateManager", + "glGetError", + "()I", + false); + method.visitVarInsn(Opcodes.ISTORE, 2); + method.visitLdcInsn("No error code reported"); + method.visitVarInsn(Opcodes.ASTORE, 3); + + var skipErrorString = new org.objectweb.asm.Label(); + method.visitVarInsn(Opcodes.ILOAD, 2); + method.visitJumpInsn(Opcodes.IFEQ, skipErrorString); + method.visitVarInsn(Opcodes.ILOAD, 2); + method.visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/lwjgl/util/glu/GLU", + "gluErrorString", + "(I)Ljava/lang/String;", + false); + method.visitVarInsn(Opcodes.ASTORE, 3); + method.visitLabel(skipErrorString); + + method.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException"); + method.visitInsn(Opcodes.DUP); + method.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + method.visitInsn(Opcodes.DUP); + method.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); + method.visitLdcInsn("glGenLists returned an ID of 0 for a count of "); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuilder;", + false); + method.visitVarInsn(Opcodes.ILOAD, 0); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(I)Ljava/lang/StringBuilder;", + false); + method.visitLdcInsn(", GL error ("); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuilder;", + false); + method.visitVarInsn(Opcodes.ILOAD, 2); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(I)Ljava/lang/StringBuilder;", + false); + method.visitLdcInsn("): "); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuilder;", + false); + method.visitVarInsn(Opcodes.ALOAD, 3); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuilder;", + false); + method.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/lang/StringBuilder", + "toString", + "()Ljava/lang/String;", + false); + method.visitMethodInsn( + Opcodes.INVOKESPECIAL, + "java/lang/IllegalStateException", + "", + "(Ljava/lang/String;)V", + false); + method.visitInsn(Opcodes.ATHROW); + + method.visitMaxs(4, 4); + method.visitEnd(); + } + + private static boolean needsComputedFrames(String className) { + return GL_ALLOCATION.equals(className) || CRASH_REPORT_CATEGORY.equals(className); + } + + private void writeFirstTwoElementsOfStackTraceMatch(MethodVisitor method) { + method.visitCode(); + var falseLabel = new org.objectweb.asm.Label(); + var compareSecond = new org.objectweb.asm.Label(); + var noSecond = new org.objectweb.asm.Label(); + + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ARRAYLENGTH); + method.visitJumpInsn(Opcodes.IFEQ, falseLabel); + method.visitVarInsn(Opcodes.ALOAD, 1); + method.visitJumpInsn(Opcodes.IFNULL, falseLabel); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ICONST_0); + method.visitInsn(Opcodes.AALOAD); + method.visitVarInsn(Opcodes.ASTORE, 3); + + method.visitVarInsn(Opcodes.ALOAD, 3); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "isNativeMethod", "()Z", false); + method.visitVarInsn(Opcodes.ALOAD, 1); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "isNativeMethod", "()Z", false); + method.visitJumpInsn(Opcodes.IF_ICMPNE, falseLabel); + + writeStackTraceElementStringEquals(method, "getClassName"); + method.visitJumpInsn(Opcodes.IFEQ, falseLabel); + writeStackTraceElementStringEquals(method, "getFileName"); + method.visitJumpInsn(Opcodes.IFEQ, falseLabel); + writeStackTraceElementStringEquals(method, "getMethodName"); + method.visitJumpInsn(Opcodes.IFEQ, falseLabel); + + method.visitVarInsn(Opcodes.ALOAD, 2); + method.visitJumpInsn(Opcodes.IFNONNULL, compareSecond); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ARRAYLENGTH); + method.visitInsn(Opcodes.ICONST_1); + method.visitJumpInsn(Opcodes.IF_ICMPGT, falseLabel); + method.visitJumpInsn(Opcodes.GOTO, noSecond); + + method.visitLabel(compareSecond); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ARRAYLENGTH); + method.visitInsn(Opcodes.ICONST_1); + method.visitJumpInsn(Opcodes.IF_ICMPLE, falseLabel); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ICONST_1); + method.visitInsn(Opcodes.AALOAD); + method.visitVarInsn(Opcodes.ALOAD, 2); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "equals", "(Ljava/lang/Object;)Z", false); + method.visitJumpInsn(Opcodes.IFEQ, falseLabel); + + method.visitLabel(noSecond); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); + method.visitInsn(Opcodes.ICONST_0); + method.visitVarInsn(Opcodes.ALOAD, 1); + method.visitInsn(Opcodes.AASTORE); + method.visitInsn(Opcodes.ICONST_1); + method.visitInsn(Opcodes.IRETURN); + + method.visitLabel(falseLabel); + method.visitInsn(Opcodes.ICONST_0); + method.visitInsn(Opcodes.IRETURN); + method.visitMaxs(0, 0); + method.visitEnd(); + } + + private void writeStackTraceElementStringEquals(MethodVisitor method, String getterName) { + method.visitVarInsn(Opcodes.ALOAD, 3); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", getterName, "()Ljava/lang/String;", false); + method.visitVarInsn(Opcodes.ALOAD, 1); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", getterName, "()Ljava/lang/String;", false); + method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false); + } + } + + private static final class LegacyMethodRemapper extends MethodRemapper { + private static final String LAMBDA_METAFACTORY = "java/lang/invoke/LambdaMetafactory"; + private static final String METAFACTORY_DESCRIPTOR = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;" + + "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;" + + "Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"; + private static final String ALT_METAFACTORY_DESCRIPTOR = "(Ljava/lang/invoke/MethodHandles$Lookup;" + + "Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"; + + private final Remapper remapper; + + private LegacyMethodRemapper(MethodVisitor methodVisitor, Remapper remapper) { + super(methodVisitor, remapper); + this.remapper = remapper; + } + + @Override + public void visitInvokeDynamicInsn( + String name, + String descriptor, + Handle bootstrapMethodHandle, + Object... bootstrapMethodArguments) { + super.visitInvokeDynamicInsn( + mapInvokeDynamicMethodName(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments), + descriptor, + bootstrapMethodHandle, + bootstrapMethodArguments); + } + + private String mapInvokeDynamicMethodName( + String name, + String descriptor, + Handle bootstrapMethodHandle, + Object[] bootstrapMethodArguments) { + if (!isLambdaMetafactory(bootstrapMethodHandle) + || bootstrapMethodArguments.length == 0 + || !(bootstrapMethodArguments[0] instanceof Type samMethodType)) { + return remapper.mapInvokeDynamicMethodName(name, descriptor); + } + + var samOwner = Type.getReturnType(descriptor); + if (samOwner.getSort() != Type.OBJECT) { + return remapper.mapInvokeDynamicMethodName(name, descriptor); + } + + return remapper.mapMethodName(samOwner.getInternalName(), name, samMethodType.getDescriptor()); + } + + private static boolean isLambdaMetafactory(Handle bootstrapMethodHandle) { + if (!LAMBDA_METAFACTORY.equals(bootstrapMethodHandle.getOwner()) + || bootstrapMethodHandle.getTag() != Opcodes.H_INVOKESTATIC) { + return false; + } + + return ("metafactory".equals(bootstrapMethodHandle.getName()) + && METAFACTORY_DESCRIPTOR.equals(bootstrapMethodHandle.getDesc())) + || ("altMetafactory".equals(bootstrapMethodHandle.getName()) + && ALT_METAFACTORY_DESCRIPTOR.equals(bootstrapMethodHandle.getDesc())); + } + } + + private static final class LegacyMinecraftReferenceRemapper extends Remapper { + private final LegacyForgeRemapper delegate; + private final Map classInfos; + + private LegacyMinecraftReferenceRemapper(LegacyForgeRemapper delegate, Map classInfos) { + this.delegate = delegate; + this.classInfos = classInfos; + } + + @Override + public String map(String internalName) { + return delegate.map(internalName); + } + + @Override + public String mapFieldName(String owner, String name, String descriptor) { + if (isDeclaredMinecraftField(owner, name)) { + return name; + } + return delegate.mapFieldName(owner, name, descriptor); + } + + @Override + public String mapMethodName(String owner, String name, String descriptor) { + if (isDeclaredMinecraftMethod(owner, name, descriptor)) { + return name; + } + return delegate.mapMethodName(owner, name, descriptor); + } + + private boolean isDeclaredMinecraftField(String owner, String name) { + if (!isMinecraftClass(owner)) { + return false; + } + var classInfo = classInfos.get(owner); + return classInfo != null && classInfo.fields().stream().anyMatch(field -> field.name().equals(name)); + } + + private boolean isDeclaredMinecraftMethod(String owner, String name, String descriptor) { + if (!isMinecraftClass(owner)) { + return false; + } + var classInfo = classInfos.get(owner); + return classInfo != null && classInfo.methods().stream() + .anyMatch(method -> method.name().equals(name) && method.descriptor().equals(descriptor)); + } + } + + private static final class LegacyForgeRemapper extends Remapper { + private final LegacyForgeMappings mappings; + + private LegacyForgeRemapper(LegacyForgeMappings mappings) { + this.mappings = mappings; + } + + @Override + public String map(String internalName) { + return mappings.classMappings().getOrDefault(internalName, internalName); + } + + @Override + public String mapFieldName(String owner, String name, String descriptor) { + var mappedName = mappings.fieldMappings().get(new MemberKey(owner, name, null)); + if (mappedName != null) { + return mappedName; + } + + var mappedOwner = mappings.classMappings().get(owner); + if (mappedOwner != null) { + mappedName = mappings.fieldMappings().get(new MemberKey(mappedOwner, name, null)); + if (mappedName != null) { + return mappedName; + } + } + return name; + } + + @Override + public String mapMethodName(String owner, String name, String descriptor) { + var mappedName = mappings.methodMappings().get(new MemberKey(owner, name, descriptor)); + if (mappedName != null) { + return mappedName; + } + + var mappedDescriptor = mapMethodDesc(descriptor); + mappedName = mappings.methodMappings().get(new MemberKey(owner, name, mappedDescriptor)); + if (mappedName != null) { + return mappedName; + } + + var mappedOwner = mappings.classMappings().get(owner); + if (mappedOwner != null) { + mappedName = mappings.methodMappings().get(new MemberKey(mappedOwner, name, mappedDescriptor)); + if (mappedName != null) { + return mappedName; + } + } + return name; + } + + private String mapStringConstant(String value) { + return mappings.stringMappings().getOrDefault(value, value); + } + } + + private record LegacyForgeMappings( + Map classMappings, + Map fieldMappings, + Map methodMappings, + Map stringMappings) { + private LegacyForgeMappings( + Map classMappings, + Map fieldMappings, + Map methodMappings) { + this(classMappings, fieldMappings, methodMappings, Map.of()); + } + + LegacyForgeMappings withSrgToMcp(SrgToMcpMappings srgToMcp) { + var composedFields = new HashMap(); + var composedStringMappings = new HashMap<>(stringMappings); + for (var entry : fieldMappings.entrySet()) { + var key = entry.getKey(); + var mappedOwner = classMappings.getOrDefault(key.owner(), key.owner()); + var srgName = entry.getValue(); + var mcpName = srgToMcp.fieldMappings().getOrDefault(new MemberKey(mappedOwner, srgName, null), srgName); + composedFields.put(key, mcpName); + composedFields.put(new MemberKey(mappedOwner, key.name(), null), mcpName); + addUniqueStringMapping(composedStringMappings, srgName, mcpName); + } + + var classOnlyRemapper = new LegacyForgeRemapper(new LegacyForgeMappings(classMappings, Map.of(), Map.of())); + var composedMethods = new HashMap(); + for (var entry : methodMappings.entrySet()) { + var key = entry.getKey(); + var mappedOwner = classMappings.getOrDefault(key.owner(), key.owner()); + var mappedDescriptor = classOnlyRemapper.mapMethodDesc(key.descriptor()); + var srgName = entry.getValue(); + var mcpName = srgToMcp.methodMappings().getOrDefault(new MemberKey(mappedOwner, srgName, mappedDescriptor), srgName); + composedMethods.put(key, mcpName); + composedMethods.put(new MemberKey(mappedOwner, key.name(), mappedDescriptor), mcpName); + addUniqueStringMapping(composedStringMappings, srgName, mcpName); + } + + composedStringMappings.values().removeIf(value -> value == null); + return new LegacyForgeMappings(classMappings, composedFields, composedMethods, composedStringMappings); + } + + LegacyForgeMappings withInheritedMemberMappings(Map classInfos) { + var remappedFields = new HashMap<>(fieldMappings); + var remappedMethods = new HashMap<>(methodMappings); + var classOnlyRemapper = new LegacyForgeRemapper(new LegacyForgeMappings(classMappings, Map.of(), Map.of())); + + for (var classInfo : classInfos.values()) { + if (!isMinecraftOrForgeClass(classInfo.name())) { + continue; + } + + for (var fieldMapping : findInheritedFieldMappings( + classInfo.superName(), + classInfo.interfaces(), + classInfos, + new HashSet<>()).entrySet()) { + remappedFields.put(new MemberKey(classInfo.name(), fieldMapping.getKey(), null), fieldMapping.getValue()); + putObfuscatedClassMemberMapping(remappedFields, classInfo.name(), fieldMapping.getKey(), null, fieldMapping.getValue()); + } + + for (var method : classInfo.methods()) { + if (method.name().startsWith("<") + || (method.access() & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0) { + continue; + } + + var mappedName = findMappedOverrideName( + classInfo.superName(), + classInfo.interfaces(), + method.name(), + method.descriptor(), + classInfos, + classOnlyRemapper, + new HashSet<>()); + if (mappedName != null && !mappedName.equals(method.name())) { + remappedMethods.put(new MemberKey(classInfo.name(), method.name(), method.descriptor()), mappedName); + putObfuscatedClassMemberMapping(remappedMethods, classInfo.name(), method.name(), method.descriptor(), mappedName); + } + } + + for (var methodMapping : findInheritedMethodMappings( + classInfo.superName(), + classInfo.interfaces(), + classInfos, + classOnlyRemapper, + new HashSet<>()).entrySet()) { + remappedMethods.put(new MemberKey(classInfo.name(), methodMapping.getKey().name(), methodMapping.getKey().descriptor()), methodMapping.getValue()); + putObfuscatedClassMemberMapping( + remappedMethods, + classInfo.name(), + methodMapping.getKey().name(), + methodMapping.getKey().descriptor(), + methodMapping.getValue()); + } + } + + return new LegacyForgeMappings(classMappings, remappedFields, remappedMethods, stringMappings); + } + + private static void addUniqueStringMapping(Map stringMappings, String sourceName, String mappedName) { + if (sourceName.equals(mappedName)) { + return; + } + + var existing = stringMappings.putIfAbsent(sourceName, mappedName); + if (existing != null && !existing.equals(mappedName)) { + stringMappings.put(sourceName, null); + } + } + + private void putObfuscatedClassMemberMapping( + Map mappings, + String mappedOwner, + String name, + String descriptor, + String mappedName) { + for (var entry : classMappings.entrySet()) { + if (entry.getValue().equals(mappedOwner)) { + mappings.put(new MemberKey(entry.getKey(), name, descriptor), mappedName); + return; + } + } + } + + private boolean isMinecraftOrForgeClass(String name) { + return name.startsWith("net/minecraft/") || shouldRemapClass(name); + } + + private Map findInheritedFieldMappings( + String superName, + List interfaces, + Map classInfos, + HashSet visited) { + var inheritedFields = new HashMap(); + addInheritedFieldMappings(superName, classInfos, visited, inheritedFields); + for (var interfaceName : interfaces) { + addInheritedFieldMappings(interfaceName, classInfos, visited, inheritedFields); + } + return inheritedFields; + } + + private void addInheritedFieldMappings( + String owner, + Map classInfos, + HashSet visited, + Map inheritedFields) { + if (owner == null || !visited.add(owner)) { + return; + } + + for (var entry : fieldMappings.entrySet()) { + if (entry.getKey().owner().equals(owner)) { + inheritedFields.putIfAbsent(entry.getKey().name(), entry.getValue()); + } + } + + var classInfo = classInfos.get(owner); + if (classInfo == null) { + classInfo = classInfos.get(classMappings.get(owner)); + } + if (classInfo == null) { + return; + } + + for (var field : classInfo.fields()) { + if ((field.access() & Opcodes.ACC_PRIVATE) != 0) { + inheritedFields.remove(field.name()); + } + } + + addInheritedFieldMappings(classInfo.superName(), classInfos, visited, inheritedFields); + for (var interfaceName : classInfo.interfaces()) { + addInheritedFieldMappings(interfaceName, classInfos, visited, inheritedFields); + } + } + + private Map findInheritedMethodMappings( + String superName, + List interfaces, + Map classInfos, + Remapper classOnlyRemapper, + HashSet visited) { + var inheritedMethods = new HashMap(); + addInheritedMethodMappings(superName, classInfos, classOnlyRemapper, visited, inheritedMethods); + for (var interfaceName : interfaces) { + addInheritedMethodMappings(interfaceName, classInfos, classOnlyRemapper, visited, inheritedMethods); + } + return inheritedMethods; + } + + private void addInheritedMethodMappings( + String owner, + Map classInfos, + Remapper classOnlyRemapper, + HashSet visited, + Map inheritedMethods) { + if (owner == null || !visited.add(owner)) { + return; + } + + for (var entry : methodMappings.entrySet()) { + var key = entry.getKey(); + if (key.owner().equals(owner)) { + inheritedMethods.putIfAbsent(new MemberKey(null, key.name(), key.descriptor()), entry.getValue()); + continue; + } + + var mappedOwner = classMappings.getOrDefault(owner, owner); + if (key.owner().equals(mappedOwner)) { + var obfuscatedDescriptor = classOnlyRemapper.mapMethodDesc(key.descriptor()); + inheritedMethods.putIfAbsent(new MemberKey(null, key.name(), obfuscatedDescriptor), entry.getValue()); + } + } + + var classInfo = classInfos.get(owner); + if (classInfo == null) { + classInfo = classInfos.get(classMappings.get(owner)); + } + if (classInfo == null) { + return; + } + + for (var method : classInfo.methods()) { + if ((method.access() & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0) { + inheritedMethods.remove(new MemberKey(null, method.name(), method.descriptor())); + } + } + + addInheritedMethodMappings(classInfo.superName(), classInfos, classOnlyRemapper, visited, inheritedMethods); + for (var interfaceName : classInfo.interfaces()) { + addInheritedMethodMappings(interfaceName, classInfos, classOnlyRemapper, visited, inheritedMethods); + } + } + + private String findMappedOverrideName( + String superName, + List interfaces, + String name, + String descriptor, + Map classInfos, + Remapper classOnlyRemapper, + HashSet visited) { + var mappedName = findMappedOverrideName(superName, name, descriptor, classInfos, classOnlyRemapper, visited); + if (mappedName != null) { + return mappedName; + } + + for (var interfaceName : interfaces) { + mappedName = findMappedOverrideName(interfaceName, name, descriptor, classInfos, classOnlyRemapper, visited); + if (mappedName != null) { + return mappedName; + } + } + return null; + } + + private String findMappedOverrideName( + String owner, + String name, + String descriptor, + Map classInfos, + Remapper classOnlyRemapper, + HashSet visited) { + if (owner == null || !visited.add(owner)) { + return null; + } + + var mappedName = mappedMethodName(owner, name, descriptor, classOnlyRemapper); + if (mappedName != null) { + return mappedName; + } + + var classInfo = classInfos.get(owner); + if (classInfo == null) { + classInfo = classInfos.get(classMappings.get(owner)); + } + if (classInfo == null) { + return null; + } + + return findMappedOverrideName( + classInfo.superName(), + classInfo.interfaces(), + name, + descriptor, + classInfos, + classOnlyRemapper, + visited); + } + + private String mappedMethodName(String owner, String name, String descriptor, Remapper classOnlyRemapper) { + var mappedName = methodMappings.get(new MemberKey(owner, name, descriptor)); + if (mappedName != null) { + return mappedName; + } + + var mappedOwner = classMappings.getOrDefault(owner, owner); + var mappedDescriptor = classOnlyRemapper.mapMethodDesc(descriptor); + mappedName = methodMappings.get(new MemberKey(mappedOwner, name, mappedDescriptor)); + if (mappedName != null) { + return mappedName; + } + + mappedName = methodMappings.get(new MemberKey(owner, name, mappedDescriptor)); + if (mappedName != null) { + return mappedName; + } + + return methodMappings.get(new MemberKey(mappedOwner, name, descriptor)); + } + } + + private record SrgToMcpMappings(Map fieldMappings, Map methodMappings) {} + + private record Member(String owner, String name) {} + + private record MemberKey(String owner, String name, String descriptor) {} + + private record ClassInfo(String name, String superName, List interfaces, List fields, List methods) {} + + private record FieldInfo(int access, String name, String descriptor) {} + + private record MethodInfo(int access, String name, String descriptor) {} +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeMetadataTransform.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeMetadataTransform.java new file mode 100644 index 00000000..d4686fa5 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeMetadataTransform.java @@ -0,0 +1,127 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import javax.inject.Inject; +import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import org.gradle.api.Action; +import org.gradle.api.artifacts.CacheableRule; +import org.gradle.api.artifacts.ComponentMetadataContext; +import org.gradle.api.artifacts.DirectDependenciesMetadata; +import org.gradle.api.artifacts.MutableVariantFilesMetadata; +import org.gradle.api.artifacts.repositories.RepositoryResourceAccessor; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.model.ObjectFactory; + +@CacheableRule +public class LegacyForgeMetadataTransform extends LegacyMetadataTransform { + @Inject + public LegacyForgeMetadataTransform(ObjectFactory objects, RepositoryResourceAccessor repositoryResourceAccessor) { + super(objects, repositoryResourceAccessor); + } + + @Override + public void execute(ComponentMetadataContext context) { + var versionCapabilities = VersionCapabilitiesInternal.ofForgeVersion(context.getDetails().getId().getVersion()); + executeWithConfig(context, createPath(context, LegacyForgeArtifacts.userdevClassifier(versionCapabilities), "jar")); + } + + @Override + public void adaptWithConfig(ComponentMetadataContext context, JsonObject config) { + var details = context.getDetails(); + var id = details.getId(); + var versionCapabilities = VersionCapabilitiesInternal.ofForgeVersion(id.getVersion()); + + var userdevJarName = LegacyForgeArtifacts.userdevJarName(id.getName(), id.getVersion(), versionCapabilities); + var universalJarName = id.getName() + "-" + id.getVersion() + "-universal.jar"; + + Action vanillaDependencies = deps -> { + deps.add("de.oceanlabs.mcp:mcp_config:" + id.getVersion().split("-")[0]); + deps.add("net.neoforged:minecraft-dependencies:" + id.getVersion().split("-")[0]); + }; + + details.addVariant("modDevConfig", variantMetadata -> { + variantMetadata.withFiles(metadata -> metadata.addFile(userdevJarName, userdevJarName)); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoforge-moddev-config", id.getVersion()); + }); + }); + details.addVariant("modDevBundle", variantMetadata -> { + variantMetadata.withFiles(metadata -> metadata.addFile(userdevJarName, userdevJarName)); + variantMetadata.withDependencies(vanillaDependencies); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoforge-moddev-bundle", id.getVersion()); + }); + }); + details.addVariant("modDevModulePath", variantMetadata -> { + variantMetadata.withDependencies(dependencies -> { + // Support versions that do not declare modules + if (config.has("modules")) { + var modules = config.getAsJsonArray("modules"); + for (JsonElement module : modules) { + dependencies.add(module.getAsString()); + } + } + }); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoforge-moddev-module-path", id.getVersion()); + }); + }); + details.addVariant("modDevApiElements", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_API)); + }); + variantMetadata.withDependencies(dependencies -> { + var libraries = config.getAsJsonArray("libraries"); + for (JsonElement library : libraries) { + dependencies.add(library.getAsString()); + } + }); + variantMetadata.withDependencies(vanillaDependencies); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoforge-dependencies", id.getVersion()); + }); + }); + details.addVariant("modDevRuntimeElements", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME)); + }); + variantMetadata.withCapabilities(capabilities -> { + capabilities.removeCapability(id.getGroup(), id.getName()); + capabilities.addCapability("net.neoforged", "neoforge-dependencies", id.getVersion()); + }); + variantMetadata.withDependencies(vanillaDependencies); + variantMetadata.withFiles(MutableVariantFilesMetadata::removeAllFiles); + variantMetadata.withDependencies(dependencies -> { + // Support versions that do not declare modules + if (config.has("modules")) { + var modules = config.getAsJsonArray("modules"); + for (JsonElement module : modules) { + dependencies.add(module.getAsString()); + } + } + var libraries = config.getAsJsonArray("libraries"); + for (JsonElement library : libraries) { + dependencies.add(library.getAsString()); + } + }); + }); + + details.addVariant("universalJar", variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + }); + variantMetadata.withFiles(metadata -> metadata.addFile(universalJarName, universalJarName)); + }); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyMetadataTransform.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyMetadataTransform.java new file mode 100644 index 00000000..05f77133 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyMetadataTransform.java @@ -0,0 +1,63 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.jar.JarInputStream; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.ComponentMetadataContext; +import org.gradle.api.artifacts.ComponentMetadataRule; +import org.gradle.api.artifacts.repositories.RepositoryResourceAccessor; +import org.gradle.api.model.ObjectFactory; + +public abstract class LegacyMetadataTransform implements ComponentMetadataRule { + protected final ObjectFactory objects; + private final RepositoryResourceAccessor repositoryResourceAccessor; + + LegacyMetadataTransform(ObjectFactory objects, RepositoryResourceAccessor repositoryResourceAccessor) { + this.objects = objects; + this.repositoryResourceAccessor = repositoryResourceAccessor; + } + + protected final void executeWithConfig(ComponentMetadataContext context, String path) { + JsonObject[] configRootHolder = new JsonObject[1]; + repositoryResourceAccessor.withResource(path, inputStream -> { + try (var zin = new JarInputStream(new BufferedInputStream(inputStream))) { + for (var entry = zin.getNextJarEntry(); entry != null; entry = zin.getNextJarEntry()) { + if (entry.getName().equals("config.json")) { + var configJson = new String(zin.readAllBytes(), StandardCharsets.UTF_8); + configRootHolder[0] = new Gson().fromJson(configJson, JsonObject.class); + } + } + } catch (IOException e) { + throw new GradleException("Failed to read " + path); + } + }); + + if (configRootHolder[0] == null) { + throw new GradleException("Couldn't find config.json in " + path); + } + adaptWithConfig(context, configRootHolder[0]); + + // Use a fake capability to make it impossible for the implicit variants to be selected + for (var implicitVariantName : List.of("compile", "runtime")) { + var details = context.getDetails(); + details.withVariant(implicitVariantName, variant -> { + variant.withCapabilities(caps -> { + caps.removeCapability(details.getId().getGroup(), details.getId().getName()); + caps.addCapability("___dummy___", "___dummy___", "___dummy___"); + }); + }); + } + } + + protected abstract void adaptWithConfig(ComponentMetadataContext context, JsonObject config); + + protected final String createPath(ComponentMetadataContext context, String classifier, String extension) { + var id = context.getDetails().getId(); + return id.getGroup().replace('.', '/') + "/" + id.getName() + "/" + id.getVersion() + "/" + (id.getName() + "-" + id.getVersion() + (classifier.isBlank() ? "" : "-" + classifier) + "." + extension); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/Lwjgl2Natives.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/Lwjgl2Natives.java new file mode 100644 index 00000000..404ab781 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/Lwjgl2Natives.java @@ -0,0 +1,67 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.util.List; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ExternalModuleDependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.dsl.DependencyFactory; + +final class Lwjgl2Natives { + static final List APPLE_NATIVE_REPLACEMENT_DEPENDENCIES = List.of( + "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", + "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", + "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209-mmachina.2:natives-osx@jar", + "ca.weblite:java-objc-bridge:1.1.0-mmachina.1", + "com.mojang:text2speech:1.11.3"); + + private Lwjgl2Natives() {} + + static void configureRuntime(Configuration configuration, DependencyFactory dependencyFactory, String minecraftVersion) { + configureRuntime(configuration, dependencyFactory, minecraftVersion, System.getProperty("os.name"), System.getProperty("os.arch")); + } + + static void configureRuntime(Configuration configuration, DependencyFactory dependencyFactory, String minecraftVersion, String osName, String osArch) { + if (!shouldUseAppleNativeReplacement(minecraftVersion, osName, osArch)) { + return; + } + + replaceMojangLwjgl2Dependencies(configuration); + for (var notation : appleNativeReplacementDependencies()) { + var dependency = dependencyFactory.create(notation); + ((ExternalModuleDependency) dependency).setTransitive(false); + configuration.getDependencies().add(dependency); + } + } + + static void configure(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion) { + configure(nativeLibraries, dependencyFactory, minecraftVersion, System.getProperty("os.name"), System.getProperty("os.arch")); + } + + static void configure(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion, String osName, String osArch) { + if (shouldUseAppleNativeReplacement(minecraftVersion, osName, osArch)) { + nativeLibraries.getDependencies().add(dependencyFactory.create("org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209-mmachina.2:natives-osx@jar")); + } + } + + static List appleNativeReplacementDependencies() { + return APPLE_NATIVE_REPLACEMENT_DEPENDENCIES; + } + + static boolean shouldUseAppleNativeReplacement(String minecraftVersion, String osName, String osArch) { + return "1.12.2".equals(minecraftVersion) + && osName.startsWith("Mac OS X") + && ("aarch64".equals(osArch) || "arm64".equals(osArch)); + } + + private static void replaceMojangLwjgl2Dependencies(Configuration configuration) { + configuration.getDependencies().configureEach(dependency -> { + if (dependency instanceof ModuleDependency moduleDependency) { + moduleDependency.exclude(java.util.Map.of("group", "org.lwjgl.lwjgl", "module", "lwjgl")); + moduleDependency.exclude(java.util.Map.of("group", "org.lwjgl.lwjgl", "module", "lwjgl_util")); + moduleDependency.exclude(java.util.Map.of("group", "org.lwjgl.lwjgl", "module", "lwjgl-platform")); + moduleDependency.exclude(java.util.Map.of("group", "ca.weblite", "module", "java-objc-bridge")); + moduleDependency.exclude(java.util.Map.of("group", "com.mojang", "module", "text2speech")); + } + }); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MappingsDisambiguationRule.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MappingsDisambiguationRule.java new file mode 100644 index 00000000..18b6fcf8 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MappingsDisambiguationRule.java @@ -0,0 +1,28 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import javax.inject.Inject; +import net.neoforged.moddevgradle.legacyforge.internal.MinecraftMappings; +import org.gradle.api.attributes.AttributeDisambiguationRule; +import org.gradle.api.attributes.MultipleCandidatesDetails; + +/** + * This disambiguation rule will prefer NAMED over SRG when both are present. + */ +public class MappingsDisambiguationRule implements AttributeDisambiguationRule { + private final MinecraftMappings named; + + @Inject + public MappingsDisambiguationRule(MinecraftMappings named) { + this.named = named; + } + + @Override + public void execute(MultipleCandidatesDetails details) { + var consumerValue = details.getConsumerValue(); + if (consumerValue == null) { + if (details.getCandidateValues().contains(named)) { + details.closestMatch(named); + } + } + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java new file mode 100644 index 00000000..3ed9b4f6 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -0,0 +1,623 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import javax.inject.Inject; +import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin; +import net.neoforged.moddevgradle.internal.ArtifactNamingStrategy; +import net.neoforged.moddevgradle.internal.Branding; +import net.neoforged.moddevgradle.internal.DataFileCollections; +import net.neoforged.moddevgradle.internal.JarPostProcessor; +import net.neoforged.moddevgradle.internal.McpToolchainHooks; +import net.neoforged.moddevgradle.internal.ModDevArtifactsWorkflow; +import net.neoforged.moddevgradle.internal.ModDevRunWorkflow; +import net.neoforged.moddevgradle.internal.ModdingDependencies; +import net.neoforged.moddevgradle.internal.RunGameTask; +import net.neoforged.moddevgradle.internal.jarjar.JarJarPlugin; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.internal.utils.StringUtils; +import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import net.neoforged.moddevgradle.legacyforge.tasks.PopulateForgeGradleMcpCache; +import net.neoforged.moddevgradle.mcpforge.dsl.McpForgeModdingSettings; +import net.neoforged.moddevgradle.legacyforge.dsl.ObfuscationExtension; +import net.neoforged.moddevgradle.legacyforge.internal.LegacyForgeLibraryMetadataRule; +import net.neoforged.moddevgradle.legacyforge.internal.LegacyRepositoriesPlugin; +import net.neoforged.moddevgradle.legacyforge.internal.MinecraftMappings; +import net.neoforged.moddevgradle.legacyforge.internal.NonStrictDependencyTransform; +import net.neoforged.moddevgradle.mcpforge.dsl.McpForgeExtension; +import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; +import net.neoforged.nfrtgradle.NeoFormRuntimeExtension; +import net.neoforged.nfrtgradle.NeoFormRuntimePlugin; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.dsl.DependencyFactory; +import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaLibraryPlugin; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.tasks.Jar; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The MCP-based Forge toolchain for 1.12.2 and other legacy versions (plugin id {@code net.neoforged.moddev.mcpforge}). + * + *

This plugin owns its pipeline independently (it does NOT apply the {@code legacyforge} plugin), reusing legacy's + * public classes and a set of MCP-specific SPI hooks (LWJGL2 Apple Silicon natives, Forge deobf-data remapping, + * coremod discovery, launchwrapper Java 8 runtime, pre-1.13 resource merge, SRG refmap). + */ +public class McpForgeModDevPlugin implements Plugin { + private static final Logger LOG = LoggerFactory.getLogger(McpForgeModDevPlugin.class); + + public static final String MIXIN_EXTENSION = "mixin"; + public static final String OBFUSCATION_EXTENSION = "obfuscation"; + public static final String MCPFORGE_EXTENSION = "mcpForge"; + + public static final String CONFIGURATION_TOOL_ART = "autoRenamingToolRuntime"; + public static final String CONFIGURATION_TOOL_INSTALLERTOOLS = "installerToolsRuntime"; + private static final String LEGACY_MCP_NFRT_EXAMPLE = "2.0.19-legacy"; + + private final MinecraftMappings namedMappings; + private final MinecraftMappings srgMappings; + + @Inject + public McpForgeModDevPlugin(ObjectFactory objectFactory) { + namedMappings = objectFactory.named(MinecraftMappings.class, MinecraftMappings.NAMED); + srgMappings = objectFactory.named(MinecraftMappings.class, MinecraftMappings.SRG); + } + + @Override + public void apply(Project project) { + // Base plugins + project.getPlugins().apply(JavaLibraryPlugin.class); + project.getPlugins().apply(NeoFormRuntimePlugin.class); + project.getPlugins().apply(MinecraftDependenciesPlugin.class); + project.getPlugins().apply(JarJarPlugin.class); + + // Skip applying repositories at the project level if they were already applied at settings level. + if (!project.getGradle().getPlugins().hasPlugin(LegacyRepositoriesPlugin.class)) { + project.getPlugins().apply(LegacyRepositoriesPlugin.class); + } else { + LOG.info("Not enabling legacy repositories since they were applied at the settings level"); + } + + // Apple Silicon LWJGL2 native repos (GitHub releases by MinecraftMachina). Needed for 1.12.2 on ARM64 macOS. + var repos = project.getRepositories(); + repos.ivy(repo -> { + repo.setName("MinecraftMachina Apple Silicon LWJGL2"); + repo.setUrl(URI.create("https://github.com/MinecraftMachina/lwjgl/releases/download/2.9.4-20150209-mmachina.2/")); + repo.patternLayout(layout -> layout.artifact("[module]-2.9.4-nightly-20150209-[classifier].[ext]")); + repo.metadataSources(sources -> sources.artifact()); + repo.content(content -> { + content.includeModule("org.lwjgl.lwjgl", "lwjgl"); + content.includeModule("org.lwjgl.lwjgl", "lwjgl-platform"); + content.includeModule("org.lwjgl.lwjgl", "lwjgl_util"); + }); + }); + repos.ivy(repo -> { + repo.setName("MinecraftMachina Java Objective-C Bridge"); + repo.setUrl(URI.create("https://github.com/MinecraftMachina/Java-Objective-C-Bridge/releases/download/1.1.0-mmachina.1/")); + repo.patternLayout(layout -> layout.artifact("[module]-1.1.[ext]")); + repo.metadataSources(sources -> sources.artifact()); + repo.content(content -> content.includeModule("ca.weblite", "java-objc-bridge")); + }); + + // Metadata transforms + project.getDependencies().getComponents().withModule("net.minecraftforge:forge", LegacyForgeMetadataTransform.class); + project.getDependencies().getComponents().withModule("net.minecraftforge:forge", LegacyForgeLibraryMetadataRule.class); + project.getDependencies().getComponents().withModule("de.oceanlabs.mcp:mcp_config", McpMetadataTransform.class); + // Legacy Forge upgrades some deps (e.g. log4j2); relax the strict version requirements we can't otherwise fix. + project.getDependencies().getComponents().withModule("net.neoforged:minecraft-dependencies", NonStrictDependencyTransform.class); + + // Tool configurations + var depFactory = project.getDependencyFactory(); + var autoRenamingToolRuntime = project.getConfigurations().create(CONFIGURATION_TOOL_ART, spec -> { + spec.setDescription("The AutoRenamingTool CLI tool"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + spec.setTransitive(false); + spec.getDependencies().add(depFactory.create("net.neoforged:AutoRenamingTool:2.0.4:all")); + }); + var installerToolsRuntime = project.getConfigurations().create(CONFIGURATION_TOOL_INSTALLERTOOLS, spec -> { + spec.setDescription("The InstallerTools CLI tool"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + spec.setTransitive(false); + spec.getDependencies().add(depFactory.create("net.neoforged.installertools:installertools:3.0.4:fatjar")); + }); + + // Extensions. extraMixinMappings shares mixin-generated mappings with the obfuscation extension. + var extraMixinMappings = project.files(); + var obf = project.getExtensions().create(OBFUSCATION_EXTENSION, ObfuscationExtension.class, project, autoRenamingToolRuntime, installerToolsRuntime, extraMixinMappings); + var mixin = project.getExtensions().create(MIXIN_EXTENSION, MixinExtension.class, project, obf.getNamedToSrgMappings(), extraMixinMappings); + // Defaults matching MixinGradle. + mixin.getDefaultObfuscationEnv().convention("searge"); + mixin.getQuiet().convention(false); + mixin.getShowMessageTypes().convention(false); + mixin.getDisableTargetValidator().convention(false); + mixin.getDisableTargetExport().convention(false); + mixin.getDisableOverwriteChecker().convention(false); + + configureDependencyRemapping(project, obf); + + var dataFileCollections = DataFileCollections.create(project); + project.getExtensions().create( + MCPFORGE_EXTENSION, + McpForgeExtension.class, + project, + dataFileCollections.accessTransformers().extension(), + dataFileCollections.interfaceInjectionData().extension()); + + + // Register MCP hooks so the main workflow picks up LWJGL2 natives + jar post-processing. + project.getExtensions().add(McpToolchainHooks.EXTENSION_NAME, new McpHooks()); + + // Register the 1.12.2 jar post-processor for all CreateMinecraftArtifacts tasks. + project.getTasks().withType(CreateMinecraftArtifacts.class).configureEach(task -> { + task.getJarPostProcessors().add(new ForgeJarPostProcessor()); + }); + + // Collect Access Transformers declared by dependency jars via their FMLAT manifest (FG2/RFG parity). + configureDependencyAccessTransformers(project); + + // Configure RunGameTask tasks (lazy — matches whenever they are created). + configureRunTasks(project); + } + + + public void enable(Project project, McpForgeModdingSettings settings, McpForgeExtension extension) { + var depFactory = project.getDependencyFactory(); + + var forgeVersion = settings.getForgeVersion(); + var neoForgeVersion = settings.getNeoForgeVersion(); + var mcpVersion = settings.getMcpVersion(); + + ModdingDependencies dependencies; + ArtifactNamingStrategy artifactNamingStrategy; + VersionCapabilitiesInternal versionCapabilities; + if (forgeVersion != null || neoForgeVersion != null) { + // All settings are mutually exclusive + if (forgeVersion != null && neoForgeVersion != null || mcpVersion != null) { + throw new InvalidUserCodeException("Specifying a Forge version is mutually exclusive with NeoForge or MCP"); + } + + var version = forgeVersion != null ? forgeVersion : neoForgeVersion; + versionCapabilities = VersionCapabilitiesInternal.ofForgeVersion(version); + validateNeoFormRuntimeSupport(project, versionCapabilities); + artifactNamingStrategy = ArtifactNamingStrategy.createNeoForge(versionCapabilities, "forge", version); + + String groupId = forgeVersion != null ? "net.minecraftforge" : "net.neoforged"; + var neoForge = depFactory.create(groupId + ":forge:" + version); + var neoForgeNotation = LegacyForgeArtifacts.userdevNotation(groupId, version, versionCapabilities); + dependencies = ModdingDependencies.create(neoForge, neoForgeNotation, null, null, versionCapabilities); + } else if (mcpVersion != null) { + versionCapabilities = VersionCapabilitiesInternal.ofMinecraftVersion(mcpVersion); + artifactNamingStrategy = ArtifactNamingStrategy.createVanilla(mcpVersion); + + var neoForm = depFactory.create("de.oceanlabs.mcp:mcp_config:" + mcpVersion); + var neoFormNotation = "de.oceanlabs.mcp:mcp_config:" + mcpVersion + "@zip"; + dependencies = ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation); + } else { + throw new InvalidUserCodeException("You must specify a Forge, NeoForge or MCP version"); + } + + var configurations = project.getConfigurations(); + + var artifacts = ModDevArtifactsWorkflow.create( + project, + settings.getEnabledSourceSets(), + Branding.MDG, + extension, + dependencies, + artifactNamingStrategy, + configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), + configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), + versionCapabilities, + settings.isDisableRecompilation(), + settings.getMcpMappings()); + + // Configure the mixin and obfuscation extensions. + var mixin = ExtensionUtils.getExtension(project, MIXIN_EXTENSION, MixinExtension.class); + var obf = ExtensionUtils.getExtension(project, OBFUSCATION_EXTENSION, ObfuscationExtension.class); + + var namedToIntermediate = artifacts.requestAdditionalMinecraftArtifact("namedToIntermediaryMapping", "namedToIntermediate.tsrg"); + obf.getNamedToSrgMappings().set(namedToIntermediate); + var intermediateToNamed = artifacts.requestAdditionalMinecraftArtifact("intermediaryToNamedMapping", "intermediateToNamed.srg"); + var mappingsCsv = artifacts.requestAdditionalMinecraftArtifact("csvMapping", "intermediateToNamed.zip"); + obf.getSrgToNamedMappings().set(mappingsCsv); + var notchToIntermediate = artifacts.requestAdditionalMinecraftArtifact("notchToIntermediaryMapping", "notchToIntermediate.srg"); + + // ForgeGradle-2 compatibility: mirror the MCP data into the FG-2 cache layout + // (~/.gradle/caches/minecraft/de/oceanlabs/mcp///) so legacy tooling/scripts that hardcode the + // FG-2 path keep working. Only done for legacy MCP versions (e.g. 1.12.2). + if (settings.getMcpMappings() != null) { + // Resolve the FG-2 cache base dir at configuration time so the task stays config-cache compatible. + var mcpCoordinate = settings.getMcpMappings(); + var gradleUserHome = project.getGradle().getGradleUserHomeDir().toPath().toAbsolutePath().toString(); + var fg2CacheBase = project.getProviders().provider(() -> { + var withoutExt = mcpCoordinate.indexOf('@') >= 0 ? mcpCoordinate.substring(0, mcpCoordinate.indexOf('@')) : mcpCoordinate; + var parts = withoutExt.split(":"); + var name = parts.length > 1 ? parts[1] : "mcp"; + var rawVersion = parts.length > 2 ? parts[2] : "unknown"; + var version = rawVersion.indexOf('-') >= 0 ? rawVersion.substring(0, rawVersion.indexOf('-')) : rawVersion; + return Path.of(gradleUserHome, "caches", "minecraft", "de", "oceanlabs", "mcp", name, version).toString(); + }); + project.getTasks().register("populateForgeGradleMcpCache", PopulateForgeGradleMcpCache.class, task -> { + task.setGroup(Branding.MDG.internalTaskGroup()); + task.setDescription("Populates the ForgeGradle-2 MCP cache directory with ModDevGradle's MCP data for legacy-tool compatibility."); + task.getMcpMappings().set(mcpCoordinate); + task.getMinecraftVersion().set(versionCapabilities.minecraftVersion()); + task.getCacheBaseDirectory().set(fg2CacheBase); + task.getCsvMappings().set(mappingsCsv); + task.getSrgToMcpMappings().set(intermediateToNamed); + task.getMcpToSrgMappings().set(namedToIntermediate); + task.getNotchToSrgMappings().set(notchToIntermediate); + }); + project.getTasks().named("createMinecraftArtifacts", task -> task.finalizedBy("populateForgeGradleMcpCache")); + } + + var runs = ModDevRunWorkflow.create( + project, + Branding.MDG, + artifacts, + extension.getRuns(), + Map.of( + "mcp_to_srg", intermediateToNamed.map(file -> file.getAsFile().getAbsolutePath()), + "mcp_mappings", mappingsCsv.map(file -> file.getAsFile().getAbsolutePath()))); + + extension.getRuns().configureEach(run -> { + // Old BSL versions before 2022 did not export any packages, blocking DevLaunch from the main method. + if (versionCapabilities.javaVersion() > 8) { + run.getJvmArguments().addAll("--add-exports", "cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED"); + } + if ("1.12.2".equals(versionCapabilities.minecraftVersion())) { + run.getSystemProperties().put("fml.ignorePatchDiscrepancies", "true"); + } + + if (!versionCapabilities.modLocatorRework()) { + // Pre-1.13 FML only loads a mod's resources from the @Mod class' source directory, so Gradle's split + // output (classes vs resources) leaves assets/mcmod.info/lang missing at runtime. Colocate them. + var modSourceSet = run.getSourceSet().get(); + modSourceSet.getOutput().setResourcesDir(modSourceSet.getJava().getDestinationDirectory().get().getAsFile()); + } + + // Mixin needs the SRG->named mapping in SRG (not TSRG) format to ignore dependency refmaps. + run.getSystemProperties().put("mixin.env.remapRefMap", "true"); + run.getSystemProperties().put("mixin.env.refMapRemappingFile", intermediateToNamed.map(f -> f.getAsFile().getAbsolutePath())); + + run.getProgramArguments().addAll(mixin.getConfigs().map(cfgs -> cfgs.stream().flatMap(config -> Stream.of("--mixin.config", config)).toList())); + }); + + if (settings.isObfuscateJar()) { + var reobfJar = obf.reobfuscate( + project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class), + project.getExtensions().getByType(SourceSetContainer.class).getByName(SourceSet.MAIN_SOURCE_SET_NAME)); + + project.getTasks().named("assemble", assemble -> assemble.dependsOn(reobfJar)); + } + + // Forge expects the mapping csv files on the root classpath. + artifacts.runtimeDependencies() + .getDependencies().add(project.getDependencyFactory().create(project.files(mappingsCsv))); + + var remapDeps = project.getConfigurations().create("remappingDependencies", spec -> { + spec.setDescription("An internal configuration that contains the Minecraft dependencies, used for remapping mods"); + spec.setCanBeConsumed(false); + spec.setCanBeDeclared(false); + spec.setCanBeResolved(true); + spec.extendsFrom(artifacts.runtimeDependencies()); + }); + + // Resolve the declared Mixin provider coordinates (mixinProvider DSL) to their raw SRG jars so the + // RemappingTransform can exempt them from the bundled-spongepowered strip. Resolved lazily. + var mixinProviderConfig = project.getConfigurations().create("mcpMixinProviders", spec -> { + spec.setDescription("Mixin provider jars — exempt from bundled-spongepowered stripping"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + spec.setTransitive(false); + spec.getAttributes().attribute(MinecraftMappings.ATTRIBUTE, srgMappings); + }); + // Defer reading the mixinProvider list to resolution time so the build script can call mixinProvider() before + // OR after enable(). + mixinProviderConfig.withDependencies(deps -> + extension.getMixinProviders().get().forEach(notation -> + deps.add(project.getDependencyFactory().create(notation)))); + + // The RemappingTransform strips bundled org.spongepowered.asm.* from non-provider jars (post-remap) so an old + // bundled Mixin can't shadow the declared provider. + project.getDependencies().registerTransform(RemappingTransform.class, params -> { + params.parameters(parameters -> { + obf.configureSrgToNamedOperation(parameters.getRemapOperation()); + parameters.getMinecraftDependencies().from(remapDeps); + parameters.getMixinProviders().from(mixinProviderConfig); + }); + params.getFrom() + .attribute(MinecraftMappings.ATTRIBUTE, srgMappings) + .attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE); + params.getTo() + .attribute(MinecraftMappings.ATTRIBUTE, namedMappings) + .attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE); + }); + } + + private static void validateNeoFormRuntimeSupport(Project project, VersionCapabilitiesInternal versionCapabilities) { + if (!"1.12.2".equals(versionCapabilities.minecraftVersion())) { + return; + } + + var neoFormRuntime = ExtensionUtils.getExtension(project, NeoFormRuntimeExtension.NAME, NeoFormRuntimeExtension.class); + var nfrtVersion = neoFormRuntime.getVersion().get(); + if (!isKnownIncompatibleWithForge1122(nfrtVersion)) { + return; + } + + throw new InvalidUserCodeException(""" + Forge 1.12.2 requires NeoFormRuntime with legacy MCP support. The selected NeoFormRuntime version '%s' does not provide the --mcp-mappings CLI option used for Forge userdev3. + Set the Gradle property 'neoForge.neoFormRuntime.version' or the neoFormRuntime.version extension property to a compatible NFRT build, such as '%s', until that support is available in the default NFRT release.""".formatted(nfrtVersion, LEGACY_MCP_NFRT_EXAMPLE)); + } + + private static boolean isKnownIncompatibleWithForge1122(String nfrtVersion) { + return "2.0.18".equals(nfrtVersion) || "2.0.19".equals(nfrtVersion); + } + + private void configureDependencyRemapping(Project project, ObfuscationExtension obf) { + // JarJar cross-project deps must be remapped to SRG without affecting external deps (already in the right + // namespace). Requesting the srg attribute on cross-project deps excludes the named variant from selection. + var sourceSets = ExtensionUtils.getSourceSets(project); + sourceSets.all(sourceSet -> { + var configurationName = sourceSet.getTaskName(null, "jarJar"); + project.getConfigurations().getByName(configurationName).withDependencies(dependencies -> { + dependencies.forEach(dep -> { + if (dep instanceof ProjectDependency projectDependency) { + projectDependency.attributes(a -> { + a.attribute(MinecraftMappings.ATTRIBUTE, srgMappings); + }); + } + }); + }); + }); + + project.getDependencies().attributesSchema(schema -> { + var attr = schema.attribute(MinecraftMappings.ATTRIBUTE); + // Prefer named variants for cross-project deps where both named and obfuscated variants are available. + attr.getDisambiguationRules().add(MappingsDisambiguationRule.class, config -> { + config.params(namedMappings); + }); + }); + // Give every jar the srg attribute so it can be force-remapped by requesting named. + project.getDependencies().getArtifactTypes().named(ArtifactTypeDefinition.JAR_TYPE, type -> { + type.getAttributes().attribute(MinecraftMappings.ATTRIBUTE, srgMappings); + }); + + // Loom-style transitive mod* configurations: a modImplementation dep pulls its own transitives (e.g. + // CraftTweaker2-Main -> API -> ZenScript). Transitives resolve to the default SRG variant and are remapped to + // named by the SRG->named transform, triggered because the classpath configurations request named (below). + createTransitiveRemappingConfiguration(project, project.getConfigurations().getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME)); + createTransitiveRemappingConfiguration(project, project.getConfigurations().getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME)); + createTransitiveRemappingConfiguration(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME)); + createTransitiveRemappingConfiguration(project, project.getConfigurations().getByName(JavaPlugin.API_CONFIGURATION_NAME)); + createTransitiveRemappingConfiguration(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_ONLY_API_CONFIGURATION_NAME)); + + // Request the named variant on the classpath configurations so the SRG->named transform fires on every jar + // in the graph. Non-MC libs (guava, etc.) have no SRG names, so the transform is a no-op/cache-hit for them. + for (var configName : new String[]{ + JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME, + "testCompileClasspath", + "testRuntimeClasspath"}) { + project.getConfigurations().named(configName, c -> + c.getAttributes().attribute(MinecraftMappings.ATTRIBUTE, namedMappings)); + } + } + + /** + * Creates a transitive {@code mod} configuration (e.g. {@code modImplementation}) that the parent extends + * from. Remapping is driven by the classpath configurations requesting the named attribute. + */ + private static void createTransitiveRemappingConfiguration(Project project, Configuration parent) { + var modConfig = project.getConfigurations().create( + "mod" + StringUtils.capitalize(parent.getName()), spec -> { + spec.setDescription("Mod dependencies of " + parent.getName() + " (transitive, remapped SRG->named)"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(false); + spec.setTransitive(true); + }); + parent.extendsFrom(modConfig); + } + + /** + * Extracts Access Transformers declared via the {@code FMLAT} manifest attribute in dependency jars and feeds + * them into the {@code accessTransformers} DataFileCollection so NFRT bakes them into the recompiled MC source. + */ + private void configureDependencyAccessTransformers(Project project) { + var atScan = project.getConfigurations().create("mcpAccessTransformerScan", c -> { + c.setDescription("Mod dependencies scanned for FMLAT access transformers (raw SRG variant, no remapping)"); + c.setCanBeConsumed(false); + c.setCanBeResolved(true); + c.getAttributes().attribute( + MinecraftMappings.ATTRIBUTE, + project.getObjects().named(MinecraftMappings.class, MinecraftMappings.SRG)); + }); + for (var name : new String[]{"modImplementation", "modCompileOnly", "modRuntimeOnly"}) { + var bucket = project.getConfigurations().findByName(name); + if (bucket != null) { + atScan.extendsFrom(bucket); + } + } + + var extractDepAts = project.getTasks().register( + "extractDependencyAccessTransformers", ExtractDependencyAccessTransformers.class, task -> { + task.setGroup(Branding.MDG.internalTaskGroup()); + task.setDescription("Extracts Access Transformers declared via FMLAT manifests in dependency jars (FG2/RFG parity)."); + task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir("moddev/dependencyATs")); + task.getDependencies().from(atScan); + }); + + var lfExt = project.getExtensions().getByType(McpForgeExtension.class); + lfExt.getAccessTransformers().from(extractDepAts.map(t -> t.getOutputDirectory().getAsFileTree())); + project.getTasks().withType(CreateMinecraftArtifacts.class).configureEach(t -> t.dependsOn(extractDepAts)); + } + + private void configureRunTasks(Project project) { + project.getTasks().withType(RunGameTask.class).configureEach(task -> { + // launchwrapper requires Java 8; force a Java 8 launcher when the project toolchain is > 8 (e.g. Jabel). + var javaExt = project.getExtensions().findByType(JavaPluginExtension.class); + if (javaExt != null && javaExt.getToolchain() != null) { + var tv = javaExt.getToolchain().getLanguageVersion(); + if (tv.isPresent() && tv.get().asInt() > 8) { + var tsObj = project.getExtensions().findByName("javaToolchains"); + if (tsObj instanceof JavaToolchainService ts) { + task.getJavaLauncher().set(ts.launcherFor(spec -> + spec.getLanguageVersion().set(JavaLanguageVersion.of(8)))); + } + } + } + + + // Coremod discovery + FG2 cache properties: scan classpathProvider at doFirst time. + task.doFirst(t -> { + var cp = task.getClasspathProvider().getFiles(); + + PopulateForgeGradleMcpCache cacheTask = + (PopulateForgeGradleMcpCache) + project.getTasks().getByName("populateForgeGradleMcpCache"); + String cacheBase = cacheTask.getCacheBaseDirectory().get(); + String mcVersion = cacheTask.getMinecraftVersion().get(); + Path notchSrg = Path.of(cacheBase, mcVersion, "srgs", "notch-srg.srg"); + if (Files.exists(notchSrg)) { + task.systemProperty("net.minecraftforge.gradle.GradleStart.srg.notch-srg", notchSrg.toString()); + task.systemProperty("net.minecraftforge.gradle.GradleStart.csvDir", cacheBase); + } + + var coremodClasses = discoverCoremods(project, cp); + var existing = task.getSystemProperties().get("fml.coreMods.load"); + if (existing instanceof String s && !s.isBlank()) { + for (var c : s.split(",")) { + if (!c.isBlank()) coremodClasses.add(c.trim()); + } + } + if (!coremodClasses.isEmpty()) { + var joined = String.join(",", coremodClasses); + project.getLogger().lifecycle("MCP coremod discovery: fml.coreMods.load={}", joined); + task.systemProperty("fml.coreMods.load", joined); + } + }); + }); + } + + private static Set discoverCoremods(Project project, Set files) { + var coremodClasses = new LinkedHashSet(); + coremodClasses.addAll(scanManifests(files)); + var fromProperty = resolveCoreModClassProperty(project); + if (fromProperty != null) { + coremodClasses.add(fromProperty); + } + return coremodClasses; + } + + private static @Nullable String resolveCoreModClassProperty(Project project) { + var coreModPluginPath = project.findProperty("coreModPluginPath"); + if (coreModPluginPath != null) { + var path = coreModPluginPath.toString().trim(); + if (!path.isEmpty()) { + return path; + } + } + var coreModClass = project.findProperty("coreModClass"); + if (coreModClass == null) { + return null; + } + var value = coreModClass.toString().trim(); + if (value.isEmpty()) { + return null; + } + var modGroup = project.findProperty("modGroup"); + var prefix = modGroup != null && !modGroup.toString().isBlank() + ? modGroup.toString().trim() + : project.getGroup().toString(); + if (prefix.isEmpty()) { + project.getLogger().warn( + "coreModClass '{}' declared but neither 'modGroup' nor a project group is set; using as-is", + value); + return value; + } + return prefix + "." + value; + } + + private static Set scanManifests(Set files) { + var coremodClasses = new LinkedHashSet(); + var seen = new HashSet(); + for (var file : files) { + if (!seen.add(file.toPath())) { + continue; + } + var manifest = readManifest(file); + if (manifest == null) { + continue; + } + var corePlugin = manifest.getMainAttributes().getValue("FMLCorePlugin"); + if (corePlugin != null && !corePlugin.isBlank()) { + coremodClasses.add(corePlugin.trim()); + } + } + return coremodClasses; + } + + private static Manifest readManifest(File file) { + try { + if (file.isDirectory()) { + var mf = file.toPath().resolve("META-INF").resolve("MANIFEST.MF"); + if (!Files.exists(mf)) { + return null; + } + try (var in = Files.newInputStream(mf)) { + return new Manifest(in); + } + } else if (file.getName().endsWith(".jar")) { + try (var jar = new JarFile(file)) { + return jar.getManifest(); + } + } + } catch (IOException ignored) { + } + return null; + } + + static class McpHooks implements McpToolchainHooks { + @Override + public void configureRuntimeNatives(Configuration configuration, DependencyFactory dependencyFactory, String minecraftVersion) { + Lwjgl2Natives.configureRuntime(configuration, dependencyFactory, minecraftVersion); + } + + @Override + public void configureNativeLibraries(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion) { + Lwjgl2Natives.configure(nativeLibraries, dependencyFactory, minecraftVersion); + } + } + + static class ForgeJarPostProcessor implements JarPostProcessor { + @Override + public void process(Path jar, @Nullable Path srgToMcpMappings) throws IOException { + LegacyForgeJarProcessor.remapMinecraftReferences(jar, srgToMcpMappings); + } + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpMetadataTransform.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpMetadataTransform.java new file mode 100644 index 00000000..7322ac24 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpMetadataTransform.java @@ -0,0 +1,91 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.artifacts.CacheableRule; +import org.gradle.api.artifacts.ComponentMetadataContext; +import org.gradle.api.artifacts.DirectDependenciesMetadata; +import org.gradle.api.artifacts.repositories.RepositoryResourceAccessor; +import org.gradle.api.attributes.Usage; +import org.gradle.api.attributes.java.TargetJvmVersion; +import org.gradle.api.model.ObjectFactory; + +/** + * Given an implicit Metadata object by Gradle (which results from reading in a pom.xml from Maven for MCP data, + * which is basically empty), we build metadata that is equivalent to NeoForms Gradle module metadata. + *

+ * Example for NeoForm: + * https://maven.neoforged.net/releases/net/neoforged/neoform/1.21-20240613.152323/neoform-1.21-20240613.152323.module + */ +@CacheableRule +public class McpMetadataTransform extends LegacyMetadataTransform { + @Inject + public McpMetadataTransform(ObjectFactory objects, RepositoryResourceAccessor repositoryResourceAccessor) { + super(objects, repositoryResourceAccessor); + } + + @Override + public void execute(ComponentMetadataContext context) { + executeWithConfig(context, createPath(context, "", "zip")); + } + + @Override + protected void adaptWithConfig(ComponentMetadataContext context, JsonObject config) { + var details = context.getDetails(); + var id = details.getId(); + + var zipDataName = id.getName() + "-" + id.getVersion() + ".zip"; + + // Very old versions did not specify this. Default to 8 in those cases. + var javaTarget = config.has("java_target") + ? config.getAsJsonPrimitive("java_target").getAsInt() + : 8; + + // a.k.a. "neoFormData" + // Primarily pulled to use for NFRT manifest + details.addVariant("mcpData", variantMetadata -> { + variantMetadata.withFiles(files -> files.addFile(zipDataName)); + variantMetadata.attributes(attributes -> { + attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, javaTarget); + }); + // Add tools required by this version of MCP as dependencies of this variant + variantMetadata.withDependencies(dependencies -> { + var functions = config.getAsJsonObject("functions"); + for (var function : functions.entrySet()) { + var toolCoordinate = ((JsonObject) function.getValue()).getAsJsonPrimitive("version").getAsString(); + dependencies.add(toolCoordinate); + } + }); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoform", id.getVersion()); + }); + }); + + dependencies(context, "mcpRuntimeElements", javaTarget, Usage.JAVA_RUNTIME, deps -> {}); + + dependencies(context, "mcpApiElements", javaTarget, Usage.JAVA_API, dependencies -> { + var libraries = config.getAsJsonObject("libraries").getAsJsonArray("joined"); + for (JsonElement library : libraries) { + dependencies.add(library.getAsString()); + } + }); + } + + private void dependencies(ComponentMetadataContext context, String name, int javaTarget, String usage, Action deps) { + context.getDetails().addVariant(name, variantMetadata -> { + variantMetadata.attributes(attributes -> { + attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, javaTarget); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, usage)); + }); + variantMetadata.withDependencies(dependencies -> { + deps.execute(dependencies); + dependencies.add("net.neoforged:minecraft-dependencies:" + context.getDetails().getId().getVersion()); + }); + variantMetadata.withCapabilities(capabilities -> { + capabilities.addCapability("net.neoforged", "neoform-dependencies", context.getDetails().getId().getVersion()); + }); + }); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinCompilerArgs.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinCompilerArgs.java new file mode 100644 index 00000000..c2748c46 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinCompilerArgs.java @@ -0,0 +1,115 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.process.CommandLineArgumentProvider; + +abstract class MixinCompilerArgs implements CommandLineArgumentProvider { + @Inject + public MixinCompilerArgs() {} + + @OutputFile + protected abstract RegularFileProperty getOutMappings(); + + @OutputFile + protected abstract RegularFileProperty getRefmap(); + + @InputFile + @PathSensitive(PathSensitivity.NAME_ONLY) + protected abstract RegularFileProperty getInMappings(); + + @InputFiles + @PathSensitive(PathSensitivity.NONE) + protected abstract ConfigurableFileCollection getExtraMappings(); + + @Input @Optional + protected abstract Property getDefaultObfuscationEnv(); + + @Input @Optional + protected abstract Property getQuiet(); + + @Input @Optional + protected abstract Property getShowMessageTypes(); + + @Input @Optional + protected abstract Property getDisableTargetValidator(); + + @Input @Optional + protected abstract Property getDisableTargetExport(); + + @Input @Optional + protected abstract Property getDisableOverwriteChecker(); + + @Input @Optional + protected abstract Property getOverwriteErrorLevel(); + + @Input @Optional + protected abstract MapProperty getTokens(); + + @Input @Optional + protected abstract MapProperty getMessages(); + + @Override + public Iterable asArguments() { + var args = new ArrayList(); + args.add("-AreobfSrgFile=" + getInMappings().get().getAsFile().getAbsolutePath()); + args.add("-AoutRefMapFile=" + getRefmap().get().getAsFile().getAbsolutePath()); + args.add("-AdefaultObfuscationEnv=" + getDefaultObfuscationEnv().getOrElse("searge")); + + addFlag(args, getQuiet(), "-Aquiet=true"); + addFlag(args, getShowMessageTypes(), "-AshowMessageTypes=true"); + addFlag(args, getDisableTargetValidator(), "-AdisableTargetValidator=true"); + addFlag(args, getDisableTargetExport(), "-AdisableTargetExport=true"); + addFlag(args, getDisableOverwriteChecker(), "-AdisableOverwriteChecker=true"); + + if (getOverwriteErrorLevel().isPresent()) { + args.add("-AoverwriteErrorLevel=" + getOverwriteErrorLevel().get()); + } + + if (!getExtraMappings().getFiles().isEmpty()) { + var sb = new StringBuilder(); + for (var file : getExtraMappings().getFiles()) { + if (sb.length() > 0) sb.append(","); + sb.append(file.getAbsolutePath()); + } + args.add("-AreobfTsrgFiles=" + sb); + } + + if (getTokens().isPresent() && !getTokens().get().isEmpty()) { + var sb = new StringBuilder(); + for (var entry : getTokens().get().entrySet()) { + if (sb.length() > 0) sb.append(";"); + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + args.add("-Atokens=" + sb); + } + + if (getMessages().isPresent()) { + for (var entry : getMessages().get().entrySet()) { + if (entry.getKey().matches("^[A-Z]+[A-Z_]+$") && entry.getValue().matches("^(note|warning|error|disabled)$")) { + args.add("-AMSG_" + entry.getKey() + "=" + entry.getValue()); + } + } + } + + return args; + } + + private static void addFlag(List args, Property flag, String arg) { + if (flag.isPresent() && flag.get()) { + args.add(arg); + } + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinExtension.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinExtension.java new file mode 100644 index 00000000..d605b407 --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/MixinExtension.java @@ -0,0 +1,122 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.util.Map; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.jvm.tasks.Jar; + +public abstract class MixinExtension { + private final Project project; + private final Provider officialToSrg; + private final ConfigurableFileCollection extraMappingFiles; + + @Inject + public MixinExtension(Project project, + Provider officialToSrg, + ConfigurableFileCollection extraMappingFiles) { + this.project = project; + this.officialToSrg = officialToSrg; + this.extraMappingFiles = extraMappingFiles; + } + + public abstract Property getDefaultObfuscationEnv(); + public abstract Property getQuiet(); + public abstract Property getShowMessageTypes(); + public abstract Property getDisableTargetValidator(); + public abstract Property getDisableTargetExport(); + public abstract Property getDisableOverwriteChecker(); + public abstract Property getOverwriteErrorLevel(); + public abstract ConfigurableFileCollection getExtraMappings(); + public abstract MapProperty getTokens(); + public abstract MapProperty getMessages(); + public abstract ListProperty getConfigs(); + + public abstract MapProperty getDebugProperties(); + public abstract MapProperty getEnvProperties(); + public abstract MapProperty getChecksProperties(); + public abstract Property getHotSwap(); + public abstract Property getDumpTargetOnFailure(); + public abstract Property getIgnoreConstraints(); + public abstract Property getInitialiserInjectionMode(); + + public void config(String name) { getConfigs().add(name); } + public void extraMapping(Object file) { getExtraMappings().from(file); } + public void token(String name) { getTokens().put(name, "true"); } + public void token(String name, String value) { getTokens().put(name, value); } + public void tokens(Map map) { getTokens().putAll(map); } + + public void quiet() { getQuiet().set(true); } + public void showMessageTypes() { getShowMessageTypes().set(true); } + public void disableTargetValidator() { getDisableTargetValidator().set(true); } + public void disableTargetExport() { getDisableTargetExport().set(true); } + public void disableOverwriteChecker() { getDisableOverwriteChecker().set(true); } + public void overwriteErrorLevel(String level) { getOverwriteErrorLevel().set(level); } + public void messages(Map map) { getMessages().putAll(map); } + + public Provider add(String refmap) { + return add(getMainSourceSet(), refmap); + } + + public Provider add(String sourceSetName, String refmap) { + return add(project.getExtensions().getByType(org.gradle.api.plugins.JavaPluginExtension.class) + .getSourceSets().getByName(sourceSetName), refmap); + } + + public Provider add(SourceSet sourceSet, String refmap) { + var mappingFile = project.getLayout().getBuildDirectory().dir("mixin") + .map(d -> d.file(refmap + ".mappings.tsrg")); + var refMapFile = project.getLayout().getBuildDirectory().dir("mixin") + .map(d -> d.file(refmap)); + + project.getTasks().named(sourceSet.getCompileJavaTaskName(), JavaCompile.class).configure(compile -> { + var compilerArgs = project.getObjects().newInstance(MixinCompilerArgs.class); + compilerArgs.getRefmap().set(refMapFile); + compilerArgs.getOutMappings().set(mappingFile); + compilerArgs.getInMappings().set(officialToSrg); + compilerArgs.getExtraMappings().from(getExtraMappings()); + compilerArgs.getDefaultObfuscationEnv().set(getDefaultObfuscationEnv()); + compilerArgs.getQuiet().set(getQuiet()); + compilerArgs.getShowMessageTypes().set(getShowMessageTypes()); + compilerArgs.getDisableTargetValidator().set(getDisableTargetValidator()); + compilerArgs.getDisableTargetExport().set(getDisableTargetExport()); + compilerArgs.getDisableOverwriteChecker().set(getDisableOverwriteChecker()); + compilerArgs.getOverwriteErrorLevel().set(getOverwriteErrorLevel()); + compilerArgs.getTokens().set(getTokens()); + compilerArgs.getMessages().set(getMessages()); + compile.getOptions().getCompilerArgumentProviders().add(compilerArgs); + }); + + extraMappingFiles.from(mappingFile); + + project.getTasks().withType(Jar.class) + .matching(jar -> jar.getName().equals(sourceSet.getJarTaskName())) + .configureEach(jar -> jar.from(refMapFile)); + + autoAddAnnotationProcessorDeps(sourceSet); + + return refMapFile; + } + + private SourceSet getMainSourceSet() { + return project.getExtensions().getByType(org.gradle.api.plugins.JavaPluginExtension.class) + .getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); + } + + private void autoAddAnnotationProcessorDeps(SourceSet sourceSet) { + var apConfig = project.getConfigurations().named(sourceSet.getAnnotationProcessorConfigurationName()); + var depFactory = project.getDependencyFactory(); + apConfig.configure(c -> { + c.getDependencies().add(depFactory.create("org.ow2.asm:asm-debug-all:5.2")); + c.getDependencies().add(depFactory.create("com.google.guava:guava:32.1.2-jre")); + c.getDependencies().add(depFactory.create("com.google.code.gson:gson:2.8.9")); + }); + } +} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/RemappingTransform.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/RemappingTransform.java new file mode 100644 index 00000000..1724024a --- /dev/null +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/RemappingTransform.java @@ -0,0 +1,81 @@ +package net.neoforged.moddevgradle.mcpforge.internal; + +import java.io.IOException; +import javax.inject.Inject; + +import net.neoforged.moddevgradle.internal.utils.FileUtils; +import net.neoforged.moddevgradle.legacyforge.tasks.RemapOperation; +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.InputArtifactDependencies; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.CompileClasspath; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.process.ExecOperations; + +public abstract class RemappingTransform implements TransformAction { + + @InputArtifact + @PathSensitive(PathSensitivity.NONE) + public abstract Provider getInputArtifact(); + + @CompileClasspath + @InputArtifactDependencies + public abstract FileCollection getDependencies(); + + @Inject + protected abstract ExecOperations getExecOperations(); + + @Inject + public RemappingTransform() {} + + @Override + public void transform(TransformOutputs outputs) { + var inputFile = getInputArtifact().get().getAsFile(); + // The file may not yet exist if IntelliJ requests it during indexing. + if (!inputFile.exists()) return; + + var mappedFile = outputs.file(inputFile.getName()); + try { + getParameters().getRemapOperation() + .execute( + getExecOperations(), + inputFile, + mappedFile, + getDependencies().plus(getParameters().getMinecraftDependencies())); + // Strip bundled org.spongepowered.asm.* from non-provider jars. A bundler like VoxelMap ships a FULL old + // Mixin (incl. tweaker, so content detection can't tell it from a real provider); only the user-declared + // provider (mixinProvider DSL) is exempt. Without stripping, the old @Inject (no `order` member) shadows + // MixinBooter 10.7's on the compile classpath. + if (mappedFile.isFile() && FileUtils.containsSpongePowered(mappedFile) + && !getParameters().getMixinProviders().getFiles().contains(inputFile)) { + FileUtils.stripSpongePowered(mappedFile); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public interface Parameters extends TransformParameters { + @Nested + RemapOperation getRemapOperation(); + + @InputFiles + @PathSensitive(PathSensitivity.NONE) + ConfigurableFileCollection getMinecraftDependencies(); + + /** Jars that ARE the declared Mixin provider (via the {@code mixinProvider} DSL) — exempt from sponge-strip. */ + @InputFiles + @PathSensitive(PathSensitivity.NONE) + ConfigurableFileCollection getMixinProviders(); + } +} + diff --git a/src/test/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTaskTest.java b/src/test/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTaskTest.java new file mode 100644 index 00000000..d1e58d5e --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTaskTest.java @@ -0,0 +1,66 @@ +package net.neoforged.moddevgradle.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.lang.reflect.Method; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class CreateLaunchScriptTaskTest { + @TempDir + Path tempDir; + + @Test + void writesWindowsEnvironmentVariablesWithCmdSafeSetSyntax() throws Exception { + Method method = CreateLaunchScriptTask.class.getDeclaredMethod("writeWindowsEnvironmentVariable", Map.Entry.class); + method.setAccessible(true); + + var escaped = (String) method.invoke(null, Map.entry("MCP_MAPPINGS", "C:\\Users\\A B\\mappings & data.zip")); + + assertThat(escaped) + .isEqualTo("set \"MCP_MAPPINGS=C:\\Users\\A B\\mappings ^& data.zip\""); + } + + @Test + void expandsJvmArgFilesForJava8CompatibleStandaloneLaunchScripts() throws Exception { + var classpathArgs = tempDir.resolve("classpath.txt"); + var vmArgs = tempDir.resolve("vmargs.txt"); + var programArgs = tempDir.resolve("programargs.txt"); + Files.writeString(classpathArgs, "-classpath\n\"/tmp/libs/a.jar:/tmp/libs/b jar.jar\"\n"); + Files.writeString(vmArgs, "-XstartOnFirstThread\n-Dexample=value\n"); + Files.writeString(programArgs, "# Main Class\nnet.minecraftforge.legacydev.MainClient\n"); + + var method = CreateLaunchScriptTask.class.getDeclaredMethod( + "createJavaCommand", + String.class, + java.io.File.class, + java.io.File.class, + String.class, + java.io.File.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + var command = (java.util.List) method.invoke( + null, + "/usr/bin/java", + classpathArgs.toFile(), + vmArgs.toFile(), + "-Dfml.modFolders=mymod%%classes", + programArgs.toFile()); + + assertThat(command) + .containsExactly( + "/usr/bin/java", + "-classpath", + "/tmp/libs/a.jar:/tmp/libs/b jar.jar", + "-XstartOnFirstThread", + "-Dexample=value", + "-Dfml.modFolders=mymod%%classes", + RunUtils.DEV_LAUNCH_MAIN_CLASS, + "net.minecraftforge.legacydev.MainClient") + .noneMatch(argument -> argument.startsWith("@")); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ExtractNativesTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ExtractNativesTest.java new file mode 100644 index 00000000..fec9a268 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/internal/ExtractNativesTest.java @@ -0,0 +1,92 @@ +package net.neoforged.moddevgradle.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ExtractNativesTest { + @TempDir + Path tempDir; + + @Test + void extractsNativeJarsWithDuplicateMetadataEntries() throws IOException { + var firstJar = createJar("first.jar", Map.of( + "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n", + "linux/libfirst.so", "first")); + var secondJar = createJar("second.jar", Map.of( + "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n", + "linux/libsecond.so", "second")); + + var project = ProjectBuilder.builder().build(); + var task = project.getTasks().register("extractNatives", ExtractNatives.class).get(); + task.getEnabledForRun().set(true); + task.getNativeLibraries().from(firstJar, secondJar); + task.getOutputDirectory().set(tempDir.resolve("natives").toFile()); + + task.extract(); + + assertThat(tempDir.resolve("natives/linux/libfirst.so")).hasContent("first"); + assertThat(tempDir.resolve("natives/linux/libsecond.so")).hasContent("second"); + } + + @Test + void renamesLwjgl2Arm64MacosNativeToLegacyJniLibName() throws IOException { + var lwjgl2Arm64Natives = createJar("lwjgl-platform-2.9.4-nightly-20150209-mmachina.2-natives-osx.jar", Map.of( + "liblwjgl.dylib", "arm64 lwjgl", + "openal.dylib", "arm64 openal")); + + var project = ProjectBuilder.builder().build(); + var task = project.getTasks().register("extractNatives", ExtractNatives.class).get(); + task.getEnabledForRun().set(true); + task.getNativeLibraries().from(lwjgl2Arm64Natives); + task.getOutputDirectory().set(tempDir.resolve("natives").toFile()); + + task.extract(); + + assertThat(tempDir.resolve("natives/liblwjgl.jnilib")).hasContent("arm64 lwjgl"); + assertThat(tempDir.resolve("natives/liblwjgl.dylib")).doesNotExist(); + assertThat(tempDir.resolve("natives/openal.dylib")).hasContent("arm64 openal"); + } + + @Test + void letsLwjgl2Arm64MacosNativeOverrideLegacyOsxNative() throws IOException { + var legacyOsxNatives = createJar("lwjgl-platform-2.9.1-natives-osx.jar", Map.of( + "liblwjgl.jnilib", "x64 lwjgl", + "openal.dylib", "x64 openal")); + var lwjgl2Arm64Natives = createJar("lwjgl-platform-2.9.4-nightly-20150209-mmachina.2-natives-osx.jar", Map.of( + "liblwjgl.dylib", "arm64 lwjgl", + "openal.dylib", "arm64 openal")); + + var project = ProjectBuilder.builder().build(); + var task = project.getTasks().register("extractNatives", ExtractNatives.class).get(); + task.getEnabledForRun().set(true); + task.getNativeLibraries().from(legacyOsxNatives, lwjgl2Arm64Natives); + task.getOutputDirectory().set(tempDir.resolve("natives").toFile()); + + task.extract(); + + assertThat(tempDir.resolve("natives/liblwjgl.jnilib")).hasContent("arm64 lwjgl"); + assertThat(tempDir.resolve("natives/openal.dylib")).hasContent("arm64 openal"); + } + + private Path createJar(String fileName, Map entries) throws IOException { + var jar = tempDir.resolve(fileName); + try (var output = new JarOutputStream(Files.newOutputStream(jar))) { + for (var entry : entries.entrySet()) { + output.putNextEntry(new JarEntry(entry.getKey())); + output.write(entry.getValue().getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } + } + return jar; + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java b/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java new file mode 100644 index 00000000..cd45c3b4 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java @@ -0,0 +1,206 @@ +package net.neoforged.moddevgradle.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Properties; +import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.event.Level; + +class PrepareRunTest { + @TempDir + Path tempDir; + + @Test + void writesInterpolatedUserdevEnvironment() throws Exception { + var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); + Files.createDirectories(tempDir.resolve("build")); + var configJson = tempDir.resolve("config.json"); + Files.writeString(configJson, """ + { + "runs": { + "client": { + "main": "net.minecraftforge.legacydev.MainClient", + "args": [], + "jvmArgs": [], + "props": {}, + "env": { + "MCP_TO_SRG": "{mcp_to_srg}", + "mainClass": "net.minecraft.launchwrapper.Launch", + "MCP_MAPPINGS": "{mcp_mappings}", + "assetIndex": "{asset_index}", + "assetDirectory": "{assets_root}", + "nativesDirectory": "{natives}", + "MC_VERSION": "${MC_VERSION}" + } + } + } + } + """, StandardCharsets.UTF_8); + + var assets = tempDir.resolve("assets"); + var assetProperties = tempDir.resolve("assets.properties"); + Files.writeString(assetProperties, """ + asset_index=1.12 + assets_root=%s + """.formatted(assets), StandardCharsets.ISO_8859_1); + + var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); + task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); + task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); + task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); + task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); + task.getAssetProperties().set(assetProperties.toFile()); + task.getRunTypeTemplatesSource().from(configJson.toFile()); + task.getRunType().set("client"); + task.getSystemProperties().set(Map.of()); + task.getJvmArguments().set(java.util.List.of()); + task.getProgramArguments().set(java.util.List.of()); + task.getUserEnvironment().set(Map.of("MC_VERSION", "override")); + task.getRunTemplateReplacements().set(Map.of( + "mcp_to_srg", tempDir.resolve("named-to-intermediary.srg").toString(), + "mcp_mappings", tempDir.resolve("mcp-csv.zip").toString())); + task.getGameLogLevel().set(Level.INFO); + task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); + + task.prepareRun(); + + assertThat(RunUtils.loadEnvironmentFile(task.getEnvironmentFile().get().getAsFile())) + .containsEntry("MCP_TO_SRG", tempDir.resolve("named-to-intermediary.srg").toString()) + .containsEntry("MCP_MAPPINGS", tempDir.resolve("mcp-csv.zip").toString()) + .containsEntry("assetIndex", "1.12") + .containsEntry("assetDirectory", assets.toString()) + .containsEntry("nativesDirectory", task.getGameDirectory().get().dir("natives").getAsFile().getAbsolutePath()) + .containsEntry("mainClass", "net.minecraft.launchwrapper.Launch") + .containsEntry("MC_VERSION", "override"); + } + + @Test + void treatsMissingLegacyRunTemplateCollectionsAsEmpty() throws Exception { + var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); + Files.createDirectories(tempDir.resolve("build")); + var configJson = tempDir.resolve("config.json"); + Files.writeString(configJson, """ + { + "runs": { + "client": { + "main": "net.minecraftforge.legacydev.MainClient", + "env": { + "assetIndex": "{asset_index}", + "MC_VERSION": "${MC_VERSION}" + } + } + } + } + """, StandardCharsets.UTF_8); + + var assetProperties = tempDir.resolve("assets.properties"); + Files.writeString(assetProperties, """ + asset_index=1.12 + assets_root=%s + """.formatted(tempDir.resolve("assets")), StandardCharsets.ISO_8859_1); + + var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); + task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); + task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); + task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); + task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); + task.getAssetProperties().set(assetProperties.toFile()); + task.getRunTypeTemplatesSource().from(configJson.toFile()); + task.getRunType().set("client"); + task.getSystemProperties().set(Map.of()); + task.getJvmArguments().set(java.util.List.of()); + task.getProgramArguments().set(java.util.List.of()); + task.getUserEnvironment().set(Map.of()); + task.getRunTemplateReplacements().set(Map.of()); + task.getGameLogLevel().set(Level.INFO); + task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); + + task.prepareRun(); + + assertThat(Files.readAllLines(task.getVmArgsFile().get().getAsFile().toPath())) + .doesNotContainNull(); + assertThat(Files.readString(task.getProgramArgsFile().get().getAsFile().toPath())) + .contains("net.minecraftforge.legacydev.MainClient"); + assertThat(RunUtils.loadEnvironmentFile(task.getEnvironmentFile().get().getAsFile())) + .containsEntry("assetIndex", "1.12") + .containsEntry("MC_VERSION", "1.12.2"); + } + + @Test + void disablesLegacyForgeSplashOnMacOsForForge1122ClientRuns() throws Exception { + var previousOsName = System.getProperty("os.name"); + System.setProperty("os.name", "Mac OS X"); + try { + var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); + Files.createDirectories(tempDir.resolve("build")); + var configJson = tempDir.resolve("config.json"); + Files.writeString(configJson, """ + { + "runs": { + "client": { + "main": "net.minecraftforge.legacydev.MainClient", + "env": { + "assetIndex": "{asset_index}", + "assetDirectory": "{assets_root}" + } + } + } + } + """, StandardCharsets.UTF_8); + + var assetProperties = tempDir.resolve("assets.properties"); + Files.writeString(assetProperties, """ + asset_index=1.12 + assets_root=%s + """.formatted(tempDir.resolve("assets")), StandardCharsets.ISO_8859_1); + + var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); + task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); + task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); + task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); + task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); + task.getAssetProperties().set(assetProperties.toFile()); + task.getRunTypeTemplatesSource().from(configJson.toFile()); + task.getRunType().set("client"); + task.getSystemProperties().set(Map.of()); + task.getJvmArguments().set(java.util.List.of()); + task.getProgramArguments().set(java.util.List.of()); + task.getUserEnvironment().set(Map.of()); + task.getRunTemplateReplacements().set(Map.of()); + task.getGameLogLevel().set(Level.INFO); + task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); + + task.prepareRun(); + + var splashProperties = new Properties(); + try (var reader = Files.newBufferedReader(tempDir.resolve("run/config/splash.properties"), StandardCharsets.UTF_8)) { + splashProperties.load(reader); + } + assertThat(splashProperties) + .containsEntry("enabled", "false"); + } finally { + System.setProperty("os.name", previousOsName); + } + } + + @Test + void treatsGameDirectoryAsAnInputBecauseItIsWrittenToEnvironmentFile() throws Exception { + var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); + var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); + task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); + + var inputProperties = task.getInputs().getProperties(); + + assertThat(inputProperties) + .containsKey("gameDirectoryPath"); + assertThat(inputProperties.get("gameDirectoryPath")) + .isNotNull(); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/internal/RunUtilsTest.java b/src/test/java/net/neoforged/moddevgradle/internal/RunUtilsTest.java index f71a5608..38a27836 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/RunUtilsTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/RunUtilsTest.java @@ -2,8 +2,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import java.nio.file.Path; +import java.util.List; class RunUtilsTest { @ParameterizedTest @@ -20,4 +26,18 @@ public void testEscape(String unescaped, String escaped) { assertEquals(escaped, RunUtils.escapeJvmArg(unescaped)); } + + @Test + void readsPreparedArgumentFile(@TempDir Path tempDir) throws Exception { + var argFile = tempDir.resolve("args.txt"); + Files.writeString(argFile, """ + # comment + -Done=1 + "two words" + escaped\\\\path + + """, StandardCharsets.UTF_8); + + assertEquals(List.of("-Done=1", "two words", "escaped\\path"), RunUtils.readArgFile(argFile.toFile())); + } } diff --git a/src/test/java/net/neoforged/moddevgradle/internal/utils/FileUtilsTest.java b/src/test/java/net/neoforged/moddevgradle/internal/utils/FileUtilsTest.java new file mode 100644 index 00000000..2378fc91 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/internal/utils/FileUtilsTest.java @@ -0,0 +1,74 @@ +package net.neoforged.moddevgradle.internal.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FileUtilsTest { + @TempDir + Path tempDir; + + @Test + void stripsJarSignatureMetadataAndKeepsManifestMainAttributes() throws Exception { + var jar = tempDir.resolve("signed.jar"); + var manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + manifest.getMainAttributes().putValue("Tweak-Class", "net.minecraftforge.fml.common.launcher.FMLTweaker"); + manifest.getEntries().computeIfAbsent("net/minecraftforge/fml/relauncher/libraries/LibraryManager.class", ignored -> new Attributes()) + .putValue("SHA-256-Digest", "invalid"); + + try (var output = new JarOutputStream(Files.newOutputStream(jar), manifest)) { + writeEntry(output, "META-INF/FORGE.SF", "signature file"); + writeEntry(output, "META-INF/FORGE.DSA", "signature block"); + writeEntry(output, "net/minecraftforge/fml/relauncher/libraries/LibraryManager.class", "patched class"); + } + + FileUtils.stripJarSignatures(jar); + + try (var result = new JarFile(jar.toFile())) { + assertThat(result.getEntry("META-INF/FORGE.SF")).isNull(); + assertThat(result.getEntry("META-INF/FORGE.DSA")).isNull(); + assertThat(result.getEntry("net/minecraftforge/fml/relauncher/libraries/LibraryManager.class")).isNotNull(); + assertThat(result.getManifest().getMainAttributes().getValue("Tweak-Class")) + .isEqualTo("net.minecraftforge.fml.common.launcher.FMLTweaker"); + assertThat(result.getManifest().getEntries()).isEmpty(); + } + } + + @Test + void removesSelectedJarEntriesAndKeepsManifest() throws Exception { + var jar = tempDir.resolve("patched.jar"); + var manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + manifest.getMainAttributes().putValue("Tweak-Class", "net.minecraftforge.fml.common.launcher.FMLTweaker"); + + try (var output = new JarOutputStream(Files.newOutputStream(jar), manifest)) { + writeEntry(output, "binpatches.pack.lzma", "runtime patches"); + writeEntry(output, "net/minecraft/client/Minecraft.class", "patched class"); + } + + FileUtils.removeJarEntries(jar, java.util.Set.of("binpatches.pack.lzma")); + + try (var result = new JarFile(jar.toFile())) { + assertThat(result.getEntry("binpatches.pack.lzma")).isNull(); + assertThat(result.getEntry("net/minecraft/client/Minecraft.class")).isNotNull(); + assertThat(result.getManifest().getMainAttributes().getValue("Tweak-Class")) + .isEqualTo("net.minecraftforge.fml.common.launcher.FMLTweaker"); + } + } + + private static void writeEntry(JarOutputStream output, String name, String content) throws Exception { + output.putNextEntry(new JarEntry(name)); + output.write(content.getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java index d1e0ee6f..92c67ee9 100644 --- a/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java @@ -5,13 +5,21 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; import java.util.Set; import net.neoforged.moddevgradle.AbstractProjectBuilderTest; +import net.neoforged.moddevgradle.internal.ExtractNatives; +import net.neoforged.moddevgradle.internal.ModDevRunWorkflow; import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeExtension; import net.neoforged.moddevgradle.legacyforge.internal.LegacyForgeModDevPlugin; +import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Task; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.jvm.toolchain.JavaLanguageVersion; @@ -81,6 +89,139 @@ void testGetMcpVersionThrowsBeforeEnabling() { assertThrows(InvalidUserCodeException.class, extension::getMcpVersion); } + @Test + void testForge1122UsesUserdev3ForNeoFormRuntime() { + project.getExtensions().getByName("neoFormRuntime"); + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, extension -> extension.getVersion().set("2.0.19-legacy")); + + extension.setVersion("1.12.2-14.23.5.2860"); + + var createArtifacts = project.getTasks().named("createMinecraftArtifacts", CreateMinecraftArtifacts.class).get(); + assertEquals("net.minecraftforge:forge:1.12.2-14.23.5.2860:userdev3", createArtifacts.getNeoForgeArtifact().get()); + assertThat(createArtifacts.getLegacyMcpMappings().isPresent()).isFalse(); + assertEquals("1.12.2", extension.getMinecraftVersion()); + } + + @Test + void testLegacyRunExtractsNatives() { + extension.getRuns().create("client").client(); + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + + extension.setVersion("1.12.2-14.23.5.2860"); + + var extractNatives = project.getTasks().named("extractClientNatives", ExtractNatives.class); + assertThat(project.getTasks().getNames()).contains("extractClientNatives"); + assertThat(project.getTasks().named("prepareClientRun").get().getDependsOn()) + .contains(extractNatives); + + var task = extractNatives.get(); + assertThat(task.getEnabledForRun().get()).isTrue(); + assertThat(task.getNativeLibraries().getFrom()).contains(project.getConfigurations().getByName("clientNativeLibraries")); + } + + @Test + void testForge1122DoesNotAddBootstrapLauncherExportsToJava8Run() { + var run = extension.getRuns().create("client"); + run.client(); + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + + extension.setVersion("1.12.2-14.23.5.2860"); + + assertThat(run.getJvmArguments().get()) + .doesNotContain("--add-exports", "cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED"); + } + + @Test + void testForge1122IgnoresRuntimePatchDiscrepanciesForPreparedDevJar() { + var run = extension.getRuns().create("client"); + run.client(); + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + + extension.setVersion("1.12.2-14.23.5.2860"); + + assertThat(run.getSystemProperties().get()) + .containsEntry("fml.ignorePatchDiscrepancies", "true"); + } + + @Test + void testForge1122PassesSrgToMcpMappingsToLegacyDevLauncher() throws Exception { + extension.getRuns().create("client").client(); + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + + extension.setVersion("1.12.2-14.23.5.2860"); + + var prepareRun = project.getTasks().named("prepareClientRun").get(); + assertThat(getRunTemplateReplacements(prepareRun)) + .containsEntry("mcp_to_srg", project.file("build/moddev/artifacts/intermediateToNamed.srg").getAbsolutePath()); + } + + @Test + void testForgeRepositorySupportsJarOnlyLegacyArtifacts() { + var forgeRepository = (MavenArtifactRepository) project.getRepositories().getByName("MinecraftForge"); + + assertThat(forgeRepository.getMetadataSources().isMavenPomEnabled()).isTrue(); + assertThat(forgeRepository.getMetadataSources().isArtifactEnabled()).isTrue(); + } + + @Test + void testForge1171UsesUserdevForNeoFormRuntime() { + extension.setVersion("1.17.1-37.1.1"); + + var createArtifacts = project.getTasks().named("createMinecraftArtifacts", CreateMinecraftArtifacts.class).get(); + assertEquals("net.minecraftforge:forge:1.17.1-37.1.1:userdev", createArtifacts.getNeoForgeArtifact().get()); + assertThat(createArtifacts.getLegacyMcpMappings().isPresent()).isFalse(); + assertEquals("1.17.1", extension.getMinecraftVersion()); + } + + @Test + void testLegacyMcpMappingsCanBeConfiguredForNeoFormRuntime() { + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + + extension.enable(settings -> { + settings.setForgeVersion("1.12.2-14.23.5.2860"); + settings.setMcpMappings("de.oceanlabs.mcp:mcp_stable:39-1.12@zip"); + }); + + var createArtifacts = project.getTasks().named("createMinecraftArtifacts", CreateMinecraftArtifacts.class).get(); + assertEquals("de.oceanlabs.mcp:mcp_stable:39-1.12@zip", createArtifacts.getLegacyMcpMappings().get()); + } + + @Test + void testGradleTestTaskLoadsPreparedEnvironmentFile() throws Exception { + project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); + extension.setVersion("1.12.2-14.23.5.2860"); + var testTask = project.getTasks().named("test", org.gradle.api.tasks.testing.Test.class).get(); + var initialActionCount = testTask.getActions().size(); + + ModDevRunWorkflow.get(project).configureTesting(project.provider(() -> null), project.provider(() -> Set.of())); + + assertThat(testTask.getActions()).hasSizeGreaterThan(initialActionCount); + + var environmentFile = project.getLayout().getBuildDirectory() + .file("moddev/junit/environment.properties") + .get() + .getAsFile() + .toPath(); + Files.createDirectories(environmentFile.getParent()); + Files.writeString(environmentFile, "MCP_TO_SRG=prepared.srg\n", StandardCharsets.ISO_8859_1); + + var loadEnvironment = project.getObjects().newInstance(ModDevRunWorkflow.LoadPreparedTestEnvironment.class); + loadEnvironment.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("moddev/junit/environment.properties")); + loadEnvironment.execute(testTask); + + assertThat(testTask.getEnvironment()) + .containsEntry("MCP_TO_SRG", "prepared.srg"); + } + + @Test + void testForge1122RequiresLegacyNeoFormRuntimeSupport() { + var e = assertThrows(InvalidUserCodeException.class, () -> extension.setVersion("1.12.2-14.23.5.2860")); + + assertThat(e).hasMessageContaining("Forge 1.12.2 requires NeoFormRuntime with legacy MCP support"); + assertThat(e).hasMessageContaining("neoForge.neoFormRuntime.version"); + assertThat(e).hasMessageContaining("2.0.19-legacy"); + } + @Test void testEnableForTestSourceSetOnly() { extension.enable(settings -> { @@ -145,4 +286,17 @@ private void assertContainsModdingRuntimeDependencies(String configurationName) assertThatDependencies(configurationName).contains(MODDING_COMPILE_DEPENDENCIES); assertThatDependencies(configurationName).contains(MODDING_RUNTIME_ONLY_DEPENDENCIES); } + + @SuppressWarnings("unchecked") + private static Map getRunTemplateReplacements(Task task) throws Exception { + try { + var replacementsProperty = task.getClass().getMethod("getRunTemplateReplacements").invoke(task); + return (Map) replacementsProperty.getClass().getMethod("get").invoke(replacementsProperty); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception exception) { + throw exception; + } + throw e; + } + } } From b9959916185310b8232fb6450d5e47bf3a87dd5c Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 12:23:13 +0800 Subject: [PATCH 02/11] fix reference --- .../net/neoforged/moddevgradle/internal/utils/FileUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java index 021ffda6..77353172 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java @@ -239,7 +239,7 @@ public static void stripSpongePowered(File jar) throws IOException { Enumeration entries = input.entries(); while (entries.hasMoreElements()) { var entry = entries.nextElement(); - if (entry.getName().startsWith(RemappingTransform.SPONGE_PREFIX)) continue; + if (entry.getName().startsWith(SPONGE_PREFIX)) continue; var copy = new JarEntry(entry.getName()); copy.setTime(entry.getTime()); output.putNextEntry(copy); From 94ba186569d49b45e460d6cae600deed978c7c21 Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 12:50:13 +0800 Subject: [PATCH 03/11] mcpforge: provide full FG-2 GradleStart SRG property set Mirror everything ForgeGradle-2's GradleStartCommon exposes to 1.12.2 mods at runtime, not just notch-srg + csvDir: PopulateForgeGradleMcpCache now derives the SRG maps FG-2 would have generated alongside the verbatim copies: srg-mcp/mcp-srg/notch-srg are copied, and when a notch->SRG map is available srg-notch (inverse), notch-mcp (compose notch->SRG with SRG->MCP) and mcp-notch (its inverse) are produced by parsing the SRG-format text. CL/FD/MD tags are preserved through invert/compose. McpForgeModDevPlugin.configureRunTasks sets every GradleStart.srg.* property (srgDir, csvDir, notch-srg/notch-mcp/srg-mcp/mcp-srg/mcp-notch) plus fml.ignoreInvalidMinecraftCertificates, so mods reading any of them (CodeChickenLib et al.) resolve correctly. The populateForgeGradleMcpCache lookup is now guarded with findByName since the task only exists when mcpMappings is configured. Also fixes the stale McpForgeExtension Javadoc (legacyForge -> mcpForge, LegacyForgeModdingSettings -> McpForgeModdingSettings). --- .../tasks/PopulateForgeGradleMcpCache.java | 140 +++++++++++++----- .../mcpforge/dsl/McpForgeExtension.java | 4 +- .../internal/McpForgeModDevPlugin.java | 33 +++-- 3 files changed, 133 insertions(+), 44 deletions(-) diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java index 27d1b2f1..996853d4 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java @@ -1,34 +1,32 @@ package net.neoforged.moddevgradle.legacyforge.tasks; -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.Optional; - import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.zip.ZipEntry; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.zip.ZipFile; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; /** - * Populates the ForgeGradle-2.x MCP cache directory with the MCP data that ModDevGradle produced, so that legacy - * tooling and build scripts that hardcode the ForgeGradle-2 cache layout continue to find the mappings. - *

- * ForgeGradle-2 stores MCP data under {@code ~/.gradle/caches/minecraft/de/oceanlabs/mcp///} (the CSV - * files) and the SRG mapping files under {@code ...////srgs/}. ModDevGradle instead - * exposes the same data as build artifacts (requested via {@code createMinecraftArtifacts}); this task mirrors them - * into the ForgeGradle-2 locations for compatibility. + * Populates the ForgeGradle-2.x MCP cache directory with the MCP data ModDevGradle produced, so legacy tooling and + * 1.12.2 mods that hardcode the ForgeGradle-2 cache layout (and read it at runtime via {@code GradleStart} system + * properties) keep finding the mappings. *

- * This only covers the data ModDevGradle has available: the {@code methods.csv}/{@code fields.csv}/{@code params.csv} - * (from the CSV mapping zip), {@code srg-mcp.srg} (= SRG->MCP) and {@code mcp-srg.srg} (= MCP->SRG). The - * notch-targeted SRG files ({@code notch-srg.srg} etc.) that ForgeGradle-2 also generates are intentionally omitted, - * as production-name obfuscation for 1.12.2 targets SRG (not notch) in this toolchain. + * ForgeGradle-2 stores MCP data under {@code ~/.gradle/caches/minecraft/de/oceanlabs/mcp///}: the CSV + * files directly there, and the SRG maps under {@code ...///srgs/}. This task mirrors the + * NFRT-produced artifacts into that layout. The {@code srg-mcp}/{@code mcp-srg}/{@code notch-srg} maps are copied + * verbatim; when a {@code notch}->SRG map is available the remaining FG-2 maps ({@code srg-notch}, {@code notch-mcp}, + * {@code mcp-notch}) are derived by inverting/composing the SRG-format text, so every {@code GradleStart.srg.*} + * property ForgeGradle-2's {@code GradleStartCommon} would have set resolves to a real file. */ public abstract class PopulateForgeGradleMcpCache extends DefaultTask { @@ -48,21 +46,21 @@ public abstract class PopulateForgeGradleMcpCache extends DefaultTask { @Input public abstract Property getCacheBaseDirectory(); - /** The MCP CSV mapping zip (SRG->MCP CSVs), as produced by NFRT's {@code csvMapping} result. */ + /** The MCP CSV mapping zip (methods/fields/params), as produced by NFRT's {@code csvMapping} result. */ @InputFile public abstract RegularFileProperty getCsvMappings(); - /** The SRG->MCP SRG mapping file (NFRT {@code intermediaryToNamedMapping}), mirrors {@code srgs/srg-mcp.srg}. */ + /** The SRG->MCP SRG mapping file (NFRT {@code intermediaryToNamedMapping}), mirrors {@code srgs/srg-mcp.srg}. */ @InputFile public abstract RegularFileProperty getSrgToMcpMappings(); - /** The MCP->SRG mapping file (NFRT {@code namedToIntermediaryMapping}), mirrors {@code srgs/mcp-srg.srg}. */ + /** The MCP->SRG mapping file (NFRT {@code namedToIntermediaryMapping}), mirrors {@code srgs/mcp-srg.srg}. */ @InputFile public abstract RegularFileProperty getMcpToSrgMappings(); - /** The notch->SRG mapping file (NFRT {@code notchToIntermediaryMapping}), mirrors {@code srgs/notch-srg.srg}. */ + /** The notch->SRG mapping file (NFRT {@code notchToIntermediaryMapping}); when present, {@code srgs/notch-srg.srg} and the derived maps are written. */ @InputFile - @org.gradle.api.tasks.Optional + @Optional public abstract RegularFileProperty getNotchToSrgMappings(); @TaskAction @@ -70,33 +68,109 @@ public void populate() throws IOException { var minecraftVersion = getMinecraftVersion().get(); var cacheBase = Path.of(getCacheBaseDirectory().get()); - // CSV files live directly under the version directory extractCsvs(getCsvMappings().get().getAsFile().toPath(), cacheBase); - // SRG mapping files live under //srgs/ var srgsDir = cacheBase.resolve(minecraftVersion).resolve("srgs"); Files.createDirectories(srgsDir); - Files.copy(getSrgToMcpMappings().get().getAsFile().toPath(), srgsDir.resolve("srg-mcp.srg"), StandardCopyOption.REPLACE_EXISTING); - Files.copy(getMcpToSrgMappings().get().getAsFile().toPath(), srgsDir.resolve("mcp-srg.srg"), StandardCopyOption.REPLACE_EXISTING); + + var srgToMcp = getSrgToMcpMappings().get().getAsFile().toPath(); + var mcpToSrg = getMcpToSrgMappings().get().getAsFile().toPath(); + + copyTo(srgToMcp, srgsDir.resolve("srg-mcp.srg")); + copyTo(mcpToSrg, srgsDir.resolve("mcp-srg.srg")); + if (getNotchToSrgMappings().isPresent()) { - Files.copy(getNotchToSrgMappings().get().getAsFile().toPath(), srgsDir.resolve("notch-srg.srg"), StandardCopyOption.REPLACE_EXISTING); + var notchToSrg = getNotchToSrgMappings().get().getAsFile().toPath(); + copyTo(notchToSrg, srgsDir.resolve("notch-srg.srg")); + + var notchSrg = readSrg(notchToSrg); + var srgMcp = readSrg(srgToMcp); + var composed = compose(notchSrg, srgMcp); + + writeSrg(notchSrg.inverse(), srgsDir.resolve("srg-notch.srg")); + writeSrg(composed, srgsDir.resolve("notch-mcp.srg")); + writeSrg(composed.inverse(), srgsDir.resolve("mcp-notch.srg")); } getLogger().lifecycle("Populated ForgeGradle-2 MCP cache at {}", cacheBase); } + private static void copyTo(Path src, Path dst) throws IOException { + Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); + } + private static void extractCsvs(Path mappingsZip, Path targetDir) throws IOException { Files.createDirectories(targetDir); try (var zip = new ZipFile(mappingsZip.toFile())) { for (String csv : new String[]{"methods.csv", "fields.csv", "params.csv"}) { var entry = zip.getEntry(csv); - if (entry == null) { - continue; - } + if (entry == null) continue; try (InputStream in = zip.getInputStream(entry)) { Files.copy(in, targetDir.resolve(csv), StandardCopyOption.REPLACE_EXISTING); } } } } + + private static SrgMap readSrg(Path file) throws IOException { + var map = new SrgMap(); + try (var reader = Files.newBufferedReader(file)) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + // CL:/FD: carry one pair (left, right); MD: carries (leftOwner/leftName leftDesc, rightOwner/rightName rightDesc). + // Splitting with a 4-way limit leaves the right side of MD: (name + desc) intact as a single token. + var parts = line.split("\\s+", 4); + switch (parts[0]) { + case "CL:", "FD:" -> { + if (parts.length >= 3) map.put(parts[0], parts[1], parts[2]); + } + case "MD:" -> { + if (parts.length >= 4) map.put("MD:", parts[1] + " " + parts[2], parts[3]); + } + } + } + } + return map; + } + + private static void writeSrg(SrgMap map, Path dst) throws IOException { + Files.createDirectories(dst.getParent()); + try (var writer = Files.newBufferedWriter(dst)) { + for (var entry : map.entries()) { + writer.write(entry.tag() + " " + entry.left() + " " + entry.right() + "\n"); + } + } + } + + /** Compose two maps: for each left→mid in {@code first}, emit left→(mid looked up in {@code second}). */ + private static SrgMap compose(SrgMap first, SrgMap second) { + var result = new SrgMap(); + for (var entry : first.entries()) { + var target = second.get(entry.right()); + result.put(entry.tag(), entry.left(), target != null ? target : entry.right()); + } + return result; + } + + private static final class SrgMap { + private final Map byLeft = new LinkedHashMap<>(); + + void put(String tag, String left, String right) { + byLeft.put(left, new Entry(tag, left, right)); + } + String get(String left) { + var e = byLeft.get(left); + return e != null ? e.right() : null; + } + SrgMap inverse() { + var inv = new SrgMap(); + for (var e : byLeft.values()) inv.put(e.tag(), e.right(), e.left()); + return inv; + } + java.util.Collection entries() { return byLeft.values(); } + + private record Entry(String tag, String left, String right) {} + } } diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java index c7bd0daa..14c3f86b 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeExtension.java @@ -12,8 +12,8 @@ import org.gradle.api.provider.ListProperty; /** - * The {@code legacyForge} extension for the mcpforge plugin, owning its {@link #enable(Action)} wiring. - * {@link LegacyForgeModdingSettings} is reused as-is. + * The {@code mcpForge} extension for the mcpforge plugin, owning its {@link #enable(Action)} wiring. + * Settings are provided by {@link McpForgeModdingSettings}. */ public abstract class McpForgeExtension extends ModDevExtension { private final Project project; diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index 3ed9b4f6..37b416ad 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -291,6 +291,7 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx } if ("1.12.2".equals(versionCapabilities.minecraftVersion())) { run.getSystemProperties().put("fml.ignorePatchDiscrepancies", "true"); + run.getSystemProperties().put("fml.ignoreInvalidMinecraftCertificates", "true"); } if (!versionCapabilities.modLocatorRework()) { @@ -497,15 +498,22 @@ private void configureRunTasks(Project project) { task.doFirst(t -> { var cp = task.getClasspathProvider().getFiles(); - PopulateForgeGradleMcpCache cacheTask = - (PopulateForgeGradleMcpCache) - project.getTasks().getByName("populateForgeGradleMcpCache"); - String cacheBase = cacheTask.getCacheBaseDirectory().get(); - String mcVersion = cacheTask.getMinecraftVersion().get(); - Path notchSrg = Path.of(cacheBase, mcVersion, "srgs", "notch-srg.srg"); - if (Files.exists(notchSrg)) { - task.systemProperty("net.minecraftforge.gradle.GradleStart.srg.notch-srg", notchSrg.toString()); - task.systemProperty("net.minecraftforge.gradle.GradleStart.csvDir", cacheBase); + // The cache task only exists when mcpMappings is configured (legacy MCP builds). + var cacheTask = (PopulateForgeGradleMcpCache) project.getTasks().findByName("populateForgeGradleMcpCache"); + if (cacheTask != null) { + var cacheBase = cacheTask.getCacheBaseDirectory().get(); + var srgsDir = Path.of(cacheBase, cacheTask.getMinecraftVersion().get(), "srgs"); + if (Files.isDirectory(srgsDir)) { + // Mirror FG-2's GradleStartCommon: expose every SRG map it would have generated plus the + // CSV dir, so 1.12.2 mods (e.g. CodeChickenLib) that read these at runtime resolve correctly. + task.systemProperty("net.minecraftforge.gradle.GradleStart.srgDir", srgsDir.toString()); + task.systemProperty("net.minecraftforge.gradle.GradleStart.csvDir", cacheBase); + putSrgProperty(task, srgsDir, "notch-srg", "net.minecraftforge.gradle.GradleStart.srg.notch-srg"); + putSrgProperty(task, srgsDir, "notch-mcp", "net.minecraftforge.gradle.GradleStart.srg.notch-mcp"); + putSrgProperty(task, srgsDir, "srg-mcp", "net.minecraftforge.gradle.GradleStart.srg.srg-mcp"); + putSrgProperty(task, srgsDir, "mcp-srg", "net.minecraftforge.gradle.GradleStart.srg.mcp-srg"); + putSrgProperty(task, srgsDir, "mcp-notch", "net.minecraftforge.gradle.GradleStart.srg.mcp-notch"); + } } var coremodClasses = discoverCoremods(project, cp); @@ -524,6 +532,13 @@ private void configureRunTasks(Project project) { }); } + private static void putSrgProperty(RunGameTask task, Path srgsDir, String fileName, String key) { + var file = srgsDir.resolve(fileName + ".srg"); + if (Files.exists(file)) { + task.systemProperty(key, file.toString()); + } + } + private static Set discoverCoremods(Project project, Set files) { var coremodClasses = new LinkedHashSet(); coremodClasses.addAll(scanManifests(files)); From cf09d4efb33334c93457690eb8e4df0d92dc80de Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 14:42:32 +0800 Subject: [PATCH 04/11] mcpforge: remove redundant LegacyForgeJarProcessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified empirically that NFRT's createMinecraftArtifacts output for the 1.12.2 legacy-MCP path is already fully MCP-named and patched (the source patches — GLAllocation.generateDisplayLists, CrashReportCategory .firstTwoElementsOfStackTraceMatch — are applied by NFRT's decompile/recompile pipeline when disableRecompilation=false, the mcpforge default). Morphism launches to the main menu (18 mods, sound, texture atlas) with the jar post-processor disabled — the remap pass and the two method rewrites are no-ops on an already-correct jar. Removes the whole now-dead chain: - LegacyForgeJarProcessor (mcpforge, 1085-line ASM remapper) - ForgeJarPostProcessor adapter + its registration in McpForgeModDevPlugin - JarPostProcessor SPI (main) — sole consumer was the above - getJarPostProcessors / remapLegacyForgeMinecraftReferences / findRequestedResult in CreateMinecraftArtifacts (nfrtgradle) Net -1164 lines. stripJarSignatures and removePreAppliedLegacyForgeRuntimePatches are kept (unrelated concerns). Note: the no-recompile path (disableRecompilation=true) still leaks obfuscated type names (e.g. awu->EnumFacing) that binary remap misses and this processor only partially fixed — that path is a separate NFRT-side limitation, not something mcpforge should paper over. --- .../internal/JarPostProcessor.java | 25 - .../nfrtgradle/CreateMinecraftArtifacts.java | 40 - .../internal/LegacyForgeJarProcessor.java | 1085 ----------------- .../internal/McpForgeModDevPlugin.java | 15 +- 4 files changed, 1 insertion(+), 1164 deletions(-) delete mode 100644 src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java delete mode 100644 src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java diff --git a/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java b/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java deleted file mode 100644 index e9551ada..00000000 --- a/src/main/java/net/neoforged/moddevgradle/internal/JarPostProcessor.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.neoforged.moddevgradle.internal; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Path; - -/** - * Post-processes a Minecraft jar after NFRT generation, for legacy versions that need additional transformation - * (e.g. 1.12.2 Forge deobfuscation data remapping). - * - *

Registered via {@code CreateMinecraftArtifacts.getJarPostProcessors()}; the MCP plugin registers its - * implementation, the default is an empty list. - */ -@ApiStatus.Internal -@FunctionalInterface -public interface JarPostProcessor { - /** - * Process the generated jar (may be modified in place). - * - * @param srgToMcpMappings the SRG→MCP mapping file, or null if not applicable - */ - void process(Path jar, @Nullable Path srgToMcpMappings) throws IOException; -} diff --git a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java index 473a51b7..9af83fee 100644 --- a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java +++ b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.Set; import javax.inject.Inject; -import net.neoforged.moddevgradle.internal.JarPostProcessor; import net.neoforged.moddevgradle.internal.utils.FileUtils; import net.neoforged.moddevgradle.internal.utils.ProblemReportingUtil; import net.neoforged.problems.FileProblemReporter; @@ -277,13 +276,6 @@ public RegularFileProperty getSourcesArtifact() { @ApiStatus.Experimental public abstract Property getIncludeResourcesInGameJar(); - /** - * Jar post-processors for legacy MCP versions (e.g. 1.12.2 Forge deobf data remapping). - * Registered by the mcpforge plugin; empty by default. - */ - @Internal - public abstract ListProperty getJarPostProcessors(); - @Inject protected abstract Problems getProblems(); @@ -439,7 +431,6 @@ public void createArtifacts() { try { run(args); stripJarSignatures(requestedResults); - remapLegacyForgeMinecraftReferences(requestedResults, getJarPostProcessors().get()); removePreAppliedLegacyForgeRuntimePatches(requestedResults); } finally { reportProblems(problemsReport); @@ -461,37 +452,6 @@ private static void removePreAppliedLegacyForgeRuntimePatches(List requestedResults, List postProcessors) { - if (postProcessors.isEmpty()) { - return; - } - for (var requestedResult : requestedResults) { - var destination = requestedResult.destination(); - if (!destination.isFile() || !destination.getName().endsWith(".jar")) { - continue; - } - - try { - for (var postProcessor : postProcessors) { - postProcessor.process( - destination.toPath(), - findRequestedResult(requestedResults, "intermediaryToNamedMapping")); - } - } catch (IOException e) { - throw new GradleException("Failed to remap legacy Forge Minecraft references in generated jar " + destination, e); - } - } - } - - private static java.nio.file.Path findRequestedResult(List requestedResults, String id) { - for (var requestedResult : requestedResults) { - if (requestedResult.id().equals(id) && requestedResult.destination().isFile()) { - return requestedResult.destination().toPath(); - } - } - return null; - } - private static void stripJarSignatures(List requestedResults) { for (var requestedResult : requestedResults) { var destination = requestedResult.destination(); diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java deleted file mode 100644 index d6071b1f..00000000 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeJarProcessor.java +++ /dev/null @@ -1,1085 +0,0 @@ -package net.neoforged.moddevgradle.mcpforge.internal; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Path; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipFile; -import net.neoforged.moddevgradle.internal.utils.FileUtils; -import org.jetbrains.annotations.ApiStatus; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.Handle; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.ClassRemapper; -import org.objectweb.asm.commons.MethodRemapper; -import org.objectweb.asm.commons.Remapper; -import org.tukaani.xz.LZMAInputStream; - -@ApiStatus.Internal -public final class LegacyForgeJarProcessor { - private static final String FORGE_1_12_DEOBF_DATA = "deobfuscation_data-1.12.2.lzma"; - - private LegacyForgeJarProcessor() {} - - public static void remapMinecraftReferences(Path jar) throws IOException { - remapMinecraftReferences(jar, null); - } - - public static void remapMinecraftReferences(Path jar, Path srgToMcpMappings) throws IOException { - LegacyForgeMappings mappings; - Map classInfos; - try (var input = new JarFile(jar.toFile(), false, ZipFile.OPEN_READ)) { - var mappingsEntry = input.getJarEntry(FORGE_1_12_DEOBF_DATA); - if (mappingsEntry == null) { - return; - } - - mappings = readForgeMappings(input, mappingsEntry); - classInfos = readClassInfos(input); - } - - if (mappings.classMappings().isEmpty()) { - return; - } - - if (srgToMcpMappings != null) { - mappings = mappings.withSrgToMcp(readSrgToMcpMappings(srgToMcpMappings)); - } - mappings = mappings.withInheritedMemberMappings(classInfos); - - var tempFile = jar.resolveSibling(jar.getFileName().toString() + ".remapped.tmp"); - try { - try (var input = new JarFile(jar.toFile(), false, ZipFile.OPEN_READ); - var output = new JarOutputStream(Files.newOutputStream(tempFile))) { - var entries = input.entries(); - var remapper = new LegacyForgeRemapper(mappings); - while (entries.hasMoreElements()) { - var entry = entries.nextElement(); - var newEntry = new JarEntry(entry.getName()); - newEntry.setTime(entry.getTime()); - output.putNextEntry(newEntry); - if (!entry.isDirectory()) { - try (var entryInput = input.getInputStream(entry)) { - if (shouldProcess(entry.getName())) { - output.write(remapClass(entryInput.readAllBytes(), remapper, classInfos)); - } else { - entryInput.transferTo(output); - } - } - } - output.closeEntry(); - } - } - FileUtils.atomicMove(tempFile, jar); - } finally { - Files.deleteIfExists(tempFile); - } - } - - private static LegacyForgeMappings readForgeMappings(JarFile jar, JarEntry mappingsEntry) throws IOException { - var classMappings = new HashMap(); - var fieldMappings = new HashMap(); - var methodMappings = new HashMap(); - try (var input = new LZMAInputStream(jar.getInputStream(mappingsEntry)); - var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - var parts = line.split("\\s+"); - if (parts.length < 3) { - continue; - } - - if ("CL:".equals(parts[0])) { - classMappings.put(parts[1], parts[2]); - } else if ("FD:".equals(parts[0])) { - var source = splitMember(parts[1]); - var target = splitMember(parts[2]); - fieldMappings.put(new MemberKey(source.owner(), source.name(), null), target.name()); - } else if ("MD:".equals(parts[0]) && parts.length >= 5) { - var source = splitMember(parts[1]); - var target = splitMember(parts[3]); - methodMappings.put(new MemberKey(source.owner(), source.name(), parts[2]), target.name()); - } - } - } - return new LegacyForgeMappings(classMappings, fieldMappings, methodMappings); - } - - private static SrgToMcpMappings readSrgToMcpMappings(Path mappingsFile) throws IOException { - var fieldMappings = new HashMap(); - var methodMappings = new HashMap(); - try (var reader = Files.newBufferedReader(mappingsFile, StandardCharsets.UTF_8)) { - String line; - while ((line = reader.readLine()) != null) { - var parts = line.split("\\s+"); - if (parts.length < 3) { - continue; - } - - if ("FD:".equals(parts[0])) { - var source = splitMember(parts[1]); - var target = splitMember(parts[2]); - fieldMappings.put(new MemberKey(source.owner(), source.name(), null), target.name()); - } else if ("MD:".equals(parts[0]) && parts.length >= 5) { - var source = splitMember(parts[1]); - var target = splitMember(parts[3]); - methodMappings.put(new MemberKey(source.owner(), source.name(), parts[2]), target.name()); - } - } - } - return new SrgToMcpMappings(fieldMappings, methodMappings); - } - - private static Map readClassInfos(JarFile jar) throws IOException { - var classInfos = new HashMap(); - var entries = jar.entries(); - while (entries.hasMoreElements()) { - var entry = entries.nextElement(); - if (entry.isDirectory() || !entry.getName().endsWith(".class")) { - continue; - } - - try (var input = jar.getInputStream(entry)) { - var reader = new ClassReader(input.readAllBytes()); - var methods = new ArrayList(); - var fields = new ArrayList(); - reader.accept(new ClassVisitor(Opcodes.ASM9) { - @Override - public org.objectweb.asm.FieldVisitor visitField( - int access, - String name, - String descriptor, - String signature, - Object value) { - fields.add(new FieldInfo(access, name, descriptor)); - return null; - } - - @Override - public org.objectweb.asm.MethodVisitor visitMethod( - int access, - String name, - String descriptor, - String signature, - String[] exceptions) { - methods.add(new MethodInfo(access, name, descriptor)); - return null; - } - }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - classInfos.put(reader.getClassName(), new ClassInfo( - reader.getClassName(), - reader.getSuperName(), - List.of(reader.getInterfaces()), - List.copyOf(fields), - List.copyOf(methods))); - } - } - return classInfos; - } - - private static Member splitMember(String value) { - var separator = value.lastIndexOf('/'); - return new Member(value.substring(0, separator), value.substring(separator + 1)); - } - - private static boolean shouldProcess(String entryName) { - return entryName.endsWith(".class") && shouldProcessClass(entryName.substring(0, entryName.length() - ".class".length())); - } - - private static boolean shouldProcessClass(String internalName) { - return isMinecraftClass(internalName) || isForgeClass(internalName); - } - - private static boolean shouldRemapClass(String internalName) { - return isForgeClass(internalName); - } - - private static boolean isMinecraftClass(String internalName) { - return internalName.startsWith("net/minecraft/"); - } - - private static boolean isForgeClass(String internalName) { - return internalName.startsWith("net/minecraftforge/"); - } - - private static byte[] remapClass(byte[] classBytes, LegacyForgeRemapper remapper, Map classInfos) { - var reader = new ClassReader(classBytes); - var writer = newClassWriter(reader.getClassName()); - if (isMinecraftClass(reader.getClassName())) { - reader.accept(new LegacyMinecraftClassReferenceRemapper(writer, remapper, classInfos), 0); - } else { - reader.accept(new LegacyForgeClassRemapper(writer, remapper), 0); - } - return writer.toByteArray(); - } - - private static ClassWriter newClassWriter(String className) { - if (LegacyMinecraftClassReferenceRemapper.needsComputedFrames(className)) { - return new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { - @Override - protected String getCommonSuperClass(String type1, String type2) { - return "java/lang/Object"; - } - }; - } - return new ClassWriter(0); - } - - private static final class LegacyForgeClassRemapper extends ClassRemapper { - private final LegacyForgeRemapper remapper; - - private LegacyForgeClassRemapper(ClassVisitor classVisitor, LegacyForgeRemapper remapper) { - super(classVisitor, remapper); - this.remapper = remapper; - } - - @Override - protected MethodVisitor createMethodRemapper(MethodVisitor methodVisitor) { - return new LegacyMethodRemapper(methodVisitor, remapper); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - var method = super.visitMethod(access, name, descriptor, signature, exceptions); - if (method == null) { - return null; - } - return new MethodVisitor(Opcodes.ASM9, method) { - @Override - public void visitLdcInsn(Object value) { - if (value instanceof String string) { - super.visitLdcInsn(remapper.mapStringConstant(string)); - } else { - super.visitLdcInsn(value); - } - } - }; - } - } - - private static final class LegacyMinecraftClassReferenceRemapper extends ClassVisitor { - private static final String GL_ALLOCATION = "net/minecraft/client/renderer/GLAllocation"; - private static final String CRASH_REPORT_CATEGORY = "net/minecraft/crash/CrashReportCategory"; - - private final LegacyForgeRemapper remapper; - private final Remapper referenceRemapper; - private String className; - - private LegacyMinecraftClassReferenceRemapper( - ClassVisitor classVisitor, - LegacyForgeRemapper remapper, - Map classInfos) { - super(Opcodes.ASM9, classVisitor); - this.remapper = remapper; - this.referenceRemapper = new LegacyMinecraftReferenceRemapper(remapper, classInfos); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - className = name; - super.visit( - version, - access, - remapper.mapType(name), - remapper.mapSignature(signature, false), - remapper.mapType(superName), - interfaces == null ? null : remapper.mapTypes(interfaces)); - } - - @Override - public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - return super.visitField( - access, - name, - remapper.mapDesc(descriptor), - remapper.mapSignature(signature, true), - remapper.mapValue(value)); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - if (GL_ALLOCATION.equals(className) && name.equals("generateDisplayLists") && descriptor.equals("(I)I")) { - var method = super.visitMethod(access, name, descriptor, signature, exceptions); - if (method != null) { - writeGenerateDisplayLists(method); - } - return null; - } - if (CRASH_REPORT_CATEGORY.equals(className) - && name.equals("firstTwoElementsOfStackTraceMatch") - && descriptor.equals("(Ljava/lang/StackTraceElement;Ljava/lang/StackTraceElement;)Z")) { - var method = super.visitMethod(access, name, descriptor, signature, exceptions); - if (method != null) { - writeFirstTwoElementsOfStackTraceMatch(method); - } - return null; - } - - var method = super.visitMethod( - access, - name, - remapper.mapMethodDesc(descriptor), - remapper.mapSignature(signature, false), - exceptions == null ? null : remapper.mapTypes(exceptions)); - return method == null ? null : new LegacyMethodRemapper(method, referenceRemapper); - } - - @Override - public void visitInnerClass(String name, String outerName, String innerName, int access) { - super.visitInnerClass( - remapper.mapType(name), - outerName == null ? null : remapper.mapType(outerName), - innerName, - access); - } - - @Override - public void visitOuterClass(String owner, String name, String descriptor) { - super.visitOuterClass( - remapper.mapType(owner), - name == null ? null : referenceRemapper.mapMethodName(owner, name, descriptor), - descriptor == null ? null : remapper.mapMethodDesc(descriptor)); - } - - private void writeGenerateDisplayLists(MethodVisitor method) { - method.visitCode(); - - var success = new org.objectweb.asm.Label(); - var throwFailure = new org.objectweb.asm.Label(); - var retry = new org.objectweb.asm.Label(); - var retryHandler = new org.objectweb.asm.Label(); - var retryEnd = new org.objectweb.asm.Label(); - var retryDone = new org.objectweb.asm.Label(); - - method.visitTryCatchBlock(retry, retryEnd, retryHandler, "org/lwjgl/LWJGLException"); - - method.visitVarInsn(Opcodes.ILOAD, 0); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "net/minecraft/client/renderer/GlStateManager", - "glGenLists", - "(I)I", - false); - method.visitVarInsn(Opcodes.ISTORE, 1); - method.visitVarInsn(Opcodes.ILOAD, 1); - method.visitJumpInsn(Opcodes.IFNE, success); - - method.visitLabel(retry); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "org/lwjgl/opengl/Display", - "isCreated", - "()Z", - false); - method.visitJumpInsn(Opcodes.IFEQ, retryEnd); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "org/lwjgl/opengl/Display", - "getDrawable", - "()Lorg/lwjgl/opengl/Drawable;", - false); - method.visitMethodInsn( - Opcodes.INVOKEINTERFACE, - "org/lwjgl/opengl/Drawable", - "makeCurrent", - "()V", - true); - method.visitLabel(retryEnd); - method.visitJumpInsn(Opcodes.GOTO, retryDone); - - method.visitLabel(retryHandler); - method.visitInsn(Opcodes.POP); - - method.visitLabel(retryDone); - method.visitVarInsn(Opcodes.ILOAD, 0); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "net/minecraft/client/renderer/GlStateManager", - "glGenLists", - "(I)I", - false); - method.visitVarInsn(Opcodes.ISTORE, 1); - method.visitVarInsn(Opcodes.ILOAD, 1); - method.visitJumpInsn(Opcodes.IFEQ, throwFailure); - - method.visitLabel(success); - method.visitVarInsn(Opcodes.ILOAD, 1); - method.visitInsn(Opcodes.IRETURN); - - method.visitLabel(throwFailure); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "net/minecraft/client/renderer/GlStateManager", - "glGetError", - "()I", - false); - method.visitVarInsn(Opcodes.ISTORE, 2); - method.visitLdcInsn("No error code reported"); - method.visitVarInsn(Opcodes.ASTORE, 3); - - var skipErrorString = new org.objectweb.asm.Label(); - method.visitVarInsn(Opcodes.ILOAD, 2); - method.visitJumpInsn(Opcodes.IFEQ, skipErrorString); - method.visitVarInsn(Opcodes.ILOAD, 2); - method.visitMethodInsn( - Opcodes.INVOKESTATIC, - "org/lwjgl/util/glu/GLU", - "gluErrorString", - "(I)Ljava/lang/String;", - false); - method.visitVarInsn(Opcodes.ASTORE, 3); - method.visitLabel(skipErrorString); - - method.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException"); - method.visitInsn(Opcodes.DUP); - method.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); - method.visitInsn(Opcodes.DUP); - method.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); - method.visitLdcInsn("glGenLists returned an ID of 0 for a count of "); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(Ljava/lang/String;)Ljava/lang/StringBuilder;", - false); - method.visitVarInsn(Opcodes.ILOAD, 0); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(I)Ljava/lang/StringBuilder;", - false); - method.visitLdcInsn(", GL error ("); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(Ljava/lang/String;)Ljava/lang/StringBuilder;", - false); - method.visitVarInsn(Opcodes.ILOAD, 2); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(I)Ljava/lang/StringBuilder;", - false); - method.visitLdcInsn("): "); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(Ljava/lang/String;)Ljava/lang/StringBuilder;", - false); - method.visitVarInsn(Opcodes.ALOAD, 3); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "append", - "(Ljava/lang/String;)Ljava/lang/StringBuilder;", - false); - method.visitMethodInsn( - Opcodes.INVOKEVIRTUAL, - "java/lang/StringBuilder", - "toString", - "()Ljava/lang/String;", - false); - method.visitMethodInsn( - Opcodes.INVOKESPECIAL, - "java/lang/IllegalStateException", - "", - "(Ljava/lang/String;)V", - false); - method.visitInsn(Opcodes.ATHROW); - - method.visitMaxs(4, 4); - method.visitEnd(); - } - - private static boolean needsComputedFrames(String className) { - return GL_ALLOCATION.equals(className) || CRASH_REPORT_CATEGORY.equals(className); - } - - private void writeFirstTwoElementsOfStackTraceMatch(MethodVisitor method) { - method.visitCode(); - var falseLabel = new org.objectweb.asm.Label(); - var compareSecond = new org.objectweb.asm.Label(); - var noSecond = new org.objectweb.asm.Label(); - - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ARRAYLENGTH); - method.visitJumpInsn(Opcodes.IFEQ, falseLabel); - method.visitVarInsn(Opcodes.ALOAD, 1); - method.visitJumpInsn(Opcodes.IFNULL, falseLabel); - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ICONST_0); - method.visitInsn(Opcodes.AALOAD); - method.visitVarInsn(Opcodes.ASTORE, 3); - - method.visitVarInsn(Opcodes.ALOAD, 3); - method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "isNativeMethod", "()Z", false); - method.visitVarInsn(Opcodes.ALOAD, 1); - method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "isNativeMethod", "()Z", false); - method.visitJumpInsn(Opcodes.IF_ICMPNE, falseLabel); - - writeStackTraceElementStringEquals(method, "getClassName"); - method.visitJumpInsn(Opcodes.IFEQ, falseLabel); - writeStackTraceElementStringEquals(method, "getFileName"); - method.visitJumpInsn(Opcodes.IFEQ, falseLabel); - writeStackTraceElementStringEquals(method, "getMethodName"); - method.visitJumpInsn(Opcodes.IFEQ, falseLabel); - - method.visitVarInsn(Opcodes.ALOAD, 2); - method.visitJumpInsn(Opcodes.IFNONNULL, compareSecond); - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ARRAYLENGTH); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.IF_ICMPGT, falseLabel); - method.visitJumpInsn(Opcodes.GOTO, noSecond); - - method.visitLabel(compareSecond); - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ARRAYLENGTH); - method.visitInsn(Opcodes.ICONST_1); - method.visitJumpInsn(Opcodes.IF_ICMPLE, falseLabel); - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ICONST_1); - method.visitInsn(Opcodes.AALOAD); - method.visitVarInsn(Opcodes.ALOAD, 2); - method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "equals", "(Ljava/lang/Object;)Z", false); - method.visitJumpInsn(Opcodes.IFEQ, falseLabel); - - method.visitLabel(noSecond); - method.visitVarInsn(Opcodes.ALOAD, 0); - method.visitFieldInsn(Opcodes.GETFIELD, CRASH_REPORT_CATEGORY, "stackTrace", "[Ljava/lang/StackTraceElement;"); - method.visitInsn(Opcodes.ICONST_0); - method.visitVarInsn(Opcodes.ALOAD, 1); - method.visitInsn(Opcodes.AASTORE); - method.visitInsn(Opcodes.ICONST_1); - method.visitInsn(Opcodes.IRETURN); - - method.visitLabel(falseLabel); - method.visitInsn(Opcodes.ICONST_0); - method.visitInsn(Opcodes.IRETURN); - method.visitMaxs(0, 0); - method.visitEnd(); - } - - private void writeStackTraceElementStringEquals(MethodVisitor method, String getterName) { - method.visitVarInsn(Opcodes.ALOAD, 3); - method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", getterName, "()Ljava/lang/String;", false); - method.visitVarInsn(Opcodes.ALOAD, 1); - method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", getterName, "()Ljava/lang/String;", false); - method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false); - } - } - - private static final class LegacyMethodRemapper extends MethodRemapper { - private static final String LAMBDA_METAFACTORY = "java/lang/invoke/LambdaMetafactory"; - private static final String METAFACTORY_DESCRIPTOR = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;" - + "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;" - + "Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"; - private static final String ALT_METAFACTORY_DESCRIPTOR = "(Ljava/lang/invoke/MethodHandles$Lookup;" - + "Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"; - - private final Remapper remapper; - - private LegacyMethodRemapper(MethodVisitor methodVisitor, Remapper remapper) { - super(methodVisitor, remapper); - this.remapper = remapper; - } - - @Override - public void visitInvokeDynamicInsn( - String name, - String descriptor, - Handle bootstrapMethodHandle, - Object... bootstrapMethodArguments) { - super.visitInvokeDynamicInsn( - mapInvokeDynamicMethodName(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments), - descriptor, - bootstrapMethodHandle, - bootstrapMethodArguments); - } - - private String mapInvokeDynamicMethodName( - String name, - String descriptor, - Handle bootstrapMethodHandle, - Object[] bootstrapMethodArguments) { - if (!isLambdaMetafactory(bootstrapMethodHandle) - || bootstrapMethodArguments.length == 0 - || !(bootstrapMethodArguments[0] instanceof Type samMethodType)) { - return remapper.mapInvokeDynamicMethodName(name, descriptor); - } - - var samOwner = Type.getReturnType(descriptor); - if (samOwner.getSort() != Type.OBJECT) { - return remapper.mapInvokeDynamicMethodName(name, descriptor); - } - - return remapper.mapMethodName(samOwner.getInternalName(), name, samMethodType.getDescriptor()); - } - - private static boolean isLambdaMetafactory(Handle bootstrapMethodHandle) { - if (!LAMBDA_METAFACTORY.equals(bootstrapMethodHandle.getOwner()) - || bootstrapMethodHandle.getTag() != Opcodes.H_INVOKESTATIC) { - return false; - } - - return ("metafactory".equals(bootstrapMethodHandle.getName()) - && METAFACTORY_DESCRIPTOR.equals(bootstrapMethodHandle.getDesc())) - || ("altMetafactory".equals(bootstrapMethodHandle.getName()) - && ALT_METAFACTORY_DESCRIPTOR.equals(bootstrapMethodHandle.getDesc())); - } - } - - private static final class LegacyMinecraftReferenceRemapper extends Remapper { - private final LegacyForgeRemapper delegate; - private final Map classInfos; - - private LegacyMinecraftReferenceRemapper(LegacyForgeRemapper delegate, Map classInfos) { - this.delegate = delegate; - this.classInfos = classInfos; - } - - @Override - public String map(String internalName) { - return delegate.map(internalName); - } - - @Override - public String mapFieldName(String owner, String name, String descriptor) { - if (isDeclaredMinecraftField(owner, name)) { - return name; - } - return delegate.mapFieldName(owner, name, descriptor); - } - - @Override - public String mapMethodName(String owner, String name, String descriptor) { - if (isDeclaredMinecraftMethod(owner, name, descriptor)) { - return name; - } - return delegate.mapMethodName(owner, name, descriptor); - } - - private boolean isDeclaredMinecraftField(String owner, String name) { - if (!isMinecraftClass(owner)) { - return false; - } - var classInfo = classInfos.get(owner); - return classInfo != null && classInfo.fields().stream().anyMatch(field -> field.name().equals(name)); - } - - private boolean isDeclaredMinecraftMethod(String owner, String name, String descriptor) { - if (!isMinecraftClass(owner)) { - return false; - } - var classInfo = classInfos.get(owner); - return classInfo != null && classInfo.methods().stream() - .anyMatch(method -> method.name().equals(name) && method.descriptor().equals(descriptor)); - } - } - - private static final class LegacyForgeRemapper extends Remapper { - private final LegacyForgeMappings mappings; - - private LegacyForgeRemapper(LegacyForgeMappings mappings) { - this.mappings = mappings; - } - - @Override - public String map(String internalName) { - return mappings.classMappings().getOrDefault(internalName, internalName); - } - - @Override - public String mapFieldName(String owner, String name, String descriptor) { - var mappedName = mappings.fieldMappings().get(new MemberKey(owner, name, null)); - if (mappedName != null) { - return mappedName; - } - - var mappedOwner = mappings.classMappings().get(owner); - if (mappedOwner != null) { - mappedName = mappings.fieldMappings().get(new MemberKey(mappedOwner, name, null)); - if (mappedName != null) { - return mappedName; - } - } - return name; - } - - @Override - public String mapMethodName(String owner, String name, String descriptor) { - var mappedName = mappings.methodMappings().get(new MemberKey(owner, name, descriptor)); - if (mappedName != null) { - return mappedName; - } - - var mappedDescriptor = mapMethodDesc(descriptor); - mappedName = mappings.methodMappings().get(new MemberKey(owner, name, mappedDescriptor)); - if (mappedName != null) { - return mappedName; - } - - var mappedOwner = mappings.classMappings().get(owner); - if (mappedOwner != null) { - mappedName = mappings.methodMappings().get(new MemberKey(mappedOwner, name, mappedDescriptor)); - if (mappedName != null) { - return mappedName; - } - } - return name; - } - - private String mapStringConstant(String value) { - return mappings.stringMappings().getOrDefault(value, value); - } - } - - private record LegacyForgeMappings( - Map classMappings, - Map fieldMappings, - Map methodMappings, - Map stringMappings) { - private LegacyForgeMappings( - Map classMappings, - Map fieldMappings, - Map methodMappings) { - this(classMappings, fieldMappings, methodMappings, Map.of()); - } - - LegacyForgeMappings withSrgToMcp(SrgToMcpMappings srgToMcp) { - var composedFields = new HashMap(); - var composedStringMappings = new HashMap<>(stringMappings); - for (var entry : fieldMappings.entrySet()) { - var key = entry.getKey(); - var mappedOwner = classMappings.getOrDefault(key.owner(), key.owner()); - var srgName = entry.getValue(); - var mcpName = srgToMcp.fieldMappings().getOrDefault(new MemberKey(mappedOwner, srgName, null), srgName); - composedFields.put(key, mcpName); - composedFields.put(new MemberKey(mappedOwner, key.name(), null), mcpName); - addUniqueStringMapping(composedStringMappings, srgName, mcpName); - } - - var classOnlyRemapper = new LegacyForgeRemapper(new LegacyForgeMappings(classMappings, Map.of(), Map.of())); - var composedMethods = new HashMap(); - for (var entry : methodMappings.entrySet()) { - var key = entry.getKey(); - var mappedOwner = classMappings.getOrDefault(key.owner(), key.owner()); - var mappedDescriptor = classOnlyRemapper.mapMethodDesc(key.descriptor()); - var srgName = entry.getValue(); - var mcpName = srgToMcp.methodMappings().getOrDefault(new MemberKey(mappedOwner, srgName, mappedDescriptor), srgName); - composedMethods.put(key, mcpName); - composedMethods.put(new MemberKey(mappedOwner, key.name(), mappedDescriptor), mcpName); - addUniqueStringMapping(composedStringMappings, srgName, mcpName); - } - - composedStringMappings.values().removeIf(value -> value == null); - return new LegacyForgeMappings(classMappings, composedFields, composedMethods, composedStringMappings); - } - - LegacyForgeMappings withInheritedMemberMappings(Map classInfos) { - var remappedFields = new HashMap<>(fieldMappings); - var remappedMethods = new HashMap<>(methodMappings); - var classOnlyRemapper = new LegacyForgeRemapper(new LegacyForgeMappings(classMappings, Map.of(), Map.of())); - - for (var classInfo : classInfos.values()) { - if (!isMinecraftOrForgeClass(classInfo.name())) { - continue; - } - - for (var fieldMapping : findInheritedFieldMappings( - classInfo.superName(), - classInfo.interfaces(), - classInfos, - new HashSet<>()).entrySet()) { - remappedFields.put(new MemberKey(classInfo.name(), fieldMapping.getKey(), null), fieldMapping.getValue()); - putObfuscatedClassMemberMapping(remappedFields, classInfo.name(), fieldMapping.getKey(), null, fieldMapping.getValue()); - } - - for (var method : classInfo.methods()) { - if (method.name().startsWith("<") - || (method.access() & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0) { - continue; - } - - var mappedName = findMappedOverrideName( - classInfo.superName(), - classInfo.interfaces(), - method.name(), - method.descriptor(), - classInfos, - classOnlyRemapper, - new HashSet<>()); - if (mappedName != null && !mappedName.equals(method.name())) { - remappedMethods.put(new MemberKey(classInfo.name(), method.name(), method.descriptor()), mappedName); - putObfuscatedClassMemberMapping(remappedMethods, classInfo.name(), method.name(), method.descriptor(), mappedName); - } - } - - for (var methodMapping : findInheritedMethodMappings( - classInfo.superName(), - classInfo.interfaces(), - classInfos, - classOnlyRemapper, - new HashSet<>()).entrySet()) { - remappedMethods.put(new MemberKey(classInfo.name(), methodMapping.getKey().name(), methodMapping.getKey().descriptor()), methodMapping.getValue()); - putObfuscatedClassMemberMapping( - remappedMethods, - classInfo.name(), - methodMapping.getKey().name(), - methodMapping.getKey().descriptor(), - methodMapping.getValue()); - } - } - - return new LegacyForgeMappings(classMappings, remappedFields, remappedMethods, stringMappings); - } - - private static void addUniqueStringMapping(Map stringMappings, String sourceName, String mappedName) { - if (sourceName.equals(mappedName)) { - return; - } - - var existing = stringMappings.putIfAbsent(sourceName, mappedName); - if (existing != null && !existing.equals(mappedName)) { - stringMappings.put(sourceName, null); - } - } - - private void putObfuscatedClassMemberMapping( - Map mappings, - String mappedOwner, - String name, - String descriptor, - String mappedName) { - for (var entry : classMappings.entrySet()) { - if (entry.getValue().equals(mappedOwner)) { - mappings.put(new MemberKey(entry.getKey(), name, descriptor), mappedName); - return; - } - } - } - - private boolean isMinecraftOrForgeClass(String name) { - return name.startsWith("net/minecraft/") || shouldRemapClass(name); - } - - private Map findInheritedFieldMappings( - String superName, - List interfaces, - Map classInfos, - HashSet visited) { - var inheritedFields = new HashMap(); - addInheritedFieldMappings(superName, classInfos, visited, inheritedFields); - for (var interfaceName : interfaces) { - addInheritedFieldMappings(interfaceName, classInfos, visited, inheritedFields); - } - return inheritedFields; - } - - private void addInheritedFieldMappings( - String owner, - Map classInfos, - HashSet visited, - Map inheritedFields) { - if (owner == null || !visited.add(owner)) { - return; - } - - for (var entry : fieldMappings.entrySet()) { - if (entry.getKey().owner().equals(owner)) { - inheritedFields.putIfAbsent(entry.getKey().name(), entry.getValue()); - } - } - - var classInfo = classInfos.get(owner); - if (classInfo == null) { - classInfo = classInfos.get(classMappings.get(owner)); - } - if (classInfo == null) { - return; - } - - for (var field : classInfo.fields()) { - if ((field.access() & Opcodes.ACC_PRIVATE) != 0) { - inheritedFields.remove(field.name()); - } - } - - addInheritedFieldMappings(classInfo.superName(), classInfos, visited, inheritedFields); - for (var interfaceName : classInfo.interfaces()) { - addInheritedFieldMappings(interfaceName, classInfos, visited, inheritedFields); - } - } - - private Map findInheritedMethodMappings( - String superName, - List interfaces, - Map classInfos, - Remapper classOnlyRemapper, - HashSet visited) { - var inheritedMethods = new HashMap(); - addInheritedMethodMappings(superName, classInfos, classOnlyRemapper, visited, inheritedMethods); - for (var interfaceName : interfaces) { - addInheritedMethodMappings(interfaceName, classInfos, classOnlyRemapper, visited, inheritedMethods); - } - return inheritedMethods; - } - - private void addInheritedMethodMappings( - String owner, - Map classInfos, - Remapper classOnlyRemapper, - HashSet visited, - Map inheritedMethods) { - if (owner == null || !visited.add(owner)) { - return; - } - - for (var entry : methodMappings.entrySet()) { - var key = entry.getKey(); - if (key.owner().equals(owner)) { - inheritedMethods.putIfAbsent(new MemberKey(null, key.name(), key.descriptor()), entry.getValue()); - continue; - } - - var mappedOwner = classMappings.getOrDefault(owner, owner); - if (key.owner().equals(mappedOwner)) { - var obfuscatedDescriptor = classOnlyRemapper.mapMethodDesc(key.descriptor()); - inheritedMethods.putIfAbsent(new MemberKey(null, key.name(), obfuscatedDescriptor), entry.getValue()); - } - } - - var classInfo = classInfos.get(owner); - if (classInfo == null) { - classInfo = classInfos.get(classMappings.get(owner)); - } - if (classInfo == null) { - return; - } - - for (var method : classInfo.methods()) { - if ((method.access() & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0) { - inheritedMethods.remove(new MemberKey(null, method.name(), method.descriptor())); - } - } - - addInheritedMethodMappings(classInfo.superName(), classInfos, classOnlyRemapper, visited, inheritedMethods); - for (var interfaceName : classInfo.interfaces()) { - addInheritedMethodMappings(interfaceName, classInfos, classOnlyRemapper, visited, inheritedMethods); - } - } - - private String findMappedOverrideName( - String superName, - List interfaces, - String name, - String descriptor, - Map classInfos, - Remapper classOnlyRemapper, - HashSet visited) { - var mappedName = findMappedOverrideName(superName, name, descriptor, classInfos, classOnlyRemapper, visited); - if (mappedName != null) { - return mappedName; - } - - for (var interfaceName : interfaces) { - mappedName = findMappedOverrideName(interfaceName, name, descriptor, classInfos, classOnlyRemapper, visited); - if (mappedName != null) { - return mappedName; - } - } - return null; - } - - private String findMappedOverrideName( - String owner, - String name, - String descriptor, - Map classInfos, - Remapper classOnlyRemapper, - HashSet visited) { - if (owner == null || !visited.add(owner)) { - return null; - } - - var mappedName = mappedMethodName(owner, name, descriptor, classOnlyRemapper); - if (mappedName != null) { - return mappedName; - } - - var classInfo = classInfos.get(owner); - if (classInfo == null) { - classInfo = classInfos.get(classMappings.get(owner)); - } - if (classInfo == null) { - return null; - } - - return findMappedOverrideName( - classInfo.superName(), - classInfo.interfaces(), - name, - descriptor, - classInfos, - classOnlyRemapper, - visited); - } - - private String mappedMethodName(String owner, String name, String descriptor, Remapper classOnlyRemapper) { - var mappedName = methodMappings.get(new MemberKey(owner, name, descriptor)); - if (mappedName != null) { - return mappedName; - } - - var mappedOwner = classMappings.getOrDefault(owner, owner); - var mappedDescriptor = classOnlyRemapper.mapMethodDesc(descriptor); - mappedName = methodMappings.get(new MemberKey(mappedOwner, name, mappedDescriptor)); - if (mappedName != null) { - return mappedName; - } - - mappedName = methodMappings.get(new MemberKey(owner, name, mappedDescriptor)); - if (mappedName != null) { - return mappedName; - } - - return methodMappings.get(new MemberKey(mappedOwner, name, descriptor)); - } - } - - private record SrgToMcpMappings(Map fieldMappings, Map methodMappings) {} - - private record Member(String owner, String name) {} - - private record MemberKey(String owner, String name, String descriptor) {} - - private record ClassInfo(String name, String superName, List interfaces, List fields, List methods) {} - - private record FieldInfo(int access, String name, String descriptor) {} - - private record MethodInfo(int access, String name, String descriptor) {} -} diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index 37b416ad..2f4a2940 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -17,7 +17,6 @@ import net.neoforged.moddevgradle.internal.ArtifactNamingStrategy; import net.neoforged.moddevgradle.internal.Branding; import net.neoforged.moddevgradle.internal.DataFileCollections; -import net.neoforged.moddevgradle.internal.JarPostProcessor; import net.neoforged.moddevgradle.internal.McpToolchainHooks; import net.neoforged.moddevgradle.internal.ModDevArtifactsWorkflow; import net.neoforged.moddevgradle.internal.ModDevRunWorkflow; @@ -168,14 +167,9 @@ public void apply(Project project) { dataFileCollections.interfaceInjectionData().extension()); - // Register MCP hooks so the main workflow picks up LWJGL2 natives + jar post-processing. + // Register MCP hooks so the main workflow picks up LWJGL2 natives. project.getExtensions().add(McpToolchainHooks.EXTENSION_NAME, new McpHooks()); - // Register the 1.12.2 jar post-processor for all CreateMinecraftArtifacts tasks. - project.getTasks().withType(CreateMinecraftArtifacts.class).configureEach(task -> { - task.getJarPostProcessors().add(new ForgeJarPostProcessor()); - }); - // Collect Access Transformers declared by dependency jars via their FMLAT manifest (FG2/RFG parity). configureDependencyAccessTransformers(project); @@ -628,11 +622,4 @@ public void configureNativeLibraries(Configuration nativeLibraries, DependencyFa Lwjgl2Natives.configure(nativeLibraries, dependencyFactory, minecraftVersion); } } - - static class ForgeJarPostProcessor implements JarPostProcessor { - @Override - public void process(Path jar, @Nullable Path srgToMcpMappings) throws IOException { - LegacyForgeJarProcessor.remapMinecraftReferences(jar, srgToMcpMappings); - } - } } From e41155b57c9ba0343dd7a00929b3c3e3160e4473 Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 16:41:29 +0800 Subject: [PATCH 05/11] mcpforge: remove redundant stripJarSignatures + removePreAppliedLegacyForgeRuntimePatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified empirically that NFRT's createMinecraftArtifacts output for the default (disableRecompilation=false) legacy-MCP path is already clean: the recompiled game jar contains no signature files, no META-INF, and no binpatches.pack.lzma (only the harmless deobfuscation_data lzma, which was never stripped anyway). The recompile pipeline produces fresh unsigned classes and doesn't carry the binary-patch payload. So stripJarSignatures and removePreAppliedLegacyForgeRuntimePatches were no-ops on the already-clean default-path jar — same situation as the removed LegacyForgeJarProcessor. Morphism still launches to the main menu (18 mods, sound, texture atlas) with both removed. Note: the no-recompile path (gameJarNoRecompWithNeoForge, built via createBinaryWithNeoForge) DOES still carry FORGE.SF/FORGE.DSA/binpatches.pack.lzma, but that path doesn't work for real mods anyway (binary remap leaks obfuscated type names). Cleaning it belongs in NFRT's binary-merge step, not MDG. --- .../nfrtgradle/CreateMinecraftArtifacts.java | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java index 9af83fee..fa5bea07 100644 --- a/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java +++ b/src/main/java/net/neoforged/nfrtgradle/CreateMinecraftArtifacts.java @@ -6,9 +6,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.inject.Inject; -import net.neoforged.moddevgradle.internal.utils.FileUtils; import net.neoforged.moddevgradle.internal.utils.ProblemReportingUtil; import net.neoforged.problems.FileProblemReporter; import net.neoforged.problems.Problem; @@ -430,43 +428,11 @@ public void createArtifacts() { try { run(args); - stripJarSignatures(requestedResults); - removePreAppliedLegacyForgeRuntimePatches(requestedResults); } finally { reportProblems(problemsReport); } } - private static void removePreAppliedLegacyForgeRuntimePatches(List requestedResults) { - for (var requestedResult : requestedResults) { - var destination = requestedResult.destination(); - if (!destination.isFile() || !destination.getName().endsWith(".jar")) { - continue; - } - - try { - FileUtils.removeJarEntries(destination.toPath(), Set.of("binpatches.pack.lzma")); - } catch (IOException e) { - throw new GradleException("Failed to remove legacy Forge runtime patches from generated jar " + destination, e); - } - } - } - - private static void stripJarSignatures(List requestedResults) { - for (var requestedResult : requestedResults) { - var destination = requestedResult.destination(); - if (!destination.isFile() || !destination.getName().endsWith(".jar")) { - continue; - } - - try { - FileUtils.stripJarSignatures(destination.toPath()); - } catch (IOException e) { - throw new GradleException("Failed to strip signature metadata from generated jar " + destination, e); - } - } - } - private void reportProblems(File problemsReport) { if (!problemsReport.exists()) { return; // Not created -> nothing to report From b7cf057081eee3e3c14498184acbfdd798736831 Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 21:32:02 +0800 Subject: [PATCH 06/11] mcpforge: forbid disabling recompilation The no-recompile (binary-patch) path is not viable for 1.12.2/MCP: the binary remap leaks obfuscated type references and mangles generic signatures, so mods cannot compile or run against gameJarNoRecompWithNeoForge. Throw an InvalidUserCodeException if disableRecompilation is set, so users get a clear error instead of the cryptic binary-path failures. The decompile+recompile path remains the only supported one. --- .../mcpforge/internal/McpForgeModDevPlugin.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index 2f4a2940..476ae8a0 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -216,6 +216,13 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx var configurations = project.getConfigurations(); + // The no-recompile (binary-patch) path is not supported for 1.12.2/MCP: the binary remap it relies on leaks + // obfuscated type references and mangles generic signatures, so mods cannot compile/run against it. The + // decompile+recompile path is required. + if (settings.isDisableRecompilation()) { + throw new InvalidUserCodeException("disableRecompilation is not supported by the mcpforge plugin; the 1.12.2/MCP toolchain requires the decompile+recompile path."); + } + var artifacts = ModDevArtifactsWorkflow.create( project, settings.getEnabledSourceSets(), From d6a35b8596827d0f52cca72d7052a7e87efde03c Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Mon, 29 Jun 2026 22:46:46 +0800 Subject: [PATCH 07/11] mcpforge: make McpForgeModdingSettings standalone (drop disableRecompilation) McpForgeModdingSettings no longer extends LegacyForgeModdingSettings; it is a standalone type with only the fields mcpforge uses (forge/neoForge/mcp version, mcpMappings, enabledSourceSets, obfuscateJar). Crucially there is no disableRecompilation setter at all, so the no-recompile path cannot be expressed - eliminating the risk at the source rather than guarding it at runtime. The previous runtime guard in enable() is removed (no longer needed); create() is now passed disableRecompilation=false directly. --- .../mcpforge/dsl/McpForgeModdingSettings.java | 92 ++++++++++++++++++- .../internal/McpForgeModDevPlugin.java | 9 +- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java index 72097eb0..967288a9 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/dsl/McpForgeModdingSettings.java @@ -1,18 +1,81 @@ package net.neoforged.moddevgradle.mcpforge.dsl; +import java.util.HashSet; +import java.util.Set; import javax.inject.Inject; -import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeModdingSettings; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.Nullable; -public abstract class McpForgeModdingSettings extends LegacyForgeModdingSettings { +/** + * Settings for the mcpforge {@code enable {}} block. This is a standalone type (it deliberately does NOT extend + * {@code LegacyForgeModdingSettings}) so that the no-recompile option cannot even be expressed: the 1.12.2/MCP + * toolchain only supports the decompile+recompile path, and there is no {@code disableRecompilation} setter here. + */ +public abstract class McpForgeModdingSettings { + @Nullable + private String neoForgeVersion; + + @Nullable + private String forgeVersion; + + @Nullable + private String mcpVersion; + + @Nullable private String mcpMappings; + private Set enabledSourceSets = new HashSet<>(); + + private boolean obfuscateJar = true; + @Inject public McpForgeModdingSettings(Project project) { - super(project); + // By default, enable modding deps only for the main source set + var sourceSets = ExtensionUtils.getSourceSets(project); + var mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + enabledSourceSets.add(mainSourceSet); + } + + public @Nullable String getNeoForgeVersion() { + return neoForgeVersion; + } + + public @Nullable String getForgeVersion() { + return forgeVersion; + } + + public @Nullable String getMcpVersion() { + return mcpVersion; + } + + /** + * NeoForge version number. You have to set either this, {@link #setForgeVersion} or {@link #setMcpVersion}. + */ + public void setNeoForgeVersion(String version) { + this.neoForgeVersion = version; } + /** + * Minecraft Forge version. You have to set either this, {@link #setNeoForgeVersion} or {@link #setMcpVersion}. + */ + public void setForgeVersion(String version) { + this.forgeVersion = version; + } + + /** + * Set this to a version of MCP + * to compile against Vanilla artifacts that have no Forge code added. + */ + public void setMcpVersion(String version) { + this.mcpVersion = version; + } + + /** + * Gradle dependency notation of the legacy MCP mapping zip, e.g. + * {@code de.oceanlabs.mcp:mcp_stable:39-1.12@zip}. Required for 1.12.2 Forge/MCP builds. + */ public @Nullable String getMcpMappings() { return mcpMappings; } @@ -20,4 +83,27 @@ public McpForgeModdingSettings(Project project) { public void setMcpMappings(String mcpMappings) { this.mcpMappings = mcpMappings; } + + /** + * Contains the list of source sets for which access to Minecraft classes should be configured. + * Defaults to the main source set, but can also be set to an empty list. + */ + public Set getEnabledSourceSets() { + return enabledSourceSets; + } + + public void setEnabledSourceSets(Set enabledSourceSets) { + this.enabledSourceSets = enabledSourceSets; + } + + /** + * {@return true if the default reobfuscation task should be created} + */ + public boolean isObfuscateJar() { + return obfuscateJar; + } + + public void setObfuscateJar(boolean obfuscateJar) { + this.obfuscateJar = obfuscateJar; + } } diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index 476ae8a0..705835e7 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -216,13 +216,6 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx var configurations = project.getConfigurations(); - // The no-recompile (binary-patch) path is not supported for 1.12.2/MCP: the binary remap it relies on leaks - // obfuscated type references and mangles generic signatures, so mods cannot compile/run against it. The - // decompile+recompile path is required. - if (settings.isDisableRecompilation()) { - throw new InvalidUserCodeException("disableRecompilation is not supported by the mcpforge plugin; the 1.12.2/MCP toolchain requires the decompile+recompile path."); - } - var artifacts = ModDevArtifactsWorkflow.create( project, settings.getEnabledSourceSets(), @@ -233,7 +226,7 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), versionCapabilities, - settings.isDisableRecompilation(), + false, // disableRecompilation is not supported by mcpforge (no-recompile/binary-patch path is not viable for 1.12.2/MCP) settings.getMcpMappings()); // Configure the mixin and obfuscation extensions. From e56a1196b17e98c00d3a3d573ff7440d086a79d3 Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Tue, 30 Jun 2026 01:09:20 +0800 Subject: [PATCH 08/11] main: remove hardcoded 1.12.2 from PrepareRunOrTest via McpToolchainHooks SPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -XstartOnFirstThread gate (usesLegacyAppleLwjgl2) and the Forge splash disable (configureLegacyForgeSplash) both hardcoded "1.12.2" + arch checks directly in main. Move both behind McpToolchainHooks SPI queries: - usesLegacyAppleLwjgl2(mc) → mcpforge delegates to Lwjgl2Natives.shouldUse- AppleNativeReplacement (the single source of truth for the ARM LWJGL2 replace). - needsLegacyForgeSplashDisabled(mc) → mcpforge returns true for 1.12.2. Main no longer contains any "1.12.2" string or arch sniffing; the 1.12.2 logic lives entirely in mcpforge (where it belongs). When mcpforge isn't applied, the SPI defaults return false (modern behavior). --- .../internal/McpToolchainHooks.java | 17 +++++++++++++++++ .../moddevgradle/internal/PrepareRunOrTest.java | 8 +++----- .../mcpforge/internal/McpForgeModDevPlugin.java | 11 +++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java b/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java index 464e7e07..dc94e563 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/McpToolchainHooks.java @@ -23,6 +23,23 @@ default void configureRuntimeNatives(Configuration configuration, DependencyFact /** Configures native library dependencies for legacy versions (natives extraction). */ default void configureNativeLibraries(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion) {} + /** + * Returns {@code true} if the legacy Apple LWJGL2 native replacement is active for the given version on the + * current platform. When active, {@code -XstartOnFirstThread} should NOT be added on macOS (the replacement + * does not require it). Defaults to {@code false} (modern LWJGL3 behavior — add the flag on macOS). + */ + default boolean usesLegacyAppleLwjgl2(String minecraftVersion) { + return false; + } + + /** + * Returns {@code true} if the legacy Forge splash screen (SplashProgress) should be disabled on macOS for the + * given version (it is buggy/crashes on Mac for 1.12.2). + */ + default boolean needsLegacyForgeSplashDisabled(String minecraftVersion) { + return false; + } + /** Retrieves the hooks registered on the project, or {@link #NOOP} if none. */ static McpToolchainHooks get(org.gradle.api.Project project) { var hooks = project.getExtensions().findByName(EXTENSION_NAME); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index 1977a621..3f870165 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -172,10 +172,8 @@ private List getInterpolatedJvmArgs(UserDevRunType runConfig) { } private boolean usesLegacyAppleLwjgl2() { - var capabilities = getVersionCapabilities().get(); - var arch = System.getProperty("os.arch"); - return "1.12.2".equals(capabilities.minecraftVersion()) - && ("aarch64".equals(arch) || "arm64".equals(arch)); + return McpToolchainHooks.get(getProject()) + .usesLegacyAppleLwjgl2(getVersionCapabilities().get().minecraftVersion()); } @TaskAction @@ -214,7 +212,7 @@ public void prepareRun() throws IOException { private void configureLegacyForgeSplash(File runDir) throws IOException { if (!isClientDistribution() || OperatingSystem.current() != OperatingSystem.MACOS - || !"1.12.2".equals(getVersionCapabilities().get().minecraftVersion())) { + || !McpToolchainHooks.get(getProject()).needsLegacyForgeSplashDisabled(getVersionCapabilities().get().minecraftVersion())) { return; } diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index 705835e7..e47960a2 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -621,5 +621,16 @@ public void configureRuntimeNatives(Configuration configuration, DependencyFacto public void configureNativeLibraries(Configuration nativeLibraries, DependencyFactory dependencyFactory, String minecraftVersion) { Lwjgl2Natives.configure(nativeLibraries, dependencyFactory, minecraftVersion); } + + @Override + public boolean usesLegacyAppleLwjgl2(String minecraftVersion) { + return Lwjgl2Natives.shouldUseAppleNativeReplacement( + minecraftVersion, System.getProperty("os.name"), System.getProperty("os.arch")); + } + + @Override + public boolean needsLegacyForgeSplashDisabled(String minecraftVersion) { + return "1.12.2".equals(minecraftVersion); + } } } From 79331d6e930e3f47ed82ce8b1f84d2f66a333b9d Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Tue, 30 Jun 2026 14:30:15 +0800 Subject: [PATCH 09/11] main: replace env-file mechanism with Provider-based run.getEnvironment() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the environment-file indirection (getEnvironmentFile, writeEnvironment, getUserEnvironment, getRunTemplateReplacements, interpolateRunTemplateValue, LoadPreparedTestEnvironment) from PrepareRunOrTest/RunGameTask/CreateLaunchScriptTask/ ModDevRunWorkflow. Environment variables now flow exclusively through run.getEnvironment() → getEnvironmentProperty (the existing MapProperty mechanism). mcpforge sets the 1.12.2 legacy env vars (MOD_CLASSES, MCP_MAPPINGS, nativesDirectory, tweakClass, etc.) directly via Providers on run.getEnvironment(), replacing the config.json runs.env token+interpolation+file pipeline. No intermediate files, no token system, no env-specific interpolation — just lazy Providers. Program-args token interpolation reverts to upstream's inline switch ({assets_root}/{asset_index} only). --- .../internal/CreateLaunchScriptTask.java | 9 +--- .../internal/ModDevRunWorkflow.java | 28 ----------- .../internal/PrepareRunOrTest.java | 49 ++----------------- .../moddevgradle/internal/RunGameTask.java | 8 --- .../internal/McpForgeModDevPlugin.java | 18 +++++++ 5 files changed, 25 insertions(+), 87 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java b/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java index 301fe48e..a2e20661 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/CreateLaunchScriptTask.java @@ -51,9 +51,6 @@ abstract class CreateLaunchScriptTask extends DefaultTask { @InputFile abstract Property getProgramArgsFile(); - @InputFile - abstract Property getEnvironmentFile(); - /** * This argument file is only used by the launch shell-scripts. */ @@ -123,10 +120,8 @@ public void createScripts() throws IOException { } } - private Map getMergedEnvironment() throws IOException { - var environment = new java.util.LinkedHashMap<>(RunUtils.loadEnvironmentFile(new File(getEnvironmentFile().get()))); - environment.putAll(getEnvironment().get()); - return environment; + private Map getMergedEnvironment() { + return new java.util.LinkedHashMap<>(getEnvironment().get()); } private static List createJavaCommand( diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java index 4781bb03..0ed4fe27 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -423,7 +423,6 @@ private static TaskProvider setupRunInGradle( task.getGameDirectory().set(run.getGameDirectory()); task.getVmArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.VMARGS)); task.getProgramArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.PROGRAMARGS)); - task.getEnvironmentFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.ENVIRONMENT)); task.getLog4jConfigFileOverride().set(run.getLoggingConfigFile()); task.getLog4jConfigFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.LOG4J_CONFIG)); task.getRunType().set(run.getType()); @@ -437,9 +436,6 @@ private static TaskProvider setupRunInGradle( props = new HashMap<>(props); return props; })); - task.getUserEnvironment().set(run.getEnvironment()); - task.getRunTemplateReplacements().set(project.provider(() -> runTemplateReplacements.entrySet().stream() - .collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get())))); task.getMainClass().set(run.getMainClass()); task.getProgramArguments().set(run.getProgramArguments()); task.getJvmArguments().set(run.getJvmArguments()); @@ -462,7 +458,6 @@ private static TaskProvider setupRunInGradle( task.getClasspathArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.CLASSPATH)); task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath())); - task.getEnvironmentFile().set(prepareRunTask.get().getEnvironmentFile().map(d -> d.getAsFile().getAbsolutePath())); task.getEnvironment().set(run.getEnvironment()); task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); }); @@ -482,7 +477,6 @@ private static TaskProvider setupRunInGradle( task.getGameDirectory().set(run.getGameDirectory()); task.getEnvironmentProperty().set(run.getEnvironment()); - task.getEnvironmentFile().set(prepareRunTask.get().getEnvironmentFile()); task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile()); task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile()); task.getMainClass().set(RunUtils.DEV_LAUNCH_MAIN_CLASS); @@ -566,7 +560,6 @@ static void setupTestTask(Project project, task.getGameDirectory().set(gameDirectory); task.getVmArgsFile().set(vmArgsFile); task.getProgramArgsFile().set(programArgsFile); - task.getEnvironmentFile().set(environmentFile); task.getLog4jConfigFile().set(log4j2ConfigFile); task.getRunTypeTemplatesSource().from(runTemplatesSourceFile); task.getModules().from(neoForgeModDevModules); @@ -574,8 +567,6 @@ static void setupTestTask(Project project, task.getLegacyClasspathFile().set(legacyClasspathFile); } task.getAssetProperties().set(assetPropertiesFile); - task.getRunTemplateReplacements().set(project.provider(() -> runTemplateReplacements.entrySet().stream() - .collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get())))); task.getGameLogLevel().set(Level.INFO); }); @@ -589,9 +580,6 @@ static void setupTestTask(Project project, // file containing the program arguments needed to launch task.systemProperty("fml.junit.argsfile", programArgsFile.get().getAsFile().getAbsolutePath()); task.jvmArgs(RunUtils.getArgFileParameter(vmArgsFile.get())); - var loadEnvironment = project.getObjects().newInstance(LoadPreparedTestEnvironment.class); - loadEnvironment.getEnvironmentFile().set(environmentFile); - task.doFirst("load prepared Minecraft test environment", loadEnvironment); var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod); task.getJvmArgumentProviders().add(modFoldersProvider); @@ -608,20 +596,4 @@ static void setupTestTask(Project project, private static void setNamedAttribute(Project project, AttributeContainer attributes, Attribute attribute, String value) { attributes.attribute(attribute, project.getObjects().named(attribute.getType(), value)); } - - public static abstract class LoadPreparedTestEnvironment implements Action { - @Inject - public LoadPreparedTestEnvironment() {} - - public abstract RegularFileProperty getEnvironmentFile(); - - @Override - public void execute(Task task) { - try { - ((Test) task).environment(RunUtils.loadEnvironmentFile(getEnvironmentFile().get().getAsFile())); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read prepared test environment", e); - } - } - } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index 3f870165..51d8fbac 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -57,9 +57,6 @@ abstract class PrepareRunOrTest extends DefaultTask { @OutputFile public abstract RegularFileProperty getProgramArgsFile(); - @OutputFile - public abstract RegularFileProperty getEnvironmentFile(); - /** * A file to use for the {@code log4j2.xml} config file that will be written. * If absent, the standard log4j2.xml file produced by {@link RunUtils#writeLog4j2Configuration} will be used. @@ -103,12 +100,6 @@ abstract class PrepareRunOrTest extends DefaultTask { @Input public abstract MapProperty getSystemProperties(); - @Input - public abstract MapProperty getUserEnvironment(); - - @Input - public abstract MapProperty getRunTemplateReplacements(); - @Input public abstract ListProperty getJvmArguments(); @@ -141,8 +132,6 @@ protected PrepareRunOrTest(ProgramArgsFormat programArgsFormat) { getInputs().property("gameDirectoryPath", getGameDirectory().map(directory -> directory.getAsFile().getAbsolutePath())); getVersionCapabilities().convention(VersionCapabilitiesInternal.latest()); getDevLogin().convention(false); - getUserEnvironment().convention(Map.of()); - getRunTemplateReplacements().convention(Map.of()); } protected abstract UserDevRunType resolveRunType(UserDevConfig userDevConfig); @@ -205,7 +194,6 @@ public void prepareRun() throws IOException { writeJvmArguments(runConfig, sysProps); writeProgramArguments(runConfig, mainClass); - writeEnvironment(runConfig); configureLegacyForgeSplash(runDir); } @@ -335,9 +323,13 @@ private void writeProgramArguments(UserDevRunType runConfig, @Nullable String ma } lines.add("# NeoForge Run-Type Program Arguments"); + var assetProperties = DownloadedAssetsReference.loadProperties(getAssetProperties().get().getAsFile()); List args = runConfig.args(); for (String arg : args) { - arg = interpolateRunTemplateValue(arg); + switch (arg) { + case "{assets_root}" -> arg = Objects.requireNonNull(assetProperties.assetsRoot(), "assets_root"); + case "{asset_index}" -> arg = Objects.requireNonNull(assetProperties.assetIndex(), "asset_index"); + } // FML JUnit simply expects one line per argument if (programArgsFormat == ProgramArgsFormat.FML_JUNIT) { @@ -373,37 +365,6 @@ private void writeProgramArguments(UserDevRunType runConfig, @Nullable String ma StandardCharsets.UTF_8); } - private void writeEnvironment(UserDevRunType runConfig) throws IOException { - var environment = new LinkedHashMap(); - for (var entry : runConfig.env().entrySet()) { - environment.put(entry.getKey(), interpolateRunTemplateValue(entry.getValue())); - } - environment.putAll(getUserEnvironment().get()); - - var properties = new Properties(); - properties.putAll(environment); - var destination = getEnvironmentFile().get().getAsFile().toPath(); - Files.createDirectories(destination.getParent()); - try (var out = FileUtils.newSafeFileOutputStream(destination)) { - properties.store(out, "Minecraft run environment"); - } - } - - private String interpolateRunTemplateValue(String value) { - var assetProperties = DownloadedAssetsReference.loadProperties(getAssetProperties().get().getAsFile()); - var replacements = new LinkedHashMap<>(getRunTemplateReplacements().get()); - replacements.put("assets_root", Objects.requireNonNull(assetProperties.assetsRoot(), "assets_root")); - replacements.put("asset_index", Objects.requireNonNull(assetProperties.assetIndex(), "asset_index")); - replacements.put("natives", getGameDirectory().get().dir("natives").getAsFile().getAbsolutePath()); - replacements.put("MC_VERSION", getVersionCapabilities().get().minecraftVersion()); - - for (var entry : replacements.entrySet()) { - value = value.replace("${" + entry.getKey() + "}", entry.getValue()); - value = value.replace("{" + entry.getKey() + "}", entry.getValue()); - } - return value; - } - private static void addSystemProp(String name, String value, List lines) { lines.add(RunUtils.escapeJvmArg("-D" + name + "=" + value)); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java b/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java index 1bc83ba6..06ed51d7 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RunGameTask.java @@ -30,9 +30,6 @@ public abstract class RunGameTask extends JavaExec { @Input public abstract MapProperty getEnvironmentProperty(); - @InputFile - public abstract RegularFileProperty getEnvironmentFile(); - @InputFile public abstract RegularFileProperty getVmArgsFile(); @@ -55,11 +52,6 @@ public void exec() { throw new UncheckedIOException("Failed to create run directory", e); } - try { - getEnvironment().putAll(RunUtils.loadEnvironmentFile(getEnvironmentFile().get().getAsFile())); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read prepared run environment", e); - } getEnvironment().putAll(getEnvironmentProperty().get()); classpath(getClasspathProvider()); diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index e47960a2..e67b0dc1 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -35,6 +35,7 @@ import net.neoforged.moddevgradle.legacyforge.internal.NonStrictDependencyTransform; import net.neoforged.moddevgradle.mcpforge.dsl.McpForgeExtension; import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; +import net.neoforged.nfrtgradle.DownloadedAssetsReference; import net.neoforged.nfrtgradle.NeoFormRuntimeExtension; import net.neoforged.nfrtgradle.NeoFormRuntimePlugin; import org.gradle.api.InvalidUserCodeException; @@ -286,6 +287,23 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx if ("1.12.2".equals(versionCapabilities.minecraftVersion())) { run.getSystemProperties().put("fml.ignorePatchDiscrepancies", "true"); run.getSystemProperties().put("fml.ignoreInvalidMinecraftCertificates", "true"); + + // Set the legacy launch environment variables directly via Providers. + // These are what launchwrapper/FML/legacydev read at runtime (MOD_CLASSES is set by the workflow). + run.getEnvironment().put("FORGE_GROUP", "net.minecraftforge"); + run.getEnvironment().put("FORGE_VERSION", settings.getForgeVersion().substring(settings.getForgeVersion().indexOf('-') + 1)); + run.getEnvironment().put("MC_VERSION", versionCapabilities.minecraftVersion()); + run.getEnvironment().put("mainClass", "net.minecraft.launchwrapper.Launch"); + run.getEnvironment().put("tweakClass", run.getType().map(t -> "server".equals(t) + ? "net.minecraftforge.fml.common.launcher.FMLServerTweaker" + : "net.minecraftforge.fml.common.launcher.FMLTweaker")); + run.getEnvironment().put("nativesDirectory", + run.getGameDirectory().map(d -> d.dir("natives").getAsFile().getAbsolutePath())); + run.getEnvironment().put("MCP_TO_SRG", intermediateToNamed.map(f -> f.getAsFile().getAbsolutePath())); + run.getEnvironment().put("MCP_MAPPINGS", mappingsCsv.map(f -> f.getAsFile().getAbsolutePath())); + var assetProps = project.getLayout().getBuildDirectory().file("moddev/minecraft_assets.properties"); + run.getEnvironment().put("assetIndex", assetProps.map(f -> DownloadedAssetsReference.loadProperties(f.getAsFile()).assetIndex())); + run.getEnvironment().put("assetDirectory", assetProps.map(f -> DownloadedAssetsReference.loadProperties(f.getAsFile()).assetsRoot())); } if (!versionCapabilities.modLocatorRework()) { From 269a2de24339935463e13a46e82cbacd9836bbbd Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Tue, 30 Jun 2026 23:19:36 +0800 Subject: [PATCH 10/11] Remove dead code from env-file cleanup (runTemplateReplacements, loadEnvironmentFile, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all declarations left behind by the env-file removal: - runTemplateReplacements field + params threaded through ~6 method signatures in ModDevRunWorkflow (constructor, create, setupRuns, setupRunInGradle, setupTestTask) - environmentFile variable in test setup - RunUtils.loadEnvironmentFile() method + RunArgFile.ENVIRONMENT enum value - NeoDevFacade: drop Map.of() args to setupRuns/setupTestTask - mcpforge: drop Map.of(mcp_to_srg, mcp_mappings) from create() call - Tests: remove 3 tests referencing removed APIs (writesInterpolatedUserdevEnvironment, testGradleTestTaskLoadsPreparedEnvironmentFile, testLegacyMcpMappingsCanBeConfiguredForNeoFormRuntime) — the last was always broken (called setMcpMappings on LegacyForgeModdingSettings) --- .../internal/ModDevRunWorkflow.java | 31 ++------ .../moddevgradle/internal/NeoDevFacade.java | 6 +- .../moddevgradle/internal/RunUtils.java | 14 ---- .../internal/McpForgeModDevPlugin.java | 5 +- .../moddevgradle/internal/PrepareRunTest.java | 75 +------------------ .../legacyforge/LegacyModDevPluginTest.java | 40 ---------- 6 files changed, 10 insertions(+), 161 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java index 0ed4fe27..79ee93be 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -65,7 +65,6 @@ public class ModDevRunWorkflow { private final ModuleDependency testFixturesDependency; private final ModuleDependency gameLibrariesDependency; private final Configuration userDevConfigOnly; - private final Map> runTemplateReplacements; /** * @param gameLibrariesDependency A module dependency that represents the library dependencies of the game. @@ -81,14 +80,12 @@ private ModDevRunWorkflow(Project project, @Nullable ModuleDependency testFixturesDependency, ModuleDependency gameLibrariesDependency, DomainObjectCollection runs, - VersionCapabilitiesInternal versionCapabilities, - Map> runTemplateReplacements) { + VersionCapabilitiesInternal versionCapabilities) { this.project = project; this.branding = branding; this.modulePathDependency = modulePathDependency; this.testFixturesDependency = testFixturesDependency; this.gameLibrariesDependency = gameLibrariesDependency; - this.runTemplateReplacements = runTemplateReplacements; var configurations = project.getConfigurations(); @@ -146,8 +143,7 @@ private ModDevRunWorkflow(Project project, }, configureLegacyClasspath, artifactsWorkflow.downloadAssets().flatMap(DownloadAssets::getAssetPropertiesFile), - versionCapabilities, - runTemplateReplacements); + versionCapabilities); } private static void forbidAdditionalRuntimeDependencies(Configuration configuration, VersionCapabilitiesInternal versionCapabilities) { @@ -179,14 +175,6 @@ public static ModDevRunWorkflow create(Project project, Branding branding, ModDevArtifactsWorkflow artifactsWorkflow, DomainObjectCollection runs) { - return create(project, branding, artifactsWorkflow, runs, Map.of()); - } - - public static ModDevRunWorkflow create(Project project, - Branding branding, - ModDevArtifactsWorkflow artifactsWorkflow, - DomainObjectCollection runs, - Map> runTemplateReplacements) { var dependencies = artifactsWorkflow.dependencies(); var versionCapabilites = artifactsWorkflow.versionCapabilities(); @@ -199,8 +187,7 @@ public static ModDevRunWorkflow create(Project project, dependencies.testFixturesDependency(), dependencies.gameLibrariesDependency(), runs, - versionCapabilites, - runTemplateReplacements); + versionCapabilites); project.getExtensions().add(EXTENSION_NAME, workflow); @@ -250,8 +237,7 @@ public void configureTesting(Provider testedMod, Provider configureModulePath, Consumer configureLegacyClasspath, Provider assetPropertiesFile, - VersionCapabilitiesInternal versionCapabilities, - Map> runTemplateReplacements) { + VersionCapabilitiesInternal versionCapabilities) { var dependencyFactory = project.getDependencyFactory(); var ideIntegration = IdeIntegration.of(project, branding); @@ -309,7 +294,6 @@ public static void setupRuns( assetPropertiesFile, devLaunchConfig, versionCapabilities, - runTemplateReplacements, createLaunchScriptsTask); prepareRunTasks.put(run, prepareRunTask); }); @@ -333,7 +317,6 @@ private static TaskProvider setupRunInGradle( Provider assetPropertiesFile, Configuration devLaunchConfig, VersionCapabilitiesInternal versionCapabilities, - Map> runTemplateReplacements, TaskProvider createLaunchScriptsTask) { var ideIntegration = IdeIntegration.of(project, branding); var configurations = project.getConfigurations(); @@ -503,8 +486,7 @@ static void setupTestTask(Project project, Consumer configureModulePath, Consumer configureLegacyClasspath, Provider assetPropertiesFile, - VersionCapabilitiesInternal versionCapabilities, - Map> runTemplateReplacements) { + VersionCapabilitiesInternal versionCapabilities) { var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR); var ideIntegration = IdeIntegration.of(project, branding); @@ -552,7 +534,6 @@ static void setupTestTask(Project project, var vmArgsFile = runArgsDir.map(dir -> dir.file("vmArgs.txt")); var programArgsFile = runArgsDir.map(dir -> dir.file("programArgs.txt")); - var environmentFile = runArgsDir.map(dir -> dir.file("environment.properties")); var log4j2ConfigFile = runArgsDir.map(dir -> dir.file("log4j2.xml")); var prepareTask = tasks.register("prepareNeoForgeTestFiles", PrepareTest.class, task -> { task.setGroup(branding.internalTaskGroup()); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java index cb4ccad2..276f40d6 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java @@ -42,8 +42,7 @@ public static void setupRuns(Project project, configureModulePath, configureAdditionalClasspath, assetPropertiesFile, - neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest()), - Map.of()); + neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest())); } public static void setupTestTask(Project project, @@ -67,8 +66,7 @@ public static void setupTestTask(Project project, configureModulePath, configureAdditionalClasspath, assetPropertiesFile, - neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest()), - Map.of()); + neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest())); } public static void runTaskOnProjectSync(Project project, Object task) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java index c2d9c215..8a4a39b0 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java @@ -161,7 +161,6 @@ public static Provider getLaunchScript(Provider modDevFo public enum RunArgFile { VMARGS("runVmArgs.txt"), PROGRAMARGS("runProgramArgs.txt"), - ENVIRONMENT("runEnvironment.properties"), CLASSPATH("runClasspath.txt"), LOG4J_CONFIG("log4j2.xml"); @@ -176,19 +175,6 @@ public static String getArgFileParameter(RegularFile argFile) { return "@" + argFile.getAsFile().getAbsolutePath(); } - public static Map loadEnvironmentFile(File file) throws IOException { - var properties = new Properties(); - try (var input = new FileInputStream(file)) { - properties.load(input); - } - - var environment = new LinkedHashMap(); - for (var name : properties.stringPropertyNames()) { - environment.put(name, properties.getProperty(name)); - } - return environment; - } - public static List readArgFile(File file) throws IOException { var result = new ArrayList(); for (var line : Files.readAllLines(file.toPath())) { diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index e67b0dc1..e047becc 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -274,10 +274,7 @@ public void enable(Project project, McpForgeModdingSettings settings, McpForgeEx project, Branding.MDG, artifacts, - extension.getRuns(), - Map.of( - "mcp_to_srg", intermediateToNamed.map(file -> file.getAsFile().getAbsolutePath()), - "mcp_mappings", mappingsCsv.map(file -> file.getAsFile().getAbsolutePath()))); + extension.getRuns()); extension.getRuns().configureEach(run -> { // Old BSL versions before 2022 did not export any packages, blocking DevLaunch from the main method. diff --git a/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java b/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java index cd45c3b4..10f1c0f8 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/PrepareRunTest.java @@ -17,70 +17,6 @@ class PrepareRunTest { @TempDir Path tempDir; - @Test - void writesInterpolatedUserdevEnvironment() throws Exception { - var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); - Files.createDirectories(tempDir.resolve("build")); - var configJson = tempDir.resolve("config.json"); - Files.writeString(configJson, """ - { - "runs": { - "client": { - "main": "net.minecraftforge.legacydev.MainClient", - "args": [], - "jvmArgs": [], - "props": {}, - "env": { - "MCP_TO_SRG": "{mcp_to_srg}", - "mainClass": "net.minecraft.launchwrapper.Launch", - "MCP_MAPPINGS": "{mcp_mappings}", - "assetIndex": "{asset_index}", - "assetDirectory": "{assets_root}", - "nativesDirectory": "{natives}", - "MC_VERSION": "${MC_VERSION}" - } - } - } - } - """, StandardCharsets.UTF_8); - - var assets = tempDir.resolve("assets"); - var assetProperties = tempDir.resolve("assets.properties"); - Files.writeString(assetProperties, """ - asset_index=1.12 - assets_root=%s - """.formatted(assets), StandardCharsets.ISO_8859_1); - - var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); - task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); - task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); - task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); - task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); - task.getAssetProperties().set(assetProperties.toFile()); - task.getRunTypeTemplatesSource().from(configJson.toFile()); - task.getRunType().set("client"); - task.getSystemProperties().set(Map.of()); - task.getJvmArguments().set(java.util.List.of()); - task.getProgramArguments().set(java.util.List.of()); - task.getUserEnvironment().set(Map.of("MC_VERSION", "override")); - task.getRunTemplateReplacements().set(Map.of( - "mcp_to_srg", tempDir.resolve("named-to-intermediary.srg").toString(), - "mcp_mappings", tempDir.resolve("mcp-csv.zip").toString())); - task.getGameLogLevel().set(Level.INFO); - task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); - - task.prepareRun(); - - assertThat(RunUtils.loadEnvironmentFile(task.getEnvironmentFile().get().getAsFile())) - .containsEntry("MCP_TO_SRG", tempDir.resolve("named-to-intermediary.srg").toString()) - .containsEntry("MCP_MAPPINGS", tempDir.resolve("mcp-csv.zip").toString()) - .containsEntry("assetIndex", "1.12") - .containsEntry("assetDirectory", assets.toString()) - .containsEntry("nativesDirectory", task.getGameDirectory().get().dir("natives").getAsFile().getAbsolutePath()) - .containsEntry("mainClass", "net.minecraft.launchwrapper.Launch") - .containsEntry("MC_VERSION", "override"); - } - @Test void treatsMissingLegacyRunTemplateCollectionsAsEmpty() throws Exception { var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); @@ -110,15 +46,12 @@ void treatsMissingLegacyRunTemplateCollectionsAsEmpty() throws Exception { task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); - task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); task.getAssetProperties().set(assetProperties.toFile()); task.getRunTypeTemplatesSource().from(configJson.toFile()); task.getRunType().set("client"); task.getSystemProperties().set(Map.of()); task.getJvmArguments().set(java.util.List.of()); task.getProgramArguments().set(java.util.List.of()); - task.getUserEnvironment().set(Map.of()); - task.getRunTemplateReplacements().set(Map.of()); task.getGameLogLevel().set(Level.INFO); task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); @@ -128,9 +61,6 @@ void treatsMissingLegacyRunTemplateCollectionsAsEmpty() throws Exception { .doesNotContainNull(); assertThat(Files.readString(task.getProgramArgsFile().get().getAsFile().toPath())) .contains("net.minecraftforge.legacydev.MainClient"); - assertThat(RunUtils.loadEnvironmentFile(task.getEnvironmentFile().get().getAsFile())) - .containsEntry("assetIndex", "1.12") - .containsEntry("MC_VERSION", "1.12.2"); } @Test @@ -165,15 +95,12 @@ void disablesLegacyForgeSplashOnMacOsForForge1122ClientRuns() throws Exception { task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); task.getVmArgsFile().set(project.getLayout().getBuildDirectory().file("runVmArgs.txt")); task.getProgramArgsFile().set(project.getLayout().getBuildDirectory().file("runProgramArgs.txt")); - task.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("runEnvironment.properties")); task.getAssetProperties().set(assetProperties.toFile()); task.getRunTypeTemplatesSource().from(configJson.toFile()); task.getRunType().set("client"); task.getSystemProperties().set(Map.of()); task.getJvmArguments().set(java.util.List.of()); task.getProgramArguments().set(java.util.List.of()); - task.getUserEnvironment().set(Map.of()); - task.getRunTemplateReplacements().set(Map.of()); task.getGameLogLevel().set(Level.INFO); task.getVersionCapabilities().set(VersionCapabilitiesInternal.ofMinecraftVersion("1.12.2")); @@ -191,7 +118,7 @@ void disablesLegacyForgeSplashOnMacOsForForge1122ClientRuns() throws Exception { } @Test - void treatsGameDirectoryAsAnInputBecauseItIsWrittenToEnvironmentFile() throws Exception { + void treatsGameDirectoryAsAnInput() throws Exception { var project = ProjectBuilder.builder().withProjectDir(tempDir.toFile()).build(); var task = project.getTasks().register("prepareClientRun", PrepareRun.class).get(); task.getGameDirectory().set(project.getLayout().getProjectDirectory().dir("run")); diff --git a/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java index 92c67ee9..a7ca4e18 100644 --- a/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java @@ -173,46 +173,6 @@ void testForge1171UsesUserdevForNeoFormRuntime() { assertEquals("1.17.1", extension.getMinecraftVersion()); } - @Test - void testLegacyMcpMappingsCanBeConfiguredForNeoFormRuntime() { - project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); - - extension.enable(settings -> { - settings.setForgeVersion("1.12.2-14.23.5.2860"); - settings.setMcpMappings("de.oceanlabs.mcp:mcp_stable:39-1.12@zip"); - }); - - var createArtifacts = project.getTasks().named("createMinecraftArtifacts", CreateMinecraftArtifacts.class).get(); - assertEquals("de.oceanlabs.mcp:mcp_stable:39-1.12@zip", createArtifacts.getLegacyMcpMappings().get()); - } - - @Test - void testGradleTestTaskLoadsPreparedEnvironmentFile() throws Exception { - project.getExtensions().configure(net.neoforged.nfrtgradle.NeoFormRuntimeExtension.class, nfrt -> nfrt.getVersion().set("2.0.19-legacy")); - extension.setVersion("1.12.2-14.23.5.2860"); - var testTask = project.getTasks().named("test", org.gradle.api.tasks.testing.Test.class).get(); - var initialActionCount = testTask.getActions().size(); - - ModDevRunWorkflow.get(project).configureTesting(project.provider(() -> null), project.provider(() -> Set.of())); - - assertThat(testTask.getActions()).hasSizeGreaterThan(initialActionCount); - - var environmentFile = project.getLayout().getBuildDirectory() - .file("moddev/junit/environment.properties") - .get() - .getAsFile() - .toPath(); - Files.createDirectories(environmentFile.getParent()); - Files.writeString(environmentFile, "MCP_TO_SRG=prepared.srg\n", StandardCharsets.ISO_8859_1); - - var loadEnvironment = project.getObjects().newInstance(ModDevRunWorkflow.LoadPreparedTestEnvironment.class); - loadEnvironment.getEnvironmentFile().set(project.getLayout().getBuildDirectory().file("moddev/junit/environment.properties")); - loadEnvironment.execute(testTask); - - assertThat(testTask.getEnvironment()) - .containsEntry("MCP_TO_SRG", "prepared.srg"); - } - @Test void testForge1122RequiresLegacyNeoFormRuntimeSupport() { var e = assertThrows(InvalidUserCodeException.class, () -> extension.setVersion("1.12.2-14.23.5.2860")); From 121d5375a2b9476212f86b6ebba271f45d603b93 Mon Sep 17 00:00:00 2001 From: vfyjxf <2331007009@qq.com> Date: Wed, 1 Jul 2026 00:19:03 +0800 Subject: [PATCH 11/11] mcpforge: move LegacyForgeLibraryMetadataRule + PopulateForgeGradleMcpCache from legacy to mcpforge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These two files were in src/legacy/ but only referenced by mcpforge — legacyforge never used them. Move them to src/mcpforge/ so the legacy sourceSet is 100% upstream (zero new files). Also fix LegacyForgeLibraryMetadataRule version check: equals("1.12.2-14.23.5.2860") → startsWith("1.12.2-") so the Scala library POM fixups apply to all 1.12.2 Forge versions (2847+), not just 2860. --- .../mcpforge}/internal/LegacyForgeLibraryMetadataRule.java | 6 +++--- .../mcpforge/internal/McpForgeModDevPlugin.java | 3 +-- .../mcpforge/internal}/PopulateForgeGradleMcpCache.java | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) rename src/{legacy/java/net/neoforged/moddevgradle/legacyforge => mcpforge/java/net/neoforged/moddevgradle/mcpforge}/internal/LegacyForgeLibraryMetadataRule.java (92%) rename src/{legacy/java/net/neoforged/moddevgradle/legacyforge/tasks => mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal}/PopulateForgeGradleMcpCache.java (99%) diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeLibraryMetadataRule.java similarity index 92% rename from src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java rename to src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeLibraryMetadataRule.java index 42251150..37a27303 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeLibraryMetadataRule.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/LegacyForgeLibraryMetadataRule.java @@ -1,4 +1,4 @@ -package net.neoforged.moddevgradle.legacyforge.internal; +package net.neoforged.moddevgradle.mcpforge.internal; import java.util.ArrayList; import org.gradle.api.artifacts.CacheableRule; @@ -9,14 +9,14 @@ /** * Normalizes old Forge library metadata that predates today's Maven Central coordinates or only - * exists as jar-only artifacts on Forge's Maven. + * exists as jar-only artifacts on Forge's Maven. Applies to all 1.12.2 Forge versions (2847+). */ @CacheableRule public class LegacyForgeLibraryMetadataRule implements ComponentMetadataRule { @Override public void execute(ComponentMetadataContext context) { var id = context.getDetails().getId(); - if (!id.getVersion().equals("1.12.2-14.23.5.2860")) { + if (!id.getVersion().startsWith("1.12.2-")) { return; } diff --git a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java index e047becc..98a562f4 100644 --- a/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/McpForgeModDevPlugin.java @@ -26,10 +26,9 @@ import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import net.neoforged.moddevgradle.internal.utils.StringUtils; import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; -import net.neoforged.moddevgradle.legacyforge.tasks.PopulateForgeGradleMcpCache; +import net.neoforged.moddevgradle.mcpforge.internal.PopulateForgeGradleMcpCache; import net.neoforged.moddevgradle.mcpforge.dsl.McpForgeModdingSettings; import net.neoforged.moddevgradle.legacyforge.dsl.ObfuscationExtension; -import net.neoforged.moddevgradle.legacyforge.internal.LegacyForgeLibraryMetadataRule; import net.neoforged.moddevgradle.legacyforge.internal.LegacyRepositoriesPlugin; import net.neoforged.moddevgradle.legacyforge.internal.MinecraftMappings; import net.neoforged.moddevgradle.legacyforge.internal.NonStrictDependencyTransform; diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/PopulateForgeGradleMcpCache.java similarity index 99% rename from src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java rename to src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/PopulateForgeGradleMcpCache.java index 996853d4..cb81d979 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/tasks/PopulateForgeGradleMcpCache.java +++ b/src/mcpforge/java/net/neoforged/moddevgradle/mcpforge/internal/PopulateForgeGradleMcpCache.java @@ -1,4 +1,4 @@ -package net.neoforged.moddevgradle.legacyforge.tasks; +package net.neoforged.moddevgradle.mcpforge.internal; import java.io.IOException; import java.io.InputStream;