├── .github └── stale.yml ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main └── kotlin └── io └── heartpattern └── mcremapper ├── JavaTokens.kt ├── MCRemapper.kt ├── Utils.kt ├── commandline └── MCRemapperApp.kt ├── model ├── ArtifactType.kt ├── LocalVariableFixType.kt ├── Mapping.kt └── Utils.kt ├── parser ├── MappingParser.kt ├── csrg │ └── MappingCsrgParser.kt ├── proguard │ ├── MappingProguardParser.kt │ └── parseProguardType.kt └── srg │ └── MappingSrgParser.kt ├── preprocess ├── AutoLoggerPreprocessor.kt ├── AutoTokenPreprocessor.kt ├── InheritabilityPreprocessor.kt └── SuperTypeResolver.kt └── visitor ├── LocalVariableFixVisitor.kt ├── MappingRemapper.kt └── ParameterAnnotationFixVisitor.kt /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 30 2 | daysUntilClose: 7 3 | staleLabel: wontfix 4 | markComment: > 5 | This issue has benn automatically marked as stale because it has not had recent activity. 6 | It will be closed if no further activity occurs. 7 | Thank you for your contributions. 8 | closeComment: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/gradle,kotlin,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=gradle,kotlin,intellij+all 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | ### Intellij+all Patch ### 76 | # Ignores the whole .idea folder and all .iml files 77 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 78 | 79 | .idea/ 80 | 81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 82 | 83 | *.iml 84 | modules.xml 85 | .idea/misc.xml 86 | *.ipr 87 | 88 | # Sonarlint plugin 89 | .idea/sonarlint 90 | 91 | ### Kotlin ### 92 | # Compiled class file 93 | *.class 94 | 95 | # Log file 96 | *.log 97 | 98 | # BlueJ files 99 | *.ctxt 100 | 101 | # Mobile Tools for Java (J2ME) 102 | .mtj.tmp/ 103 | 104 | # Package Files # 105 | *.jar 106 | *.war 107 | *.nar 108 | *.ear 109 | *.zip 110 | *.tar.gz 111 | *.rar 112 | 113 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 114 | hs_err_pid* 115 | 116 | ### Gradle ### 117 | .gradle 118 | build/ 119 | 120 | # Ignore Gradle GUI config 121 | gradle-app.setting 122 | 123 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 124 | !gradle-wrapper.jar 125 | 126 | # Cache of project 127 | .gradletasknamecache 128 | 129 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 130 | # gradle/wrapper/gradle-wrapper.properties 131 | 132 | ### Gradle Patch ### 133 | **/build/ 134 | 135 | # End of https://www.gitignore.io/api/gradle,kotlin,intellij+all -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline{ 2 | agent any 3 | 4 | environment{ 5 | MAVEN_CREDENTIAL = credentials('heartpattern-maven-repository') 6 | } 7 | 8 | stages{ 9 | stage('publish'){ 10 | steps{ 11 | sh './gradlew publish -Pmaven.username=${MAVEN_CREDENTIAL_USR} -Pmaven.password=${MAVEN_CREDENTIAL_PSW}' 12 | } 13 | } 14 | } 15 | 16 | post{ 17 | always{ 18 | archiveArtifacts artifacts: '**/build/libs/*' 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 MC-Remapper contributors 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fmaven.heartpattern.io%2Frepository%2Fmaven-public%2Fio%2Fheartpattern%2Fmcremapper%2Fmaven-metadata.xml) ![Jenkins](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fjenkins.heartpattern.io%2Fjob%2FHeartPattern%2Fjob%2FMC-Remapper%2Fjob%2Fmaster%2F) ![GitHub top language](https://img.shields.io/github/languages/top/HeartPattern/MC-Remapper) ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/HeartPattern/MC-Remapper) ![GitHub repo size](https://img.shields.io/github/repo-size/HeartPattern/MC-Remapper) ![GitHub](https://img.shields.io/github/license/HeartPattern/MC-Remapper) ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/HeartPattern/MC-Remapper) ![GitHub last commit](https://img.shields.io/github/last-commit/HeartPattern/MC-Remapper) 2 | # MC-Remapper 3 | 4 | Deobfuscator for Minecraft 5 | 6 | Mojang provide mapping file of obfuscated class, field, and method name for minecraft mod developer. 7 | This program applies mapping file to original minecraft code. It does not decompile jar, 8 | but only apply mapping. You have to decompile jar with your favorite decompiler after deobfuscate minecraft with mc-remapper. 9 | 10 | # Compile 11 | Java 17 is required to compile MC-Remapper.\ 12 | Clone project to your local machine and open terminal in the directory where build.gradle located. 13 | Run following command to compile. 14 | 15 | ``` 16 | ./gradlew installDist 17 | ``` 18 | 19 | Runnable script and runtime libraries will generate under `build/install/MC-Remapper`. 20 | 21 | # Usage 22 | 23 | Open terminal at `build/install/MC-Remapper/bin`. 24 | Execute MC-Remapper (Mac/Linux) or MC-Remapper.bat(Windows) with following parameters. 25 | 26 | ## Arguments 27 | ### Execute with specific file 28 | To run mc remapper with a specific file, you can provide input jar and mapping txt. 29 | 30 | Input jar is path to file or url of obfuscated minecraft client or server. 31 | 32 | Mapping txt is path to file or url of proguard's mapping txt file. You can find it at `.minecraft/versions/$version$/$version$.json` file. 33 | 34 | ### Execute with version 35 | 36 | To run mc remapper with automatically download artifact and mapping tt, you can provide artifact name and version id. 37 | 38 | Artifact name is either server or client. 39 | 40 | Version id is release version or snapshot version. 41 | 42 | 43 | ## Options 44 | 45 | ### --output or --output-name(Default=Generate from other arguments) 46 | 47 | Path to output file. If file already exists, overwrite it. 48 | 49 | ### --thread (Default=8) 50 | Number of thread used for apply mapping to class. 51 | 52 | ### --fixlocalvar (Default=no) 53 | Fix local variable name \u2603(☃). 54 | This variable name declared multiple time in same scope, 55 | so some decompiler does not work. There are three options. 56 | 57 | |option|description| 58 | |---|---| 59 | |no|Do not fix| 60 | |rename|Rename problematic local variable to debug$index like debug1, debug2...| 61 | |delete|Delete problematic local variable| 62 | 63 | 64 | ### --reobf (Flag, Default=no) 65 | 66 | __This option is useless for now. Both forge and bukkit modify nms with their own mapping, therefore applying this option is meaningless__ 67 | 68 | Reobf option reverses mapping direction. By default, MC-Remapper map obfuscated code to deobfuscated code. However, if you write your mod/plugin with deobfuscated minecraft source and apply it to forge/bukkit, minecraft will crash with NoClassDefFoundException, NoSuchMethodException, or else. That's because, forge or bukkit use obfuscated code, but your plugin tried to access to deobfuscated code. In this case, you have to re-obfuscate your mod/plugin to use obfuscated minecraft code. You can 69 | use this option to map deobfuscated to. obfuscated code. 70 | 71 | For csrg/srg mappings, this option is needed to apply deobfuscating mapping. i.e. deobfuscating = --reobf, reobfuscating = no --reobf. 72 | 73 | 74 | ### --format (Default=proguard) 75 | 76 | Supported mapping format: proguard, csrg, srg 77 | 78 | ### --autologger (Flag, default=no) 79 | 80 | Static final fields with type org.apache.logging.log4j.Logger is automatically renamed to `LOGGER` if it is not explicitly renamed by the mapping. 81 | 82 | ### --autotoken (Flag, default=no) 83 | 84 | Members' names that are Java keywords are automatically appended an underscore (_). 85 | 86 | ### --mappackage (Map, default=None) 87 | 88 | Renames packages AFTER other mappings are applied. Can occur multiple times, where they are merged. 89 | 90 | Example: --mappackage =net.minecraft.server.v1_15 --mappackage net.minecraft.server=net.minecraft.server.v1_15 91 | 92 | The unnamed package is renamed to net.minecraft.server.v1_15, and net.minecraft.server is renamed to net.minecraft.server.v1_15. 93 | 94 | # Example usages 95 | 96 | ## Execute by specifying input file and mapping url 97 | 98 | ``` 99 | ./MC-Remapper server.jar https://launcher.mojang.com/v1/objects/448ccb7b455f156bb5cb9cdadd7f96cd68134dbd/server.txt 100 | ``` 101 | 102 | ## Execute by specifying input and mapping file with options 103 | 104 | ``` 105 | ./MC-Remapper server.jar server_mapping.txt --output deobf.jar --thread 8 --fixlocalvar=delete --reobf 106 | ``` 107 | 108 | ## Execute by specifying version 109 | 110 | ``` 111 | ./MC-Remapper server 1.15.2 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.9.22" 3 | application 4 | `maven-publish` 5 | } 6 | 7 | group = "io.heartpattern" 8 | version = "2.0.7-SNAPSHOT" 9 | 10 | repositories { 11 | maven("https://repo.heartpattern.io/repository/maven-public/") 12 | } 13 | 14 | dependencies { 15 | implementation(kotlin("stdlib")) 16 | implementation("com.github.ajalt.clikt", "clikt", "3.0.1") 17 | implementation("org.ow2.asm", "asm", "9.6") 18 | implementation("org.ow2.asm", "asm-commons", "9.6") 19 | implementation("org.ow2.asm", "asm-tree", "9.6") 20 | implementation("me.tongfei", "progressbar", "0.9.3") 21 | implementation("io.heartpattern","mcversions","1.0.3-SNAPSHOT") 22 | 23 | 24 | testImplementation(kotlin("test")) 25 | testImplementation(kotlin("test-junit")) 26 | } 27 | 28 | application { 29 | mainClass.set( "io.heartpattern.mcremapper.commandline.MCRemapperAppKt" ) 30 | } 31 | 32 | tasks { 33 | compileKotlin { 34 | kotlinOptions { 35 | freeCompilerArgs = listOf("-progressive") 36 | } 37 | } 38 | 39 | create("sourcesJar") { 40 | archiveClassifier.set("sources") 41 | from(sourceSets["main"].allSource) 42 | } 43 | } 44 | 45 | 46 | if("maven.username" in properties && "maven.password" in properties){ 47 | publishing{ 48 | repositories{ 49 | maven( 50 | if(version.toString().endsWith("SNAPSHOT")) 51 | "https://repo.heartpattern.io/repository/maven-public-snapshots/" 52 | else 53 | "https://repo.heartpattern.io/repository/maven-public-releases/" 54 | ){ 55 | credentials{ 56 | username = properties["maven.username"].toString() 57 | password = properties["maven.password"].toString() 58 | } 59 | } 60 | } 61 | 62 | publications{ 63 | create("maven"){ 64 | artifactId = "mcremapper" 65 | from(components["java"]) 66 | artifact(tasks["sourcesJar"]) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeartPattern/MC-Remapper/484c780726e2ec2d43d08c22bfb9f144c08d5408/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/5.6.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "MC-Remapper" 11 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/JavaTokens.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper 2 | 3 | /** 4 | * Emulates `--auto-member TOKENS` behavior of SpecialSource-2, which is used by SpigotMC 5 | */ 6 | object JavaTokens { 7 | val TOKENS: Set = setOf( 8 | "abstract", 9 | "assert", 10 | "boolean", 11 | "break", 12 | "byte", 13 | "case", 14 | "catch", 15 | "char", 16 | "class", 17 | "const", 18 | "continue", 19 | "default", 20 | "do", 21 | "double", 22 | "else", 23 | "enum", 24 | "extends", 25 | "final", 26 | "finally", 27 | "float", 28 | "for", 29 | "goto", 30 | "if", 31 | "implements", 32 | "import", 33 | "instanceof", 34 | "int", 35 | "interface", 36 | "long", 37 | "native", 38 | "new", 39 | "package", 40 | "private", 41 | "protected", 42 | "public", 43 | "return", 44 | "short", 45 | "static", 46 | "strictfp", 47 | "super", 48 | "switch", 49 | "synchronized", 50 | "this", 51 | "throw", 52 | "throws", 53 | "transient", 54 | "try", 55 | "void", 56 | "volatile", 57 | "while", 58 | "true", 59 | "false", 60 | "null" 61 | ) 62 | 63 | fun appendIfToken(name: String): String? { 64 | val found = TOKENS.find { token -> 65 | name.startsWith(token) 66 | } ?: return null 67 | 68 | val rest = name.substring(found.length) 69 | if (rest.any {c -> c != '_'}) { 70 | return null 71 | } 72 | return name + "_" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/MCRemapper.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper 2 | 3 | import io.heartpattern.mcremapper.model.ArtifactType 4 | import io.heartpattern.mcremapper.model.LocalVariableFixType 5 | import io.heartpattern.mcremapper.model.Mapping 6 | import io.heartpattern.mcremapper.parser.proguard.MappingProguardParser 7 | import io.heartpattern.mcremapper.preprocess.SuperTypeResolver 8 | import io.heartpattern.mcremapper.visitor.LocalVariableFixVisitor 9 | import io.heartpattern.mcremapper.visitor.MappingRemapper 10 | import io.heartpattern.mcremapper.visitor.ParameterAnnotationFixVisitor 11 | import io.heartpattern.mcversions.MCVersions 12 | import org.objectweb.asm.ClassReader 13 | import org.objectweb.asm.ClassWriter 14 | import org.objectweb.asm.commons.ClassRemapper 15 | import org.objectweb.asm.tree.ClassNode 16 | import java.io.ByteArrayInputStream 17 | import java.io.File 18 | import java.io.FileOutputStream 19 | import java.util.concurrent.Executors 20 | import java.util.concurrent.TimeUnit 21 | import java.util.zip.ZipEntry 22 | import java.util.zip.ZipInputStream 23 | import java.util.zip.ZipOutputStream 24 | 25 | /** 26 | * Utility class for applying mapping 27 | */ 28 | class MCRemapper( 29 | private val mapping: Mapping, 30 | private val superResolver: SuperTypeResolver, 31 | private val fixType: LocalVariableFixType 32 | ) { 33 | 34 | /** 35 | * Apply mapping to given [byteArray] of bytecode 36 | */ 37 | @Suppress("unused") 38 | fun applyMapping(byteArray: ByteArray): ByteArray { 39 | val reader = ClassReader(byteArray) 40 | val node = applyMapping(reader) 41 | val writer = ClassWriter(0) 42 | node.accept(writer) 43 | return writer.toByteArray() 44 | } 45 | 46 | /** 47 | * Apply mapping to given [reader] and return mapped [ClassNode] 48 | */ 49 | fun applyMapping(reader: ClassReader): ClassNode { 50 | val node = ClassNode() 51 | val visitor = ClassRemapper( 52 | LocalVariableFixVisitor( 53 | ParameterAnnotationFixVisitor( 54 | node 55 | ), 56 | fixType 57 | ), 58 | MappingRemapper(mapping, superResolver) 59 | ) 60 | 61 | reader.accept(visitor, 0) 62 | return node 63 | } 64 | 65 | /** 66 | * Apply mapping to given [input] and save to [output] 67 | */ 68 | fun applyMapping( 69 | input: File, 70 | output: File, 71 | thread: Int = 8 72 | ) { 73 | val mappingExecutor = Executors.newFixedThreadPool(thread) 74 | val outputExecutor = Executors.newSingleThreadExecutor() 75 | 76 | output.delete() 77 | output.createNewFile() 78 | 79 | val zipInput = ZipInputStream(input.getMinecraftSourceZipInputStream()) 80 | val zipOutput = ZipOutputStream(FileOutputStream(output)) 81 | 82 | while(true) { 83 | val entry = zipInput.nextEntry ?: break 84 | 85 | val bytes = zipInput.readBytes() 86 | if (entry.name.endsWith(".class")) { 87 | mappingExecutor.execute { 88 | val reader = ClassReader(bytes) 89 | val node = applyMapping(reader) 90 | val writer = ClassWriter(0) 91 | node.accept(writer) 92 | 93 | outputExecutor.execute { 94 | zipOutput.putNextEntry(ZipEntry("${node.name.toInternal()}.class")) 95 | zipOutput.write(writer.toByteArray()) 96 | zipOutput.closeEntry() 97 | } 98 | } 99 | } else { 100 | outputExecutor.execute { 101 | zipOutput.putNextEntry(ZipEntry(entry.name)) 102 | ByteArrayInputStream(bytes).copyTo(zipOutput) 103 | zipOutput.closeEntry() 104 | } 105 | } 106 | } 107 | 108 | mappingExecutor.shutdown() 109 | mappingExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS) 110 | outputExecutor.shutdown() 111 | outputExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS) 112 | 113 | zipOutput.close() 114 | } 115 | 116 | companion object { 117 | fun applyVersionMapping( 118 | output: File, 119 | version: String, 120 | type: ArtifactType, 121 | thread: Int = 8, 122 | fixLocalVar: LocalVariableFixType = LocalVariableFixType.NO 123 | ) { 124 | val client = MCVersions() 125 | val versionList = client.requestVersionSetAsync().get() 126 | val versionInfo = client.requestVersionAsync( 127 | versionList.versions.find { it.id == version } 128 | ?: throw IllegalArgumentException("$version does not exists") 129 | ).get() 130 | 131 | val input = when (type) { 132 | ArtifactType.CLIENT -> versionInfo.downloads.client 133 | ArtifactType.SERVER -> versionInfo.downloads.server 134 | }?.url?.download("input") 135 | ?: throw IllegalArgumentException("$version does not provide $type") 136 | 137 | val rawMapping = when (type) { 138 | ArtifactType.CLIENT -> versionInfo.downloads.client_mappings 139 | ArtifactType.SERVER -> versionInfo.downloads.server_mappings 140 | }?.url?.readText() 141 | ?: throw IllegalArgumentException("$version does not provide mapping of $type") 142 | 143 | val mapping = MappingProguardParser.parse(rawMapping).reversed() 144 | val resolver = SuperTypeResolver.fromFile(input) 145 | 146 | val remapper = MCRemapper(mapping, resolver, fixLocalVar) 147 | 148 | remapper.applyMapping(input, output, thread) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper 2 | 3 | import java.io.File 4 | import java.io.InputStream 5 | import java.net.URL 6 | import java.util.zip.ZipFile 7 | 8 | /** 9 | * Convert a binary name of a class or an interface in its internal form to the normal form. Simply replace '/' to '.' 10 | * @see "Java Virtual Machine Specification §4.2.1" 11 | */ 12 | fun String.fromInternal(): String { 13 | return replace('/', '.') 14 | } 15 | 16 | /** 17 | * Convert a binary name of a class or an interface in its normal form to the internal form. Simply replace '.' to '/' 18 | * @see "Java Virtual Machine Specification §4.2.1" 19 | */ 20 | fun String.toInternal(): String { 21 | return replace('.', '/') 22 | } 23 | 24 | fun URL.download(prefix: String): File { 25 | val tempFile = File.createTempFile(prefix, null) 26 | tempFile.outputStream().use { output -> 27 | this.openStream().use { input -> 28 | input.copyTo(output) 29 | } 30 | } 31 | 32 | tempFile.deleteOnExit() 33 | return tempFile 34 | } 35 | 36 | /** 37 | * Get jar containing minecraft source file. 38 | * Since 21w39a, minecraft server contains nested jar file. 39 | */ 40 | fun File.getMinecraftSourceZipInputStream(): InputStream { 41 | val zipFile = ZipFile(this) 42 | 43 | val versionEntry = zipFile.getEntry("META-INF/versions.list") 44 | ?: return inputStream() // If versions.list does not exist, it is plain-old-minecraft-server 45 | 46 | val nestedJarPath = 47 | "META-INF/versions/" + zipFile.getInputStream(versionEntry) 48 | .bufferedReader().use { 49 | it.readText() 50 | }.substringAfterLast("\t") 51 | 52 | return zipFile.getInputStream(zipFile.getEntry(nestedJarPath)) 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/commandline/MCRemapperApp.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DuplicatedCode") 2 | 3 | package io.heartpattern.mcremapper.commandline 4 | 5 | import com.github.ajalt.clikt.core.CliktCommand 6 | import com.github.ajalt.clikt.parameters.arguments.argument 7 | import com.github.ajalt.clikt.parameters.options.* 8 | import com.github.ajalt.clikt.parameters.types.choice 9 | import com.github.ajalt.clikt.parameters.types.int 10 | import io.heartpattern.mcremapper.MCRemapper 11 | import io.heartpattern.mcremapper.download 12 | import io.heartpattern.mcremapper.getMinecraftSourceZipInputStream 13 | import io.heartpattern.mcremapper.model.LocalVariableFixType 14 | import io.heartpattern.mcremapper.parser.MappingParser 15 | import io.heartpattern.mcremapper.parser.csrg.MappingCsrgParser 16 | import io.heartpattern.mcremapper.parser.proguard.MappingProguardParser 17 | import io.heartpattern.mcremapper.parser.srg.MappingSrgParser 18 | import io.heartpattern.mcremapper.preprocess.AutoLoggerPreprocessor 19 | import io.heartpattern.mcremapper.preprocess.AutoTokenPreprocessor 20 | import io.heartpattern.mcremapper.preprocess.InheritabilityPreprocessor 21 | import io.heartpattern.mcremapper.preprocess.SuperTypeResolver 22 | import io.heartpattern.mcremapper.toInternal 23 | import io.heartpattern.mcversions.MCVersions 24 | import me.tongfei.progressbar.ProgressBarBuilder 25 | import me.tongfei.progressbar.ProgressBarStyle 26 | import org.objectweb.asm.ClassReader 27 | import org.objectweb.asm.ClassWriter 28 | import java.io.ByteArrayInputStream 29 | import java.io.File 30 | import java.io.FileOutputStream 31 | import java.net.URL 32 | import java.util.concurrent.Executors 33 | import java.util.concurrent.TimeUnit 34 | import java.util.zip.ZipEntry 35 | import java.util.zip.ZipFile 36 | import java.util.zip.ZipInputStream 37 | import java.util.zip.ZipOutputStream 38 | 39 | class MCRemapperApp : CliktCommand() { 40 | // Input file or URL or server/client 41 | private val arg0: String by argument() 42 | 43 | // Mapping file or URL or version id 44 | private val arg1: String by argument() 45 | 46 | // Options 47 | private val outputName: String? by option("--output", "--output-name") 48 | private val reobf: Boolean by option().flag() 49 | private val thread: Int by option().int().default(8) 50 | private val fixlocalvar: LocalVariableFixType by option().choice("no", "rename", "delete").convert { 51 | when (it) { 52 | "no" -> LocalVariableFixType.NO 53 | "rename" -> LocalVariableFixType.RENAME 54 | "delete" -> LocalVariableFixType.DELETE 55 | else -> error("") // Never happen 56 | } 57 | }.default(LocalVariableFixType.NO) 58 | private val autologger: Boolean by option().flag() 59 | private val autotoken: Boolean by option().flag() 60 | private val mappackage: Map by option().associate() 61 | private val mappingParser: MappingParser by option("--format").choice("proguard", "csrg", "srg").convert { 62 | when (it) { 63 | "proguard" -> MappingProguardParser 64 | "csrg" -> MappingCsrgParser 65 | "srg" -> MappingSrgParser 66 | else -> error("") 67 | } 68 | }.default(MappingProguardParser) 69 | 70 | 71 | private val versionInfo by lazy { 72 | val client = MCVersions() 73 | val list = client.requestVersionSetAsync().get() 74 | client.requestVersionAsync( 75 | list.versions.find { it.id == arg1 } 76 | ?: throw IllegalArgumentException("$arg1 does not exists") 77 | ).get() 78 | } 79 | 80 | override fun run() { 81 | println("Download input") 82 | val input = when { 83 | arg0 == "server" -> versionInfo.downloads.server?.url?.download("input") 84 | ?: throw IllegalArgumentException("$arg0 does not provide server download url") 85 | arg0 == "client" -> versionInfo.downloads.client.url.download("input") 86 | arg0.startsWith("http") -> URL(arg0).download("input") 87 | else -> File(arg0) 88 | } 89 | 90 | println("Download mapping") 91 | val rawMapping = when { 92 | arg0 == "server" -> versionInfo.downloads.server_mappings?.url?.readText() 93 | ?: throw IllegalArgumentException("$arg0 does not provide server mapping") 94 | arg0 == "client" -> versionInfo.downloads.client_mappings?.url?.readText() 95 | ?: throw IllegalArgumentException("$arg0 does not provide client mapping") 96 | arg1.startsWith("http") -> URL(arg1).readText() 97 | else -> File(arg1).readText() 98 | } 99 | 100 | val output = File(when { 101 | outputName != null -> outputName!! 102 | arg0 == "server" -> "server_${versionInfo.id}.jar" 103 | arg0 == "client" -> "client_${versionInfo.id}.jar" 104 | arg0.startsWith("http") -> "${URL(arg0).host}-deobfuscated.jar" 105 | else -> "${arg1}-deobfuscated.jar" 106 | }) 107 | 108 | println("Parse mapping") 109 | val originalMapping = mappingParser.parse(rawMapping) 110 | var mapping = if (reobf) originalMapping else originalMapping.reversed() 111 | mapping = mapping.copy(packageMapping=mapping.packageMapping + mappackage.asSequence().map { (original, mapped) -> 112 | original.toInternal() to mapped.toInternal() 113 | }.toMap()) 114 | 115 | println("Resolve super type") 116 | val superResolver = SuperTypeResolver.fromFile(input) 117 | 118 | if (autologger) { 119 | println("Preprocess auto logger") 120 | mapping = AutoLoggerPreprocessor.preprocess(mapping, input, superResolver) 121 | } 122 | 123 | if (autotoken) { 124 | println("Preprocess auto token") 125 | mapping = AutoTokenPreprocessor.preprocess(mapping, input, superResolver) 126 | } 127 | 128 | println("Preprocess inheritability") 129 | mapping = InheritabilityPreprocessor.preprocess(mapping, input) 130 | 131 | println("Start mapping") 132 | val mappingExecutor = Executors.newFixedThreadPool(thread) 133 | val outputExecutor = Executors.newSingleThreadExecutor() 134 | 135 | val progress = ProgressBarBuilder() 136 | .setStyle(ProgressBarStyle.COLORFUL_UNICODE_BLOCK) 137 | .setInitialMax(0) 138 | .showSpeed() 139 | .build() 140 | 141 | val applier = MCRemapper( 142 | mapping, 143 | superResolver, 144 | fixlocalvar 145 | ) 146 | 147 | output.delete() 148 | 149 | val zipInput = ZipInputStream(input.getMinecraftSourceZipInputStream()) 150 | val zipOutput = ZipOutputStream(FileOutputStream(output)) 151 | 152 | while(true) { 153 | val entry = zipInput.nextEntry ?: break 154 | progress.maxHint(progress.max + 1) 155 | val bytes = zipInput.readBytes() 156 | if (entry.name.endsWith(".class")) { 157 | mappingExecutor.execute { 158 | val reader = ClassReader(bytes) 159 | val node = applier.applyMapping(reader) 160 | val writer = ClassWriter(0) 161 | node.accept(writer) 162 | 163 | outputExecutor.execute { 164 | zipOutput.putNextEntry(ZipEntry("${node.name.toInternal()}.class")) 165 | zipOutput.write(writer.toByteArray()) 166 | zipOutput.closeEntry() 167 | progress.step() 168 | } 169 | } 170 | } else { 171 | outputExecutor.execute { 172 | zipOutput.putNextEntry(ZipEntry(entry.name)) 173 | ByteArrayInputStream(bytes).copyTo(zipOutput) 174 | zipOutput.closeEntry() 175 | progress.step() 176 | } 177 | } 178 | } 179 | 180 | mappingExecutor.shutdown() 181 | mappingExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS) 182 | outputExecutor.shutdown() 183 | outputExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS) 184 | 185 | zipOutput.close() 186 | progress.close() 187 | println("Complete") 188 | } 189 | } 190 | 191 | fun main(args: Array) { 192 | MCRemapperApp().main(args) 193 | } 194 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/model/ArtifactType.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.model 2 | 3 | enum class ArtifactType{ 4 | SERVER, CLIENT 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/model/LocalVariableFixType.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.model 2 | 3 | enum class LocalVariableFixType{ 4 | NO, 5 | RENAME, 6 | DELETE 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/model/Mapping.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.model 2 | 3 | /** 4 | * Set of mappings 5 | */ 6 | data class Mapping( 7 | val classMapping: Map, 8 | val fieldMapping: Map, 9 | val methodMapping: Map, 10 | val packageMapping: Map 11 | ) { 12 | fun reversed(): Mapping = 13 | Mapping( 14 | classMapping.map { (original, mapped) -> 15 | mapped to original 16 | }.toMap(), 17 | fieldMapping.map { (ref, mappingValue) -> 18 | FieldRef( 19 | classMapping.getOrKey(ref.owner), 20 | ref.type?.let { mapType(it) }, mappingValue.mapped 21 | ) to MemberMappingValue(ref.name, mappingValue.inheritable) 22 | }.toMap(), 23 | methodMapping.map { (ref, mappingValue) -> 24 | val descriptor = ParsedMethodDescriptor(ref.descriptor) 25 | MethodRef( 26 | classMapping.getOrKey(ref.owner), 27 | ParsedMethodDescriptor(mapType(descriptor.returnType), descriptor.parameters.map { mapType(it) }).toString(), 28 | mappingValue.mapped 29 | ) to MemberMappingValue(ref.name, mappingValue.inheritable) 30 | }.toMap(), 31 | packageMapping.map { (key, value) -> 32 | value to key 33 | }.toMap() 34 | ) 35 | 36 | fun mapType(type: String): String = 37 | when (type.getOrNull(0)) { 38 | 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z', 'V' -> type 39 | '[' -> '[' + mapType(type.substring(1)) 40 | 'L' -> 'L' + classMapping.getOrKey(type.substring(1, type.length - 1)) + ';' 41 | else -> error("Illegal type signature: $type") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/model/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.model 2 | 3 | import org.objectweb.asm.Type 4 | 5 | fun Map.getOrKey(key: T): T = 6 | getOrDefault(key, key) 7 | 8 | data class FieldRef( 9 | val owner: String, 10 | val type: String?, 11 | val name: String 12 | ) 13 | 14 | data class MethodRef( 15 | val owner: String, 16 | val descriptor: String, 17 | val name: String 18 | ) 19 | 20 | data class ParsedMethodDescriptor( 21 | val returnType: String, 22 | val parameters: List 23 | ) { 24 | constructor(s: String) : this(Type.getReturnType(s).descriptor, Type.getArgumentTypes(s).map { it.descriptor }) 25 | 26 | override fun toString(): String = 27 | "(${parameters.joinToString(separator = "") { it }})${returnType}" 28 | } 29 | 30 | data class MemberMappingValue( 31 | val mapped: String, 32 | val inheritable: Boolean = true 33 | ) 34 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/parser/MappingParser.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.parser 2 | 3 | import io.heartpattern.mcremapper.model.Mapping 4 | 5 | interface MappingParser { 6 | fun parse(raw: String): Mapping 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/parser/csrg/MappingCsrgParser.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.parser.csrg 2 | 3 | import io.heartpattern.mcremapper.model.FieldRef 4 | import io.heartpattern.mcremapper.model.Mapping 5 | import io.heartpattern.mcremapper.model.MemberMappingValue 6 | import io.heartpattern.mcremapper.model.MethodRef 7 | import io.heartpattern.mcremapper.parser.MappingParser 8 | 9 | object MappingCsrgParser : MappingParser { 10 | 11 | override fun parse(raw: String): Mapping { 12 | val lines = raw.lines() 13 | 14 | val classMapping = mutableMapOf() 15 | val fieldMapping = mutableMapOf() 16 | val methodMapping = mutableMapOf() 17 | val packageMapping = mutableMapOf() 18 | 19 | for (line in lines) { 20 | val trimmed = line.trim() 21 | if (trimmed.isEmpty() || trimmed.startsWith('#') ) 22 | continue 23 | 24 | when (trimmed.count {it == ' '}) { 25 | 1 -> { 26 | val (original, mapped) = line.split(' ') 27 | if (!original.endsWith('/')) { 28 | classMapping[original] = mapped 29 | } else { 30 | packageMapping[original.substring(0, original.length - 1)] = mapped 31 | } 32 | } 33 | 2 -> { 34 | val (owner, original, mapped) = line.split(' ') 35 | fieldMapping[FieldRef(owner, null, original)] = MemberMappingValue(mapped) 36 | } 37 | 3 -> { 38 | val (owner, original, descriptor, mapped) = line.split(' ') 39 | methodMapping[MethodRef(owner, descriptor, original)] = MemberMappingValue(mapped) 40 | } 41 | } 42 | } 43 | 44 | return Mapping(classMapping, fieldMapping, methodMapping, packageMapping) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/parser/proguard/MappingProguardParser.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.parser.proguard 2 | 3 | import io.heartpattern.mcremapper.model.* 4 | import io.heartpattern.mcremapper.parser.MappingParser 5 | import io.heartpattern.mcremapper.toInternal 6 | 7 | object MappingProguardParser : MappingParser { 8 | 9 | private val CLASS_REGEX = Regex("""(\S+) -> (\S+):""") 10 | private val FIELD_REGEX = Regex("""(\S+) ([^\s(]+) -> (\S+)""") 11 | private val METHOD_REGEX = Regex("""([^\s:]+) (\S+)\((\S*)\) -> (\S+)""") 12 | private val METHOD_WITH_LINE_NUMBER_REGEX = Regex("(\\d+):(\\d+):(\\S+) (\\S+)\\((\\S*)\\) -> (\\S+)") 13 | 14 | override fun parse(raw: String): Mapping { 15 | val lines = raw.lines() 16 | 17 | val classMapping = mutableMapOf() 18 | val fieldMapping = mutableMapOf() 19 | val methodMapping = mutableMapOf() 20 | 21 | var currentClass: String? = null 22 | 23 | for (line in lines) { 24 | val trimmed = line.trim() 25 | if (trimmed.startsWith('#') || trimmed.isEmpty()) { 26 | continue 27 | } 28 | 29 | if (!line.startsWith(" ")) { 30 | val (originalNotInternal, mappedNotInternal) = CLASS_REGEX.matchEntire(trimmed)!!.destructured 31 | val original = originalNotInternal.toInternal() 32 | val mapped = mappedNotInternal.toInternal() 33 | classMapping[original] = mapped 34 | currentClass = original 35 | } else { 36 | val fieldMatchResult = FIELD_REGEX.matchEntire(trimmed) 37 | if (fieldMatchResult != null) { 38 | val (proguardType, original, mapped) = fieldMatchResult.destructured 39 | val type = parseProguardType(proguardType) 40 | fieldMapping[FieldRef(currentClass!!, type, original)] = MemberMappingValue(mapped) 41 | continue 42 | } 43 | val methodMatchResult = METHOD_REGEX.matchEntire(trimmed) 44 | if (methodMatchResult != null){ 45 | val (proguardReturnType, original, proguardParameterTypes, mapped) = methodMatchResult.destructured 46 | val returnType = parseProguardType(proguardReturnType) 47 | val parameterTypes = proguardParameterTypes.split(',').filter { it.isNotBlank() }.map { parseProguardType(it.trim()) } 48 | methodMapping[MethodRef(currentClass!!, ParsedMethodDescriptor(returnType, parameterTypes).toString(), original)] = MemberMappingValue(mapped) 49 | continue 50 | } 51 | val methodWithLineNumberMatchResult = METHOD_WITH_LINE_NUMBER_REGEX.matchEntire(trimmed) 52 | if (methodWithLineNumberMatchResult != null) { 53 | val (_, _, proguardReturnType, original, proguardParameterTypes, mapped) = methodWithLineNumberMatchResult.destructured 54 | val returnType = parseProguardType(proguardReturnType) 55 | val parameterTypes = proguardParameterTypes.split(',').filter { it.isNotBlank() }.map { parseProguardType(it.trim()) } 56 | methodMapping[MethodRef(currentClass!!, ParsedMethodDescriptor(returnType, parameterTypes).toString(), original)] = MemberMappingValue(mapped) 57 | continue 58 | } 59 | error("Invalid member mapping: $trimmed") 60 | } 61 | } 62 | 63 | return Mapping(classMapping, fieldMapping, methodMapping, mapOf()) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/parser/proguard/parseProguardType.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.parser.proguard 2 | 3 | import io.heartpattern.mcremapper.toInternal 4 | 5 | fun parseProguardType(raw: String): String { 6 | return when (raw) { 7 | "void" -> "V" 8 | "byte" -> "B" 9 | "char" -> "C" 10 | "double" -> "D" 11 | "float" -> "F" 12 | "int" -> "I" 13 | "long" -> "J" 14 | "short" -> "S" 15 | "boolean" -> "Z" 16 | "" -> throw IllegalArgumentException("Empty type signature") 17 | else -> { 18 | if (raw.endsWith("[]")) 19 | '[' + parseProguardType(raw.substring(0, raw.length - 2)) 20 | else 21 | "L${raw.toInternal()};" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/parser/srg/MappingSrgParser.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.parser.srg 2 | 3 | import io.heartpattern.mcremapper.model.FieldRef 4 | import io.heartpattern.mcremapper.model.Mapping 5 | import io.heartpattern.mcremapper.model.MemberMappingValue 6 | import io.heartpattern.mcremapper.model.MethodRef 7 | import io.heartpattern.mcremapper.parser.MappingParser 8 | 9 | object MappingSrgParser : MappingParser { 10 | private const val INSTRUCTION_SEPARATOR = ' ' 11 | 12 | override fun parse(raw: String): Mapping { 13 | val lines = raw.replace('\t', INSTRUCTION_SEPARATOR).lines() 14 | 15 | val classMapping = mutableMapOf() 16 | val fieldMapping = mutableMapOf() 17 | val methodMapping = mutableMapOf() 18 | val packageMapping = mutableMapOf() 19 | 20 | 21 | // there are 2 types of csrg mappings. I'll call this one v1, see: 22 | /* 23 | FD: net/minecraft/util/text/TextFormatting/AQUA net/minecraft/util/text/TextFormatting/AQUA 24 | FD: net/minecraft/util/text/TextFormatting/BLACK net/minecraft/util/text/TextFormatting/BLACK 25 | */ 26 | 27 | // and the other one will be v2 now 28 | /* 29 | a net/minecraft/util/text/TextFormatting 30 | a BLACK 31 | b DARK_BLUE 32 | */ 33 | 34 | 35 | fun parseV1() { 36 | for (line in lines) { 37 | val trimmed = line.trim() 38 | if (trimmed.isEmpty() || trimmed.startsWith('#')) { 39 | continue 40 | } 41 | var currentLineType: SrgLineTypes? = null 42 | 43 | for (lineType in SrgLineTypes.VALUES) { 44 | if (line.startsWith(lineType.prefix)) { 45 | currentLineType = lineType 46 | break 47 | } 48 | } 49 | check(currentLineType != null) { "Invalid mappings line: $line" } 50 | 51 | val lineInstructions = line.removePrefix(currentLineType.prefix).split(INSTRUCTION_SEPARATOR) 52 | 53 | 54 | fun getOriginalClassFieldAndMappedName(v2MappedIndex: Int = 1): Triple { 55 | val classFieldSplit = lineInstructions[0].split("/") 56 | val originalName: String = classFieldSplit.last() 57 | val originalClass: String = lineInstructions[0].removeSuffix("/$originalName") 58 | val mappedName: String = lineInstructions[v2MappedIndex].split("/").last() 59 | return Triple(originalClass, originalName, mappedName) 60 | } 61 | 62 | when (currentLineType) { 63 | SrgLineTypes.PACKAGE -> { 64 | // PK: net/minecraft net/minecraft 65 | packageMapping[lineInstructions[0]] = lineInstructions[1] 66 | } 67 | SrgLineTypes.CLASS -> { 68 | // CL: a net/minecraft/util/text/TextFormatting 69 | classMapping[lineInstructions[0]] = lineInstructions[1] 70 | } 71 | SrgLineTypes.FIELD -> { 72 | // FD: a/a net/minecraft/util/text/TextFormatting/BLACK 73 | // FD: net/minecraft/util/text/TextFormatting/BLACK net/minecraft/util/text/TextFormatting/BLACK 74 | val (originalClass, originalFieldName, mappedFieldName) = getOriginalClassFieldAndMappedName() 75 | fieldMapping[FieldRef(originalClass, null, originalFieldName)] = MemberMappingValue(mappedFieldName) 76 | } 77 | SrgLineTypes.METHOD -> { 78 | // MD: a/a (I)La; net/minecraft/util/text/TextFormatting/func_175744_a (I)Lnet/minecraft/util/text/TextFormatting; 79 | // MD: net/minecraft/util/text/TextFormatting/func_175744_a (I)Lnet/minecraft/util/text/TextFormatting; net/minecraft/util/text/TextFormatting/fromColorIndex (I)Lnet/minecraft/util/text/TextFormatting; 80 | val (originalClass, originalMethodName, mappedMethodName) = getOriginalClassFieldAndMappedName(v2MappedIndex = 2) 81 | 82 | methodMapping[MethodRef(originalClass, lineInstructions[3], originalMethodName)] = MemberMappingValue(mappedMethodName) 83 | } 84 | } 85 | } 86 | } 87 | 88 | fun parseV2() { 89 | var currentClass: String? = null 90 | for (line in lines) { 91 | val trimmed = line.trim() 92 | if (trimmed.isEmpty() || trimmed.startsWith('#')) { 93 | continue 94 | } 95 | 96 | when (line.count { it == INSTRUCTION_SEPARATOR }) { 97 | 1 -> { 98 | val (original, mapped) = line.split(INSTRUCTION_SEPARATOR) 99 | if (!original.endsWith('/')) { 100 | classMapping[original] = mapped 101 | currentClass = original 102 | } else { 103 | packageMapping[original.substring(0, original.length - 1)] = mapped 104 | } 105 | } 106 | 2 -> { 107 | val (original, mapped) = trimmed.split(INSTRUCTION_SEPARATOR) 108 | fieldMapping[FieldRef(currentClass!!, null, original)] = MemberMappingValue(mapped) 109 | } 110 | 3 -> { 111 | val (original, descriptor, mapped) = trimmed.split(INSTRUCTION_SEPARATOR) 112 | methodMapping[MethodRef(currentClass!!, descriptor, original)] = MemberMappingValue(mapped) 113 | } 114 | } 115 | } 116 | } 117 | 118 | var useV2 = true 119 | for (lineType in SrgLineTypes.VALUES) { 120 | if (raw.contains(lineType.prefix)) { 121 | // format is v1 122 | useV2 = false 123 | break 124 | } 125 | } 126 | 127 | 128 | if (useV2) { 129 | parseV2() 130 | } else { 131 | parseV1() 132 | } 133 | 134 | return Mapping(classMapping, fieldMapping, methodMapping, packageMapping) 135 | } 136 | 137 | private enum class SrgLineTypes(val prefix: String) { 138 | PACKAGE("PK: "), 139 | CLASS("CL: "), 140 | FIELD("FD: "), 141 | METHOD("MD: "), 142 | ; 143 | 144 | companion object { 145 | val VALUES = values() 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/preprocess/AutoLoggerPreprocessor.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.preprocess 2 | 3 | import io.heartpattern.mcremapper.model.FieldRef 4 | import io.heartpattern.mcremapper.model.Mapping 5 | import io.heartpattern.mcremapper.model.MemberMappingValue 6 | import org.objectweb.asm.ClassReader 7 | import org.objectweb.asm.ClassVisitor 8 | import org.objectweb.asm.FieldVisitor 9 | import org.objectweb.asm.Opcodes 10 | import java.io.File 11 | import java.lang.reflect.Modifier 12 | import java.util.zip.ZipFile 13 | 14 | /** 15 | * Add mappings that automatically renames logger fields to LOGGER 16 | */ 17 | object AutoLoggerPreprocessor { 18 | 19 | /** 20 | * Preprocess [mapping] with given JAR [file] and [superResolver] 21 | */ 22 | fun preprocess(mapping: Mapping, file: File, superResolver: SuperTypeResolver) : Mapping { 23 | val fieldMapping = mapping.fieldMapping.toMutableMap() 24 | val zipFile = ZipFile(file) 25 | for (entry in zipFile.entries()) { 26 | if (!entry.name.endsWith(".class")) continue 27 | zipFile.getInputStream(entry).use { 28 | ClassReader(it) 29 | }.accept(object : ClassVisitor(Opcodes.ASM9) { 30 | var className: String? = null 31 | 32 | override fun visit( 33 | version: Int, 34 | access: Int, 35 | name: String, 36 | signature: String?, 37 | superName: String?, 38 | interfaces: Array? 39 | ) { 40 | className = name 41 | } 42 | 43 | override fun visitField( 44 | access: Int, 45 | name: String, 46 | descriptor: String, 47 | signature: String?, 48 | value: Any? 49 | ): FieldVisitor? { 50 | for (superName in superResolver.iterateSuperNames(className!!)) { 51 | val ref = FieldRef(superName, descriptor, name) 52 | val mappingValue = mapping.fieldMapping[ref] 53 | if (mappingValue != null && (superName == className || mappingValue.inheritable)) { 54 | return null 55 | } 56 | val refWithoutType = FieldRef(superName, null, name) 57 | val mappingValueMatchedWithoutType = mapping.fieldMapping[refWithoutType] 58 | if (mappingValueMatchedWithoutType != null && (superName == className || mappingValueMatchedWithoutType.inheritable)) { 59 | return null 60 | } 61 | } 62 | if (Modifier.isStatic(access) && Modifier.isFinal(access) && descriptor == "Lorg.apache.logging.log4j.Logger;") { 63 | fieldMapping[FieldRef(className!!, "Lorg.apache.logging.log4j.Logger", name)] = MemberMappingValue("LOGGER") 64 | } 65 | return null 66 | } 67 | }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) 68 | } 69 | zipFile.close() 70 | return mapping.copy(fieldMapping = fieldMapping) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/preprocess/AutoTokenPreprocessor.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.preprocess 2 | 3 | import io.heartpattern.mcremapper.JavaTokens 4 | import io.heartpattern.mcremapper.model.FieldRef 5 | import io.heartpattern.mcremapper.model.Mapping 6 | import io.heartpattern.mcremapper.model.MemberMappingValue 7 | import io.heartpattern.mcremapper.model.MethodRef 8 | import org.objectweb.asm.* 9 | import java.io.File 10 | import java.util.zip.ZipFile 11 | 12 | /** 13 | * Add mappings that automatically renames members whose names are java keywords 14 | */ 15 | object AutoTokenPreprocessor { 16 | 17 | /** 18 | * Preprocess [mapping] with given JAR [file] and [superResolver] 19 | */ 20 | fun preprocess(mapping: Mapping, file: File, superResolver: SuperTypeResolver) : Mapping { 21 | val fieldMapping = mapping.fieldMapping.toMutableMap() 22 | val methodMapping = mapping.methodMapping.toMutableMap() 23 | val zipFile = ZipFile(file) 24 | for (entry in zipFile.entries()) { 25 | if (!entry.name.endsWith(".class")) continue 26 | zipFile.getInputStream(entry).use { 27 | ClassReader(it) 28 | }.accept(object : ClassVisitor(Opcodes.ASM9) { 29 | var className: String? = null 30 | 31 | override fun visit( 32 | version: Int, 33 | access: Int, 34 | name: String, 35 | signature: String?, 36 | superName: String?, 37 | interfaces: Array? 38 | ) { 39 | className = name 40 | } 41 | 42 | override fun visitField( 43 | access: Int, 44 | name: String, 45 | descriptor: String, 46 | signature: String?, 47 | value: Any? 48 | ): FieldVisitor? { 49 | for (superName in superResolver.iterateSuperNames(className!!)) { 50 | val ref = FieldRef(superName, descriptor, name) 51 | val mappingValue = mapping.fieldMapping[ref] 52 | if (mappingValue != null && (superName == className || mappingValue.inheritable)) { 53 | return null 54 | } 55 | val refWithoutType = FieldRef(superName, null, name) 56 | val mappingValueMatchedWithoutType = mapping.fieldMapping[refWithoutType] 57 | if (mappingValueMatchedWithoutType != null && (superName == className || mappingValueMatchedWithoutType.inheritable)) { 58 | return null 59 | } 60 | } 61 | JavaTokens.appendIfToken(name)?.let { fieldMapping[FieldRef(className!!, descriptor, name)] = MemberMappingValue(it) } 62 | return null 63 | } 64 | 65 | override fun visitMethod( 66 | access: Int, 67 | name: String, 68 | descriptor: String, 69 | signature: String?, 70 | exceptions: Array? 71 | ): MethodVisitor? { 72 | for (superName in superResolver.iterateSuperNames(className!!)) { 73 | val ref = MethodRef(superName, descriptor, name) 74 | val mappingValue = mapping.methodMapping[ref] 75 | if (mappingValue != null && (superName == className || mappingValue.inheritable)) { 76 | return null 77 | } 78 | } 79 | JavaTokens.appendIfToken(name)?.let { methodMapping[MethodRef(className!!, descriptor, name)] = MemberMappingValue(it) } 80 | return null 81 | } 82 | }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) 83 | } 84 | zipFile.close() 85 | return mapping.copy(fieldMapping = fieldMapping, methodMapping = methodMapping) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/preprocess/InheritabilityPreprocessor.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.preprocess 2 | 3 | import io.heartpattern.mcremapper.model.FieldRef 4 | import io.heartpattern.mcremapper.model.Mapping 5 | import io.heartpattern.mcremapper.model.MethodRef 6 | import org.objectweb.asm.* 7 | import java.io.File 8 | import java.lang.reflect.Modifier 9 | import java.util.zip.ZipFile 10 | 11 | /** 12 | * Add mappings that automatically renames logger fields to LOGGER 13 | */ 14 | object InheritabilityPreprocessor { 15 | 16 | /** 17 | * Preprocess [mapping] with given JAR [file] 18 | */ 19 | fun preprocess(mapping: Mapping, file: File) : Mapping { 20 | val fieldMapping = mapping.fieldMapping.toMutableMap() 21 | val methodMapping = mapping.methodMapping.toMutableMap() 22 | val zipFile = ZipFile(file) 23 | for (entry in zipFile.entries()) { 24 | if (!entry.name.endsWith(".class")) continue 25 | zipFile.getInputStream(entry).use { 26 | ClassReader(it) 27 | }.accept(object : ClassVisitor(Opcodes.ASM9) { 28 | var className: String? = null 29 | 30 | override fun visit( 31 | version: Int, 32 | access: Int, 33 | name: String, 34 | signature: String?, 35 | superName: String?, 36 | interfaces: Array? 37 | ) { 38 | className = name 39 | } 40 | 41 | override fun visitField( 42 | access: Int, 43 | name: String, 44 | descriptor: String, 45 | signature: String?, 46 | value: Any? 47 | ): FieldVisitor? { 48 | val ref = FieldRef(className!!, descriptor, name) 49 | val mappingValue = fieldMapping[ref] 50 | if (mappingValue != null && mappingValue.inheritable) { 51 | fieldMapping[ref] = mappingValue.copy(inheritable = !Modifier.isPrivate(access)) 52 | } 53 | 54 | val refWithoutType = FieldRef(className!!, null, name) 55 | val mappingValueMatchedWithoutType = fieldMapping[refWithoutType] 56 | if (mappingValueMatchedWithoutType != null && mappingValueMatchedWithoutType.inheritable) { 57 | fieldMapping[refWithoutType] = mappingValueMatchedWithoutType.copy(inheritable = !Modifier.isPrivate(access)) 58 | } 59 | return null 60 | } 61 | 62 | override fun visitMethod( 63 | access: Int, 64 | name: String, 65 | descriptor: String, 66 | signature: String?, 67 | exceptions: Array? 68 | ): MethodVisitor? { 69 | val ref = MethodRef(className!!, descriptor, name) 70 | val mappingValue = methodMapping[ref] 71 | if (mappingValue != null && mappingValue.inheritable) { 72 | methodMapping[ref] = mappingValue.copy(inheritable = !Modifier.isPrivate(access)) 73 | } 74 | return null 75 | } 76 | }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) 77 | } 78 | zipFile.close() 79 | return mapping.copy(fieldMapping = fieldMapping, methodMapping = methodMapping) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/preprocess/SuperTypeResolver.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.preprocess 2 | 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.ClassVisitor 5 | import org.objectweb.asm.Opcodes 6 | import java.io.File 7 | import java.util.* 8 | import java.util.zip.ZipFile 9 | 10 | /** 11 | * Resolve super type of class 12 | */ 13 | class SuperTypeResolver(private val directSuperNames: Map>) { 14 | 15 | companion object { 16 | /** 17 | * Create [SuperTypeResolver] from given JAR [file] 18 | */ 19 | fun fromFile(file: File) : SuperTypeResolver { 20 | val directSuperTypes = mutableMapOf>().withDefault { mutableListOf() } 21 | 22 | val zipFile = ZipFile(file) 23 | for (entry in zipFile.entries()) { 24 | if (!entry.name.endsWith(".class")) continue 25 | zipFile.getInputStream(entry).use { 26 | ClassReader(it) 27 | }.accept(object : ClassVisitor(Opcodes.ASM9) { 28 | override fun visit( 29 | version: Int, 30 | access: Int, 31 | name: String, 32 | signature: String?, 33 | superName: String?, 34 | interfaces: Array? 35 | ) { 36 | if (superName != null || !interfaces.isNullOrEmpty()) { 37 | val list = mutableListOf() 38 | if (superName != null) { 39 | list.add(superName) 40 | } 41 | if (interfaces != null) { 42 | list.addAll(interfaces) 43 | } 44 | directSuperTypes[name] = list 45 | } 46 | } 47 | }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) 48 | } 49 | zipFile.close() 50 | return SuperTypeResolver(directSuperTypes) 51 | } 52 | } 53 | 54 | /** 55 | * Iterate through all supertype names of given class [name] including [name] itself 56 | */ 57 | fun iterateSuperNames(name: String): Iterator { 58 | return iterator { 59 | val queue = ArrayDeque() 60 | val queued = mutableSetOf() 61 | 62 | queue.addLast(name) 63 | while (queue.isNotEmpty()) { 64 | val target = queue.removeFirst() 65 | yield(target) 66 | 67 | for (superclass in directSuperNames[target] ?: continue) { 68 | if (superclass !in queued) { 69 | queue.addLast(superclass) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/visitor/LocalVariableFixVisitor.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.visitor 2 | 3 | import io.heartpattern.mcremapper.model.LocalVariableFixType 4 | import io.heartpattern.mcremapper.model.LocalVariableFixType.* 5 | import org.objectweb.asm.ClassVisitor 6 | import org.objectweb.asm.Label 7 | import org.objectweb.asm.MethodVisitor 8 | import org.objectweb.asm.Opcodes 9 | 10 | /** 11 | * Fix local variable name is always \u2603(☃) 12 | */ 13 | class LocalVariableFixVisitor( 14 | cv: ClassVisitor, 15 | val type: LocalVariableFixType 16 | ) : ClassVisitor(Opcodes.ASM9, cv) { 17 | override fun visitMethod( 18 | access: Int, 19 | name: String?, 20 | descriptor: String?, 21 | signature: String?, 22 | exceptions: Array? 23 | ): MethodVisitor { 24 | val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) 25 | 26 | return object : MethodVisitor(Opcodes.ASM9, methodVisitor) { 27 | override fun visitLocalVariable( 28 | name: String?, 29 | descriptor: String?, 30 | signature: String?, 31 | start: Label?, 32 | end: Label?, 33 | index: Int 34 | ) { 35 | if (name == null || name != "\u2603") { 36 | super.visitLocalVariable(name, descriptor, signature, start, end, index) 37 | return 38 | } 39 | 40 | when (type) { 41 | RENAME -> super.visitLocalVariable("debug$index", descriptor, signature, start, end, index) 42 | NO -> super.visitLocalVariable(name, descriptor, signature, start, end, index) 43 | DELETE -> Unit// Do nothing 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/visitor/MappingRemapper.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.visitor 2 | 3 | import io.heartpattern.mcremapper.model.FieldRef 4 | import io.heartpattern.mcremapper.model.Mapping 5 | import io.heartpattern.mcremapper.model.MethodRef 6 | import io.heartpattern.mcremapper.model.getOrKey 7 | import io.heartpattern.mcremapper.preprocess.SuperTypeResolver 8 | import org.objectweb.asm.commons.Remapper 9 | 10 | /** 11 | * Apply given [mapping] 12 | */ 13 | class MappingRemapper( 14 | private val mapping: Mapping, 15 | private val superResolver: SuperTypeResolver 16 | ) : Remapper() { 17 | override fun map(name: String): String { 18 | val mappedName = (name.length downTo 0).asSequence() 19 | .filter { index -> index == name.length || name[index] == '$'} 20 | .firstOrNull { suffixStart -> 21 | val prefix = name.substring(0, suffixStart) 22 | prefix in mapping.classMapping 23 | }?.let { suffixStart -> 24 | val prefix = name.substring(0, suffixStart) 25 | val suffix = name.substring(suffixStart) 26 | mapping.classMapping.getOrKey(prefix) + suffix 27 | } ?: name 28 | 29 | val packageName = if ('/' in mappedName) mappedName.substring(0, mappedName.lastIndexOf('/')) else "" 30 | return (if (packageName in mapping.packageMapping) { 31 | mapping.packageMapping.getValue(packageName).let { mapped -> if (mapped.isNotEmpty()) "$mapped/" else ""} + mappedName.substring(if (packageName.isNotEmpty()) packageName.length + 1 else 0) 32 | } else { 33 | mappedName 34 | }) 35 | } 36 | 37 | override fun mapFieldName(owner: String, name: String, descriptor: String): String { 38 | for (superName in superResolver.iterateSuperNames(owner)) { 39 | val ref = FieldRef(superName, descriptor, name) 40 | val mappingValue = mapping.fieldMapping[ref] 41 | if (mappingValue != null && (superName == owner || mappingValue.inheritable)) { 42 | return mappingValue.mapped 43 | } 44 | val refWithoutType = FieldRef(superName, null, name) 45 | val mappedWithoutType = mapping.fieldMapping[refWithoutType] 46 | if (mappedWithoutType != null && (superName == owner || mappedWithoutType.inheritable)) { 47 | return mappedWithoutType.mapped 48 | } 49 | } 50 | 51 | return name 52 | } 53 | 54 | override fun mapMethodName(owner: String, name: String, descriptor: String): String { 55 | for (superName in superResolver.iterateSuperNames(owner)) { 56 | val ref = MethodRef(superName, descriptor, name) 57 | val mappingValue = mapping.methodMapping[ref] 58 | if (mappingValue != null && (superName == owner || mappingValue.inheritable)) { 59 | return mappingValue.mapped 60 | } 61 | } 62 | 63 | return name 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/io/heartpattern/mcremapper/visitor/ParameterAnnotationFixVisitor.kt: -------------------------------------------------------------------------------- 1 | package io.heartpattern.mcremapper.visitor 2 | 3 | import org.objectweb.asm.ClassVisitor 4 | import org.objectweb.asm.Opcodes 5 | import org.objectweb.asm.Type 6 | import org.objectweb.asm.tree.AnnotationNode 7 | import org.objectweb.asm.tree.ClassNode 8 | import org.objectweb.asm.tree.MethodNode 9 | 10 | /** 11 | * ProGuard, the obfuscator used by Minecraft, invalidates information about the number of parameters 12 | * when there are synthetic ones, e.g., in the constructor of an inner class. 13 | * This class visitor fixes them. 14 | * 15 | * Originally written by Pokechu22 for MCP, translated to Kotlin for MC-Remapper by pdinklag. 16 | */ 17 | class ParameterAnnotationFixVisitor(cv: ClassNode) : ClassVisitor(Opcodes.ASM9, cv) { 18 | override fun visitEnd() { 19 | super.visitEnd() 20 | 21 | val cls = (cv as ClassNode) 22 | val syntheticParams = getExpectedSyntheticParams(cls) 23 | if (syntheticParams != null) 24 | for (mn in cls.methods) 25 | if (mn.name == "") 26 | processConstructor(cls, mn, syntheticParams) 27 | } 28 | 29 | /** 30 | * Checks if the given class might have synthetic parameters in the 31 | * constructor. There are two cases where this might happen: 32 | *
    33 | *
  1. If the given class is an inner class, the first parameter is the 34 | * instance of the outer class.
  2. 35 | *
  3. If the given class is an enum, the first parameter is the enum 36 | * constant name and the second parameter is its ordinal.
  4. 37 | *
