diff options
author | Jonas Herzig <jonas@spark-squared.com> | 2022-01-06 16:31:13 +0100 |
---|---|---|
committer | Jonas Herzig <jonas@spark-squared.com> | 2022-01-10 14:23:17 +0100 |
commit | f0cafe844107eb484e9d769dd0cad7c7e5f27e9d (patch) | |
tree | 518da54517f5bdaca4fbf0d43a1a07760974d02e | |
parent | 64a1aba1db539ea854fdc832b1a8e53ac7428263 (diff) | |
download | architectury-loom-f0cafe844107eb484e9d769dd0cad7c7e5f27e9d.tar.gz architectury-loom-f0cafe844107eb484e9d769dd0cad7c7e5f27e9d.tar.bz2 architectury-loom-f0cafe844107eb484e9d769dd0cad7c7e5f27e9d.zip |
Support for FG2-era Forge
25 files changed, 852 insertions, 48 deletions
diff --git a/build.gradle b/build.gradle index 2cc27e47..ef0a1da8 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,9 @@ dependencies { implementation ('de.oceanlabs.mcp:mcinjector:3.8.0') implementation ('com.opencsv:opencsv:5.4') + // Legacy Forge access transformers + implementation ('net.md-5:SpecialSource:1.10.0') + // Testing testImplementation(gradleTestKit()) testImplementation('org.spockframework:spock-core:2.0-groovy-3.0') { @@ -126,6 +129,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 85855067..b5566765 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -139,6 +139,14 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { return isForge() && !getMcpConfigProvider().isOfficial(); } + default boolean isLegacyForge() { + return isForge() && getForgeUserdevProvider().isLegacyForge(); + } + + default boolean isModLauncher() { + return isForge() && !isLegacyForge(); + } + boolean supportsInclude(); default SrgProvider getSrgProvider() { diff --git a/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java b/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java index 28045071..3d93bad6 100644 --- a/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java @@ -90,6 +90,20 @@ public class LoomRepositoryPlugin implements Plugin<PluginAware> { sources.ignoreGradleMetadataRedirection(); }); }); + repositories.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); + }); repositories.mavenCentral(); repositories.ivy(repo -> { diff --git a/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/ForgeExtensionAPI.java index e455d742..6b77f842 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.Property; 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 @@ public interface ForgeExtensionAPI { * @see ForgeLocalMod */ NamedDomainObjectContainer<ForgeLocalMod> getLocalMods(); + + Property<Pack200Provider> getPack200Provider(); } diff --git a/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java b/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java index 76475caa..6fe72b18 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 @@ public class ModCompileRemapper { } 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/build/mixin/AnnotationProcessorInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java index 33fec4e6..1793ca72 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java @@ -101,7 +101,7 @@ public abstract class AnnotationProcessorInvoker<T extends Task> { ConfigurationContainer configs = project.getConfigurations(); LoomGradleExtension extension = LoomGradleExtension.get(project); - if (!extension.ideSync()) { + if (!extension.ideSync() || extension.isLegacyForge()) { for (Configuration processorConfig : apConfigurations) { project.getLogger().info("Adding mixin to classpath of AP config: " + processorConfig.getName()); // Pass named MC classpath to mixin AP classpath diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index eaac38ad..50febf4e 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 final class CompileConfiguration { } 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 2ea5c812..ae52c76f 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.charset.StandardCharsets; 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 class LaunchProvider extends DependencyProvider { .property("client", "java.library.path", getExtension().getMinecraftProvider().nativesDir().getAbsolutePath()) .property("client", "org.lwjgl.librarypath", getExtension().getMinecraftProvider().nativesDir().getAbsolutePath()); - if (!getExtension().isForge()) { + if (!getExtension().isModLauncher()) { launchConfig .argument("client", "--assetIndex") .argument("client", getExtension().getMinecraftProvider().getVersionInfo().assetIndex().fabricId(getExtension().getMinecraftProvider().minecraftVersion())) @@ -74,7 +74,7 @@ public class LaunchProvider extends DependencyProvider { .argument("client", new File(getDirectories().getUserCache(), "assets").getAbsolutePath()); } - if (getExtension().isForge()) { + if (getExtension().isModLauncher()) { launchConfig // Should match YarnNamingService.PATH_TO_MAPPINGS in forge-runtime .property("fabric.yarnWithSrg.path", getExtension().getMappingsProvider().tinyMappingsWithSrg.toAbsolutePath().toString()) @@ -103,11 +103,29 @@ public class LaunchProvider extends DependencyProvider { } } + 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().isForge() && !getExtension().isLegacyForge()) { 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 class LaunchProvider extends DependencyProvider { } public static class LaunchConfig { - private final Map<String, List<String>> values = new HashMap<>(); + private final Map<String, List<String>> 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 b2ebfa71..aa041162 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 @@ public class FieldMigratedMappingsProvider extends MappingsProviderImpl { 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 class FieldMigratedMappingsProvider extends MappingsProviderImpl { 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 358fd110..ec98da78 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 @@ -72,6 +72,7 @@ public class ForgeUserdevProvider extends DependencyProvider { private File userdevJar; private JsonObject json; private Consumer<Runnable> postPopulationScheduler; + private boolean isLegacyForge; public ForgeUserdevProvider(Project project) { super(project); @@ -99,7 +100,13 @@ public class ForgeUserdevProvider extends DependencyProvider { 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 (!Files.exists(configEntry)) { + configEntry = fs.getPath("dev.json"); + } + + Files.copy(configEntry, configJson, StandardCopyOption.REPLACE_EXISTING); } } @@ -107,11 +114,30 @@ public class ForgeUserdevProvider extends DependencyProvider { 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) { + Map<String, String> 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); + } else { + 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); + } 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 +154,7 @@ public class ForgeUserdevProvider extends DependencyProvider { 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 +163,10 @@ public class ForgeUserdevProvider extends DependencyProvider { // 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<String, JsonElement> entry : json.getAsJsonObject("runs").entrySet()) { + + JsonObject runs = isLegacyForge ? new JsonObject() : json.getAsJsonObject("runs"); + + for (Map.Entry<String, JsonElement> entry : runs.entrySet()) { LaunchProviderSettings launchSettings = getExtension().getLaunchConfigs().findByName(entry.getKey()); RunConfigSettings settings = getExtension().getRunConfigs().findByName(entry.getKey()); JsonObject value = entry.getValue().getAsJsonObject(); @@ -182,6 +211,18 @@ public class ForgeUserdevProvider extends DependencyProvider { }); } } + + if (isLegacyForge) { + getExtension().getRunConfigs().configureEach(config -> { + if (Constants.Forge.LAUNCH_TESTING.equals(config.getDefaultMainClass())) { + config.setDefaultMainClass(Constants.LegacyForge.LAUNCH_WRAPPER); + } + }); + } + } + + public boolean isLegacyForge() { + return isLegacyForge; } public abstract static class RemoveNameProvider implements TransformAction<TransformParameters.None> { 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 7404cc0a..160f14d6 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 class McpConfigProvider extends DependencyProvider { @Override public void provide(DependencyInfo dependency, Consumer<Runnable> 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 0c089e8f..05a216bd 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<File> projectAts = new HashSet<>(); - private boolean atDirty = false; - private boolean filesDirty = false; + protected Set<File> projectAts = new HashSet<>(); + protected boolean atDirty = false; + protected boolean filesDirty = false; private Path mcpConfigMappings; private Path[] mergedMojangTsrg2Files; @@ -130,8 +130,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { 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 class MinecraftPatchedProvider extends DependencyProvider { atDirty = mismatched; } + } + + public void initFiles() throws IOException { + filesDirty = false; + initAts(); MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); PatchProvider patchProvider = getExtension().getPatchProvider(); @@ -215,7 +219,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { cleanProjectCache(); } - private File[] getGlobalCaches() { + protected File[] getGlobalCaches() { File[] files = { minecraftClientSrgJar, minecraftServerSrgJar, @@ -234,7 +238,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { } } - private File[] getProjectCache() { + protected File[] getProjectCache() { return new File[] { minecraftMergedPatchedSrgAtJar, minecraftMergedPatchedJar @@ -451,7 +455,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { 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"); @@ -594,7 +598,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { applyLoomPatchVersion(mcOutput); } - private void patchJars(Logger logger) throws IOException { + private void patchJars(Logger logger) throws Exception { Stopwatch stopwatch = Stopwatch.createStarted(); logger.lifecycle(":patching jars"); @@ -614,7 +618,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { 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 { @@ -677,7 +681,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { } } - private void walkFileSystems(File source, File target, Predicate<Path> filter, FsPathConsumer action) throws IOException { + protected void walkFileSystems(File source, File target, Predicate<Path> filter, FsPathConsumer action) throws IOException { walkFileSystems(source, target, filter, FileSystem::getRootDirectories, action); } @@ -685,7 +689,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { 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(); @@ -698,7 +702,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { }); } - private void copyNonClassFiles(File source, File target) throws IOException { + protected void copyNonClassFiles(File source, File target) throws IOException { Predicate<Path> filter = getExtension().isForgeAndOfficial() ? file -> { String s = file.toString(); return !s.endsWith(".class"); @@ -710,7 +714,7 @@ public class MinecraftPatchedProvider extends DependencyProvider { 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 d49ae8b4..ca55e8d7 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.Files; 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 class PatchProvider extends DependencyProvider { 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().isLegacyForge() + ? getExtension().getForgeUniversalProvider().getForge().toPath() + : dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge installer")).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().isLegacyForge()) { + splitAndConvertLegacyPatches(fs.getPath("binpatches.pack.lzma")); + } else { + Files.copy(fs.getPath("data", "client.lzma"), clientPatches, StandardCopyOption.REPLACE_EXISTING); + Files.copy(fs.getPath("data", "server.lzma"), serverPatches, StandardCopyOption.REPLACE_EXISTING); + } } } } @@ -77,6 +102,73 @@ public class PatchProvider extends DependencyProvider { } } + 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 55a19fd6..920d269e 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.BufferedWriter; 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.base.Stopwatch; 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,15 @@ public class SrgProvider extends DependencyProvider { 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)) { + 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 00000000..e87004c2 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/fg2/MinecraftLegacyPatchedProvider.java @@ -0,0 +1,447 @@ +/* + * 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.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +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.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 net.md_5.specialsource.AccessChange; +import net.md_5.specialsource.AccessMap; +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.ThreadingUtils; +import net.fabricmc.loom.util.TinyRemapperHelper; +import net.fabricmc.loom.util.ZipUtils; +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>) File::exists).negate()) + || !isPatchedJarUpToDate(forgeJar) || !isPatchedJarUpToDate(minecraftMergedPatchedAtJar)) { + cleanAllCache(); + } else if (atDirty || Stream.of(getProjectCache()).anyMatch(((Predicate<File>) 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<Runnable> 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<Path> isHelper = path -> path.startsWith("/org/apache/logging/log4j/core/helpers"); + walkFileSystems(log4jBeta9.toFile(), forgeJar, isHelper, this::copyReplacing); + } + + 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 remove the Fabric ones. + modifyClasses(minecraftMergedPatchedJar, FabricSideStripper::new); + + logger.info(":merged jars in " + stopwatch); + } + + private void accessTransformForge(Logger logger) throws Exception { + Stopwatch stopwatch = Stopwatch.createStarted(); + + logger.lifecycle(":access transforming minecraft"); + + MappingTree mappingTree = getExtension().getMappingsProvider().getMappingsWithSrg(); + + AccessMap accessMap = new LegacyAccessMap(); + + byte[] forgeAt = ZipUtils.unpack(forgeJar.toPath(), "forge_at.cfg"); + accessMap.loadAccessTransformer(new BufferedReader(new InputStreamReader(new ByteArrayInputStream(forgeAt)))); + + for (File projectAt : projectAts) { + accessMap.loadAccessTransformer(projectAt); + } + + Files.copy(minecraftMergedPatchedJar.toPath(), minecraftMergedPatchedAtJar.toPath(), StandardCopyOption.REPLACE_EXISTING); + + modifyClasses(minecraftMergedPatchedAtJar, writer -> new AccessTransformingVisitor(accessMap, mappingTree, writer)); + + logger.lifecycle(":access transformed minecraft in " + stopwatch.stop()); + } + + private void modifyClasses(File jarFile, Function<ClassVisitor, ClassVisitor> 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<? extends Path>) 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. + * <br> + * 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). + * <br> + * 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("<init>")) { + 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 FabricSideStripper extends ClassVisitor { + private static final String SIDED_DESCRIPTOR = "Lnet/fabricmc/api/Environment;"; + + private FabricSideStripper(ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(SIDED_DESCRIPTOR)) { + return null; + } + + return super.visitAnnotation(descriptor, visible); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return new FieldStripper(super.visitField(access, name, descriptor, signature, value)); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new MethodStripper(super.visitMethod(access, name, descriptor, signature, exceptions)); + } + + private static class FieldStripper extends FieldVisitor { + private FieldStripper(FieldVisitor fieldVisitor) { + super(Opcodes.ASM9, fieldVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(SIDED_DESCRIPTOR)) { + return null; + } + + return super.visitAnnotation(descriptor, visible); + } + } + + private static class MethodStripper extends MethodVisitor { + private MethodStripper(MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(SIDED_DESCRIPTOR)) { + return null; + } + + return super.visitAnnotation(descriptor, visible); + } + } + } + + private static class LegacyAccessMap extends AccessMap { + @Override + public void addAccessChange(String key, AccessChange accessChange) { + // Forge's AT separates fields/methods from their owner by a space but we require a slash + int spaceIdx = key.indexOf(' '); + + if (spaceIdx != -1 && key.charAt(spaceIdx + 1) != '(') { + key = key.replaceFirst(" ", "/"); + } + + super.addAccessChange(key, accessChange); + } + } + + private static class AccessTransformingVisitor extends ClassVisitor { + private final AccessMap accessMap; + private final MappingTree mappingTree; + private final int src; + private final int dst; + + private MappingTree.ClassMapping classMapping; + private String mappedClassName; + + private AccessTransformingVisitor(AccessMap accessMap, MappingTree mappingTree, ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + this.accessMap = accessMap; + this.mappingTree = mappingTree; + this.src = mappingTree.getNamespaceId("official"); + this.dst = mappingTree.getNamespaceId("srg"); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classMapping = mappingTree.getClass(name, src); + mappedClassName = classMapping != null ? classMapping.getName(dst) : name; + access = accessMap.applyClassAccess(mappedClassName, access); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + MappingTree.FieldMapping field = classMapping != null ? classMapping.getField(name, descriptor, src) : null; + String mappedName = field != null ? field.getName(dst) : name; + access = accessMap.applyFieldAccess(mappedClassName, mappedName, access); + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MappingTree.MethodMapping method = classMapping != null ? classMapping.getMethod(name, desc, src) : null; + String mappedName = method != null ? method.getName(dst) : name; + String mappedDesc = method != null ? method.getDesc(dst) : mappingTree.mapDesc(desc, src, dst); + access = accessMap.applyMethodAccess(mappedClassName, mappedName, mappedDesc, access); + return super.visitMethod(access, name, desc, signature, exceptions); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + MappingTree.ClassMapping classMapping = mappingTree.getClass(name, src); + access = accessMap.applyClassAccess(classMapping != null ? classMapping.getName(dst) : name, access); + super.visitInnerClass(name, outerName, innerName, access); + } + } +} 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 00000000..8912c42c --- /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 0f4614ce..344071f0 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 com.google.gson.JsonObject; 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.processors.MinecraftProcessedProvider; 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.format.Tiny2Writer; 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 class MappingsProviderImpl extends DependencyProvider implements Mappings } if (getExtension().isForge()) { - patchedProvider = new MinecraftPatchedProvider(getProject()); + if (getExtension().isLegacyForge()) { + patchedProvider = new MinecraftLegacyPatchedProvider(getProject()); + } else { + patchedProvider = new MinecraftPatchedProvider(getProject()); + } + patchedProvider.provide(dependency, postPopulationScheduler); } @@ -157,7 +167,7 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings 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); @@ -578,17 +588,48 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings 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()) { + Path intermediaryV1Tiny = getMinecraftProvider().file("intermediary-v1.tiny").toPath(); + generateDummyIntermediary(getProject().getLogger(), intermediaryV1Tiny, intermediaryTiny); + } else { + 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); + } } } return intermediaryTiny; } + private void generateDummyIntermediary(Logger logger, Path tinyV1, Path tinyV2) throws IOException { + Stopwatch stopwatch = Stopwatch.createStarted(); + logger.lifecycle(":generating dummy intermediary"); + + Path minecraftJar = getExtension().getMinecraftProvider().getMergedJar().toPath(); + + Files.deleteIfExists(tinyV1); // file must not exist, otherwise stitch will try to read it + + 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); + } + + 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 ec003995..a137aa8b 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 class MinecraftMappedProvider extends DependencyProvider { } public void remap(TinyRemapper remapper, Mutable<MemoryMappingTree> mappings, List<TinyRemapper.ApplyVisitorProvider> postApply, Info vanilla, @Nullable Info forge, String fromM) throws IOException { - Set<String> classNames = getExtension().isForge() ? InnerClassRemapper.readClassNames(vanilla.input) : null; + Set<String> 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/extension/ForgeExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/ForgeExtensionImpl.java index 25da6717..5a4956d9 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.LoomGradleExtension; 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<Boolean> useCustomMixin; private final List<String> dataGenMods = new ArrayList<>(); // not a property because it has custom adding logic private final NamedDomainObjectContainer<ForgeLocalMod> localMods; + private final Property<Pack200Provider> pack200Provider; @Inject public ForgeExtensionImpl(Project project, LoomGradleExtension extension) { @@ -63,6 +65,7 @@ public class ForgeExtensionImpl implements ForgeExtensionAPI { 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 class ForgeExtensionImpl implements ForgeExtensionAPI { public NamedDomainObjectContainer<ForgeLocalMod> getLocalMods() { return localMods; } + + @Override + public Property<Pack200Provider> 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 8a68b0fb..2ce96c46 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 class Constants { 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 1f9f9b96..712172dc 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 @@ package net.fabricmc.loom.util; import java.io.PrintStream; +import org.apache.commons.io.output.NullOutputStream; import org.jetbrains.annotations.NotNull; public class LoggerFilter { @@ -46,4 +47,31 @@ public class LoggerFilter { // Failed to replace logger filter, just ignore } } + + public static <T extends Throwable> void withSystemOutAndErrSuppressed(CheckedRunnable<T> 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<T extends Throwable> { + void run() throws T; + } } 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 ede2dbad..93125b3d 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<Runnable> postProcesses = new ArrayList<>(); private final boolean lenient; + private final boolean legacy; private final Set<String> methodSrgNames = new HashSet<>(); - public SrgMerger(Logger logger, Path srg, @Nullable Supplier<Path> mojmap, Path tiny, boolean lenient) throws IOException { + public SrgMerger(Logger logger, Path srg, @Nullable Supplier<Path> 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 final class SrgMerger { * @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<Path> mojmap, Path srg, Path tiny, Path out, boolean lenient) + public static void mergeSrg(Logger logger, @Nullable Supplier<Path> 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 final class SrgMerger { * @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<Path> mojmap, Path srg, Path tiny, boolean lenient) + public static MemoryMappingTree mergeSrg(Logger logger, @Nullable Supplier<Path> 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<Path> mojmap) throws IOException { @@ -256,7 +260,7 @@ public final class SrgMerger { 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 456b8b1b..f999f936 100644 --- a/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java @@ -28,7 +28,14 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.impl.MappingSetModelFactoryImpl; +import org.cadixdev.lorenz.impl.model.InnerClassMappingImpl; +import org.cadixdev.lorenz.impl.model.TopLevelClassMappingImpl; import org.cadixdev.lorenz.io.srg.SrgWriter; +import org.cadixdev.lorenz.model.ClassMapping; +import org.cadixdev.lorenz.model.InnerClassMapping; +import org.cadixdev.lorenz.model.TopLevelClassMapping; import org.gradle.api.logging.Logger; import net.fabricmc.lorenztiny.TinyMappingsReader; @@ -40,8 +47,35 @@ public class SrgNamedWriter { try (SrgWriter writer = new SrgWriter(Files.newBufferedWriter(srgFile))) { try (TinyMappingsReader reader = new TinyMappingsReader(mappings, from, to)) { - writer.write(reader.read()); + writer.write(reader.read(MappingSet.create(new ClassesAlwaysHaveDeobfNameFactory()))); } } } + + /** + * 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, we create a custom mapping set which always + * claims to have deobfuscated names set for classes. + */ + private static class ClassesAlwaysHaveDeobfNameFactory extends MappingSetModelFactoryImpl { + @Override + public TopLevelClassMapping createTopLevelClassMapping(MappingSet parent, String obfuscatedName, String deobfuscatedName) { + return new TopLevelClassMappingImpl(parent, obfuscatedName, deobfuscatedName) { + @Override + public boolean hasDeobfuscatedName() { + return true; + } + }; + } + + @Override + public InnerClassMapping createInnerClassMapping(ClassMapping parent, String obfuscatedName, String deobfuscatedName) { + return new InnerClassMappingImpl(parent, obfuscatedName, deobfuscatedName) { + @Override + public boolean hasDeobfuscatedName() { + return true; + } + }; + } + } } 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 28dfde1b..1b4ca4b4 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 dcee84dc..2e1d7582 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 { |