├── .github └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── main │ ├── java │ │ ├── com │ │ │ └── cleanroommc │ │ │ │ └── bouncepad │ │ │ │ ├── Bouncepad.java │ │ │ │ ├── BouncepadClassLoader.java │ │ │ │ ├── api │ │ │ │ ├── Blackboard.java │ │ │ │ ├── JavaMetadata.java │ │ │ │ ├── asm │ │ │ │ │ ├── cp │ │ │ │ │ │ ├── ConstantPool.java │ │ │ │ │ │ └── ConstantPoolTags.java │ │ │ │ │ └── generator │ │ │ │ │ │ └── ClassGenerator.java │ │ │ │ ├── transformer │ │ │ │ │ ├── FailedClassTransformationException.java │ │ │ │ │ ├── TransformationContext.java │ │ │ │ │ └── Transformer.java │ │ │ │ └── tweaker │ │ │ │ │ ├── Launcher.java │ │ │ │ │ └── Tweaker.java │ │ │ │ ├── debug │ │ │ │ ├── DebugDirectories.java │ │ │ │ └── DebugOption.java │ │ │ │ └── impl │ │ │ │ ├── asm │ │ │ │ └── BumpASMAPITransformer.java │ │ │ │ └── logger │ │ │ │ └── RGBPatternConverter.java │ │ └── net │ │ │ └── minecraft │ │ │ └── launchwrapper │ │ │ ├── IClassNameTransformer.java │ │ │ ├── IClassTransformer.java │ │ │ ├── ITweaker.java │ │ │ ├── Launch.java │ │ │ ├── LaunchClassLoader.java │ │ │ ├── LogWrapper.java │ │ │ └── package-info.java │ └── resources │ │ └── log4j2.xml └── test │ └── java │ └── com │ └── cleanroommc │ └── bouncepad │ └── test │ ├── bumpasmapi │ └── BumpASMAPITest.java │ ├── classloader │ └── CodeSourceLocationTest.java │ └── util │ ├── Benchmarker.java │ ├── TestClassLoader.java │ ├── TestClassVisitor.java │ └── TransformingClassLoader.java └── test ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle └── src └── main └── java └── com └── cleanroommc └── test └── Main.java /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+' 7 | - '[0-9]+.[0-9]+.[0-9]+' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Grant Execute Permission for gradlew 17 | run: chmod +x gradlew 18 | 19 | - name: Setup Java 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '17' 23 | distribution: 'zulu' 24 | cache: gradle 25 | 26 | - name: Publish to Maven 27 | uses: gradle/gradle-build-action@v2 28 | with: 29 | arguments: | 30 | publish 31 | -PCleanroomMaven=${{ vars.CLEANROOM_MAVEN }} 32 | -PCleanroomMavenUsername=${{ secrets.MAVEN_NAME }} 33 | -PCleanroomMavenPassword=${{ secrets.MAVEN_PASSWORD }} 34 | 35 | - name: Upload Artifacts 36 | uses: actions/upload-artifact@v3 37 | with: 38 | path: build/libs/ 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | 5 | *.iml 6 | *.ipr 7 | *.iws 8 | 9 | logs/ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bouncepad 2 | Specialized 1.12.2 primer running on modern Java for [CleanroomLoader](https://github.com/CleanroomMC/Cleanroom) to launch Minecraft with tweakers. 3 | 4 | All Rights Reserved licensing until LegacyLauncher (LaunchWrapper) is removed. 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'maven-publish' 4 | id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.7' 5 | } 6 | 7 | group 'com.cleanroommc' 8 | version '0.10' 9 | archivesBaseName = 'bouncepad' 10 | 11 | java { 12 | toolchain { 13 | vendor = JvmVendorSpec.AZUL 14 | languageVersion.set(JavaLanguageVersion.of(21)) 15 | } 16 | } 17 | 18 | def compilerArgs = [ 19 | '--add-exports=java.base/jdk.internal.access=ALL-UNNAMED', 20 | '--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED', 21 | ] 22 | 23 | repositories { 24 | mavenCentral() 25 | maven { 26 | name 'Minecraft' 27 | url 'https://libraries.minecraft.net/' 28 | } 29 | maven { 30 | name 'CleanroomMC' 31 | url 'https://maven.cleanroommc.com' 32 | } 33 | } 34 | 35 | configurations { 36 | embed 37 | implementation.extendsFrom(embed) 38 | } 39 | 40 | dependencies { 41 | implementation 'zone.rong:imaginebreaker:2.1' 42 | 43 | implementation 'org.apache.logging.log4j:log4j-api:2.22.0' 44 | implementation 'org.apache.logging.log4j:log4j-core:2.22.0' 45 | annotationProcessor 'org.apache.logging.log4j:log4j-core:2.22.0' 46 | 47 | implementation 'org.jline:jline:3.25.1' 48 | implementation 'org.jline:jline-terminal-jansi:3.25.1' 49 | implementation ('net.minecrell:terminalconsoleappender:1.3.0') { 50 | transitive = false 51 | } 52 | 53 | implementation 'org.ow2.asm:asm:9.6' 54 | implementation 'org.ow2.asm:asm-commons:9.6' 55 | implementation 'org.ow2.asm:asm-tree:9.6' 56 | 57 | implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' 58 | 59 | testCompileOnly project(':test') 60 | 61 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 62 | testImplementation 'org.openjdk.jmh:jmh-core:1.36' 63 | testImplementation 'org.openjdk.jol:jol-core:0.17' 64 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 65 | testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.36' 66 | } 67 | 68 | processResources { 69 | from (configurations.embed.collect { it.isDirectory() ? it : zipTree(it) }) { 70 | exclude 'META-INF/**' 71 | } 72 | } 73 | 74 | test { 75 | dependsOn ':test:build' 76 | useJUnitPlatform() 77 | } 78 | 79 | tasks.withType(JavaCompile).configureEach { 80 | options.encoding 'UTF-8' 81 | options.compilerArgs.addAll(compilerArgs) 82 | } 83 | 84 | tasks.register('bounce', JavaExec) { 85 | classpath = sourceSets.main.runtimeClasspath 86 | jvmArgs '-Djava.system.class.loader=com.cleanroommc.bouncepad.BouncepadClassLoader' 87 | systemProperty 'bouncepad.doNotProcessArguments', 'true' 88 | mainClass = 'com.cleanroommc.bouncepad.Bouncepad' 89 | } 90 | 91 | idea.project.settings.compiler.javac.javacAdditionalOptions = compilerArgs.join(' ') 92 | 93 | publishing { 94 | publications { 95 | mavenJava(MavenPublication) { 96 | from components.java 97 | artifactId = 'bouncepad' 98 | } 99 | } 100 | repositories { 101 | maven { 102 | name = 'CleanroomMaven' 103 | url = CleanroomMaven 104 | credentials(PasswordCredentials) 105 | authentication { 106 | basic(BasicAuthentication) 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanroomMC/Bouncepad/faff7a6f281098a4b2789470c9f8061898416c84/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.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' 10 | } 11 | 12 | rootProject.name = 'Bouncepad' 13 | 14 | include 'test' -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/Bouncepad.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad; 2 | 3 | import com.cleanroommc.bouncepad.api.Blackboard; 4 | import com.cleanroommc.bouncepad.debug.DebugOption; 5 | import joptsimple.OptionParser; 6 | import joptsimple.util.PathConverter; 7 | import net.minecraft.launchwrapper.Launch; 8 | import org.apache.logging.log4j.LogManager; 9 | import org.apache.logging.log4j.Logger; 10 | import zone.rong.imaginebreaker.ImagineBreaker; 11 | 12 | import java.nio.file.Path; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | 16 | public final class Bouncepad { 17 | 18 | private static BouncepadClassLoader classLoader; 19 | private static Logger logger; 20 | private static Blackboard globalBlackboard; 21 | private static Path minecraftHome, assetsDirectory; 22 | 23 | public static void main(String[] args) { 24 | if (ClassLoader.getSystemClassLoader().getClass() != BouncepadClassLoader.class) { 25 | throw new RuntimeException("java.system.class.loader property should be pointed towards com.cleanroommc.bouncepad.BouncepadClassLoader."); 26 | } 27 | 28 | classLoader = (BouncepadClassLoader) ClassLoader.getSystemClassLoader(); 29 | classLoader.configureClasspath(); 30 | 31 | initLogger(); 32 | logger.info("Initializing Bouncepad"); 33 | 34 | var imagineBreakerLogger = LogManager.getLogger("ImagineBreaker"); 35 | imagineBreakerLogger.info("Opening Boot Modules"); 36 | ImagineBreaker.openBootModules(); 37 | imagineBreakerLogger.info("Wiping Field Filters"); 38 | ImagineBreaker.wipeFieldFilters(); 39 | imagineBreakerLogger.info("Wiping Method Filters"); 40 | ImagineBreaker.wipeMethodFilters(); 41 | 42 | for (var field : ClassLoader.class.getDeclaredFields()) { 43 | logger.info(field); 44 | } 45 | 46 | initBlackboard(); 47 | logger.info("Initializing Default Blackboard"); 48 | logger.info("Processing Starting Arguments"); 49 | 50 | if (DebugOption.DO_NOT_PROCESS_ARGUMENTS.isOff()) { 51 | processArgs(args); 52 | } 53 | } 54 | 55 | public static BouncepadClassLoader mainClassLoader() { 56 | return classLoader; 57 | } 58 | 59 | public static ClassLoader platformClassLoader() { 60 | return ClassLoader.getPlatformClassLoader(); 61 | } 62 | 63 | public static ClassLoader appClassLoader() { 64 | return classLoader.getParent(); 65 | } 66 | 67 | public static Logger logger() { 68 | return logger; 69 | } 70 | 71 | public static Blackboard globalBlackboard() { 72 | return globalBlackboard; 73 | } 74 | 75 | public static Path minecraftHome() { 76 | return minecraftHome; 77 | } 78 | 79 | public static Path assetsDirectory() { 80 | return assetsDirectory; 81 | } 82 | 83 | public static ProcessHandle process() { 84 | return ProcessHandle.current(); 85 | } 86 | 87 | private static void initLogger() { 88 | logger = LogManager.getLogger("Bouncepad"); 89 | } 90 | 91 | private static void initBlackboard() { 92 | var map = new HashMap(); 93 | Bouncepad.globalBlackboard = new Blackboard(map); 94 | Launch.blackboard = map; 95 | } 96 | 97 | private static void processArgs(String[] args) { 98 | var parser = new OptionParser(); 99 | parser.allowsUnrecognizedOptions(); 100 | 101 | var profileOption = parser.accepts("version", "The version we launched with").withRequiredArg(); 102 | var gameDirOption = parser.accepts("gameDir", "Game Directory").withRequiredArg().withValuesConvertedBy(new PathConverter()); 103 | var assetsDirOption = parser.accepts("assetsDir", "Assets Directory").withRequiredArg().withValuesConvertedBy(new PathConverter()); 104 | var tweakClassOption = parser.accepts("tweakClass", "Tweak class(es) to load").withRequiredArg(); 105 | var nonOption = parser.nonOptions(); 106 | 107 | var options = parser.parse(args); 108 | 109 | minecraftHome = options.valueOf(gameDirOption); 110 | assetsDirectory = options.valueOf(assetsDirOption); 111 | Launch.minecraftHome = minecraftHome.toFile(); 112 | Launch.assetsDir = assetsDirectory.toFile(); 113 | 114 | var profileName = options.valueOf(profileOption); 115 | var tweakClassNames = new ArrayList<>(options.valuesOf(tweakClassOption)); 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/BouncepadClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad; 2 | 3 | import com.cleanroommc.bouncepad.api.transformer.Transformer; 4 | import net.minecraft.launchwrapper.LaunchClassLoader; 5 | import net.minecraft.launchwrapper.IClassTransformer; 6 | 7 | import java.io.File; 8 | import java.net.MalformedURLException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class BouncepadClassLoader extends LaunchClassLoader { 13 | 14 | static { 15 | ClassLoader.registerAsParallelCapable(); 16 | } 17 | 18 | private final List transformers = new ArrayList<>(); 19 | 20 | // Called by VM via -Djava.system.class.loader 21 | @SuppressWarnings("unused") 22 | public BouncepadClassLoader(ClassLoader parent) { 23 | this("BouncepadClassLoader"); 24 | } 25 | 26 | protected BouncepadClassLoader(String name) { 27 | super(name); 28 | } 29 | 30 | public Class getClass(String name) { 31 | return (Class) this.findLoadedClass(name); 32 | } 33 | 34 | public boolean isClassLoaded(String name) { 35 | return this.getClass(name) != null; 36 | } 37 | 38 | @Override 39 | public void registerTransformer(String transformerName) { 40 | throw new UnsupportedOperationException("BouncepadClassLoader only allows registration of transformers through services."); 41 | } 42 | 43 | @Override 44 | public List getTransformers() { 45 | return super.getTransformers(); 46 | } 47 | 48 | @Override 49 | public byte[] getClassBytes(String name) { 50 | return super.getClassBytes(name); 51 | } 52 | 53 | /** 54 | * Required for Java Agents to work on HotSpot 55 | * @param path The file path added to the classpath 56 | */ 57 | @SuppressWarnings("unused") 58 | public void appendToClassPathForInstrumentation(String path) { 59 | try { 60 | this.addURL(new File(path).toURI().toURL()); 61 | } catch (MalformedURLException e) { 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | 66 | /** 67 | *
    68 | *
  1. Default ClassLoader Exclusions:
      69 | *
    • java.
    • 70 | *
    • sun.
    • 71 | *
    • org.lwjgl.
    • 72 | *
    • org.apache.logging.
    • 73 | *
    • net.minecraft.launchwrapper.
    • 74 | *
  2. 75 | *
  3. Bouncepad ClassLoader Exclusions:
      76 | *
    • com.cleanroommc.bouncepad.
    • 77 | *
    • zone.rong.imaginebreaker.
    • 78 | *
  4. 79 | *
  5. Default Transformer Exclusions:
      80 | *
    • javax.
    • 81 | *
    • argo.
    • 82 | *
    • org.objectweb.asm.
    • 83 | *
    • com.google.common.
    • 84 | *
    • org.bouncycastle.
    • 85 | *
    • net.minecraft.launchwrapper.injector.
    • 86 | *
  6. 87 | *
88 | */ 89 | @Override 90 | protected void configureDefaultExclusions() { 91 | this.addClassLoaderExclusion("com.cleanroommc.bouncepad."); 92 | this.addClassLoaderExclusion("zone.rong.imaginebreaker."); 93 | } 94 | 95 | void configureClasspath() { 96 | for (var classpath : System.getProperty("java.class.path").split(File.pathSeparator)) { 97 | try { 98 | this.addURL(new File(classpath).toURI().toURL()); 99 | } catch (MalformedURLException e2) { 100 | Bouncepad.logger().error("Unable to parse {} as an URL to be added to the classpath.", classpath, e2); 101 | } 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/Blackboard.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.function.Consumer; 10 | import java.util.function.Supplier; 11 | 12 | public class Blackboard { 13 | 14 | private final Map backing; 15 | private final Map>> callbacks; 16 | 17 | public Blackboard(Map backing) { 18 | this.backing = backing; 19 | this.callbacks = initializeCallbacks(); 20 | } 21 | 22 | public Blackboard() { 23 | this(new HashMap<>()); 24 | } 25 | 26 | public Object get(String key) { 27 | return this.backing.get(key); 28 | } 29 | 30 | public Object get(String key, Object defaultValue) { 31 | return this.backing.getOrDefault(key, defaultValue); 32 | } 33 | 34 | public Object get(String key, Supplier defaultValue) { 35 | return this.backing.getOrDefault(key, defaultValue.get()); 36 | } 37 | 38 | public T get(String key, Class expected) { 39 | var value = get(key); 40 | if (expected.isInstance(value)) { 41 | return expected.cast(value); 42 | } 43 | throw new IllegalArgumentException("Expected class of type: %s | actual class of type: %s".formatted(expected, value.getClass())); 44 | } 45 | 46 | public T get(String key, Class expected, Supplier defaultValue) { 47 | var value = get(key); 48 | if (value == null) { 49 | return defaultValue.get(); 50 | } 51 | if (expected.isInstance(value)) { 52 | return expected.cast(value); 53 | } 54 | throw new IllegalArgumentException("Expected class of type: %s | actual class of type: %s".formatted(expected, value.getClass())); 55 | } 56 | 57 | public void put(String key, T value) { 58 | if (this.backing.containsKey(key)) { 59 | return; 60 | } 61 | this.backing.put(key, value); 62 | if (this.callbacks != null) { 63 | this.callbacks.get(key).forEach(callback -> callback.accept(value)); 64 | } 65 | } 66 | 67 | public Object override(String key, T value) { 68 | var previous = this.backing.put(key, value); 69 | if (previous == null && this.callbacks != null) { 70 | this.callbacks.get(key).forEach(callback -> callback.accept(value)); 71 | } 72 | return previous; 73 | } 74 | 75 | public Object remove(String key) { 76 | return this.backing.remove(key); 77 | } 78 | 79 | public Object remove(String key, Class expected) { 80 | var value = get(key); 81 | if (value == null) { 82 | return null; 83 | } 84 | if (expected.isInstance(value)) { 85 | return this.backing.remove(key); 86 | } 87 | throw new IllegalArgumentException("Expected class of type: %s | actual class of type: %s".formatted(expected, value.getClass())); 88 | } 89 | 90 | public void callback(String name, Class expected, Consumer callback) { 91 | if (this.callbacks == null) { 92 | throw new UnsupportedOperationException("Callbacks not supported on this Blackboard."); 93 | } 94 | this.callbacks.computeIfAbsent(name, k -> new ArrayList<>()).add(new InstanceOfChecker(expected, callback)); 95 | } 96 | 97 | public Set keys() { 98 | return Set.copyOf(this.backing.keySet()); 99 | } 100 | 101 | public Collection values() { 102 | return List.copyOf(this.backing.values()); 103 | } 104 | 105 | protected Map>> initializeCallbacks() { 106 | return new HashMap<>(); 107 | } 108 | 109 | private static class InstanceOfChecker implements Consumer { 110 | 111 | private final Class expected; 112 | private final Consumer callback; 113 | 114 | private InstanceOfChecker(Class expected, Consumer callback) { 115 | this.expected = expected; 116 | this.callback = callback; 117 | } 118 | 119 | @Override 120 | public void accept(Object value) { 121 | if (this.expected.isInstance(value)) { 122 | this.callback.accept(this.expected.cast(value)); 123 | } 124 | } 125 | 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/JavaMetadata.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api; 2 | 3 | public final class JavaMetadata { 4 | 5 | public static String getVersionString() { 6 | var version = Runtime.version().toString(); 7 | final int plus = version.indexOf('+'); 8 | if (plus != -1) { 9 | version = version.substring(0, plus); 10 | } 11 | return version.replace('_', '.'); 12 | } 13 | 14 | public static int getMajor() { 15 | return Runtime.version().version().get(0); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/asm/cp/ConstantPool.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.asm.cp; 2 | 3 | // TODO: implement reading of different types 4 | // TODO: replacement/redirecting method helpers 5 | public class ConstantPool implements ConstantPoolTags { 6 | 7 | private static final int MAGIC_NUMBER = 0xCAFEBABE; 8 | private static final int CP_ITEM_START = 8; 9 | private static final int CP_START = 10; 10 | 11 | protected final byte[] classData; 12 | protected final int items; 13 | protected final int[] offsets; 14 | 15 | public ConstantPool(byte[] classData) { 16 | if (classData == null || classData.length < 4) { 17 | throw new IllegalArgumentException("classData is null, empty or incomplete!"); 18 | } 19 | if (this.readInt(0) != MAGIC_NUMBER) { 20 | throw new IllegalArgumentException("classData does not represent a valid class file!"); 21 | } 22 | this.classData = classData; 23 | this.items = this.readUnsignedShort(CP_ITEM_START); 24 | this.offsets = new int[this.items]; 25 | this.read(); 26 | } 27 | 28 | private void read() { 29 | int pointer = CP_START; 30 | for (int i = 1; i < this.offsets.length; i++) { 31 | int tag = this.readByte(pointer++); 32 | this.offsets[i] = tag; 33 | switch (tag) { 34 | case CLASS, METHOD_TYPE, MODULE, STRING, PACKAGE -> pointer += 2; 35 | case METHOD_HANDLE -> pointer += 3; 36 | case CONSTANT_DYNAMIC, FIELD_REF, FLOAT, INTEGER, INTERFACE_METHOD_REF, INVOKE_DYNAMIC, METHOD_REF, NAME_AND_TYPE -> pointer += 4; 37 | case LONG, DOUBLE -> { // Longs and Doubles take 2 "slots" 38 | pointer += 8; 39 | i++; 40 | } 41 | case UTF8 -> { 42 | int length = this.readUnsignedShort(pointer); 43 | pointer += (2 + length); 44 | } 45 | default -> throw new IllegalStateException("Bad tag (" + tag + ") @ index (" + i + ") @ position (" + pointer + ")"); 46 | } 47 | } 48 | } 49 | 50 | public int readByte(int pointer) { 51 | return this.classData[pointer] & 0xFF; 52 | } 53 | 54 | public int readUnsignedShort(int pointer) { 55 | return ((this.classData[pointer] & 0xFF) << 8) | (this.classData[pointer + 1] & 0xFF); 56 | } 57 | 58 | public int readSignedShort(int pointer) { 59 | return (short) ((this.classData[pointer] & 0xFF) << 8) | (this.classData[pointer + 1] & 0xFF); 60 | } 61 | 62 | public int readInt(int pointer) { 63 | return ((this.classData[pointer] & 0xFF) << 24) | 64 | ((this.classData[pointer + 1] & 0xFF) << 16) | 65 | ((this.classData[pointer + 2] & 0xFF) << 8) | 66 | (this.classData[pointer + 3] & 0xFF); 67 | } 68 | 69 | public long readLong(int pointer) { 70 | return ((long) this.readInt(pointer) << 32) | (this.readInt(pointer + 4) & 0xFFFFFFFFL); 71 | } 72 | 73 | public float readFloat(int pointer) { 74 | return Float.intBitsToFloat(this.readInt(pointer)); 75 | } 76 | 77 | public double readDouble(int pointer) { 78 | return Double.longBitsToDouble(this.readLong(pointer)); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/asm/cp/ConstantPoolTags.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.asm.cp; 2 | 3 | public interface ConstantPoolTags { 4 | 5 | int UTF8 = 1; 6 | int UNICODE = 2; 7 | int INTEGER = 3; 8 | int FLOAT = 4; 9 | int LONG = 5; 10 | int DOUBLE = 6; 11 | int CLASS = 7; 12 | int STRING = 8; 13 | int FIELD_REF = 9; 14 | int METHOD_REF = 10; 15 | int INTERFACE_METHOD_REF = 11; 16 | int NAME_AND_TYPE = 12; 17 | int METHOD_HANDLE = 15; 18 | int METHOD_TYPE = 16; 19 | int CONSTANT_DYNAMIC = 17; 20 | int INVOKE_DYNAMIC = 18; 21 | int MODULE = 19; 22 | int PACKAGE = 20; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/asm/generator/ClassGenerator.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.asm.generator; 2 | 3 | public interface ClassGenerator { 4 | 5 | boolean accept(String className); 6 | 7 | boolean acceptTransformers(String className); 8 | 9 | byte[] generateClass(int asmApi, String className) throws Throwable; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/transformer/FailedClassTransformationException.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.transformer; 2 | 3 | public class FailedClassTransformationException extends Exception { 4 | 5 | public FailedClassTransformationException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/transformer/TransformationContext.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.transformer; 2 | 3 | import com.cleanroommc.bouncepad.api.asm.cp.ConstantPool; 4 | import org.objectweb.asm.tree.ClassNode; 5 | 6 | import java.util.function.Supplier; 7 | 8 | public record TransformationContext(ConstantPool pool, Supplier nodeGetter) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/transformer/Transformer.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.transformer; 2 | 3 | // TODO: more expressive? 4 | public interface Transformer { 5 | 6 | boolean allow(String className); 7 | 8 | void transform(String className, TransformationContext context); 9 | 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/tweaker/Launcher.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.tweaker; 2 | 3 | import java.util.List; 4 | 5 | @FunctionalInterface 6 | public interface Launcher { 7 | 8 | void launch(List arguments) throws Exception; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/api/tweaker/Tweaker.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.api.tweaker; 2 | 3 | import com.cleanroommc.bouncepad.BouncepadClassLoader; 4 | 5 | import java.nio.file.Path; 6 | import java.util.List; 7 | 8 | public interface Tweaker { 9 | 10 | default void acceptOptions(List arguments, Path gameDirectory, Path assetDirectory) { 11 | 12 | } 13 | 14 | default void acceptClassLoader(BouncepadClassLoader classLoader) { 15 | 16 | } 17 | 18 | default void supplyArguments(List arguments) { 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/debug/DebugDirectories.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.debug; 2 | 3 | import com.cleanroommc.bouncepad.Bouncepad; 4 | 5 | import java.nio.file.Path; 6 | 7 | public enum DebugDirectories { 8 | 9 | BEFORE_ALL_TRANSFORMATIONS("transformations/before_all"), 10 | AFTER_EACH_TRANSFORMATION("transformations/after_each"), 11 | AFTER_ALL_TRANSFORMATIONS("transformations/after_all"); 12 | 13 | private final String directory; 14 | 15 | private Path path; 16 | 17 | DebugDirectories(String directory) { 18 | this.directory = directory; 19 | } 20 | 21 | public Path path() { 22 | this.init(); 23 | return this.path; 24 | } 25 | 26 | public Path path(String... paths) { 27 | this.init(); 28 | var path = this.path; 29 | for (var nextPath : paths) { 30 | path = path.resolve(nextPath); 31 | } 32 | return path; 33 | } 34 | 35 | private void init() { 36 | if (this.path == null) { 37 | this.path = Bouncepad.minecraftHome().resolve(directory); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/debug/DebugOption.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.debug; 2 | 3 | 4 | public enum DebugOption { 5 | 6 | DO_NOT_PROCESS_ARGUMENTS("bouncepad.doNotProcessArguments"), 7 | DO_NOT_TRANSFORM_CLASSES("bouncepad.doNotTransformClasses"), 8 | EXPLICIT_LOGGING("bouncepad.explicitLogging"), 9 | SAVE_CLASS_BEFORE_ALL_TRANSFORMATIONS("bouncepad.saveClassBeforeAllTransformations"), 10 | SAVE_CLASS_AFTER_EACH_TRANSFORMATION("bouncepad.saveClassAfterEachTransformation"), 11 | SAVE_CLASS_AFTER_ALL_TRANSFORMATIONS("bouncepad.saveClassAfterAllTransformations"); 12 | 13 | private final String propertyString; 14 | 15 | DebugOption(String propertyString) { 16 | this.propertyString = propertyString; 17 | } 18 | 19 | public boolean isOn() { 20 | return Boolean.parseBoolean(System.getProperty(this.propertyString)); 21 | } 22 | 23 | public boolean isOff() { 24 | return !isOn(); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return this.propertyString; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/impl/asm/BumpASMAPITransformer.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.impl.asm; 2 | 3 | import net.minecraft.launchwrapper.IClassTransformer; 4 | import org.objectweb.asm.*; 5 | 6 | import static org.objectweb.asm.ClassReader.*; 7 | 8 | public class BumpASMAPITransformer implements IClassTransformer, Opcodes { 9 | 10 | private static final String INTERNAL_TYPE_NAME = BumpASMAPITransformer.class.getName().replace('.', '/'); 11 | 12 | public static class AutoBumpClassVisitor extends ClassVisitor { 13 | 14 | protected AutoBumpClassVisitor(int api) { 15 | super(bumpOpcode(api)); 16 | } 17 | 18 | protected AutoBumpClassVisitor(int api, ClassVisitor classVisitor) { 19 | super(bumpOpcode(api), classVisitor); 20 | } 21 | 22 | } 23 | 24 | public static class AutoBumpFieldVisitor extends FieldVisitor { 25 | 26 | protected AutoBumpFieldVisitor(int api) { 27 | super(bumpOpcode(api)); 28 | } 29 | 30 | protected AutoBumpFieldVisitor(int api, FieldVisitor fieldVisitor) { 31 | super(bumpOpcode(api), fieldVisitor); 32 | } 33 | 34 | } 35 | 36 | public static class AutoBumpMethodVisitor extends MethodVisitor { 37 | 38 | protected AutoBumpMethodVisitor(int api) { 39 | super(bumpOpcode(api)); 40 | } 41 | 42 | protected AutoBumpMethodVisitor(int api, MethodVisitor methodVisitor) { 43 | super(bumpOpcode(api), methodVisitor); 44 | } 45 | 46 | } 47 | 48 | public static int bumpOpcode(int originalOpcode) { 49 | return ASM9; 50 | } 51 | 52 | @Override 53 | public byte[] transform(String name, String transformedName, byte[] bytes) { 54 | if (bytes == null) { 55 | return null; 56 | } 57 | if (bytes.length != 0) { 58 | ClassReader classReader = new ClassReader(bytes); 59 | ClassWriter classWriter = null; 60 | switch (classReader.getSuperName()) { 61 | case "org/objectweb/asm/ClassVisitor","org/objectweb/asm/MethodVisitor", "org/objectweb/asm/FieldVisitor" -> 62 | classReader.accept(new VisitToBump(classReader.getSuperName(), classWriter = new ClassWriter(classReader, 0)), SKIP_DEBUG | SKIP_FRAMES); 63 | } 64 | if (classWriter != null) { 65 | return classWriter.toByteArray(); 66 | } 67 | } 68 | return bytes; 69 | } 70 | 71 | private static class VisitToBump extends ClassVisitor { 72 | 73 | private final String superClass; 74 | private final String newSuperClass; 75 | 76 | private VisitToBump(String superClass, ClassVisitor classVisitor) { 77 | super(ASM9, classVisitor); 78 | this.superClass = superClass; 79 | this.newSuperClass = BumpASMAPITransformer.INTERNAL_TYPE_NAME + "$AutoBump" + superClass.substring(superClass.lastIndexOf('/') + 1); 80 | } 81 | 82 | @Override 83 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 84 | super.visit(version, access, name, signature, this.newSuperClass, interfaces); 85 | } 86 | 87 | @Override 88 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 89 | MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); 90 | return "".equals(name) ? new APIHijacker(this.superClass, this.newSuperClass, methodVisitor) : methodVisitor; 91 | } 92 | 93 | } 94 | 95 | private static class APIHijacker extends MethodVisitor { 96 | 97 | private final String superClass; 98 | private final String newSuperClass; 99 | 100 | private APIHijacker(String superClass, String newSuperClass, MethodVisitor methodVisitor) { 101 | super(ASM9, methodVisitor); 102 | this.superClass = superClass; 103 | this.newSuperClass = newSuperClass; 104 | } 105 | 106 | @Override 107 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 108 | if (opcode == INVOKESPECIAL && this.superClass.equals(owner) && "".equals(name)) { 109 | super.visitMethodInsn(INVOKESPECIAL, this.newSuperClass, name, descriptor, false); 110 | } else { 111 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 112 | } 113 | } 114 | 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/cleanroommc/bouncepad/impl/logger/RGBPatternConverter.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.impl.logger; 2 | 3 | import net.minecrell.terminalconsole.TerminalConsoleAppender; 4 | import org.apache.logging.log4j.core.LogEvent; 5 | import org.apache.logging.log4j.core.config.Configuration; 6 | import org.apache.logging.log4j.core.config.plugins.Plugin; 7 | import org.apache.logging.log4j.core.layout.PatternLayout; 8 | import org.apache.logging.log4j.core.pattern.ConverterKeys; 9 | import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; 10 | import org.apache.logging.log4j.core.pattern.PatternConverter; 11 | import org.apache.logging.log4j.core.pattern.PatternFormatter; 12 | import org.apache.logging.log4j.util.PerformanceSensitive; 13 | import org.apache.logging.log4j.util.PropertiesUtil; 14 | 15 | import java.util.List; 16 | 17 | import static net.minecrell.terminalconsole.MinecraftFormattingConverter.KEEP_FORMATTING_PROPERTY; 18 | import static net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY; 19 | 20 | @Plugin(name = RGBPatternConverter.PLUGIN_NAME, category = PatternConverter.CATEGORY) 21 | @ConverterKeys({ RGBPatternConverter.PLUGIN_NAME }) 22 | @PerformanceSensitive("allocation") 23 | public final class RGBPatternConverter extends LogEventPatternConverter { 24 | 25 | // TODO: Evaluate 26 | static { 27 | System.setProperty(JLINE_OVERRIDE_PROPERTY, "true"); 28 | } 29 | 30 | public static final String PLUGIN_NAME = "rgbFormat"; 31 | 32 | private static final boolean KEEP_FORMATTING = PropertiesUtil.getProperties().getBooleanProperty(KEEP_FORMATTING_PROPERTY); 33 | private static final String ANSI_RESET = "\u001B[m"; 34 | private static final String COLOR_STR = "§"; 35 | private static final String LOOKUP = "0123456789abcdefklmnor"; 36 | private static final String RGB_ANSI = "\u001B[38;2;%d;%d;%dm"; 37 | private static final String[] ANSI_CODES = new String[] { 38 | "\u001B[0;30m", // Black §0 39 | "\u001B[0;34m", // Dark Blue §1 40 | "\u001B[0;32m", // Dark Green §2 41 | "\u001B[0;36m", // Dark Aqua §3 42 | "\u001B[0;31m", // Dark Red §4 43 | "\u001B[0;35m", // Dark Purple §5 44 | "\u001B[0;33m", // Gold §6 45 | "\u001B[0;37m", // Gray §7 46 | "\u001B[0;30;1m", // Dark Gray §8 47 | "\u001B[0;34;1m", // Blue §9 48 | "\u001B[0;32;1m", // Green §a 49 | "\u001B[0;36;1m", // Aqua §b 50 | "\u001B[0;31;1m", // Red §c 51 | "\u001B[0;35;1m", // Light Purple §d 52 | "\u001B[0;33;1m", // Yellow §e 53 | "\u001B[0;37;1m", // White §f 54 | "\u001B[5m", // Obfuscated §k 55 | "\u001B[21m", // Bold §l 56 | "\u001B[9m", // Strikethrough §m 57 | "\u001B[4m", // Underline §n 58 | "\u001B[3m", // Italic §o 59 | ANSI_RESET, // Reset §r 60 | }; 61 | 62 | /** 63 | * Gets a new instance of the {@link RGBPatternConverter} with the 64 | * specified options. 65 | * 66 | * @param config The current configuration 67 | * @param options The pattern options 68 | * @return The new instance 69 | * @see RGBPatternConverter 70 | */ 71 | public static RGBPatternConverter newInstance(Configuration config, String[] options) { 72 | if (options.length < 1 || options.length > 2) { 73 | LOGGER.error("Incorrect number of options on {}. Expected at least 1, max 2 received {}", RGBPatternConverter.PLUGIN_NAME, options.length); 74 | return null; 75 | } 76 | if (options[0] == null) { 77 | LOGGER.error("No pattern supplied on {}", RGBPatternConverter.PLUGIN_NAME); 78 | return null; 79 | } 80 | var parser = PatternLayout.createPatternParser(config); 81 | var formatters = parser.parse(options[0]); 82 | boolean strip = options.length > 1 && "strip".equals(options[1]); 83 | return new RGBPatternConverter(formatters, strip); 84 | } 85 | 86 | private final boolean ansi; 87 | private final PatternFormatter[] formatters; 88 | 89 | /** 90 | * Construct the converter. 91 | * 92 | * @param formatters The pattern formatters to generate the text to manipulate 93 | * @param strip If true, the converter will strip all formatting codes 94 | */ 95 | private RGBPatternConverter(List formatters, boolean strip) { 96 | super("rgbFormat", null); 97 | this.formatters = formatters.toArray(PatternFormatter[]::new); 98 | this.ansi = !strip && TerminalConsoleAppender.isAnsiSupported(); 99 | } 100 | 101 | @Override 102 | public void format(LogEvent event, StringBuilder toAppendTo) { 103 | int start = toAppendTo.length(); 104 | for (var formatter : this.formatters) { 105 | formatter.format(event, toAppendTo); 106 | } 107 | if (KEEP_FORMATTING || toAppendTo.length() == start) { 108 | return; 109 | } 110 | this.format(toAppendTo); 111 | } 112 | 113 | private void format(StringBuilder toAppendTo) { 114 | int search = 0; 115 | int colorCharacter = toAppendTo.indexOf(COLOR_STR, search); 116 | int next = colorCharacter + 1; 117 | boolean reset = false; 118 | while (colorCharacter != -1 && next < toAppendTo.length()) { 119 | char nextChar = toAppendTo.charAt(next); 120 | int code = LOOKUP.indexOf(nextChar); 121 | if (code != -1) { 122 | if (this.ansi) { 123 | if (!reset) { 124 | reset = true; 125 | toAppendTo.replace(colorCharacter, next + 1, ANSI_RESET + ANSI_CODES[code]); 126 | } else { 127 | toAppendTo.replace(colorCharacter, next + 1, ANSI_CODES[code]); 128 | } 129 | } else { 130 | toAppendTo.replace(colorCharacter, next + 1, ""); 131 | } 132 | search++; 133 | } else if (nextChar == 'x') { 134 | int startBracket = toAppendTo.indexOf("[", next); 135 | if (startBracket != -1) { 136 | int endBracket = toAppendTo.indexOf("]", startBracket); 137 | if (endBracket != -1 && (endBracket - startBracket) == 7) { 138 | var hex = toAppendTo.substring(startBracket + 1, endBracket); 139 | int rgb = Integer.parseInt(hex, 16); 140 | if (this.ansi) { 141 | String rgbAnsi = String.format(RGB_ANSI, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); 142 | toAppendTo.replace(colorCharacter, endBracket + 1, rgbAnsi); 143 | search = (colorCharacter + rgbAnsi.length()); 144 | } else { 145 | toAppendTo.replace(colorCharacter, endBracket + 1, ""); 146 | search++; 147 | } 148 | } else { 149 | search++; 150 | } 151 | } else { 152 | search++; 153 | } 154 | } else { 155 | search++; 156 | } 157 | colorCharacter = toAppendTo.indexOf(COLOR_STR, search); 158 | next = colorCharacter + 1; 159 | } 160 | if (reset) { 161 | toAppendTo.append(ANSI_RESET); 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/IClassNameTransformer.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | public interface IClassNameTransformer { 4 | 5 | String unmapClassName(String name); 6 | 7 | String remapClassName(String name); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/IClassTransformer.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | @Deprecated(since = "0.5") 4 | public interface IClassTransformer { 5 | 6 | byte[] transform(String name, String transformedName, byte[] classBytes); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/ITweaker.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | import com.cleanroommc.bouncepad.BouncepadClassLoader; 4 | import com.cleanroommc.bouncepad.api.tweaker.Launcher; 5 | import com.cleanroommc.bouncepad.api.tweaker.Tweaker; 6 | 7 | import java.io.File; 8 | import java.lang.reflect.Method; 9 | import java.nio.file.Path; 10 | import java.util.List; 11 | 12 | @Deprecated(since = "0.5") 13 | public interface ITweaker extends Launcher, Tweaker { 14 | 15 | void acceptOptions(List args, File gameDirectory, File assetDirectory, String profile); 16 | 17 | void injectIntoClassLoader(LaunchClassLoader classLoader); 18 | 19 | String getLaunchTarget(); 20 | 21 | String[] getLaunchArguments(); 22 | 23 | @Override 24 | default void launch(List arguments) throws Exception { 25 | Class clazz = Class.forName(getLaunchTarget(), false, Thread.currentThread().getContextClassLoader()); 26 | Method mainMethod = clazz.getMethod("main", String[].class); 27 | mainMethod.invoke(null, (Object) arguments.toArray(String[]::new)); 28 | } 29 | 30 | @Override 31 | default void acceptOptions(List arguments, Path gameDirectory, Path assetDirectory) { 32 | acceptOptions(arguments, gameDirectory.toFile(), assetDirectory.toFile(), "1.12.2"); 33 | } 34 | 35 | @Override 36 | default void acceptClassLoader(BouncepadClassLoader classLoader) { 37 | injectIntoClassLoader(classLoader); 38 | } 39 | 40 | @Override 41 | default void supplyArguments(List arguments) { 42 | arguments.addAll(List.of(getLaunchArguments())); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/Launch.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | import java.io.File; 4 | import java.util.Map; 5 | 6 | @Deprecated(since = "0.5") 7 | public class Launch { 8 | 9 | public static LaunchClassLoader classLoader; 10 | public static File minecraftHome; 11 | public static File assetsDir; 12 | public static Map blackboard; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/LaunchClassLoader.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | import com.cleanroommc.bouncepad.Bouncepad; 4 | import com.cleanroommc.bouncepad.api.transformer.FailedClassTransformationException; 5 | import com.cleanroommc.bouncepad.debug.DebugOption; 6 | import jdk.internal.access.SharedSecrets; 7 | 8 | import java.io.IOException; 9 | import java.net.JarURLConnection; 10 | import java.net.URL; 11 | import java.net.URLClassLoader; 12 | import java.net.URLConnection; 13 | import java.security.CodeSigner; 14 | import java.security.CodeSource; 15 | import java.util.ArrayList; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Set; 19 | import java.util.jar.Attributes; 20 | import java.util.jar.Manifest; 21 | 22 | public class LaunchClassLoader extends URLClassLoader { 23 | 24 | static { 25 | ClassLoader.registerAsParallelCapable(); 26 | } 27 | 28 | protected final List legacyTransformers = new ArrayList<>(); 29 | 30 | private final ClassLoader parent = getClass().getClassLoader(); 31 | private final Set classLoaderExclusions = new HashSet<>(); 32 | private final Set transformationExclusions = new HashSet<>(); 33 | private final boolean transformClasses = DebugOption.DO_NOT_TRANSFORM_CLASSES.isOff(); 34 | 35 | protected IClassNameTransformer classNameTransformer; 36 | 37 | protected LaunchClassLoader(String name) { 38 | super(name, new URL[0], null); 39 | this.configureDefaultExclusions(); 40 | } 41 | 42 | /** 43 | *
    44 | *
  1. If a class name starts with any classloader exception, delegate to {@link LaunchClassLoader#parent#loadClass(String)}
  2. 45 | *
  3. If a class name starts with any transformer exception, delegate to {@link URLClassLoader#findClass}
  4. 46 | *
  5. transformedName with {@link IClassNameTransformer} if present, and check for an existing loaded class
  6. 47 | *
  7. untransformedName, find the last separator and use to determine the package name and file path of the .class file
  8. 48 | *
  9. Open a URLConnection using findCodeSourceConnectionFor on the determined filename
  10. 49 | *
  11. Check package sealing for the given URLConnection, unless the untransformed name starts with "net.minecraft.", giving a severe warning if the package is already sealed. Otherwise, create and register a new Package.
  12. 50 | *
  13. runTransformers on getClassBytes
  14. 51 | *
  15. Save the debug class if enabled
  16. 52 | *
  17. defineClass with the appropriate CodeSigners
  18. 53 | *
  19. Cache and return the class
  20. 54 | *
  21. Throw a ClassNotFoundException if any exception happens during the transforming process
  22. 55 | *
56 | */ 57 | @Override 58 | public Class findClass(String name) throws ClassNotFoundException { 59 | // TODO: profile against a trie 60 | for (var classLoaderExclusion : this.classLoaderExclusions) { 61 | if (name.startsWith(classLoaderExclusion)) { 62 | return this.parent.loadClass(name); 63 | } 64 | } 65 | boolean transformClass = this.transformClasses; 66 | for (var transformerExclusion : this.transformationExclusions) { 67 | if (name.startsWith(transformerExclusion)) { 68 | transformClass = false; 69 | break; 70 | } 71 | } 72 | try { 73 | var clazz = this.findLoadedClass(name); 74 | if (clazz == null) { 75 | var transformedName = this.remapClassName(name); 76 | clazz = this.findLoadedClass(transformedName); 77 | if (clazz == null) { 78 | var untransformedName = transformClass ? this.unmapClassName(name) : name; 79 | int lastDot = untransformedName.lastIndexOf('.'); 80 | var packageName = (lastDot == -1) ? "" : untransformedName.substring(0, lastDot); 81 | var classPath = untransformedName.replace('.', '/') + ".class"; 82 | var connection = findConnectionFor(classPath); 83 | CodeSource codeSource; 84 | byte[] classBytes = null; 85 | if (!packageName.isEmpty()) { 86 | if (!untransformedName.startsWith("net.minecraft.") && connection instanceof JarURLConnection jarConnection) { 87 | var packageUrl = jarConnection.getJarFileURL(); 88 | CodeSigner[] codeSigners = null; 89 | try { 90 | this.getAndVerifyPackage(packageName, jarConnection.getManifest(), packageUrl); 91 | classBytes = this.getClassBytes(untransformedName); 92 | codeSigners = jarConnection.getJarEntry().getCodeSigners(); 93 | } catch (IOException ignore) { } 94 | // LaunchClassLoader: uses nested jar!file URL instead of the jar URL 95 | // However the jar URL is used when the transformer exclusions is applied. 96 | var classSourceUrl = transformClass ? jarConnection.getURL() : jarConnection.getJarFileURL(); 97 | codeSource = new CodeSource(classSourceUrl, codeSigners); 98 | } else { 99 | this.getAndVerifyPackage(packageName, null, null); 100 | codeSource = connection == null ? null : new CodeSource(connection.getURL(), (CodeSigner[]) null); 101 | } 102 | } else { 103 | var url = connection == null ? null : connection.getURL(); 104 | codeSource = url == null ? null : new CodeSource(url, (CodeSigner[]) null); 105 | } 106 | if (classBytes == null) { 107 | try { 108 | classBytes = this.getClassBytes(untransformedName); 109 | } catch (IOException ignore) { } 110 | } 111 | if (transformClass) { 112 | try { 113 | classBytes = this.runTransformers(untransformedName, transformedName, classBytes); 114 | } catch (Throwable t) { 115 | throw new ClassNotFoundException("Exception caught while transforming class " + name, t); 116 | } 117 | } 118 | if (classBytes == null) { 119 | throw new ClassNotFoundException("Class bytes are null for " + name); 120 | } 121 | return this.defineClass(transformedName, classBytes, 0, classBytes.length, codeSource); 122 | } 123 | } 124 | return clazz; 125 | } catch (Throwable e) { 126 | throw new ClassNotFoundException("Failed to find class " + name, e); 127 | } 128 | } 129 | 130 | /** 131 | * Registers a legacy transformer, it is advised to use {@link com.cleanroommc.bouncepad.BouncepadClassLoader} 132 | * and its new transformers via the service method rather than this method. 133 | * This is kept here for backwards compatibility. 134 | * 135 | * @param transformerName the binary name of the transformer class (a.b.c) 136 | */ 137 | @Deprecated 138 | public void registerTransformer(String transformerName) { 139 | Bouncepad.logger().fatal("LaunchClassLoader's registerTransformer is deprecated, please refrain from registering transformers this way."); 140 | try { 141 | Class transformerClass = Class.forName(transformerName, true, this); 142 | if (!IClassTransformer.class.isAssignableFrom(transformerClass)) { 143 | Bouncepad.logger().fatal("Attempted to register legacy-style transformer {} that isn't of IClassTransformer type", transformerClass); 144 | return; 145 | } 146 | IClassTransformer transformer = (IClassTransformer) transformerClass.getConstructor().newInstance(); 147 | this.legacyTransformers.add(transformer); 148 | Bouncepad.logger().warn("Legacy-style transformer [{}] registered.", transformerName); 149 | if (this.classNameTransformer == null && transformer instanceof IClassNameTransformer) { 150 | this.classNameTransformer = (IClassNameTransformer) transformer; 151 | } 152 | } catch (Exception e) { 153 | Bouncepad.logger().error("Legacy-style transformer [{}] registration failed.", transformerName, e); 154 | } 155 | } 156 | 157 | // Keep binary compatibility 158 | @Deprecated 159 | public void clearNegativeEntries(Set entries) { 160 | Bouncepad.logger().fatal("LaunchClassLoader's clearNegativeEntries is deprecated, please refrain from calling this method."); 161 | } 162 | 163 | /** 164 | *
    165 | *
  1. Default ClassLoader Exclusions:
      166 | *
    • java.
    • 167 | *
    • sun.
    • 168 | *
    • org.lwjgl.
    • 169 | *
    • org.apache.logging.
    • 170 | *
    • net.minecraft.launchwrapper.
    • 171 | *
  2. 172 | *
  3. Default Transformer Exclusions:
      173 | *
    • javax.
    • 174 | *
    • argo.
    • 175 | *
    • org.objectweb.asm.
    • 176 | *
    • com.google.common.
    • 177 | *
    • org.bouncycastle.
    • 178 | *
    • net.minecraft.launchwrapper.injector.
    • 179 | *
  4. 180 | *
181 | */ 182 | protected void configureDefaultExclusions() { 183 | this.addClassLoaderExclusion("java."); 184 | this.addClassLoaderExclusion("sun."); 185 | this.addClassLoaderExclusion("org.lwjgl."); 186 | this.addClassLoaderExclusion("org.apache.logging."); 187 | this.addClassLoaderExclusion("net.minecraft.launchwrapper."); 188 | 189 | this.addTransformerExclusion("javax."); 190 | this.addTransformerExclusion("argo."); 191 | this.addTransformerExclusion("org.objectweb.asm."); 192 | this.addTransformerExclusion("com.google.common."); 193 | this.addTransformerExclusion("org.bouncycastle."); 194 | } 195 | 196 | protected byte[] runTransformers(String untransformedName, String transformedName, byte[] classBytes) throws FailedClassTransformationException { 197 | byte[] transformedBytes = classBytes; 198 | IClassTransformer currentTransformer = null; 199 | try { 200 | List transformers = this.legacyTransformers; 201 | for (int i = 0; i < transformers.size(); i++) { 202 | currentTransformer = transformers.get(i); 203 | transformedBytes = currentTransformer.transform(untransformedName, transformedName, transformedBytes); 204 | } 205 | } catch (Throwable t) { 206 | // TODO: give transformation stack context in exception 207 | throw new FailedClassTransformationException("Failed to transform class " + transformedName + " after transformer " + currentTransformer.getClass(), t); 208 | } 209 | 210 | return transformedBytes; 211 | } 212 | 213 | public List getTransformers() { 214 | return this.legacyTransformers; 215 | } 216 | 217 | public void addClassLoaderExclusion(String exclude) { 218 | this.classLoaderExclusions.add(exclude); 219 | } 220 | 221 | public void addTransformerExclusion(String exclude) { 222 | this.transformationExclusions.add(exclude); 223 | } 224 | 225 | /** 226 | * Lifted visibility of {@link URLClassLoader#addURL(URL)} 227 | */ 228 | @Override 229 | public void addURL(URL url) { 230 | super.addURL(url); 231 | } 232 | 233 | public List getSources() { 234 | return new ArrayList<>(List.of(this.getURLs())); 235 | } 236 | 237 | public Class getLoadedClass(String name) { 238 | return this.findLoadedClass(name); 239 | } 240 | 241 | public String remapClassName(String name) { 242 | return this.classNameTransformer == null ? name : this.classNameTransformer.remapClassName(name); 243 | } 244 | 245 | public String unmapClassName(String name) { 246 | return this.classNameTransformer == null ? name : this.classNameTransformer.unmapClassName(name); 247 | } 248 | 249 | public URLConnection findConnectionFor(final String name) { 250 | try { 251 | final URL url = this.findResource(name); 252 | if (url == null) { 253 | return null; 254 | } 255 | return url.openConnection(); 256 | } catch (Exception e) { 257 | // De-escalated from old LCL, this used to throw a RuntimeException wrapping an IOException 258 | Bouncepad.logger().warn("Couldn't find CodeSource connection for {}: {}", name, e.getMessage()); 259 | return null; 260 | } 261 | } 262 | 263 | /** 264 | * @see URLClassLoader#getAndVerifyPackage(String, Manifest, URL) 265 | */ 266 | public Package getAndVerifyPackage(String packageName, Manifest manifest, URL codeSourceURL) { 267 | Package pkg = this.getDefinedPackage(packageName); 268 | if (pkg == null) { 269 | pkg = this.parent.getDefinedPackage(packageName); 270 | } 271 | if (pkg != null) { 272 | if (pkg.isSealed()) { 273 | if (!pkg.isSealed(codeSourceURL)) { 274 | throw new SecurityException("Sealing violation: package " + packageName + " is sealed."); 275 | } 276 | } else if (manifest != null && isSealed(packageName, manifest)) { 277 | throw new SecurityException("Sealing violation: package " + packageName + " already loaded."); 278 | } 279 | } else { 280 | return this.definePackage(packageName, manifest != null ? manifest : new Manifest(), codeSourceURL); 281 | } 282 | return pkg; 283 | } 284 | 285 | /** 286 | * @see URLClassLoader#isSealed(String, Manifest) 287 | */ 288 | public boolean isSealed(String packageName, Manifest manifest) { 289 | var path = packageName.replace('.', '/').concat("/"); 290 | var attr = SharedSecrets.javaUtilJarAccess().getTrustedAttributes(manifest, path); 291 | String sealed = null; 292 | if (attr != null) { 293 | sealed = attr.getValue(Attributes.Name.SEALED); 294 | } 295 | if (sealed == null) { 296 | if ((attr = manifest.getMainAttributes()) != null) { 297 | sealed = attr.getValue(Attributes.Name.SEALED); 298 | } 299 | } 300 | return Boolean.parseBoolean(sealed); 301 | } 302 | 303 | public byte[] getClassBytes(String name) throws IOException { 304 | var classPath = name.replace('.', '/') + ".class"; 305 | var resourceUrl = findResource(classPath); 306 | var connection = resourceUrl == null ? null : resourceUrl.openConnection(); 307 | if (connection == null) { 308 | ClassLoader platform = Bouncepad.platformClassLoader(); 309 | if (platform != null) { 310 | final URL platformUrl = platform.getResource(classPath); 311 | if (platformUrl != null) { 312 | connection = platformUrl.openConnection(); 313 | } 314 | } else { 315 | final URL parentUrl = this.getResource(classPath); 316 | if (parentUrl != null) { 317 | connection = parentUrl.openConnection(); 318 | } 319 | } 320 | } 321 | if (connection == null) { 322 | return null; 323 | } 324 | byte[] contents; 325 | try (var is = connection.getInputStream()) { 326 | contents = is.readAllBytes(); 327 | } 328 | return contents; 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/LogWrapper.java: -------------------------------------------------------------------------------- 1 | package net.minecraft.launchwrapper; 2 | 3 | import com.cleanroommc.bouncepad.Bouncepad; 4 | import org.apache.logging.log4j.Level; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | 8 | @Deprecated(since = "0.5") 9 | public class LogWrapper { 10 | 11 | public static LogWrapper log = new LogWrapper(); 12 | 13 | private Logger myLog; 14 | 15 | private static void configureLogging() { 16 | if (log.myLog == null) { 17 | log.myLog = Bouncepad.logger(); 18 | } 19 | } 20 | 21 | public static void retarget(Logger to) { 22 | if (to != null) { 23 | log.myLog = to; 24 | } else { 25 | log.myLog = Bouncepad.logger(); 26 | } 27 | } 28 | 29 | public static void log(String logChannel, Level level, String format, Object... data) { 30 | Logger logger = makeAndGetLog(logChannel); 31 | if (format.contains("{}")) { 32 | logger.log(level, format, data); 33 | } else { 34 | logger.log(level, String.format(format, data)); 35 | } 36 | } 37 | 38 | public static void log(Level level, String format, Object... data) { 39 | configureLogging(); 40 | if (format.contains("{}")) { 41 | log.myLog.log(level, format, data); 42 | } else { 43 | log.myLog.log(level, String.format(format, data)); 44 | } 45 | } 46 | 47 | public static void log(String logChannel, Level level, Throwable ex, String format, Object... data) { 48 | Logger logger = makeAndGetLog(logChannel); 49 | if (format.contains("{}")) { 50 | logger.log(level, format, data, ex); 51 | } else { 52 | logger.log(level, String.format(format, data), ex); 53 | } 54 | } 55 | 56 | public static void log(Level level, Throwable ex, String format, Object... data) { 57 | configureLogging(); 58 | if (format.contains("{}")) { 59 | log.myLog.log(level, format, data); 60 | } else { 61 | log.myLog.log(level, String.format(format, data)); 62 | } 63 | } 64 | 65 | public static void severe(String format, Object... data) { 66 | log(Level.ERROR, format, data); 67 | } 68 | 69 | public static void warning(String format, Object... data) { 70 | log(Level.WARN, format, data); 71 | } 72 | 73 | public static void info(String format, Object... data) { 74 | log(Level.INFO, format, data); 75 | } 76 | 77 | public static void fine(String format, Object... data) { 78 | log(Level.DEBUG, format, data); 79 | } 80 | 81 | public static void finer(String format, Object... data) { 82 | log(Level.TRACE, format, data); 83 | } 84 | 85 | public static void finest(String format, Object... data) { 86 | log(Level.TRACE, format, data); 87 | } 88 | 89 | public static void makeLog(String logChannel) { 90 | LogManager.getLogger(logChannel); 91 | } 92 | 93 | private static Logger makeAndGetLog(String logChannel) { 94 | return LogManager.getLogger(logChannel); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/net/minecraft/launchwrapper/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Officially deprecated as-of Bouncepad 0.5. 3 | * Seek alternatives in the `com.cleanroommc.bouncepad.api` classes. 4 | */ 5 | @Deprecated(since = "0.5", forRemoval = true) 6 | package net.minecraft.launchwrapper; -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/bumpasmapi/BumpASMAPITest.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.bumpasmapi; 2 | 3 | import com.cleanroommc.bouncepad.impl.asm.BumpASMAPITransformer; 4 | import com.cleanroommc.bouncepad.test.util.TestClassVisitor; 5 | import com.cleanroommc.bouncepad.test.util.TransformingClassLoader; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.objectweb.asm.ClassVisitor; 10 | import org.objectweb.asm.Opcodes; 11 | 12 | public class BumpASMAPITest { 13 | 14 | private TransformingClassLoader transformatingClassLoader; 15 | 16 | @BeforeEach 17 | public void beforeEach() { 18 | this.transformatingClassLoader = new TransformingClassLoader(new BumpASMAPITransformer()); 19 | this.transformatingClassLoader.addClassForTransform("com.cleanroommc.bouncepad.test.util.TestClassVisitor"); 20 | } 21 | 22 | @Test 23 | public void test() throws ReflectiveOperationException { 24 | var clazz = this.transformatingClassLoader.findClass("com.cleanroommc.bouncepad.test.util.TestClassVisitor"); 25 | Assertions.assertNotNull(clazz); 26 | 27 | var firstCtor = clazz.getDeclaredConstructor(int.class); 28 | Assertions.assertNotNull(firstCtor); 29 | var object = (TestClassVisitor) firstCtor.newInstance(Opcodes.ASM5); 30 | Assertions.assertNotNull(object); 31 | Assertions.assertEquals(object.getApi(), Opcodes.ASM9); 32 | 33 | var secondCtor = clazz.getDeclaredConstructor(int.class, ClassVisitor.class); 34 | Assertions.assertNotNull(secondCtor); 35 | object = (TestClassVisitor) secondCtor.newInstance(Opcodes.ASM5, null); 36 | Assertions.assertEquals(object.getApi(), Opcodes.ASM9); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/classloader/CodeSourceLocationTest.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.classloader; 2 | 3 | import com.cleanroommc.bouncepad.test.util.TestClassLoader; 4 | import com.cleanroommc.test.Main; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.IOException; 9 | import java.net.URISyntaxException; 10 | 11 | public class CodeSourceLocationTest { 12 | 13 | @Test 14 | public void test() throws IOException, URISyntaxException { 15 | try (var testLoader = new TestClassLoader()) { 16 | testLoader.appendTestJar(); 17 | 18 | Main.main(new String[0]); 19 | Assertions.assertNotNull(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/util/Benchmarker.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.util; 2 | 3 | import org.openjdk.jmh.annotations.Mode; 4 | import org.openjdk.jmh.runner.Runner; 5 | import org.openjdk.jmh.runner.RunnerException; 6 | import org.openjdk.jmh.runner.options.OptionsBuilder; 7 | import org.openjdk.jmh.runner.options.TimeValue; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.function.Consumer; 11 | 12 | public class Benchmarker { 13 | 14 | public static void run(Class clazz) { 15 | new Benchmarker(clazz, t -> { }).run(); 16 | } 17 | 18 | public static void run(Class clazz, Consumer options) { 19 | new Benchmarker(clazz, options).run(); 20 | } 21 | 22 | private final OptionsBuilder optionsBuilder; 23 | 24 | private Benchmarker(Class clazz, Consumer options) { 25 | this.optionsBuilder = (OptionsBuilder) new OptionsBuilder() 26 | .include(clazz.getName() + ".*") 27 | .mode(Mode.AverageTime) 28 | .timeUnit(TimeUnit.MICROSECONDS) 29 | .warmupTime(TimeValue.seconds(1)) 30 | .warmupIterations(5) 31 | .measurementTime(TimeValue.seconds(1)) 32 | .measurementIterations(5) 33 | .threads(2) 34 | .forks(1) 35 | .shouldFailOnError(true) 36 | .shouldDoGC(true); 37 | options.accept(this.optionsBuilder); 38 | } 39 | 40 | public void run() { 41 | try { 42 | new Runner(this.optionsBuilder).run(); 43 | } catch (RunnerException e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/util/TestClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.util; 2 | 3 | import com.cleanroommc.bouncepad.BouncepadClassLoader; 4 | 5 | import java.io.File; 6 | import java.net.MalformedURLException; 7 | 8 | public class TestClassLoader extends BouncepadClassLoader { 9 | 10 | public TestClassLoader() { 11 | super(TestClassLoader.class.getClassLoader()); 12 | } 13 | 14 | public void appendTestJar() { 15 | try { 16 | this.addURL(new File(".", "test/build/libs/test-1.0.jar").toURI().toURL()); 17 | } catch (MalformedURLException e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/util/TestClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.util; 2 | 3 | import org.objectweb.asm.ClassVisitor; 4 | 5 | public class TestClassVisitor extends ClassVisitor { 6 | 7 | public TestClassVisitor(int api) { 8 | super(api); 9 | } 10 | 11 | public TestClassVisitor(int api, ClassVisitor classVisitor) { 12 | super(api, classVisitor); 13 | } 14 | 15 | public int getApi() { 16 | return api; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/cleanroommc/bouncepad/test/util/TransformingClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.bouncepad.test.util; 2 | 3 | import net.minecraft.launchwrapper.IClassTransformer; 4 | import org.junit.platform.commons.util.ClassLoaderUtils; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.lang.invoke.MethodHandles; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | /** 14 | * Used for testing transformations in a more sandboxed manner, it is/(can be made) a lot more controlled here 15 | */ 16 | public class TransformingClassLoader extends ClassLoader { 17 | 18 | private final IClassTransformer transformer; 19 | private final ClassLoader parent; 20 | private final Set transformFor = new HashSet<>(); 21 | 22 | public TransformingClassLoader(IClassTransformer transformer) { 23 | this.transformer = transformer; 24 | this.parent = ClassLoaderUtils.getDefaultClassLoader(); 25 | } 26 | 27 | public void addClassForTransform(String className) { 28 | this.transformFor.add(className); 29 | } 30 | 31 | @Override 32 | public Class findClass(String name) throws ClassNotFoundException { 33 | if (this.transformFor.contains(name)) { 34 | try (InputStream is = this.parent.getResourceAsStream(name.replace('.', '/') + ".class")) { 35 | var buffer = new ByteArrayOutputStream(); 36 | int read; 37 | byte[] data = new byte[4]; 38 | while ((read = is.read(data, 0, data.length)) != -1) { 39 | buffer.write(data, 0, read); 40 | } 41 | buffer.flush(); 42 | byte[] classBytes = buffer.toByteArray(); 43 | classBytes = this.transformer.transform(name, name, classBytes); 44 | // TODO: evaluate whether class that requested for this CL will always be the right one 45 | return MethodHandles.lookup().defineClass(classBytes); 46 | } catch (IOException | IllegalAccessException e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | return super.findClass(name); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /test/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.7' 4 | } 5 | 6 | group 'com.cleanroommc' 7 | version '1.0' 8 | archivesBaseName = 'test' 9 | 10 | java { 11 | toolchain { 12 | languageVersion.set(JavaLanguageVersion.of(21)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanroomMC/Bouncepad/faff7a6f281098a4b2789470c9f8061898416c84/test/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /test/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /test/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' 10 | } 11 | 12 | rootProject.name = 'test' 13 | -------------------------------------------------------------------------------- /test/src/main/java/com/cleanroommc/test/Main.java: -------------------------------------------------------------------------------- 1 | package com.cleanroommc.test; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | System.out.println("Hello World"); 7 | } 8 | 9 | } 10 | --------------------------------------------------------------------------------