38 | * 39 | * @return An array of types for synthetic parameters if the class can have 40 | * synthetic parameters, otherwise null. 41 | */ 42 | private fun getExpectedSyntheticParams(cls: ClassNode): Array? { 43 | val info = cls.innerClasses.find { it.name == cls.name } 44 | 45 | return when { 46 | // Check for enum 47 | // http://hg.openjdk.java.net/jdk8/jdk8/langtools/file/1ff9d5118aae/src/share/classes/com/sun/tools/javac/comp/Lower.java#l2866 48 | // considering class for extra parameter annotations as it is an enum 49 | //return new Type[] { Type.getObjectType("java/lang/String"), Type.INT_TYPE }; 50 | cls.access and Opcodes.ACC_ENUM != 0 -> arrayOf(Type.getObjectType("java/lang/String"), Type.INT_TYPE) 51 | 52 | // http://hg.openjdk.java.net/jdk8/jdk8/langtools/file/1ff9d5118aae/src/share/classes/com/sun/tools/javac/code/Symbol.java#l398 53 | // not considering class for extra parameter annotations as it is not an inner class 54 | info == null -> null // It's not an inner class 55 | 56 | // not considering class for extra parameter annotations as it is an interface or static 57 | info.access and (Opcodes.ACC_STATIC or Opcodes.ACC_INTERFACE) != 0 -> null // It's static or can't have a constructor 58 | 59 | // http://hg.openjdk.java.net/jdk8/jdk8/langtools/file/1ff9d5118aae/src/share/classes/com/sun/tools/javac/jvm/ClassReader.java#l2011 60 | // not considering class for extra parameter annotations as it is anonymous 61 | info.innerName == null -> null // It's an anonymous class 62 | 63 | // considering class extra parameter annotations as it is an inner class 64 | // pdinklag: not really sure why the hell this happens, but it does for some com.google classes 65 | info.outerName == null -> null 66 | 67 | else -> arrayOf(Type.getObjectType(info.outerName)) 68 | } 69 | } 70 | 71 | /** 72 | * Removes the parameter annotations for the given synthetic parameters, 73 | * if there are parameter annotations and the synthetic parameters exist. 74 | */ 75 | private fun processConstructor(cls: ClassNode, mn: MethodNode, syntheticParams: Array) { 76 | val methodInfo = mn.name + mn.desc + " in " + cls.name 77 | val params = Type.getArgumentTypes(mn.desc) 78 | 79 | @Suppress("ControlFlowWithEmptyBody") 80 | if (beginsWith(params, syntheticParams)) { 81 | mn.visibleParameterAnnotations = process( 82 | methodInfo, 83 | "RuntimeVisibleParameterAnnotations", 84 | params.size, 85 | syntheticParams.size, 86 | mn.visibleParameterAnnotations 87 | ) 88 | 89 | mn.invisibleParameterAnnotations = process( 90 | methodInfo, 91 | "RuntimeInvisibleParameterAnnotations", 92 | params.size, 93 | syntheticParams.size, 94 | mn.invisibleParameterAnnotations 95 | ) 96 | 97 | // ASM uses this value, not the length of the array 98 | // Note that this was added in ASM 6.1 99 | if (mn.visibleParameterAnnotations != null) { 100 | mn.visibleAnnotableParameterCount = mn.visibleParameterAnnotations.size 101 | } 102 | 103 | if (mn.invisibleParameterAnnotations != null) { 104 | mn.invisibleAnnotableParameterCount = mn.invisibleParameterAnnotations.size 105 | } 106 | } else { 107 | // Unexpected lack of synthetic args to the constructor 108 | } 109 | } 110 | 111 | private fun beginsWith(values: Array, prefix: Array): Boolean { 112 | if (values.size < prefix.size) 113 | return false 114 | 115 | for (i in prefix.indices) 116 | if (values[i] != prefix[i]) 117 | return false 118 | 119 | return true 120 | } 121 | 122 | /** 123 | * Removes annotation nodes corresponding to synthetic parameters, after 124 | * the existence of synthetic parameters has already been checked. 125 | * 126 | * @param methodInfo 127 | * A description of the method, for logging 128 | * @param attributeName 129 | * The name of the attribute, for logging 130 | * @param numParams 131 | * The number of parameters in the method 132 | * @param numSynthetic 133 | * The number of synthetic parameters (should not be 0) 134 | * @param annotations 135 | * The current array of annotation nodes, may be null 136 | * @return The new array of annotation nodes, may be null 137 | */ 138 | private fun process( 139 | methodInfo: String, 140 | attributeName: String, 141 | numParams: Int, 142 | numSynthetic: Int, 143 | annotations: Array>? 144 | ): Array>? { 145 | if (annotations == null) { 146 | // method does not have the specified attribute 147 | return null 148 | } 149 | 150 | val numAnnotations = annotations.size 151 | return when (numParams) { 152 | // found extra attribute entries in method, removing numSynthetic of them 153 | numAnnotations -> annotations.copyOfRange(numSynthetic, numAnnotations) 154 | 155 | // number of attribute entries is already as we want 156 | numAnnotations - numSynthetic -> annotations 157 | 158 | // unexpected number of attribute entries 159 | else -> throw IllegalStateException("unexpected number of attribute entries for $attributeName of $methodInfo") 160 | } 161 | } 162 | } 163 | --------------------------------------------------------------------------------