diff --git a/build.gradle b/build.gradle index 2cc27e478..d88a512ce 100644 --- a/build.gradle +++ b/build.gradle @@ -126,6 +126,7 @@ dependencies { } testImplementation 'io.javalin:javalin:3.13.11' testImplementation 'net.fabricmc:fabric-installer:0.9.0' + runtimeOnly 'dev.architectury.architectury-pack200:dev.architectury.architectury-pack200.gradle.plugin:0.1.3' compileOnly 'org.jetbrains:annotations:22.0.0' } diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index 85855067b..06208777d 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -139,6 +139,14 @@ default boolean isForgeAndNotOfficial() { return isForge() && !getMcpConfigProvider().isOfficial(); } + default boolean isLegacyForge() { + return isForge() && getForgeUserdevProvider().isLegacyForge(); + } + + default boolean isModernForge() { + return isForge() && !isLegacyForge(); + } + boolean supportsInclude(); default SrgProvider getSrgProvider() { diff --git a/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java index e455d742f..6b77f8420 100644 --- a/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java @@ -33,6 +33,8 @@ import org.gradle.api.provider.SetProperty; import org.jetbrains.annotations.ApiStatus; +import net.fabricmc.loom.configuration.providers.forge.fg2.Pack200Provider; + /** * This is the forge extension api available exposed to build scripts. */ @@ -148,4 +150,6 @@ interface DataGenConsumer { * @see ForgeLocalMod */ NamedDomainObjectContainer getLocalMods(); + + Property getPack200Provider(); } diff --git a/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java b/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java index 76475caaf..6fe72b189 100644 --- a/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java +++ b/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java @@ -181,7 +181,7 @@ private static boolean shouldRemapMod(Logger logger, File artifact, Object id, b } if (forge) { - if (zipFile.getEntry("META-INF/mods.toml") != null) { + if (zipFile.getEntry("META-INF/mods.toml") != null || zipFile.getEntry("mcmod.info") != null) { logger.info("Found Forge mod in " + config + ": {}", id); return true; } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index eaac38ad3..50febf4ed 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -198,9 +198,9 @@ public static void configureCompile(Project p) { } if (extension.isForge()) { + dependencyManager.addProvider(new ForgeUniversalProvider(project)); dependencyManager.addProvider(new McpConfigProvider(project)); dependencyManager.addProvider(new PatchProvider(project)); - dependencyManager.addProvider(new ForgeUniversalProvider(project)); } dependencyManager.addProvider(extension.isForge() ? new FieldMigratedMappingsProvider(project) : new MappingsProviderImpl(project)); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java index 2ea5c812f..27fe1ba53 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java @@ -31,7 +31,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -66,7 +66,7 @@ public void provide(DependencyInfo dependency, Consumer postPopulation .property("client", "java.library.path", getExtension().getMinecraftProvider().nativesDir().getAbsolutePath()) .property("client", "org.lwjgl.librarypath", getExtension().getMinecraftProvider().nativesDir().getAbsolutePath()); - if (!getExtension().isForge()) { + if (!getExtension().isModernForge()) { launchConfig .argument("client", "--assetIndex") .argument("client", getExtension().getMinecraftProvider().getVersionInfo().assetIndex().fabricId(getExtension().getMinecraftProvider().minecraftVersion())) @@ -74,7 +74,7 @@ public void provide(DependencyInfo dependency, Consumer postPopulation .argument("client", new File(getDirectories().getUserCache(), "assets").getAbsolutePath()); } - if (getExtension().isForge()) { + if (getExtension().isModernForge()) { launchConfig // Should match YarnNamingService.PATH_TO_MAPPINGS in forge-runtime .property("fabric.yarnWithSrg.path", getExtension().getMappingsProvider().tinyMappingsWithSrg.toAbsolutePath().toString()) @@ -103,11 +103,29 @@ public void provide(DependencyInfo dependency, Consumer postPopulation } } + if (getExtension().isLegacyForge()) { + launchConfig + .argument("client", "--tweakClass") + .argument("client", Constants.LegacyForge.FML_TWEAKER) + .argument("server", "--tweakClass") + .argument("server", Constants.LegacyForge.FML_SERVER_TWEAKER) + + .argument("--accessToken") + .argument("undefined") + + .property("net.minecraftforge.gradle.GradleStart.srg.srg-mcp", getExtension().getMappingsProvider().srgToNamedSrg.toAbsolutePath().toString()) + .property("mixin.env.remapRefMap", "true"); + + for (String config : PropertyUtil.getAndFinalize(getExtension().getForge().getMixinConfigs())) { + launchConfig.argument("--mixin").argument(config); + } + } + addDependency(Constants.Dependencies.DEV_LAUNCH_INJECTOR + Constants.Dependencies.Versions.DEV_LAUNCH_INJECTOR, Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES); addDependency(Constants.Dependencies.TERMINAL_CONSOLE_APPENDER + Constants.Dependencies.Versions.TERMINAL_CONSOLE_APPENDER, Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES); addDependency(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS, JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME); - if (getExtension().isForge()) { + if (getExtension().isModernForge()) { addDependency(Constants.Dependencies.FORGE_RUNTIME + Constants.Dependencies.Versions.FORGE_RUNTIME, Constants.Configurations.FORGE_EXTRA); addDependency(Constants.Dependencies.JAVAX_ANNOTATIONS + Constants.Dependencies.Versions.JAVAX_ANNOTATIONS, JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME); } @@ -197,7 +215,7 @@ public String getTargetConfig() { } public static class LaunchConfig { - private final Map> values = new HashMap<>(); + private final Map> values = new LinkedHashMap<>(); public LaunchConfig property(String key, String value) { return property("common", key, value); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java index b2ebfa714..aa041162f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java @@ -109,6 +109,13 @@ protected String createMappingsIdentifier(String mappingsName, String version, S public void manipulateMappings(Path mappingsJar) throws IOException { Stopwatch stopwatch = Stopwatch.createStarted(); LoomGradleExtension extension = getExtension(); + + if (extension.isLegacyForge()) { + // Legacy forge patches are in official namespace, so if the type of a field is changed by them, then that + // is effectively a new field and not traceable to any mapping. Therefore this does not apply to it. + return; + } + this.rawTinyMappings = tinyMappings; this.rawTinyMappingsWithSrg = tinyMappingsWithSrg; String mappingsJarName = mappingsJar.getFileName().toString(); @@ -116,7 +123,7 @@ public void manipulateMappings(Path mappingsJar) throws IOException { if (getExtension().shouldGenerateSrgTiny()) { if (Files.notExists(rawTinyMappingsWithSrg) || isRefreshDeps()) { // Merge tiny mappings with srg - SrgMerger.mergeSrg(getProject().getLogger(), getExtension().getMappingsProvider()::getMojmapSrgFileIfPossible, getRawSrgFile(), rawTinyMappings, rawTinyMappingsWithSrg, true); + SrgMerger.mergeSrg(getProject().getLogger(), getExtension().getMappingsProvider()::getMojmapSrgFileIfPossible, getRawSrgFile(), rawTinyMappings, rawTinyMappingsWithSrg, true, false); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java index 358fd1109..b74361243 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java @@ -50,6 +50,7 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.repositories.IvyArtifactRepository; import org.gradle.api.artifacts.transform.InputArtifact; import org.gradle.api.artifacts.transform.TransformAction; import org.gradle.api.artifacts.transform.TransformOutputs; @@ -72,6 +73,7 @@ public class ForgeUserdevProvider extends DependencyProvider { private File userdevJar; private JsonObject json; private Consumer postPopulationScheduler; + private boolean isLegacyForge; public ForgeUserdevProvider(Project project) { super(project); @@ -99,7 +101,14 @@ public void provide(DependencyInfo dependency, Consumer postPopulation Files.copy(resolved.toPath(), userdevJar.toPath(), StandardCopyOption.REPLACE_EXISTING); try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + resolved.toURI()), ImmutableMap.of("create", false))) { - Files.copy(fs.getPath("config.json"), configJson, StandardCopyOption.REPLACE_EXISTING); + Path configEntry = fs.getPath("config.json"); + + // If we cannot find a modern config json, try the legacy/FG2-era one + if (Files.notExists(configEntry)) { + configEntry = fs.getPath("dev.json"); + } + + Files.copy(configEntry, configJson, StandardCopyOption.REPLACE_EXISTING); } } @@ -107,11 +116,31 @@ public void provide(DependencyInfo dependency, Consumer postPopulation json = new Gson().fromJson(reader, JsonObject.class); } - addDependency(json.get("mcp").getAsString(), Constants.Configurations.MCP_CONFIG); - addDependency(json.get("mcp").getAsString(), Constants.Configurations.SRG); - addDependency(json.get("universal").getAsString(), Constants.Configurations.FORGE_UNIVERSAL); + isLegacyForge = !json.has("mcp"); + + if (!isLegacyForge) { + addDependency(json.get("mcp").getAsString(), Constants.Configurations.MCP_CONFIG); + addDependency(json.get("mcp").getAsString(), Constants.Configurations.SRG); + addDependency(json.get("universal").getAsString(), Constants.Configurations.FORGE_UNIVERSAL); + } else { + Map mcpDep = Map.of( + "group", "de.oceanlabs.mcp", + "name", "mcp", + "version", json.get("inheritsFrom").getAsString(), + "classifier", "srg", + "ext", "zip" + ); + addDependency(mcpDep, Constants.Configurations.MCP_CONFIG); + addDependency(mcpDep, Constants.Configurations.SRG); + addDependency(dependency.getDepString() + ":universal", Constants.Configurations.FORGE_UNIVERSAL); + addLegacyMCPRepo(); + } for (JsonElement lib : json.get("libraries").getAsJsonArray()) { + if (isLegacyForge) { + lib = lib.getAsJsonObject().get("name"); + } + Dependency dep = null; if (lib.getAsString().startsWith("org.spongepowered:mixin:")) { @@ -128,7 +157,7 @@ public void provide(DependencyInfo dependency, Consumer postPopulation dep = addDependency(lib.getAsString(), Constants.Configurations.FORGE_DEPENDENCIES); } - if (lib.getAsString().split(":").length < 4) { + if (!isLegacyForge && lib.getAsString().split(":").length < 4) { ((ModuleDependency) dep).attributes(attributes -> { attributes.attribute(transformed, true); }); @@ -137,7 +166,16 @@ public void provide(DependencyInfo dependency, Consumer postPopulation // TODO: Should I copy the patches from here as well? // That'd require me to run the "MCP environment" fully up to merging. - for (Map.Entry entry : json.getAsJsonObject("runs").entrySet()) { + + if (!isLegacyForge) { + configureRuns(json.getAsJsonObject("runs")); + } else { + configureRunsForLegacyForge(); + } + } + + private void configureRuns(JsonObject runs) { + for (Map.Entry entry : runs.entrySet()) { LaunchProviderSettings launchSettings = getExtension().getLaunchConfigs().findByName(entry.getKey()); RunConfigSettings settings = getExtension().getRunConfigs().findByName(entry.getKey()); JsonObject value = entry.getValue().getAsJsonObject(); @@ -184,6 +222,35 @@ public void provide(DependencyInfo dependency, Consumer postPopulation } } + private void configureRunsForLegacyForge() { + getExtension().getRunConfigs().configureEach(config -> { + if (Constants.Forge.LAUNCH_TESTING.equals(config.getDefaultMainClass())) { + config.setDefaultMainClass(Constants.LegacyForge.LAUNCH_WRAPPER); + } + }); + } + + private void addLegacyMCPRepo() { + getProject().getRepositories().ivy(repo -> { + // Old MCP data does not have POMs + repo.setName("LegacyMCP"); + repo.setUrl("https://maven.minecraftforge.net/"); + repo.patternLayout(layout -> { + layout.artifact("[orgPath]/[artifact]/[revision]/[artifact]-[revision](-[classifier])(.[ext])"); + // also check the zip so people do not have to explicitly specify the extension for older versions + layout.artifact("[orgPath]/[artifact]/[revision]/[artifact]-[revision](-[classifier]).zip"); + }); + repo.content(descriptor -> { + descriptor.includeGroup("de.oceanlabs.mcp"); + }); + repo.metadataSources(IvyArtifactRepository.MetadataSources::artifact); + }); + } + + public boolean isLegacyForge() { + return isLegacyForge; + } + public abstract static class RemoveNameProvider implements TransformAction { @InputArtifact public abstract Provider getInput(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java index 7404cc0a6..160f14d69 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java @@ -64,6 +64,11 @@ public McpConfigProvider(Project project) { @Override public void provide(DependencyInfo dependency, Consumer postPopulationScheduler) throws Exception { + if (getExtension().isLegacyForge()) { + official = false; + return; + } + init(dependency.getDependency().getVersion()); Path mcpZip = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve MCPConfig")).toPath(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java index fadf7d337..f11907dc2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java @@ -120,9 +120,9 @@ public class MinecraftPatchedProvider extends DependencyProvider { private File minecraftClientExtra; private File projectAtHash; - private Set projectAts = new HashSet<>(); - private boolean atDirty = false; - private boolean filesDirty = false; + protected Set projectAts = new HashSet<>(); + protected boolean atDirty = false; + protected boolean filesDirty = false; private Path mcpConfigMappings; private Path[] mergedMojangTsrg2Files; @@ -130,8 +130,7 @@ public MinecraftPatchedProvider(Project project) { super(project); } - public void initFiles() throws IOException { - filesDirty = false; + protected void initAts() throws IOException { projectAtHash = new File(getDirectories().getProjectPersistentCache(), "at.sha256"); ConfigurableFileCollection accessTransformers = getExtension().getForge().getAccessTransformers(); accessTransformers.finalizeValue(); @@ -164,6 +163,11 @@ public void initFiles() throws IOException { atDirty = mismatched; } + } + + public void initFiles() throws IOException { + filesDirty = false; + initAts(); MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); PatchProvider patchProvider = getExtension().getPatchProvider(); @@ -186,7 +190,7 @@ public void initFiles() throws IOException { forgeMergedJar = getExtension().isForgeAndOfficial() ? null : new File(globalCache, "forge-official.jar"); minecraftMergedPatchedSrgAtJar = new File(projectDir, "merged-srg-at-patched.jar"); minecraftMergedPatchedJar = new File(projectDir, "merged-patched.jar"); - minecraftClientExtra = new File(globalCache, "forge-client-extra.jar"); + minecraftClientExtra = getExtension().isForgeAndOfficial() ? new File(globalCache, "forge-client-extra.jar") : null; if (isRefreshDeps() || Stream.of(getGlobalCaches()).anyMatch(((Predicate) File::exists).negate()) || !isPatchedJarUpToDate(minecraftMergedPatchedJar)) { @@ -215,7 +219,7 @@ public void cleanAllCache() { cleanProjectCache(); } - private File[] getGlobalCaches() { + protected File[] getGlobalCaches() { File[] files = { minecraftClientSrgJar, minecraftServerSrgJar, @@ -223,14 +227,9 @@ private File[] getGlobalCaches() { minecraftServerPatchedSrgJar, minecraftMergedPatchedSrgJar, minecraftClientExtra, + forgeMergedJar }; - - if (forgeMergedJar != null) { - Arrays.copyOf(files, files.length + 1); - files[files.length - 1] = forgeMergedJar; - } - - return files; + return Arrays.stream(files).filter(Objects::nonNull).toArray(File[]::new); } public void cleanProjectCache() { @@ -239,7 +238,7 @@ public void cleanProjectCache() { } } - private File[] getProjectCache() { + protected File[] getProjectCache() { return new File[] { minecraftMergedPatchedSrgAtJar, minecraftMergedPatchedJar @@ -456,7 +455,7 @@ private File getForgeUserdevJar() { return getExtension().getForgeUserdevProvider().getUserdevJar(); } - private boolean isPatchedJarUpToDate(File jar) throws IOException { + protected boolean isPatchedJarUpToDate(File jar) throws IOException { if (!jar.exists()) return false; byte[] manifestBytes = ZipUtils.unpackNullable(jar.toPath(), "META-INF/MANIFEST.MF"); @@ -478,6 +477,26 @@ private boolean isPatchedJarUpToDate(File jar) throws IOException { } private void accessTransformForge(Logger logger) throws Exception { + List ats = new ArrayList<>(); + + for (File jar : ImmutableList.of(getForgeJar(), getForgeUserdevJar(), minecraftMergedPatchedSrgJar)) { + byte[] atBytes = ZipUtils.unpackNullable(jar.toPath(), Constants.Forge.ACCESS_TRANSFORMER_PATH); + + if (atBytes != null) { + ats.add(atBytes); + } + } + + if (usesProjectCache()) { + for (File projectAt : projectAts) { + ats.add(Files.readAllBytes(projectAt.toPath())); + } + } + + accessTransformForge(logger, minecraftMergedPatchedSrgJar, minecraftMergedPatchedSrgAtJar, ats); + } + + protected void accessTransformForge(Logger logger, File input, File target, List ats) throws Exception { MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); List toDelete = new ArrayList<>(); String atDependency = Constants.Dependencies.ACCESS_TRANSFORMERS + (minecraftProvider.isNewerThan21w39a() ? Constants.Dependencies.Versions.ACCESS_TRANSFORMERS_NEW : Constants.Dependencies.Versions.ACCESS_TRANSFORMERS); @@ -486,8 +505,6 @@ private void accessTransformForge(Logger logger) throws Exception { logger.lifecycle(":access transforming minecraft"); - File input = minecraftMergedPatchedSrgJar; - File target = minecraftMergedPatchedSrgAtJar; Files.deleteIfExists(target.toPath()); List args = new ArrayList<>(); @@ -496,23 +513,12 @@ private void accessTransformForge(Logger logger) throws Exception { args.add("--outJar"); args.add(target.getAbsolutePath()); - for (File jar : ImmutableList.of(getForgeJar(), getForgeUserdevJar(), minecraftMergedPatchedSrgJar)) { - byte[] atBytes = ZipUtils.unpackNullable(jar.toPath(), Constants.Forge.ACCESS_TRANSFORMER_PATH); - - if (atBytes != null) { - File tmpFile = File.createTempFile("at-conf", ".cfg"); - toDelete.add(tmpFile); - Files.write(tmpFile.toPath(), atBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - args.add("--atFile"); - args.add(tmpFile.getAbsolutePath()); - } - } - - if (usesProjectCache()) { - for (File projectAt : projectAts) { - args.add("--atFile"); - args.add(projectAt.getAbsolutePath()); - } + for (byte[] atBytes : ats) { + File tmpFile = File.createTempFile("at-conf", ".cfg"); + toDelete.add(tmpFile); + Files.write(tmpFile.toPath(), atBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + args.add("--atFile"); + args.add(tmpFile.getAbsolutePath()); } getProject().javaexec(spec -> { @@ -599,7 +605,7 @@ private void remapPatchedJar(Logger logger) throws Exception { applyLoomPatchVersion(mcOutput); } - private void patchJars(Logger logger) throws IOException { + private void patchJars(Logger logger) throws Exception { Stopwatch stopwatch = Stopwatch.createStarted(); logger.lifecycle(":patching jars"); @@ -619,7 +625,7 @@ private void patchJars(Logger logger) throws IOException { logger.lifecycle(":patched jars in " + stopwatch.stop()); } - private void patchJars(File clean, File output, Path patches) throws IOException { + protected void patchJars(File clean, File output, Path patches) throws Exception { PrintStream previous = System.out; try { @@ -682,7 +688,7 @@ private void walkFileSystems(File source, File target, Predicate filter, F } } - private void walkFileSystems(File source, File target, Predicate filter, FsPathConsumer action) throws IOException { + protected void walkFileSystems(File source, File target, Predicate filter, FsPathConsumer action) throws IOException { walkFileSystems(source, target, filter, FileSystem::getRootDirectories, action); } @@ -690,7 +696,7 @@ private void copyAll(File source, File target) throws IOException { walkFileSystems(source, target, it -> true, this::copyReplacing); } - private void copyMissingClasses(File source, File target) throws IOException { + protected void copyMissingClasses(File source, File target) throws IOException { walkFileSystems(source, target, it -> it.toString().endsWith(".class"), (sourceFs, targetFs, sourcePath, targetPath) -> { if (Files.exists(targetPath)) return; Path parent = targetPath.getParent(); @@ -703,7 +709,7 @@ private void copyMissingClasses(File source, File target) throws IOException { }); } - private void copyNonClassFiles(File source, File target) throws IOException { + protected void copyNonClassFiles(File source, File target) throws IOException { Predicate filter = getExtension().isForgeAndOfficial() ? file -> { String s = file.toString(); return !s.endsWith(".class"); @@ -715,7 +721,7 @@ private void copyNonClassFiles(File source, File target) throws IOException { walkFileSystems(source, target, filter, this::copyReplacing); } - private void copyReplacing(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException { + protected void copyReplacing(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException { Path parent = targetPath.getParent(); if (parent != null) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java index d49ae8b49..bfd36d938 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java @@ -24,7 +24,16 @@ package net.fabricmc.loom.configuration.providers.forge; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.nio.file.FileSystem; @@ -33,11 +42,21 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; import com.google.common.collect.ImmutableMap; +import lzma.sdk.lzma.Decoder; +import lzma.sdk.lzma.Encoder; +import lzma.streams.LzmaInputStream; +import lzma.streams.LzmaOutputStream; +import org.apache.commons.io.IOUtils; import org.gradle.api.Project; import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.configuration.providers.forge.fg2.Pack200Provider; import net.fabricmc.loom.util.Constants; public class PatchProvider extends DependencyProvider { @@ -56,11 +75,17 @@ public void provide(DependencyInfo dependency, Consumer postPopulation if (Files.notExists(clientPatches) || Files.notExists(serverPatches) || isRefreshDeps()) { getProject().getLogger().info(":extracting forge patches"); - Path installerJar = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge installer")).toPath(); + Path installerJar = getExtension().isModernForge() + ? dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge installer")).toPath() + : getExtension().getForgeUniversalProvider().getForge().toPath(); try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + installerJar.toUri()), ImmutableMap.of("create", false))) { - Files.copy(fs.getPath("data", "client.lzma"), clientPatches, StandardCopyOption.REPLACE_EXISTING); - Files.copy(fs.getPath("data", "server.lzma"), serverPatches, StandardCopyOption.REPLACE_EXISTING); + if (getExtension().isModernForge()) { + Files.copy(fs.getPath("data", "client.lzma"), clientPatches, StandardCopyOption.REPLACE_EXISTING); + Files.copy(fs.getPath("data", "server.lzma"), serverPatches, StandardCopyOption.REPLACE_EXISTING); + } else { + splitAndConvertLegacyPatches(fs.getPath("binpatches.pack.lzma")); + } } } } @@ -77,6 +102,73 @@ private void init(String forgeVersion) { } } + private void splitAndConvertLegacyPatches(Path joinedLegacyPatches) throws IOException { + try (JarInputStream in = new JarInputStream(new ByteArrayInputStream(unpack200Lzma(joinedLegacyPatches))); + OutputStream clientFileOut = Files.newOutputStream(clientPatches, CREATE, TRUNCATE_EXISTING); + LzmaOutputStream clientLzmaOut = new LzmaOutputStream(clientFileOut, new Encoder()); + JarOutputStream clientJarOut = new JarOutputStream(clientLzmaOut); + OutputStream serverFileOut = Files.newOutputStream(serverPatches, CREATE, TRUNCATE_EXISTING); + LzmaOutputStream serverLzmaOut = new LzmaOutputStream(serverFileOut, new Encoder()); + JarOutputStream serverJarOut = new JarOutputStream(serverLzmaOut); + ) { + for (JarEntry entry; (entry = in.getNextJarEntry()) != null;) { + String name = entry.getName(); + + JarOutputStream out; + + if (name.startsWith("binpatch/client/")) { + out = clientJarOut; + } else if (name.startsWith("binpatch/server/")) { + out = serverJarOut; + } else { + getProject().getLogger().warn("Unexpected file in Forge binpatches archive: " + name); + continue; + } + + out.putNextEntry(new ZipEntry(name)); + + // Converting from legacy format to modern (v1) format + DataInputStream dataIn = new DataInputStream(in); + DataOutputStream dataOut = new DataOutputStream(out); + dataOut.writeByte(1); // version + dataIn.readUTF(); // unused patch name (presumably always the same as the obf class name) + dataOut.writeUTF(dataIn.readUTF().replace('.', '/')); // obf class name + dataOut.writeUTF(dataIn.readUTF().replace('.', '/')); // srg class name + IOUtils.copy(in, out); // remainder is unchanged + + out.closeEntry(); + } + } + } + + private byte[] unpack200(InputStream in) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try (JarOutputStream jarOut = new JarOutputStream(bytes)) { + Pack200Provider provider = getExtension().getForge().getPack200Provider().getOrNull(); + + if (provider == null) { + throw new IllegalStateException("No provider for Pack200 has been found. Did you declare a provider?"); + } + + provider.unpack(in, jarOut); + } + + return bytes.toByteArray(); + } + + private byte[] unpack200Lzma(InputStream in) throws IOException { + try (LzmaInputStream lzmaIn = new LzmaInputStream(in, new Decoder())) { + return unpack200(lzmaIn); + } + } + + private byte[] unpack200Lzma(Path path) throws IOException { + try (InputStream in = Files.newInputStream(path)) { + return unpack200Lzma(in); + } + } + public Path getProjectCacheFolder() { return projectCacheFolder; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java index 55a19fd66..9c24416c9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java @@ -29,8 +29,10 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.io.Reader; import java.io.StringReader; import java.io.UncheckedIOException; +import java.io.Writer; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; @@ -47,6 +49,8 @@ import com.google.common.collect.ImmutableMap; import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.NullOutputStream; +import org.cadixdev.lorenz.io.srg.SrgReader; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgWriter; import org.gradle.api.Project; import org.gradle.api.logging.LogLevel; @@ -87,7 +91,16 @@ public void provide(DependencyInfo dependency, Consumer postPopulation Path srgZip = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve srg")).toPath(); try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + srgZip.toUri()), ImmutableMap.of("create", false))) { - Files.copy(fs.getPath("config", "joined.tsrg"), srg, StandardCopyOption.REPLACE_EXISTING); + Path srgPath = fs.getPath("joined.srg"); + + if (Files.exists(srgPath)) { + // FG2-era MCP uses the older SRG format, convert it on the fly + try (Reader reader = Files.newBufferedReader(srgPath); Writer writer = Files.newBufferedWriter(srg)) { + new TSrgWriter(writer).write(new SrgReader(reader).read()); + } + } else { + Files.copy(fs.getPath("config", "joined.tsrg"), srg, StandardCopyOption.REPLACE_EXISTING); + } } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/MinecraftLegacyPatchedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/MinecraftLegacyPatchedProvider.java new file mode 100644 index 000000000..49dd48f0e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/MinecraftLegacyPatchedProvider.java @@ -0,0 +1,446 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.forge.fg2; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableMap; +import org.cadixdev.at.AccessTransformSet; +import org.cadixdev.at.io.AccessTransformFormats; +import org.cadixdev.lorenz.MappingSet; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; +import net.fabricmc.loom.configuration.providers.forge.MinecraftPatchedProvider; +import net.fabricmc.loom.configuration.providers.forge.PatchProvider; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.Pair; +import net.fabricmc.loom.util.ThreadingUtils; +import net.fabricmc.loom.util.TinyRemapperHelper; +import net.fabricmc.loom.util.ZipUtils; +import net.fabricmc.loom.util.legacyforge.CoreModManagerTransformer; +import net.fabricmc.loom.util.srg.AccessTransformSetMapper; +import net.fabricmc.lorenztiny.TinyMappingsReader; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.stitch.merge.JarMerger; + +public class MinecraftLegacyPatchedProvider extends MinecraftPatchedProvider { + // Step 1: Binary Patch (global) + private File minecraftClientPatchedJar; + private File minecraftServerPatchedJar; + // Step 2: Merge (global) + private File minecraftMergedPatchedJar; + // Step 4: Access Transform (global or project) + private File minecraftMergedPatchedAtJar; + + private File forgeJar; + + public MinecraftLegacyPatchedProvider(Project project) { + super(project); + } + + @Override + public void initFiles() throws IOException { + filesDirty = false; + initAts(); + + File globalCache = getExtension().getForgeProvider().getGlobalCache(); + File projectDir = usesProjectCache() ? getExtension().getForgeProvider().getProjectCache() : globalCache; + projectDir.mkdirs(); + + minecraftClientPatchedJar = new File(globalCache, "client-patched.jar"); + minecraftServerPatchedJar = new File(globalCache, "server-patched.jar"); + minecraftMergedPatchedJar = new File(globalCache, "merged-patched.jar"); + minecraftMergedPatchedAtJar = new File(projectDir, "merged-at-patched.jar"); + + forgeJar = new File(globalCache, "forge.jar"); + + if (isRefreshDeps() || Stream.of(getGlobalCaches()).anyMatch(((Predicate) File::exists).negate()) + || !isPatchedJarUpToDate(forgeJar) || !isPatchedJarUpToDate(minecraftMergedPatchedAtJar)) { + cleanAllCache(); + } else if (atDirty || Stream.of(getProjectCache()).anyMatch(((Predicate) File::exists).negate())) { + cleanProjectCache(); + } + } + + @Override + protected File[] getGlobalCaches() { + return new File[] { + minecraftClientPatchedJar, + minecraftServerPatchedJar, + minecraftMergedPatchedJar, + forgeJar, + }; + } + + @Override + protected File[] getProjectCache() { + return new File[] { + minecraftMergedPatchedAtJar, + }; + } + + @Override + public void provide(DependencyInfo dependency, Consumer postPopulationScheduler) throws Exception { + initFiles(); + + if (atDirty) { + getProject().getLogger().lifecycle(":found dirty access transformers"); + } + } + + @Override + public void finishProvide() throws Exception { + if (!forgeJar.exists()) { + filesDirty = true; + patchForge(getProject().getLogger()); + applyLoomPatchVersion(forgeJar.toPath()); + } + + if (!minecraftClientPatchedJar.exists() || !minecraftServerPatchedJar.exists()) { + filesDirty = true; + patchJars(getProject().getLogger()); + } + + if (filesDirty || !minecraftMergedPatchedJar.exists()) { + filesDirty = true; + mergeJars(getProject().getLogger()); + } + + if (atDirty || filesDirty || !minecraftMergedPatchedAtJar.exists()) { + filesDirty = true; + accessTransformForge(getProject().getLogger()); + applyLoomPatchVersion(minecraftMergedPatchedAtJar.toPath()); + } + } + + private void patchForge(Logger logger) throws Exception { + Stopwatch stopwatch = Stopwatch.createStarted(); + logger.lifecycle(":patching forge"); + + Files.copy(getExtension().getForgeUniversalProvider().getForge().toPath(), forgeJar.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // For the development environment, we need to remove the binpatches, otherwise forge will try to re-apply them + try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(forgeJar, false)) { + Files.delete(fs.get().getPath("binpatches.pack.lzma")); + } + + // Older versions of Forge rely on utility classes from log4j-core 2.0-beta9 but we'll upgrade the runtime to a + // release version (so we can use the TerminalConsoleAppender) where some of those classes have been moved from + // a `helpers` to a `utils` package. + // To allow Forge to work regardless, we'll re-package those helper classes into the forge jar. + Path log4jBeta9 = Arrays.stream(TinyRemapperHelper.getMinecraftDependencies(getProject())) + .filter(it -> it.getFileName().toString().equals("log4j-core-2.0-beta9.jar")) + .findAny() + .orElse(null); + if (log4jBeta9 != null) { + Predicate isHelper = path -> path.startsWith("/org/apache/logging/log4j/core/helpers"); + walkFileSystems(log4jBeta9.toFile(), forgeJar, isHelper, this::copyReplacing); + } + + // While Forge will discover mods on the classpath, it won't do the same for ATs, coremods or tweakers. + // ForgeGradle "solves" this problem using a huge, gross hack (GradleForgeHacks and related classes), and only + // for ATs and coremods, not tweakers. + // No clue why FG went the hack route when it's the same project and they could have just added first-party + // support for loading both from the classpath right into Forge (it's even really simply to do). + // We'll have none of those hacks and instead patch first-party support into Forge. + ZipUtils.transform(forgeJar.toPath(), Stream.of(new Pair<>(CoreModManagerTransformer.FILE, original -> { + ClassReader reader = new ClassReader(original); + ClassWriter writer = new ClassWriter(reader, 0); + reader.accept(new CoreModManagerTransformer(writer), 0); + return writer.toByteArray(); + }))); + + logger.lifecycle(":patched forge in " + stopwatch.stop()); + } + + private void patchJars(Logger logger) throws Exception { + Stopwatch stopwatch = Stopwatch.createStarted(); + logger.lifecycle(":patching jars"); + + MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); + PatchProvider patchProvider = getExtension().getPatchProvider(); + patchJars(minecraftProvider.minecraftServerJar, minecraftServerPatchedJar, patchProvider.serverPatches); + patchJars(minecraftProvider.minecraftClientJar, minecraftClientPatchedJar, patchProvider.clientPatches); + + logger.lifecycle(":patched jars in " + stopwatch.stop()); + } + + @Override + protected void patchJars(File clean, File output, Path patches) throws Exception { + super.patchJars(clean, output, patches); + + // Patching only preserves affected classes, everything else we need to copy manually + copyMissingClasses(clean, output); + copyNonClassFiles(clean, output); + + // Workaround Forge patches apparently violating the JVM spec (see ParameterAnnotationsFixer for details) + modifyClasses(output, ParameterAnnotationsFixer::new); + } + + private void mergeJars(Logger logger) throws Exception { + logger.info(":merging jars"); + Stopwatch stopwatch = Stopwatch.createStarted(); + + try (JarMerger jarMerger = new JarMerger(minecraftClientPatchedJar, minecraftServerPatchedJar, minecraftMergedPatchedJar)) { + jarMerger.enableSyntheticParamsOffset(); + jarMerger.merge(); + } + + // The JarMerger adds Sided annotations but so do the Forge patches. The latter doesn't require extra + // dependencies beyond Forge, so we'll keep those and convert any non-redundant Fabric ones. + modifyClasses(minecraftMergedPatchedJar, SideAnnotationMerger::new); + + logger.info(":merged jars in " + stopwatch); + } + + private void accessTransformForge(Logger logger) throws Exception { + // Load all applicable access transformers + AccessTransformSet accessTransformSet = AccessTransformSet.create(); + + byte[] forgeAt = ZipUtils.unpack(forgeJar.toPath(), "forge_at.cfg"); + AccessTransformFormats.FML.read(new InputStreamReader(new ByteArrayInputStream(forgeAt)), accessTransformSet); + + for (File projectAt : projectAts) { + AccessTransformFormats.FML.read(projectAt.toPath(), accessTransformSet); + } + + // Remap them from srg to official mappings + MappingTree mappingTree = getExtension().getMappingsProvider().getMappingsWithSrg(); + MappingSet mappingSet = new TinyMappingsReader(mappingTree, "srg", "official").read(); + accessTransformSet = AccessTransformSetMapper.remap(accessTransformSet, mappingSet); + + ByteArrayOutputStream remappedOut = new ByteArrayOutputStream(); + // TODO the extra BufferedWriter wrapper and closing can be removed once https://github.com/CadixDev/at/issues/6 is fixed + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(remappedOut)); + AccessTransformFormats.FML.write(writer, accessTransformSet); + writer.close(); + byte[] remappedAt = remappedOut.toByteArray(); + + // And finally, apply them to the merged+patched jar + accessTransformForge(logger, minecraftMergedPatchedJar, minecraftMergedPatchedAtJar, Collections.singletonList(remappedAt)); + } + + private void modifyClasses(File jarFile, Function func) throws Exception { + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + jarFile.toURI()), ImmutableMap.of("create", false))) { + ThreadingUtils.TaskCompleter completer = ThreadingUtils.taskCompleter(); + + for (Path file : (Iterable) Files.walk(fs.getPath("/"))::iterator) { + if (!file.toString().endsWith(".class")) continue; + + completer.add(() -> { + byte[] original = Files.readAllBytes(file); + + ClassReader reader = new ClassReader(original); + ClassWriter writer = new ClassWriter(reader, 0); + reader.accept(func.apply(writer), 0); + + byte[] modified = writer.toByteArray(); + + if (!Arrays.equals(original, modified)) { + Files.write(file, modified, StandardOpenOption.TRUNCATE_EXISTING); + } + }); + } + + completer.complete(); + } + } + + public File getMergedJar() { + return minecraftMergedPatchedAtJar; + } + + public File getForgeMergedJar() { + return forgeJar; + } + + /** + * It seems that Forge patches produce class files which are in violation of the JVM spec. Specifically, the value + * of Runtime[In]VisibleParameterAnnotation.num_parameters is by SE8 spec required to match the number of formal + * parameters of the method (and may be ignored in favor of directly looking at the method arguments, which is + * indeed what the OpenJDK 8 compiler does). Using a smaller value (possible if e.g. the last parameter has no + * annotations) will cause the compiler to read past the end of the table, throwing an exception and therefore being + * unable to read the class file. + *
+ * This class visitor fixes that by ignoring the original num_parameters value, letting the MethodVisitor compute a + * new value based on its signature. This will at first produce an invalid count when there are synthetic parameters + * but later, during mergeJars, those will be properly offset (enableSyntheticParamsOffset). + *
+ * SE8 JVM spec: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.18 + * Example method affected: RenderGlobal.ContainerLocalRenderInformation(RenderChunk, EnumFacing, int) + */ + private static class ParameterAnnotationsFixer extends ClassVisitor { + private ParameterAnnotationsFixer(ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); + + // This issue has so far only been observed with constructors, so we can skip everything else + if (name.equals("")) { + methodVisitor = new MethodFixer(methodVisitor); + } + + return methodVisitor; + } + + private static class MethodFixer extends MethodVisitor { + private MethodFixer(MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + } + + @Override + public void visitAnnotableParameterCount(int parameterCount, boolean visible) { + // Not calling visitAnnotableParameterCount will cause it to compute its value from the method signature + } + } + } + + private static class SideAnnotationMerger extends ClassVisitor { + private static final String FABRIC_ANNOTATION_DESCRIPTOR = "Lnet/fabricmc/api/Environment;"; + private static final String FORGE_ANNOTATION_DESCRIPTOR = "Lnet/minecraftforge/fml/relauncher/SideOnly;"; + private static final String FORGE_SIDE_DESCRIPTOR = "Lnet/minecraftforge/fml/relauncher/Side;"; + + private static boolean isSideAnnotation(String descriptor) { + return FABRIC_ANNOTATION_DESCRIPTOR.equals(descriptor) || FORGE_ANNOTATION_DESCRIPTOR.equals(descriptor); + } + + private boolean visitedAnnotation; + + private SideAnnotationMerger(ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (isSideAnnotation(descriptor)) { + if (visitedAnnotation) { + return null; + } + + visitedAnnotation = true; + return new FabricToForgeConverter(super.visitAnnotation(FORGE_ANNOTATION_DESCRIPTOR, true)); + } + + return super.visitAnnotation(descriptor, visible); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return new FieldSideAnnotationMerger(super.visitField(access, name, descriptor, signature, value)); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new MethodSideAnnotationMerger(super.visitMethod(access, name, descriptor, signature, exceptions)); + } + + private static class FieldSideAnnotationMerger extends FieldVisitor { + private boolean visitedAnnotation; + + private FieldSideAnnotationMerger(FieldVisitor fieldVisitor) { + super(Opcodes.ASM9, fieldVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (isSideAnnotation(descriptor)) { + if (visitedAnnotation) { + return null; + } + + visitedAnnotation = true; + return new FabricToForgeConverter(super.visitAnnotation(FORGE_ANNOTATION_DESCRIPTOR, true)); + } + + return super.visitAnnotation(descriptor, visible); + } + } + + private static class MethodSideAnnotationMerger extends MethodVisitor { + private boolean visitedAnnotation; + + private MethodSideAnnotationMerger(MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (isSideAnnotation(descriptor)) { + if (visitedAnnotation) { + return null; + } + + visitedAnnotation = true; + return new FabricToForgeConverter(super.visitAnnotation(FORGE_ANNOTATION_DESCRIPTOR, true)); + } + + return super.visitAnnotation(descriptor, visible); + } + } + + private static class FabricToForgeConverter extends AnnotationVisitor { + private FabricToForgeConverter(AnnotationVisitor annotationVisitor) { + super(Opcodes.ASM9, annotationVisitor); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + super.visitEnum(name, FORGE_SIDE_DESCRIPTOR, value); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/Pack200Provider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/Pack200Provider.java new file mode 100644 index 000000000..8912c42cf --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/Pack200Provider.java @@ -0,0 +1,8 @@ +package net.fabricmc.loom.configuration.providers.forge.fg2; + +import java.io.InputStream; +import java.util.jar.JarOutputStream; + +public interface Pack200Provider { + void unpack(InputStream inputStream, JarOutputStream outputStream); +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java index 23bf71c83..3be857bd6 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java @@ -52,6 +52,7 @@ import org.apache.tools.ant.util.StringUtils; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.logging.Logger; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; @@ -66,15 +67,18 @@ import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; import net.fabricmc.loom.configuration.providers.forge.MinecraftPatchedProvider; import net.fabricmc.loom.configuration.providers.forge.SrgProvider; +import net.fabricmc.loom.configuration.providers.forge.fg2.MinecraftLegacyPatchedProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.DeletingFileVisitor; import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.LoggerFilter; import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.srg.MCPReader; import net.fabricmc.loom.util.srg.SrgMerger; import net.fabricmc.loom.util.srg.SrgNamedWriter; import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.MappingWriter; import net.fabricmc.mappingio.adapter.MappingNsCompleter; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.format.MappingFormat; @@ -83,6 +87,7 @@ import net.fabricmc.mappingio.tree.MappingTree; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.stitch.Command; +import net.fabricmc.stitch.commands.CommandGenerateIntermediary; import net.fabricmc.stitch.commands.CommandProposeFieldNames; import net.fabricmc.stitch.commands.tinyv2.TinyFile; import net.fabricmc.stitch.commands.tinyv2.TinyV2Writer; @@ -147,7 +152,12 @@ public void provide(DependencyInfo dependency, Consumer postPopulation } if (getExtension().isForge()) { - patchedProvider = new MinecraftPatchedProvider(getProject()); + if (getExtension().isModernForge()) { + patchedProvider = new MinecraftPatchedProvider(getProject()); + } else { + patchedProvider = new MinecraftLegacyPatchedProvider(getProject()); + } + patchedProvider.provide(dependency, postPopulationScheduler); } @@ -157,7 +167,7 @@ public void provide(DependencyInfo dependency, Consumer postPopulation if (getExtension().shouldGenerateSrgTiny()) { if (Files.notExists(tinyMappingsWithSrg) || isRefreshDeps()) { // Merge tiny mappings with srg - SrgMerger.mergeSrg(getProject().getLogger(), getExtension().getMappingsProvider()::getMojmapSrgFileIfPossible, getRawSrgFile(), tinyMappings, tinyMappingsWithSrg, true); + SrgMerger.mergeSrg(getProject().getLogger(), getExtension().getMappingsProvider()::getMojmapSrgFileIfPossible, getRawSrgFile(), tinyMappings, tinyMappingsWithSrg, true, getExtension().isLegacyForge()); } mappingTreeWithSrg = readMappings(tinyMappingsWithSrg); @@ -192,7 +202,7 @@ public void provide(DependencyInfo dependency, Consumer postPopulation } if (Files.notExists(srgToNamedSrg) || isRefreshDeps()) { - SrgNamedWriter.writeTo(getProject().getLogger(), srgToNamedSrg, getMappingsWithSrg(), "srg", "named"); + SrgNamedWriter.writeTo(srgToNamedSrg, getMappingsWithSrg(), "srg", "named", getExtension().isLegacyForge()); } } @@ -340,6 +350,7 @@ private void readAndMergeMCP(Path mcpJar, Consumer postPopulationSched getProject().getDependencies().add(provider.getTargetConfig(), "de.oceanlabs.mcp:mcp_config:" + getMinecraftProvider().minecraftVersion()); Configuration configuration = getProject().getConfigurations().getByName(provider.getTargetConfig()); provider.provide(DependencyInfo.create(getProject(), configuration.getDependencies().iterator().next(), configuration), postPopulationScheduler); + getExtension().getDependencyManager().addProvider(provider); } Path srgPath = getRawSrgFile(); @@ -577,17 +588,53 @@ public Path getIntermediaryTiny() throws IOException { hasRefreshed = true; // Download and extract intermediary - String encodedMinecraftVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftProvider().minecraftVersion()); - String intermediaryArtifactUrl = getExtension().getIntermediaryUrl(encodedMinecraftVersion); - File intermediaryJar = getMinecraftProvider().file("intermediary-v2.jar"); - DownloadUtil.downloadIfChanged(new URL(intermediaryArtifactUrl), intermediaryJar, getProject().getLogger()); - extractMappings(intermediaryJar.toPath(), intermediaryTiny); + if (!getExtension().isLegacyForge()) { + String encodedMinecraftVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftProvider().minecraftVersion()); + String intermediaryArtifactUrl = getExtension().getIntermediaryUrl(encodedMinecraftVersion); + File intermediaryJar = getMinecraftProvider().file("intermediary-v2.jar"); + DownloadUtil.downloadIfChanged(new URL(intermediaryArtifactUrl), intermediaryJar, getProject().getLogger()); + extractMappings(intermediaryJar.toPath(), intermediaryTiny); + } else { + generateDummyIntermediary(getProject().getLogger(), intermediaryTiny); + } } } return intermediaryTiny; } + private void generateDummyIntermediary(Logger logger, Path tinyV2) throws IOException { + Stopwatch stopwatch = Stopwatch.createStarted(); + logger.lifecycle(":generating dummy intermediary"); + + Path minecraftJar = getExtension().getMinecraftProvider().getMergedJar().toPath(); + + // create a temporary folder into which stitch will output the v1 file + // we cannot just create a temporary file directly, cause stitch will try to read it if it exists + Path tmpFolder = Files.createTempDirectory("dummy-intermediary"); + Path tinyV1 = tmpFolder.resolve("intermediary-v1.tiny"); + + CommandGenerateIntermediary command = new CommandGenerateIntermediary(); + LoggerFilter.withSystemOutAndErrSuppressed(() -> { + try { + command.run(new String[]{ minecraftJar.toAbsolutePath().toString(), tinyV1.toAbsolutePath().toString() }); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to generate intermediary", e); + } + }); + + try (MappingWriter writer = MappingWriter.create(tinyV2, MappingFormat.TINY_2)) { + MappingReader.read(tinyV1, writer); + } + + Files.delete(tinyV1); + Files.delete(tmpFolder); + + logger.lifecycle(":generated dummy intermediary in " + stopwatch.stop()); + } + @Override public Path mappingsWorkingDir() { return mappingsWorkingDir; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java index ec0039958..a137aa8b7 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java @@ -266,7 +266,17 @@ public Info(Path assets, Path input, Path outputMapped, Path outputIntermediary, } public void remap(TinyRemapper remapper, Mutable mappings, List postApply, Info vanilla, @Nullable Info forge, String fromM) throws IOException { - Set classNames = getExtension().isForge() ? InnerClassRemapper.readClassNames(vanilla.input) : null; + Set classNames; + + if (getExtension().isForge()) { + classNames = InnerClassRemapper.readClassNames(vanilla.input); + + if (forge != null) { + classNames.addAll(InnerClassRemapper.readClassNames(forge.input)); + } + } else { + classNames = null; + } for (String toM : getExtension().isForge() ? Arrays.asList(MappingsNamespace.INTERMEDIARY.toString(), MappingsNamespace.SRG.toString(), MappingsNamespace.NAMED.toString()) : Arrays.asList(MappingsNamespace.INTERMEDIARY.toString(), MappingsNamespace.NAMED.toString())) { Path output = MappingsNamespace.NAMED.toString().equals(toM) ? vanilla.outputMapped : MappingsNamespace.SRG.toString().equals(toM) ? vanilla.outputSrg : vanilla.outputIntermediary; diff --git a/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java b/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java index 5b523153a..c393f9978 100644 --- a/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java +++ b/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java @@ -105,11 +105,28 @@ public static void provideForgeSources(Project project, BiConsumer forgeSources = extractSources(forgeInstallerSources); project.getLogger().lifecycle(":extracted {} forge source classes", forgeSources.size()); remapSources(project, forgeSources); forgeSources.forEach(consumer); + + if (legacySourcesZip != null) { + Files.delete(legacySourcesZip); + } } private static void remapSources(Project project, Map sources) throws IOException { diff --git a/src/main/java/net/fabricmc/loom/extension/ForgeExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/ForgeExtensionImpl.java index 25da6717a..5a4956d95 100644 --- a/src/main/java/net/fabricmc/loom/extension/ForgeExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/ForgeExtensionImpl.java @@ -42,6 +42,7 @@ import net.fabricmc.loom.api.ForgeExtensionAPI; import net.fabricmc.loom.api.ForgeLocalMod; import net.fabricmc.loom.configuration.ide.RunConfigSettings; +import net.fabricmc.loom.configuration.providers.forge.fg2.Pack200Provider; public class ForgeExtensionImpl implements ForgeExtensionAPI { private final LoomGradleExtension extension; @@ -52,6 +53,7 @@ public class ForgeExtensionImpl implements ForgeExtensionAPI { private final Property useCustomMixin; private final List dataGenMods = new ArrayList<>(); // not a property because it has custom adding logic private final NamedDomainObjectContainer localMods; + private final Property pack200Provider; @Inject public ForgeExtensionImpl(Project project, LoomGradleExtension extension) { @@ -63,6 +65,7 @@ public ForgeExtensionImpl(Project project, LoomGradleExtension extension) { useCustomMixin = project.getObjects().property(Boolean.class).convention(true); localMods = project.container(ForgeLocalMod.class, baseName -> new ForgeLocalMod(project, baseName, new ArrayList<>())); + pack200Provider = project.getObjects().property(Pack200Provider.class); // Create default mod from main source set localMods(mod -> mod.create("main").add("main")); @@ -133,4 +136,9 @@ public void localMods(Action> action) public NamedDomainObjectContainer getLocalMods() { return localMods; } + + @Override + public Property getPack200Provider() { + return pack200Provider; + } } diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 8a68b0fb5..2ce96c46d 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -177,4 +177,10 @@ public static final class Forge { private Forge() { } } + + public static final class LegacyForge { + public static final String LAUNCH_WRAPPER = "net.minecraft.launchwrapper.Launch"; + public static final String FML_TWEAKER = "net.minecraftforge.fml.common.launcher.FMLTweaker"; + public static final String FML_SERVER_TWEAKER = "net.minecraftforge.fml.common.launcher.FMLServerTweaker"; + } } diff --git a/src/main/java/net/fabricmc/loom/util/LoggerFilter.java b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java index 1f9f9b96a..712172dc2 100644 --- a/src/main/java/net/fabricmc/loom/util/LoggerFilter.java +++ b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java @@ -26,6 +26,7 @@ import java.io.PrintStream; +import org.apache.commons.io.output.NullOutputStream; import org.jetbrains.annotations.NotNull; public class LoggerFilter { @@ -46,4 +47,31 @@ public PrintStream printf(@NotNull String format, Object... args) { // Failed to replace logger filter, just ignore } } + + public static void withSystemOutAndErrSuppressed(CheckedRunnable block) throws T { + PrintStream previousOut = System.out; + PrintStream previousErr = System.err; + + try { + System.setOut(new PrintStream(NullOutputStream.NULL_OUTPUT_STREAM)); + System.setErr(new PrintStream(NullOutputStream.NULL_OUTPUT_STREAM)); + } catch (SecurityException ignored) { + // Failed to replace logger, just ignore + } + + try { + block.run(); + } finally { + try { + System.setOut(previousOut); + System.setErr(previousErr); + } catch (SecurityException ignored) { + // Failed to replace logger, just ignore + } + } + } + + public interface CheckedRunnable { + void run() throws T; + } } diff --git a/src/main/java/net/fabricmc/loom/util/legacyforge/CoreModManagerTransformer.java b/src/main/java/net/fabricmc/loom/util/legacyforge/CoreModManagerTransformer.java new file mode 100644 index 000000000..e3ee932a4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/legacyforge/CoreModManagerTransformer.java @@ -0,0 +1,238 @@ +package net.fabricmc.loom.util.legacyforge; + +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ASTORE; +import static org.objectweb.asm.Opcodes.BIPUSH; +import static org.objectweb.asm.Opcodes.CHECKCAST; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.ICONST_0; +import static org.objectweb.asm.Opcodes.IFEQ; +import static org.objectweb.asm.Opcodes.IFNONNULL; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.NEW; +import static org.objectweb.asm.Opcodes.POP; +import static org.objectweb.asm.Opcodes.RETURN; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Transforms Forge's CoreModManager class to search the classpath for coremods. + * For motivation, see comments at usage site. + */ +public class CoreModManagerTransformer extends ClassVisitor { + private static final String CLASS = "net/minecraftforge/fml/relauncher/CoreModManager"; + public static final String FILE = CLASS + ".class"; + + private static final String TARGET_METHOD = "discoverCoreMods"; + private static final String OUR_METHOD_NAME = "loom$injectCoremodsFromClasspath"; + private static final String OUR_METHOD_DESCRIPTOR = "(Lnet/minecraft/launchwrapper/LaunchClassLoader;)V"; + + public CoreModManagerTransformer(ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); + + // We inject a call to our method, which will discover and load coremods from the classpath, at the very start of the + // regular discovery method. + if (name.equals(TARGET_METHOD)) { + methodVisitor = new InjectCallAtHead(methodVisitor); + } + + return methodVisitor; + } + + @Override + public void visitEnd() { + // We add the following method, which will find all coremods on the classpath, and load them. + // + // private static void loom$injectCoremodsFromClasspath(LaunchClassLoader classLoader) throws Exception { + // Enumeration urls = classLoader.getResources("META-INF/MANIFEST.MF"); + // while (urls.hasMoreElements()) { + // URL url = urls.nextElement(); + // InputStream stream = url.openStream(); + // Manifest manifest = new Manifest(stream); + // stream.close(); + // String coreModClass = manifest.getMainAttributes().getValue("FMLCorePlugin"); + // if (coreModClass == null) continue; + // File file; + // if ("jar".equals(url.getProtocol())) { + // file = new File(new URL(url.getPath()).toURI()); + // } else if ("file".equals(url.getProtocol())) { + // file = new File(url.toURI()).getParentFile().getParentFile(); + // } else { + // continue; + // } + // loadCoreMod(classLoader, coreModClass, file); + // } + // } + // + // Converted to ASM via the "ASM Bytecode Viewer" IntelliJ plugin: + { + MethodVisitor methodVisitor = super.visitMethod(ACC_PRIVATE | ACC_STATIC, OUR_METHOD_NAME, OUR_METHOD_DESCRIPTOR, null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(25, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitLdcInsn("META-INF/MANIFEST.MF"); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "net/minecraft/launchwrapper/LaunchClassLoader", "getResources", "(Ljava/lang/String;)Ljava/util/Enumeration;", false); + methodVisitor.visitVarInsn(ASTORE, 1); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLineNumber(26, label1); + methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/util/Enumeration"}, 0, null); + methodVisitor.visitVarInsn(ALOAD, 1); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Enumeration", "hasMoreElements", "()Z", true); + Label label2 = new Label(); + methodVisitor.visitJumpInsn(IFEQ, label2); + Label label3 = new Label(); + methodVisitor.visitLabel(label3); + methodVisitor.visitLineNumber(27, label3); + methodVisitor.visitVarInsn(ALOAD, 1); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Enumeration", "nextElement", "()Ljava/lang/Object;", true); + methodVisitor.visitTypeInsn(CHECKCAST, "java/net/URL"); + methodVisitor.visitVarInsn(ASTORE, 2); + Label label4 = new Label(); + methodVisitor.visitLabel(label4); + methodVisitor.visitLineNumber(28, label4); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "openStream", "()Ljava/io/InputStream;", false); + methodVisitor.visitVarInsn(ASTORE, 3); + Label label5 = new Label(); + methodVisitor.visitLabel(label5); + methodVisitor.visitLineNumber(29, label5); + methodVisitor.visitTypeInsn(NEW, "java/util/jar/Manifest"); + methodVisitor.visitInsn(DUP); + methodVisitor.visitVarInsn(ALOAD, 3); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/jar/Manifest", "", "(Ljava/io/InputStream;)V", false); + methodVisitor.visitVarInsn(ASTORE, 4); + Label label6 = new Label(); + methodVisitor.visitLabel(label6); + methodVisitor.visitLineNumber(30, label6); + methodVisitor.visitVarInsn(ALOAD, 3); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/InputStream", "close", "()V", false); + Label label7 = new Label(); + methodVisitor.visitLabel(label7); + methodVisitor.visitLineNumber(31, label7); + methodVisitor.visitVarInsn(ALOAD, 4); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/util/jar/Manifest", "getMainAttributes", "()Ljava/util/jar/Attributes;", false); + methodVisitor.visitLdcInsn("FMLCorePlugin"); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/util/jar/Attributes", "getValue", "(Ljava/lang/String;)Ljava/lang/String;", false); + methodVisitor.visitVarInsn(ASTORE, 5); + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + methodVisitor.visitLineNumber(32, label8); + methodVisitor.visitVarInsn(ALOAD, 5); + Label label9 = new Label(); + methodVisitor.visitJumpInsn(IFNONNULL, label9); + methodVisitor.visitJumpInsn(GOTO, label1); + methodVisitor.visitLabel(label9); + methodVisitor.visitLineNumber(34, label9); + methodVisitor.visitFrame(Opcodes.F_FULL, 6, new Object[]{"net/minecraft/launchwrapper/LaunchClassLoader", "java/util/Enumeration", "java/net/URL", "java/io/InputStream", "java/util/jar/Manifest", "java/lang/String"}, 0, new Object[]{}); + methodVisitor.visitLdcInsn("jar"); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "getProtocol", "()Ljava/lang/String;", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); + Label label10 = new Label(); + methodVisitor.visitJumpInsn(IFEQ, label10); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + methodVisitor.visitLineNumber(35, label11); + methodVisitor.visitTypeInsn(NEW, "java/io/File"); + methodVisitor.visitInsn(DUP); + methodVisitor.visitTypeInsn(NEW, "java/net/URL"); + methodVisitor.visitInsn(DUP); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "getPath", "()Ljava/lang/String;", false); + methodVisitor.visitInsn(ICONST_0); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "getPath", "()Ljava/lang/String;", false); + methodVisitor.visitIntInsn(BIPUSH, 33); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "lastIndexOf", "(I)I", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "substring", "(II)Ljava/lang/String;", false); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/net/URL", "", "(Ljava/lang/String;)V", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "toURI", "()Ljava/net/URI;", false); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/io/File", "", "(Ljava/net/URI;)V", false); + methodVisitor.visitVarInsn(ASTORE, 6); + Label label12 = new Label(); + methodVisitor.visitLabel(label12); + Label label13 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label13); + methodVisitor.visitLabel(label10); + methodVisitor.visitLineNumber(36, label10); + methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + methodVisitor.visitLdcInsn("file"); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "getProtocol", "()Ljava/lang/String;", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false); + methodVisitor.visitJumpInsn(IFEQ, label1); + Label label14 = new Label(); + methodVisitor.visitLabel(label14); + methodVisitor.visitLineNumber(37, label14); + methodVisitor.visitTypeInsn(NEW, "java/io/File"); + methodVisitor.visitInsn(DUP); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/net/URL", "toURI", "()Ljava/net/URI;", false); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/io/File", "", "(Ljava/net/URI;)V", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/File", "getParentFile", "()Ljava/io/File;", false); + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/File", "getParentFile", "()Ljava/io/File;", false); + methodVisitor.visitVarInsn(ASTORE, 6); + methodVisitor.visitLabel(label13); + methodVisitor.visitLineNumber(41, label13); + methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/io/File"}, 0, null); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitVarInsn(ALOAD, 5); + methodVisitor.visitVarInsn(ALOAD, 6); + methodVisitor.visitMethodInsn(INVOKESTATIC, "net/minecraftforge/fml/relauncher/CoreModManager", "loadCoreMod", "(Lnet/minecraft/launchwrapper/LaunchClassLoader;Ljava/lang/String;Ljava/io/File;)Lnet/minecraftforge/fml/relauncher/CoreModManager$FMLPluginWrapper;", false); + methodVisitor.visitInsn(POP); + Label label15 = new Label(); + methodVisitor.visitLabel(label15); + methodVisitor.visitLineNumber(42, label15); + methodVisitor.visitJumpInsn(GOTO, label1); + methodVisitor.visitLabel(label2); + methodVisitor.visitLineNumber(43, label2); + methodVisitor.visitFrame(Opcodes.F_FULL, 2, new Object[]{"net/minecraft/launchwrapper/LaunchClassLoader", "java/util/Enumeration"}, 0, new Object[]{}); + methodVisitor.visitInsn(RETURN); + Label label16 = new Label(); + methodVisitor.visitLabel(label16); + methodVisitor.visitLocalVariable("file", "Ljava/io/File;", null, label12, label10, 6); + methodVisitor.visitLocalVariable("url", "Ljava/net/URL;", null, label4, label15, 2); + methodVisitor.visitLocalVariable("stream", "Ljava/io/InputStream;", null, label5, label15, 3); + methodVisitor.visitLocalVariable("manifest", "Ljava/util/jar/Manifest;", null, label6, label15, 4); + methodVisitor.visitLocalVariable("coreModClass", "Ljava/lang/String;", null, label8, label15, 5); + methodVisitor.visitLocalVariable("file", "Ljava/io/File;", null, label13, label15, 6); + methodVisitor.visitLocalVariable("classLoader", "Lnet/minecraft/launchwrapper/LaunchClassLoader;", null, label0, label16, 0); + methodVisitor.visitLocalVariable("urls", "Ljava/util/Enumeration;", "Ljava/util/Enumeration;", label1, label16, 1); + methodVisitor.visitMaxs(8, 7); + methodVisitor.visitEnd(); + } + + super.visitEnd(); + } + + private static class InjectCallAtHead extends MethodVisitor { + private InjectCallAtHead(MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + } + + @Override + public void visitCode() { + super.visitCode(); + + super.visitVarInsn(Opcodes.ALOAD, 1); + super.visitMethodInsn(Opcodes.INVOKESTATIC, CLASS, OUR_METHOD_NAME, OUR_METHOD_DESCRIPTOR, false); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/AccessTransformSetMapper.java b/src/main/java/net/fabricmc/loom/util/srg/AccessTransformSetMapper.java new file mode 100644 index 000000000..d17eabfb1 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/AccessTransformSetMapper.java @@ -0,0 +1,70 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 Minecrell (https://github.com/Minecrell) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.util.Objects; +import java.util.Optional; + +import org.cadixdev.at.AccessTransformSet; +import org.cadixdev.bombe.type.signature.MethodSignature; +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.model.ClassMapping; +import org.cadixdev.lorenz.model.FieldMapping; +import org.cadixdev.lorenz.model.Mapping; +import org.cadixdev.lorenz.model.MethodMapping; + +// TODO from https://github.com/CadixDev/at/blob/c2b92fc26cf26e64d3ca3b35abf3364d4b95e6c3/src/main/java/org/cadixdev/at/impl/AccessTransformSetMapper.java +// remove once https://github.com/CadixDev/at/issues/7 is fixed +public final class AccessTransformSetMapper { + private AccessTransformSetMapper() { + } + + public static AccessTransformSet remap(AccessTransformSet set, MappingSet mappings) { + Objects.requireNonNull(set, "set"); + Objects.requireNonNull(mappings, "mappings"); + + AccessTransformSet remapped = AccessTransformSet.create(); + set.getClasses().forEach((className, classSet) -> { + Optional> mapping = mappings.getClassMapping(className); + remap(mappings, mapping, classSet, remapped.getOrCreateClass(mapping.map(Mapping::getFullDeobfuscatedName).orElse(className))); + }); + return remapped; + } + + private static void remap(MappingSet mappings, Optional> mapping, AccessTransformSet.Class set, AccessTransformSet.Class remapped) { + remapped.merge(set.get()); + remapped.mergeAllFields(set.allFields()); + remapped.mergeAllMethods(set.allMethods()); + + set.getFields().forEach((name, transform) -> + remapped.mergeField(mapping.flatMap(m -> m.getFieldMapping(name)) + .map(FieldMapping::getDeobfuscatedName).orElse(name), transform)); + + set.getMethods().forEach((signature, transform) -> + remapped.mergeMethod(mapping.flatMap(m -> m.getMethodMapping(signature)) + .map(MethodMapping::getDeobfuscatedSignature) + .orElseGet(() -> new MethodSignature(signature.getName(), mappings.deobfuscate(signature.getDescriptor()))), transform)); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java index ede2dbad8..93125b3db 100644 --- a/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java @@ -72,15 +72,17 @@ public final class SrgMerger { private final FlatMappingVisitor flatOutput; private final List postProcesses = new ArrayList<>(); private final boolean lenient; + private final boolean legacy; private final Set methodSrgNames = new HashSet<>(); - public SrgMerger(Logger logger, Path srg, @Nullable Supplier mojmap, Path tiny, boolean lenient) throws IOException { + public SrgMerger(Logger logger, Path srg, @Nullable Supplier mojmap, Path tiny, boolean lenient, boolean legacy) throws IOException { this.logger = logger; this.srg = readSrg(srg, mojmap); this.src = new MemoryMappingTree(); this.output = new MemoryMappingTree(); this.flatOutput = new RegularAsFlatMappingVisitor(output); this.lenient = lenient; + this.legacy = legacy; MappingReader.read(tiny, this.src); @@ -115,14 +117,15 @@ public MemoryMappingTree merge() throws IOException { * @param tiny the tiny file * @param out the output file, will be in tiny v2 * @param lenient whether to ignore missing tiny mapping + * @param legacy treat any method as mapped, even when it is lacking the 'func_' prefix * @throws IOException if an IO error occurs while reading or writing the mappings * @throws MappingException if the input tiny tree's default namespace is not 'official' * or if an element mentioned in the SRG file does not have tiny mappings */ - public static void mergeSrg(Logger logger, @Nullable Supplier mojmap, Path srg, Path tiny, Path out, boolean lenient) + public static void mergeSrg(Logger logger, @Nullable Supplier mojmap, Path srg, Path tiny, Path out, boolean lenient, boolean legacy) throws IOException, MappingException { Stopwatch stopwatch = Stopwatch.createStarted(); - MemoryMappingTree tree = mergeSrg(logger, mojmap, srg, tiny, lenient); + MemoryMappingTree tree = mergeSrg(logger, mojmap, srg, tiny, lenient, legacy); try (Tiny2Writer writer = new Tiny2Writer(Files.newBufferedWriter(out), false)) { tree.accept(writer); @@ -140,14 +143,15 @@ public static void mergeSrg(Logger logger, @Nullable Supplier mojmap, Path * @param mojmap the path to the mojmap file used for generating mojmap+srg names, may be null * @param tiny the tiny file * @param lenient whether to ignore missing tiny mapping + * @param legacy treat any method as mapped, even when it is lacking the 'func_' prefix * @return the created mapping tree * @throws IOException if an IO error occurs while reading or writing the mappings * @throws MappingException if the input tiny tree's default namespace is not 'official' * or if an element mentioned in the SRG file does not have tiny mappings */ - public static MemoryMappingTree mergeSrg(Logger logger, @Nullable Supplier mojmap, Path srg, Path tiny, boolean lenient) + public static MemoryMappingTree mergeSrg(Logger logger, @Nullable Supplier mojmap, Path srg, Path tiny, boolean lenient, boolean legacy) throws IOException, MappingException { - return new SrgMerger(logger, srg, mojmap, tiny, lenient).merge(); + return new SrgMerger(logger, srg, mojmap, tiny, lenient, legacy).merge(); } private MemoryMappingTree readSrg(Path srg, @Nullable Supplier mojmap) throws IOException { @@ -256,7 +260,7 @@ private void classToTiny(MappingTree.ClassMapping klass) throws IOException { methodToTiny(obf, method, methodSrgName, def); - if (methodSrgName.startsWith("func_") || methodSrgName.startsWith("m_")) { + if (methodSrgName.startsWith("func_") || methodSrgName.startsWith("m_") || legacy) { methodSrgNames.add(methodSrgName); } } diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java index 456b8b1bb..cf795448c 100644 --- a/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java @@ -25,23 +25,49 @@ package net.fabricmc.loom.util.srg; import java.io.IOException; +import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; +import org.cadixdev.lorenz.MappingSet; import org.cadixdev.lorenz.io.srg.SrgWriter; -import org.gradle.api.logging.Logger; import net.fabricmc.lorenztiny.TinyMappingsReader; import net.fabricmc.mappingio.tree.MappingTree; public class SrgNamedWriter { - public static void writeTo(Logger logger, Path srgFile, MappingTree mappings, String from, String to) throws IOException { + public static void writeTo(Path srgFile, MappingTree mappings, String from, String to, boolean includeIdentityMappings) throws IOException { Files.deleteIfExists(srgFile); - try (SrgWriter writer = new SrgWriter(Files.newBufferedWriter(srgFile))) { + try (SrgWriter writer = newSrgWriter(Files.newBufferedWriter(srgFile), includeIdentityMappings)) { try (TinyMappingsReader reader = new TinyMappingsReader(mappings, from, to)) { writer.write(reader.read()); } } } + + private static SrgWriter newSrgWriter(Writer writer, boolean includeIdentityMappings) { + return includeIdentityMappings ? new SrgWithIdentitiesWriter(writer) : new SrgWriter(writer); + } + + /** + * Legacy Forge's FMLDeobfuscatingRemapper requires class mappings, even if they are identity maps, but such + * mappings are filtered out by the SrgWriter. To get around that, this SrgWriter manually emits identity mappings + * before emitting all regular mappings. + */ + private static class SrgWithIdentitiesWriter extends SrgWriter { + private SrgWithIdentitiesWriter(Writer writer) { + super(writer); + } + + @Override + public void write(MappingSet mappings) { + mappings.getTopLevelClassMappings().stream() + .filter(cls -> !cls.hasDeobfuscatedName()) + .sorted(getConfig().getClassMappingComparator()) + .forEach(cls -> writer.format("CL: %s %s%n", cls.getFullObfuscatedName(), cls.getFullDeobfuscatedName())); + + super.write(mappings); + } + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy index 28dfde1b5..1b4ca4b46 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy @@ -54,5 +54,7 @@ class ForgeTest extends Specification implements GradleProjectTestTrait { '1.16.5' | "36.2.4" | "\"net.fabricmc:yarn:1.16.5+build.5:v2\"" '1.14.4' | "28.2.23" | "loom.officialMojangMappings()" '1.14.4' | "28.2.23" | "\"net.fabricmc:yarn:1.14.4+build.18:v2\"" + '1.12.2' | "14.23.0.2486" | "\"de.oceanlabs.mcp:mcp_snapshot:20170615-1.12\"" + '1.8.9' | "11.15.1.2318-1.8.9" | "\"de.oceanlabs.mcp:mcp_stable:22-1.8.9\"" } } diff --git a/src/test/resources/projects/forge/simple/build.gradle b/src/test/resources/projects/forge/simple/build.gradle index dcee84dc3..2e1d75828 100644 --- a/src/test/resources/projects/forge/simple/build.gradle +++ b/src/test/resources/projects/forge/simple/build.gradle @@ -57,6 +57,13 @@ jar { } } +loom { + forge { + // FIXME should eventually be handled by architectury-pack200 gradle plugin? + pack200Provider = new dev.architectury.pack200.java.Pack200Adapter() + } +} + // configure the maven publication publishing { publications {