diff --git a/build.gradle b/build.gradle index e9f35e3..df38717 100644 --- a/build.gradle +++ b/build.gradle @@ -6,24 +6,49 @@ plugins { id 'checkstyle' } -checkstyle { - configFile = file('checkstyle.xml') -} +allprojects { + apply plugin: 'java-library' + apply plugin: 'maven-publish' + apply plugin: 'com.diffplug.spotless' + apply plugin: 'checkstyle' -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 -def ENV = System.getenv() -version += (ENV.GITHUB_ACTIONS ? '' : '+local') + repositories { + mavenCentral() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + } -repositories { - mavenCentral() - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' + checkstyle { + configFile = rootProject.file('checkstyle.xml') + } + + spotless { + java { + licenseHeaderFile(rootProject.file('HEADER')) + } + } + + java { + withSourcesJar() + } + + tasks.withType(JavaCompile).configureEach { + it.options.encoding = 'UTF-8' + + if (JavaVersion.current().isJava9Compatible()) { + it.options.release = 8 + } } } +def ENV = System.getenv() +version += (ENV.GITHUB_ACTIONS ? '' : '+local') + configurations { ship implementation.extendsFrom ship @@ -42,12 +67,6 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}" } -spotless { - java { - licenseHeaderFile(rootProject.file('HEADER')) - } -} - jar { manifest { attributes 'Implementation-Title': 'Stitch', @@ -57,22 +76,10 @@ jar { } shadowJar { - configurations = [project.configurations.ship] + configurations = [project.configurations.ship] archiveClassifier = 'all' } -java { - withSourcesJar() -} - -tasks.withType(JavaCompile).configureEach { - it.options.encoding = 'UTF-8' - - if (JavaVersion.current().isJava9Compatible()) { - it.options.release = 8 - } -} - publishing { publications { mavenJava(MavenPublication) { diff --git a/minecraft-plugin/build.gradle b/minecraft-plugin/build.gradle new file mode 100644 index 0000000..99759be --- /dev/null +++ b/minecraft-plugin/build.gradle @@ -0,0 +1,30 @@ +archivesBaseName = 'stitch-minecraft-plugin' +version = rootProject.version +group = rootProject.group + +dependencies { + implementation project(':') +} + +def ENV = System.getenv() + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } + + repositories { + if (ENV.MAVEN_URL) { + repositories.maven { + name 'fabric' + url ENV.MAVEN_URL + credentials { + username ENV.MAVEN_USERNAME + password ENV.MAVEN_PASSWORD + } + } + } + } +} diff --git a/minecraft-plugin/src/main/java/net/fabricmc/stitch/plugins/MinecraftPlugin.java b/minecraft-plugin/src/main/java/net/fabricmc/stitch/plugins/MinecraftPlugin.java new file mode 100644 index 0000000..a06f507 --- /dev/null +++ b/minecraft-plugin/src/main/java/net/fabricmc/stitch/plugins/MinecraftPlugin.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.stitch.plugins; + +import java.util.regex.Pattern; + +import net.fabricmc.stitch.plugin.StitchPlugin; +import net.fabricmc.stitch.representation.ClassStorage; +import net.fabricmc.stitch.representation.JarClassEntry; +import net.fabricmc.stitch.representation.JarFieldEntry; +import net.fabricmc.stitch.representation.JarMethodEntry; + +public class MinecraftPlugin implements StitchPlugin { + // Minecraft classes without a package are obfuscated. + private static Pattern classObfuscationPattern = Pattern.compile("^[^/]*$"); + + @Override + public int needsIntermediaryName(ClassStorage storage, JarClassEntry cls) { + return StitchPlugin.super.needsIntermediaryName(storage, cls) > 0 + && classObfuscationPattern.matcher(cls.getName()).matches() ? 2 : -2; + } + + @Override + public int needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarFieldEntry fld) { + String name = fld.getName(); + + return name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_') ? 2 : -2; + } + + @Override + public int needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarMethodEntry mth) { + String name = mth.getName(); + + return (name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_')) + && StitchPlugin.super.needsIntermediaryName(storage, cls, mth) > 0 ? 2 : -2; + } + + @Override + public String getIntermediaryTargetPackage() { + return "net/minecraft/"; + } +} diff --git a/minecraft-plugin/src/main/resources/META-INF/services/net.fabricmc.stitch.plugin.StitchPlugin b/minecraft-plugin/src/main/resources/META-INF/services/net.fabricmc.stitch.plugin.StitchPlugin new file mode 100644 index 0000000..4a2032f --- /dev/null +++ b/minecraft-plugin/src/main/resources/META-INF/services/net.fabricmc.stitch.plugin.StitchPlugin @@ -0,0 +1 @@ +net.fabricmc.stitch.plugin.StitchPlugin diff --git a/settings.gradle b/settings.gradle index 8c6333a..131db6d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,5 @@ pluginManagement { } rootProject.name = 'stitch' + +include ':minecraft-plugin' diff --git a/src/main/java/net/fabricmc/stitch/Main.java b/src/main/java/net/fabricmc/stitch/Main.java index c31ce03..dcc545a 100644 --- a/src/main/java/net/fabricmc/stitch/Main.java +++ b/src/main/java/net/fabricmc/stitch/Main.java @@ -28,6 +28,7 @@ import net.fabricmc.stitch.commands.CommandRewriteIntermediary; import net.fabricmc.stitch.commands.CommandUpdateIntermediary; import net.fabricmc.stitch.commands.CommandValidateRecords; +import net.fabricmc.stitch.plugin.PluginLoader; public class Main { private static final Map COMMAND_MAP = new TreeMap<>(); @@ -72,6 +73,7 @@ public static void main(String[] args) { System.arraycopy(args, 1, argsCommand, 0, argsCommand.length); } + PluginLoader.loadPlugins(); COMMAND_MAP.get(args[0].toLowerCase(Locale.ROOT)).run(argsCommand); } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java index c6f8c97..e0d2485 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandGenerateIntermediary.java @@ -31,7 +31,7 @@ public CommandGenerateIntermediary() { @Override public String getHelpString() { - return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]..."; + return " [-t|--target-namespace ] [--write-all]"; } @Override @@ -52,7 +52,6 @@ public void run(String[] args) throws Exception { } GenState state = new GenState(); - boolean clearedPatterns = false; for (int i = 2; i < args.length; i++) { switch (args[i].toLowerCase(Locale.ROOT)) { @@ -61,15 +60,8 @@ public void run(String[] args) throws Exception { state.setTargetPackage(args[i + 1]); i++; break; - case "-p": - case "--obfuscation-pattern": - if (!clearedPatterns) { - state.clearObfuscatedPatterns(); - } - - clearedPatterns = true; - state.addObfuscatedPattern(args[i + 1]); - i++; + case "--write-all": + state.setWriteAll(true); break; } } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java index c53ad09..fc134a3 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandRewriteIntermediary.java @@ -31,7 +31,7 @@ public CommandRewriteIntermediary() { @Override public String getHelpString() { - return " [--writeAll]"; + return " [--write-all]"; } @Override @@ -55,7 +55,7 @@ public void run(String[] args) throws Exception { for (int i = 3; i < args.length; i++) { switch (args[i].toLowerCase(Locale.ROOT)) { - case "--writeall": + case "--write-all": state.setWriteAll(true); break; } diff --git a/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java b/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java index 6c01315..be4c3e2 100644 --- a/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java +++ b/src/main/java/net/fabricmc/stitch/commands/CommandUpdateIntermediary.java @@ -31,7 +31,7 @@ public CommandUpdateIntermediary() { @Override public String getHelpString() { - return " [-t|--target-namespace ] [-p|--obfuscation-pattern ]"; + return " [-t|--target-namespace ] [--write-all]"; } @Override @@ -62,7 +62,6 @@ public void run(String[] args) throws Exception { } GenState state = new GenState(); - boolean clearedPatterns = false; for (int i = 5; i < args.length; i++) { switch (args[i].toLowerCase(Locale.ROOT)) { @@ -71,15 +70,8 @@ public void run(String[] args) throws Exception { state.setTargetPackage(args[i + 1]); i++; break; - case "-p": - case "--obfuscation-pattern": - if (!clearedPatterns) { - state.clearObfuscatedPatterns(); - } - - clearedPatterns = true; - state.addObfuscatedPattern(args[i + 1]); - i++; + case "--write-all": + state.setWriteAll(true); break; } } diff --git a/src/main/java/net/fabricmc/stitch/commands/GenState.java b/src/main/java/net/fabricmc/stitch/commands/GenState.java index ae32f1a..a76bbcf 100644 --- a/src/main/java/net/fabricmc/stitch/commands/GenState.java +++ b/src/main/java/net/fabricmc/stitch/commands/GenState.java @@ -34,8 +34,8 @@ import java.util.Scanner; import java.util.Set; import java.util.TreeSet; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; +import java.util.function.Function; +import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; @@ -51,6 +51,8 @@ import net.fabricmc.mappingio.tree.MappingTree.ClassMapping; import net.fabricmc.mappingio.tree.MappingTree.FieldMapping; import net.fabricmc.mappingio.tree.MappingTree.MethodMapping; +import net.fabricmc.stitch.plugin.PluginLoader; +import net.fabricmc.stitch.plugin.StitchPlugin; import net.fabricmc.stitch.representation.AbstractJarEntry; import net.fabricmc.stitch.representation.ClassStorage; import net.fabricmc.stitch.representation.JarClassEntry; @@ -76,13 +78,29 @@ class GenState { private boolean interactive = true; private boolean writeAll = false; private Scanner scanner = new Scanner(System.in); - - private String targetPackage = "net/minecraft/"; - private final List obfuscatedPatterns = new ArrayList(); + private String targetPackage = ""; GenState() throws IOException { - this.obfuscatedPatterns.add(Pattern.compile("^[^/]*$")); // Default obfuscation. Minecraft classes without a package are obfuscated. - mappingTree.visitNamespaces(official, Arrays.asList(intermediary, intermediary)); + mappingTree.visitNamespaces(official, Arrays.asList(intermediary)); + + // Set target package + List pkgs = PluginLoader.getLoadedPlugins().stream() + .map(plugin -> plugin.getIntermediaryTargetPackage()) + .filter(pkg -> pkg != null && !pkg.isEmpty()) + .collect(Collectors.toList()); + if (pkgs.size() > 1) { + throw new IllegalStateException("More than one plugin has declared an intermediary target package!"); + } else if (pkgs.size() == 1) { + setTargetPackage(pkgs.get(0)); + } + } + + public void setTargetPackage(String pkg) { + if (pkg.lastIndexOf("/") != (pkg.length() - 1)) { + this.targetPackage = pkg + "/"; + } else { + this.targetPackage = pkg; + } } private void validateNamespaces(MappingTree tree) { @@ -121,22 +139,6 @@ public String next(AbstractJarEntry entry, String name) { }); } - public void setTargetPackage(final String pkg) { - if (pkg.lastIndexOf("/") != (pkg.length() - 1)) { - this.targetPackage = pkg + "/"; - } else { - this.targetPackage = pkg; - } - } - - public void clearObfuscatedPatterns() { - this.obfuscatedPatterns.clear(); - } - - public void addObfuscatedPattern(String regex) throws PatternSyntaxException { - this.obfuscatedPatterns.add(Pattern.compile(regex)); - } - public void setCounter(String key, int value) { counters.put(key, value); } @@ -167,7 +169,11 @@ public void generate(File file, JarRootEntry jarEntry, JarRootEntry jarOld) thro readCounterFileIfPresent(); for (JarClassEntry c : jarEntry.getClasses()) { - addClass(c, jarOld, jarEntry, this.targetPackage); + MappingTree cMappingTree = addClass(c, jarOld, jarEntry, this.targetPackage); + + if (cMappingTree != null) { + cMappingTree.accept(mappingTree); + } } writeCounters(); @@ -180,30 +186,42 @@ public void generate(File file, JarRootEntry jarEntry, JarRootEntry jarOld) thro writer.close(); } - public static boolean isMappedClass(ClassStorage storage, JarClassEntry c) { - return !c.isAnonymous(); - } + private boolean needsIntermediaryName(Function priorityGetter) { + List results = PluginLoader.getLoadedPlugins().stream() + .map(plugin -> priorityGetter.apply(plugin)) + .sorted((a, b) -> Math.abs(b) - Math.abs(a)) + .collect(Collectors.toList()); + + int first = results.get(0); + + if (results.size() > 1) { + int second = results.get(1); - public static boolean isMappedField(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { - return isUnmappedFieldName(f.getName()); + if (first != 0 + && first != second + && Math.abs(first) == Math.abs(second)) { + throw new IllegalStateException("Got conflicting scores of the same priority!"); + } + } + + return first > 0; } - public static boolean isUnmappedFieldName(String name) { - return name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_'); + private boolean needsIntermediaryName(ClassStorage storage, JarClassEntry cls) { + return needsIntermediaryName((plugin) -> plugin.needsIntermediaryName(storage, cls)); } - public static boolean isMappedMethod(ClassStorage storage, JarClassEntry c, JarMethodEntry m) { - return isUnmappedMethodName(m.getName()) && m.isSource(storage, c); + private boolean needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarFieldEntry fld) { + return needsIntermediaryName((plugin) -> plugin.needsIntermediaryName(storage, cls, fld)); } - public static boolean isUnmappedMethodName(String name) { - return (name.length() <= 2 || (name.length() == 3 && name.charAt(2) == '_')) - && name.charAt(0) != '<'; + private boolean needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarMethodEntry mth) { + return needsIntermediaryName((plugin) -> plugin.needsIntermediaryName(storage, cls, mth)); } @Nullable private String getFieldName(ClassStorage storage, JarClassEntry c, JarFieldEntry f) { - if (!isMappedField(storage, c, f)) { + if (!needsIntermediaryName(storage, c, f)) { return null; } @@ -354,7 +372,7 @@ private void findNames(ClassStorage storageOld, ClassStorage storageNew, JarClas @Nullable private String getMethodName(ClassStorage storageOld, ClassStorage storageNew, JarClassEntry c, JarMethodEntry m) { - if (!isMappedMethod(storageNew, c, m)) { + if (!needsIntermediaryName(storageNew, c, m)) { return null; } @@ -426,16 +444,11 @@ private String getMethodName(ClassStorage storageOld, ClassStorage storageNew, J return next(m, "method"); } - private void addClass(JarClassEntry c, ClassStorage storageOld, ClassStorage storage, String prefix) throws IOException { + private MappingTree addClass(JarClassEntry c, ClassStorage storageOld, ClassStorage storage, String prefix) throws IOException { String cName = ""; String origPrefix = prefix; - if (!this.obfuscatedPatterns.stream().anyMatch(p -> p.matcher(c.getName()).matches())) { - // Class name is not obfuscated. We don't need to generate - // an intermediary name, so we just leave it as is and - // don't add a prefix. - prefix = ""; - } else if (!isMappedClass(storage, c)) { + if (!needsIntermediaryName(storage, c)) { cName = c.getName(); } else { cName = null; @@ -479,14 +492,19 @@ private void addClass(JarClassEntry c, ClassStorage storageOld, ClassStorage sto } } + boolean wroteAnyIntermediaries = false; + MemoryMappingTree mappingTree = new MemoryMappingTree(); + mappingTree.visitNamespaces(official, Arrays.asList(intermediary)); + mappingTree.visitClass(c.getFullyQualifiedName()); mappingTree.visitDstName(MappedElementKind.CLASS, intermediaryIndex, prefix + cName); for (JarFieldEntry f : c.getFields()) { String fName = getFieldName(storage, c, f); - if (fName == null) { - fName = f.getName(); + if (fName != null || writeAll) { + wroteAnyIntermediaries = true; + if (fName == null) fName = f.getName(); } if (fName != null) { @@ -498,10 +516,9 @@ private void addClass(JarClassEntry c, ClassStorage storageOld, ClassStorage sto for (JarMethodEntry m : c.getMethods()) { String mName = getMethodName(storageOld, storage, c, m); - if (mName == null) { - if (!m.getName().startsWith("<") && m.isSource(storage, c)) { - mName = m.getName(); - } + if (mName != null || writeAll) { + wroteAnyIntermediaries = true; + if (mName == null) mName = m.getName(); } if (mName != null) { @@ -511,8 +528,15 @@ private void addClass(JarClassEntry c, ClassStorage storageOld, ClassStorage sto } for (JarClassEntry cc : c.getInnerClasses()) { - addClass(cc, storageOld, storage, prefix + cName + "$"); + MappingTree ccMappingTree = addClass(cc, storageOld, storage, prefix + cName + "$"); + + if (ccMappingTree != null) { + ccMappingTree.accept(mappingTree); + wroteAnyIntermediaries = true; + } } + + return wroteAnyIntermediaries || writeAll ? mappingTree : null; } public void prepareRewrite(File oldMappings) throws IOException { diff --git a/src/main/java/net/fabricmc/stitch/plugin/PluginLoader.java b/src/main/java/net/fabricmc/stitch/plugin/PluginLoader.java new file mode 100644 index 0000000..77e179c --- /dev/null +++ b/src/main/java/net/fabricmc/stitch/plugin/PluginLoader.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.stitch.plugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +public class PluginLoader { + private static List loadedPlugins = new ArrayList<>(5); + private static boolean loaded; + + public static void loadPlugins() { + if (loaded) { + throw new RuntimeException("Plugins already loaded!"); + } + + Iterable plugins = ServiceLoader.load(StitchPlugin.class); + + for (StitchPlugin plugin : plugins) { + loadedPlugins.add(plugin); + } + + // Register default plugin + loadedPlugins.add(new StitchPlugin() { + }); + loaded = true; + } + + public static List getLoadedPlugins() { + return loadedPlugins; + } +} diff --git a/src/main/java/net/fabricmc/stitch/plugin/StitchPlugin.java b/src/main/java/net/fabricmc/stitch/plugin/StitchPlugin.java new file mode 100644 index 0000000..6eec6c7 --- /dev/null +++ b/src/main/java/net/fabricmc/stitch/plugin/StitchPlugin.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.stitch.plugin; + +import net.fabricmc.stitch.representation.ClassStorage; +import net.fabricmc.stitch.representation.JarClassEntry; +import net.fabricmc.stitch.representation.JarFieldEntry; +import net.fabricmc.stitch.representation.JarMethodEntry; + +public interface StitchPlugin { + /** + * Whether or not the passed class needs an intermediary name. + * A positive number means true, a negative number means false. + * The returned integer's value represents the result's priority; + * a higher priority will overwrite other plugins' results. + * Return at least +/-2 to overwrite the default plugin. + */ + default int needsIntermediaryName(ClassStorage storage, JarClassEntry cls) { + return cls.isAnonymous() ? -1 : 1; + } + + /** + * Whether or not the passed field needs an intermediary name. + * A positive number means true, a negative number means false. + * The returned integer's value represents the result's priority; + * a higher priority will overwrite other plugins' results. + * Return at least +/-2 to overwrite the default plugin. + */ + default int needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarFieldEntry fld) { + return 1; + } + + /** + * Whether or not the passed method needs an intermediary name. + * A positive number means true, a negative number means false. + * The returned integer's value represents the result's priority; + * a higher priority will overwrite other plugins' results. + * Return at least +/-2 to overwrite the default plugin. + */ + default int needsIntermediaryName(ClassStorage storage, JarClassEntry cls, JarMethodEntry mth) { + String name = mth.getName(); + + return name.charAt(0) != '<' && mth.isSource(storage, cls) ? 1 : -1; + } + + /** + * Determines the target package where intermediary classes + * get moved into. Must have a trailing slash. + * Example: {@code my/toplevel/package/} + */ + default String getIntermediaryTargetPackage() { + return null; + } +}