From f8867eae3a34adaf0f56d673c9d1dd6dc0d24c5e Mon Sep 17 00:00:00 2001 From: Lassebq Date: Fri, 23 Aug 2024 14:42:55 +0300 Subject: [PATCH] Fabric support #9. New asset index handler --- .gitignore | 8 +- build.gradle | 117 ++-- launchwrapper-fabric/build.gradle | 25 + .../launchwrapper/fabric/FabricBridge.java | 48 ++ .../launchwrapper/inject/EntrypointPatch.java | 640 ++++++++++++++++++ .../launchwrapper/inject/LWGameProvider.java | 274 ++++++++ .../inject/LWGameTransformer.java | 104 +++ .../inject/LWJGLVersionLookup.java | 113 ++++ .../launchwrapper/inject/LWLib.java | 43 ++ .../tweak/FabricLoaderTweak.java | 71 ++ ...net.fabricmc.loader.impl.game.GameProvider | 1 + settings.gradle | 1 + .../org/mcphackers/launchwrapper/Launch.java | 36 +- .../launchwrapper/LaunchConfig.java | 1 + .../launchwrapper/inject/Inject.java | 10 +- .../loader/ClassLoaderURLHandler.java | 8 +- .../loader/LaunchClassLoader.java | 63 +- .../launchwrapper/loader/SafeClassWriter.java | 5 +- .../launchwrapper/protocol/AssetRequests.java | 7 +- .../protocol/AssetURLConnection.java | 60 +- .../protocol/LegacyURLStreamHandler.java | 9 +- .../protocol/ListLevelsURLConnection.java | 7 +- .../protocol/LoadLevelURLConnection.java | 7 +- .../protocol/SaveLevelURLConnection.java | 11 +- .../launchwrapper/protocol/SkinRequests.java | 7 +- .../tweak/FabricLoaderTweak.java | 39 -- .../launchwrapper/tweak/LegacyTweak.java | 10 - .../mcphackers/launchwrapper/tweak/Tweak.java | 50 +- .../launchwrapper/util/ClassNodeProvider.java | 9 + .../launchwrapper/util/ClassNodeSource.java | 4 +- src/main/resources/icon_256x256.png | Bin 0 -> 74168 bytes src/main/resources/icon_48x48.png | Bin 0 -> 5203 bytes .../launchwrapper/test/TweakTest.java | 2 +- 33 files changed, 1549 insertions(+), 241 deletions(-) create mode 100644 launchwrapper-fabric/build.gradle create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/fabric/FabricBridge.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/EntrypointPatch.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameProvider.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameTransformer.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWJGLVersionLookup.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWLib.java create mode 100644 launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java create mode 100644 launchwrapper-fabric/src/main/resources/META-INF/services/net.fabricmc.loader.impl.game.GameProvider create mode 100644 settings.gradle delete mode 100644 src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java create mode 100644 src/main/java/org/mcphackers/launchwrapper/util/ClassNodeProvider.java create mode 100644 src/main/resources/icon_256x256.png create mode 100644 src/main/resources/icon_48x48.png diff --git a/.gitignore b/.gitignore index a628d9b..8fc57dc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ nbproject .*.sw[a-p] # various other potential build files -/build/ -/out +build/ +out # Mac filesystem dust .DS_Store @@ -28,5 +28,5 @@ nbproject .idea/ # generated -/repo/ -/bin/ +repo/ +bin/ diff --git a/build.gradle b/build.gradle index b8ee221..532a185 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,76 @@ -plugins { - id 'java' - id 'maven-publish' -} +allprojects { + apply plugin: 'java' + apply plugin: 'maven-publish' + + version = '1.0-SNAPSHOT' + + repositories { + flatDir { + dirs "libs" + } + maven { + url "https://mcphackers.github.io/libraries/" + } + maven { + url "https://libraries.minecraft.net/" + } + mavenCentral() + } -repositories { - flatDir { - dirs "libs" + task sourcesJar(type: Jar) { + archiveClassifier = 'sources' + from sourceSets.main.allSource } - maven { - url "https://mcphackers.github.io/libraries/" + + artifacts { + archives jar + archives sourcesJar } - maven { - url "https://libraries.minecraft.net/" + + publishing { + publications { + mavenJava(MavenPublication) { + artifactId = archivesBaseName + + artifact jar + artifact sourcesJar + } + } + + repositories { + mavenLocal() + + def ENV = System.getenv() + if (ENV.MAVEN_URL) { + maven { + url ENV.MAVEN_URL + if (ENV.MAVEN_USERNAME) { + credentials { + username ENV.MAVEN_USERNAME + password ENV.MAVEN_PASSWORD + } + } + } + } + } } - mavenCentral() + } group = 'org.mcphackers' archivesBaseName = 'launchwrapper' -version = '1.0-SNAPSHOT' -sourceCompatibility = 1.5 -targetCompatibility = 1.5 +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +project.ext.asm_version = 9.6 dependencies { implementation 'org.mcphackers.rdi:rdi:1.0' - implementation 'org.ow2.asm:asm:9.3' - implementation 'org.ow2.asm:asm-tree:9.3' + implementation "org.ow2.asm:asm:${project.asm_version}" + implementation "org.ow2.asm:asm-tree:${project.asm_version}" implementation 'org.json:json:20230311' // I'll bring discord RPC support later, when I have an environment to compile natives - // testImplementation 'junit:junit:4.12' testRuntimeOnly('org.junit.platform:junit-platform-launcher:1.5.2') testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.0.0' } @@ -41,42 +82,4 @@ test { events = ["passed", "failed", "skipped"] showStandardStreams = true } -} - -task sourcesJar(type: Jar) { - archiveClassifier = 'sources' - from sourceSets.main.allSource -} - -artifacts { - archives jar - archives sourcesJar -} - -publishing { - publications { - mavenJava(MavenPublication) { - artifactId = archivesBaseName - - artifact jar - artifact sourcesJar - } - } - - repositories { - mavenLocal() - - def ENV = System.getenv() - if (ENV.MAVEN_URL) { - maven { - url ENV.MAVEN_URL - if (ENV.MAVEN_USERNAME) { - credentials { - username ENV.MAVEN_USERNAME - password ENV.MAVEN_PASSWORD - } - } - } - } - } -} +} \ No newline at end of file diff --git a/launchwrapper-fabric/build.gradle b/launchwrapper-fabric/build.gradle new file mode 100644 index 0000000..1829ac3 --- /dev/null +++ b/launchwrapper-fabric/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'java' +apply plugin: 'maven-publish' + +repositories { + maven { + url "https://maven.fabricmc.net/" + } + maven { + url "https://maven.glass-launcher.net/babric/" + } + mavenCentral() +} + +archivesBaseName = 'launchwrapper-fabric' +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + implementation rootProject + implementation "babric:fabric-loader:0.14.24-babric.1" + implementation 'org.mcphackers.rdi:rdi:1.0' + implementation "org.ow2.asm:asm:${project.asm_version}" + implementation "org.ow2.asm:asm-tree:${project.asm_version}" + implementation "org.ow2.asm:asm-util:${project.asm_version}" +} \ No newline at end of file diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/fabric/FabricBridge.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/fabric/FabricBridge.java new file mode 100644 index 0000000..dd59f8a --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/fabric/FabricBridge.java @@ -0,0 +1,48 @@ +package org.mcphackers.launchwrapper.fabric; + +import java.security.CodeSource; + +import org.mcphackers.launchwrapper.Launch; +import org.mcphackers.launchwrapper.LaunchConfig; + +public class FabricBridge extends Launch { + private static final String FABRIC_KNOT_CLIENT = "net/fabricmc/loader/impl/launch/knot/KnotClient"; + private static final String FABRIC_KNOT = "net/fabricmc/loader/impl/launch/knot/Knot"; + + private static FabricBridge INSTANCE; + + protected FabricBridge(LaunchConfig config) { + super(config); + INSTANCE = this; + } + + private static String gameProdiverSource() { + // Location of META-INF/services/net.fabricmc.loader.impl.game.GameProvider + CodeSource resource = FabricBridge.class.getProtectionDomain().getCodeSource(); + if(resource == null) { + return null; + } + System.out.println("[LaunchWrapper] Fabric compat jar: " + resource.getLocation().getPath()); + return resource.getLocation().getPath(); + } + + public static void main(String[] args) { + LaunchConfig config = new LaunchConfig(args); + create(config).launch(); + } + + public void launch() { + CLASS_LOADER.overrideClassSource(FABRIC_KNOT, gameProdiverSource()); + CLASS_LOADER.overrideClassSource(FABRIC_KNOT_CLIENT, gameProdiverSource()); + CLASS_LOADER.invokeMain(FABRIC_KNOT_CLIENT, config.getArgs()); + } + + public static FabricBridge getInstance() { + return INSTANCE; + } + + public static FabricBridge create(LaunchConfig config) { + return new FabricBridge(config); + } + +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/EntrypointPatch.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/EntrypointPatch.java new file mode 100644 index 0000000..9949c0e --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/EntrypointPatch.java @@ -0,0 +1,640 @@ +/* +* Copyright 2016 FabricMC +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mcphackers.launchwrapper.inject; + +import java.util.List; +import java.util.ListIterator; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.fabricmc.loader.impl.game.minecraft.Hooks; +import net.fabricmc.loader.impl.game.patch.GamePatch; +import net.fabricmc.loader.impl.launch.FabricLauncher; +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; +import net.fabricmc.loader.impl.util.version.VersionParser; +import net.fabricmc.loader.impl.util.version.VersionPredicateParser; + +public class EntrypointPatch extends GamePatch { + private static final VersionPredicate VERSION_1_19_4 = createVersionPredicate(">=1.19.4-"); + + private final LWGameProvider gameProvider; + + public EntrypointPatch(LWGameProvider gameProvider) { + this.gameProvider = gameProvider; + } + + private void finishEntrypoint(EnvType type, ListIterator it) { + String methodName = String.format("start%s", type == EnvType.CLIENT ? "Client" : "Server"); + it.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Hooks.INTERNAL_NAME, methodName, + "(Ljava/io/File;Ljava/lang/Object;)V", false)); + } + + @Override + public void process(FabricLauncher launcher, Function classSource, + Consumer classEmitter) { + EnvType type = launcher.getEnvironmentType(); + String entrypoint = launcher.getEntrypoint(); + Version gameVersion = getGameVersion(); + + if (!entrypoint.startsWith("net.minecraft.") && !entrypoint.startsWith("com.mojang.")) { + return; + } + + String gameEntrypoint = null; + boolean serverHasFile = true; + boolean isApplet = entrypoint.contains("Applet"); + ClassNode mainClass = readClass(classSource.apply(entrypoint)); + + if (mainClass == null) { + throw new RuntimeException("Could not load main class " + entrypoint + "!"); + } + + // Main -> Game entrypoint search + // + // -- CLIENT -- + // pre-1.6 (seems to hold to 0.0.11!): find the only non-static + // non-java-packaged Object field + // 1.6.1+: [client].start() [INVOKEVIRTUAL] + // 19w04a: [client]. [INVOKESPECIAL] -> Thread.start() + // -- SERVER -- + // (1.5-1.7?)-: Just find it instantiating itself. + // (1.6-1.8?)+: an starting with java.io.File can be assumed to be + // definite + // (20w20b-20w21a): Now has its own main class, that constructs the server + // class. Find a specific regex string in the class. + // (20w22a)+: Datapacks are now reloaded in main. To ensure that mods load + // correctly, inject into Main after --safeMode check. + + boolean is20w22aServerOrHigher = false; + + if (type == EnvType.CLIENT) { + // pre-1.6 route + List newGameFields = findFields(mainClass, + (f) -> !isStatic(f.access) && f.desc.startsWith("L") && !f.desc.startsWith("Ljava/")); + + if (newGameFields.size() == 1) { + gameEntrypoint = Type.getType(newGameFields.get(0).desc).getClassName(); + } + } + + if (gameEntrypoint == null) { + // main method searches + MethodNode mainMethod = findMethod(mainClass, (method) -> method.name.equals("main") + && method.desc.equals("([Ljava/lang/String;)V") && isPublicStatic(method.access)); + + if (mainMethod == null) { + throw new RuntimeException("Could not find main method in " + entrypoint + "!"); + } + + if (type == EnvType.CLIENT && mainMethod.instructions.size() < 10) { + // 22w24+ forwards to another method in the same class instead of processing in + // main() directly, use that other method instead if that's the case + MethodInsnNode invocation = null; + + for (AbstractInsnNode insn : mainMethod.instructions) { + MethodInsnNode methodInsn; + + if (invocation == null + && insn.getType() == AbstractInsnNode.METHOD_INSN + && (methodInsn = (MethodInsnNode) insn).owner.equals(mainClass.name)) { + // capture first method insn to the same class + invocation = methodInsn; + } else if (insn.getOpcode() > Opcodes.ALOAD // ignore constant and variable loads as well as NOP, + // labels and line numbers + && insn.getOpcode() != Opcodes.RETURN) { // and RETURN + // found unexpected insn for a simple forwarding method + invocation = null; + break; + } + } + + if (invocation != null) { // simple forwarder confirmed, use its target for further processing + final MethodInsnNode reqMethod = invocation; + mainMethod = findMethod(mainClass, + m -> m.name.equals(reqMethod.name) && m.desc.equals(reqMethod.desc)); + } + } else if (type == EnvType.SERVER) { + // pre-1.6 method search route + MethodInsnNode newGameInsn = (MethodInsnNode) findInsn(mainMethod, + (insn) -> insn.getOpcode() == Opcodes.INVOKESPECIAL + && ((MethodInsnNode) insn).name.equals("") + && ((MethodInsnNode) insn).owner.equals(mainClass.name), + false); + + if (newGameInsn != null) { + gameEntrypoint = newGameInsn.owner.replace('/', '.'); + serverHasFile = newGameInsn.desc.startsWith("(Ljava/io/File;"); + } + } + + if (gameEntrypoint == null) { + // modern method search routes + MethodInsnNode newGameInsn = (MethodInsnNode) findInsn(mainMethod, + type == EnvType.CLIENT + ? (insn) -> (insn.getOpcode() == Opcodes.INVOKESPECIAL + || insn.getOpcode() == Opcodes.INVOKEVIRTUAL) + && !((MethodInsnNode) insn).owner.startsWith("java/") + : (insn) -> insn.getOpcode() == Opcodes.INVOKESPECIAL + && ((MethodInsnNode) insn).name.equals("") + && hasSuperClass(((MethodInsnNode) insn).owner, mainClass.name, classSource), + true); + + // New 20w20b way of finding the server constructor + if (newGameInsn == null && type == EnvType.SERVER) { + newGameInsn = (MethodInsnNode) findInsn(mainMethod, + insn -> (insn instanceof MethodInsnNode) && insn.getOpcode() == Opcodes.INVOKESPECIAL + && hasStrInMethod(((MethodInsnNode) insn).owner, "", "()V", + "^[a-fA-F0-9]{40}$", classSource), + false); + } + + // Detect 20w22a by searching for a specific log message + if (type == EnvType.SERVER && hasStrInMethod(mainClass.name, mainMethod.name, mainMethod.desc, + "Safe mode active, only vanilla datapack will be loaded", classSource)) { + is20w22aServerOrHigher = true; + gameEntrypoint = mainClass.name; + } + + if (newGameInsn != null) { + gameEntrypoint = newGameInsn.owner.replace('/', '.'); + serverHasFile = newGameInsn.desc.startsWith("(Ljava/io/File;"); + } + } + } + + if (gameEntrypoint == null) { + throw new RuntimeException("Could not find game constructor in " + entrypoint + "!"); + } + + Log.debug(LogCategory.GAME_PATCH, "Found game constructor: %s -> %s", entrypoint, gameEntrypoint); + ClassNode gameClass; + + if (gameEntrypoint.equals(entrypoint) || is20w22aServerOrHigher) { + gameClass = mainClass; + } else { + gameClass = readClass(classSource.apply(gameEntrypoint)); + if (gameClass == null) + throw new RuntimeException("Could not load game class " + gameEntrypoint + "!"); + } + + MethodNode gameMethod = null; + MethodNode gameConstructor = null; + AbstractInsnNode lwjglLogNode = null; + AbstractInsnNode currentThreadNode = null; + int gameMethodQuality = 0; + + if (!is20w22aServerOrHigher) { + for (MethodNode gmCandidate : gameClass.methods) { + if (gmCandidate.name.equals("")) { + gameConstructor = gmCandidate; + + if (gameMethodQuality < 1) { + gameMethod = gmCandidate; + gameMethodQuality = 1; + } + } + + if (type == EnvType.CLIENT && !isApplet && gameMethodQuality < 2) { + // Try to find a method with an LDC string "LWJGL Version: ". + // This is the "init()" method, or as of 19w38a is the constructor, or called + // somewhere in that vicinity, + // and is by far superior in hooking into for a well-off mod start. + // Also try and find a Thread.currentThread() call before the LWJGL version + // print. + + int qual = 2; + boolean hasLwjglLog = false; + + for (AbstractInsnNode insn : gmCandidate.instructions) { + if (insn.getOpcode() == Opcodes.INVOKESTATIC && insn instanceof MethodInsnNode) { + final MethodInsnNode methodInsn = (MethodInsnNode) insn; + + if ("currentThread".equals(methodInsn.name) && "java/lang/Thread".equals(methodInsn.owner) + && "()Ljava/lang/Thread;".equals(methodInsn.desc)) { + currentThreadNode = methodInsn; + } + } else if (insn instanceof LdcInsnNode) { + Object cst = ((LdcInsnNode) insn).cst; + + if (cst instanceof String) { + String s = (String) cst; + + // This log output was renamed to Backend library in 19w34a + if (s.startsWith("LWJGL Version: ") || s.startsWith("Backend library: ")) { + hasLwjglLog = true; + + if ("LWJGL Version: ".equals(s) || "LWJGL Version: {}".equals(s) + || "Backend library: {}".equals(s)) { + qual = 3; + lwjglLogNode = insn; + } + + break; + } + } + } + } + + if (hasLwjglLog) { + gameMethod = gmCandidate; + gameMethodQuality = qual; + } + } + } + } else { + gameMethod = findMethod(mainClass, (method) -> method.name.equals("main") + && method.desc.equals("([Ljava/lang/String;)V") && isPublicStatic(method.access)); + } + + if (gameMethod == null) { + throw new RuntimeException("Could not find game constructor method in " + gameClass.name + "!"); + } + + boolean patched = false; + Log.debug(LogCategory.GAME_PATCH, "Patching game constructor %s%s", gameMethod.name, gameMethod.desc); + + if (type == EnvType.SERVER) { + ListIterator it = gameMethod.instructions.iterator(); + + if (!is20w22aServerOrHigher) { + // Server-side: first argument (or null!) is runDirectory, run at end of init + moveBefore(it, Opcodes.RETURN); + + // runDirectory + if (serverHasFile) { + it.add(new VarInsnNode(Opcodes.ALOAD, 1)); + } else { + it.add(new InsnNode(Opcodes.ACONST_NULL)); + } + + it.add(new VarInsnNode(Opcodes.ALOAD, 0)); + + finishEntrypoint(type, it); + patched = true; + } else { + // Server-side: Run before `server.properties` is loaded so early logic like + // world generation is not broken due to being loaded by server properties + // before mods are initialized. + // ---------------- + // ldc "server.properties" + // iconst_0 + // anewarray java/lang/String + // invokestatic java/nio/file/Paths.get + // (Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path; + // ---------------- + Log.debug(LogCategory.GAME_PATCH, "20w22a+ detected, patching main method..."); + + // Find the "server.properties". + LdcInsnNode serverPropertiesLdc = (LdcInsnNode) findInsn(gameMethod, + insn -> insn instanceof LdcInsnNode && ((LdcInsnNode) insn).cst.equals("server.properties"), + false); + + // Move before the `server.properties` ldc is pushed onto stack + moveBefore(it, serverPropertiesLdc); + + // Detect if we are running exactly 20w22a. + // Find the synthetic method where dedicated server instance is created so we + // can set the game instance. + // This cannot be the main method, must be static (all methods are static, so + // useless to check) + // Cannot return a void or boolean + // Is only method that returns a class instance + // If we do not find this, then we are certain this is 20w22a. + MethodNode serverStartMethod = findMethod(mainClass, method -> { + if ((method.access & Opcodes.ACC_SYNTHETIC) == 0 // reject non-synthetic + || method.name.equals("main") && method.desc.equals("([Ljava/lang/String;)V")) { // reject + // main + // method + // (theoretically + // superfluous + // now) + return false; + } + + final Type methodReturnType = Type.getReturnType(method.desc); + + return methodReturnType.getSort() != Type.BOOLEAN && methodReturnType.getSort() != Type.VOID + && methodReturnType.getSort() == Type.OBJECT; + }); + + if (serverStartMethod == null) { + // We are running 20w22a, this requires a separate process for capturing game + // instance + Log.debug(LogCategory.GAME_PATCH, "Detected 20w22a"); + } else { + Log.debug(LogCategory.GAME_PATCH, "Detected version above 20w22a"); + // We are not running 20w22a. + // This means we need to position ourselves before any dynamic registries are + // initialized. + // Since it is a bit hard to figure out if we are on most 1.16-pre1+ versions. + // So if the version is below 1.16.2-pre2, this injection will be before the + // timer thread hack. This should have no adverse effects. + + // This diagram shows the intended result for 1.16.2-pre2 + // ---------------- + // invokestatic ... Bootstrap log missing + // <---- target here (1.16-pre1 to 1.16.2-pre1) + // ...misc + // invokestatic ... (Timer Thread Hack) + // <---- target here (1.16.2-pre2+) + // ... misc + // invokestatic ... (Registry Manager) [Only present in 1.16.2-pre2+] + // ldc "server.properties" + // ---------------- + + // The invokestatic insn we want is just before the ldc + AbstractInsnNode previous = serverPropertiesLdc.getPrevious(); + + while (true) { + if (previous == null) { + throw new RuntimeException("Failed to find static method before loading server properties"); + } + + if (previous.getOpcode() == Opcodes.INVOKESTATIC) { + break; + } + + previous = previous.getPrevious(); + } + + boolean foundNode = false; + + // Move the iterator back till we are just before the insn node we wanted + while (it.hasPrevious()) { + if (it.previous() == previous) { + if (it.hasPrevious()) { + foundNode = true; + // Move just before the method insn node + it.previous(); + } + + break; + } + } + + if (!foundNode) { + throw new RuntimeException("Failed to find static method before loading server properties"); + } + } + + it.add(new InsnNode(Opcodes.ACONST_NULL)); + + // Pass null for now, we will set the game instance when the dedicated server is + // created. + it.add(new InsnNode(Opcodes.ACONST_NULL)); + + finishEntrypoint(type, it); // Inject the hook entrypoint. + + // Time to find the dedicated server ctor to capture game instance + if (serverStartMethod == null) { + // FIXME: For 20w22a, find the only constructor in the game method that takes a + // DataFixer. + // That is the guaranteed to be dedicated server constructor + Log.debug(LogCategory.GAME_PATCH, "Server game instance has not be implemented yet for 20w22a"); + } else { + final ListIterator serverStartIt = serverStartMethod.instructions.iterator(); + + // 1.16-pre1+ Find the only constructor which takes a Thread as it's first + // parameter + MethodInsnNode dedicatedServerConstructor = (MethodInsnNode) findInsn(serverStartMethod, insn -> { + if (insn instanceof MethodInsnNode && ((MethodInsnNode) insn).name.equals("")) { + Type constructorType = Type.getMethodType(((MethodInsnNode) insn).desc); + + if (constructorType.getArgumentTypes().length <= 0) { + return false; + } + + return constructorType.getArgumentTypes()[0].getDescriptor().equals("Ljava/lang/Thread;"); + } + + return false; + }, false); + + if (dedicatedServerConstructor == null) { + throw new RuntimeException("Could not find dedicated server constructor"); + } + + // Jump after the call + moveAfter(serverStartIt, dedicatedServerConstructor); + + // Duplicate dedicated server instance for loader + serverStartIt.add(new InsnNode(Opcodes.DUP)); + serverStartIt.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Hooks.INTERNAL_NAME, "setGameInstance", + "(Ljava/lang/Object;)V", false)); + } + + patched = true; + } + } else if (type == EnvType.CLIENT && isApplet) { + // Applet-side: field is private static File, run at end + // At the beginning, set file field (hook) + FieldNode runDirectory = findField(gameClass, (f) -> isStatic(f.access) && f.desc.equals("Ljava/io/File;")); + + if (runDirectory == null) { + // TODO: Handle pre-indev versions. + // + // Classic has no agreed-upon run directory. + // - level.dat is always stored in CWD. We can assume CWD is set, launchers + // generally adhere to that. + // - options.txt in newer Classic versions is stored in user.home/.minecraft/. + // This is not currently handled, + // but as these versions are relatively low on options this is not a huge + // concern. + Log.warn(LogCategory.GAME_PATCH, + "Could not find applet run directory! (If you're running pre-late-indev versions, this is fine.)"); + + ListIterator it = gameMethod.instructions.iterator(); + + if (gameConstructor == gameMethod) { + moveBefore(it, Opcodes.RETURN); + } + + /* + * it.add(new TypeInsnNode(Opcodes.NEW, "java/io/File")); + * it.add(new InsnNode(Opcodes.DUP)); + * it.add(new LdcInsnNode(".")); + * it.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/io/File", "", + * "(Ljava/lang/String;)V", false)); + */ + it.add(new InsnNode(Opcodes.ACONST_NULL)); + it.add(new MethodInsnNode(Opcodes.INVOKESTATIC, + "net/fabricmc/loader/impl/game/minecraft/applet/AppletMain", "hookGameDir", + "(Ljava/io/File;)Ljava/io/File;", false)); + it.add(new VarInsnNode(Opcodes.ALOAD, 0)); + finishEntrypoint(type, it); + } else { + // Indev and above. + ListIterator it = gameConstructor.instructions.iterator(); + moveAfter(it, Opcodes.INVOKESPECIAL); /* Object.init */ + it.add(new FieldInsnNode(Opcodes.GETSTATIC, gameClass.name, runDirectory.name, runDirectory.desc)); + it.add(new MethodInsnNode(Opcodes.INVOKESTATIC, + "net/fabricmc/loader/impl/game/minecraft/applet/AppletMain", "hookGameDir", + "(Ljava/io/File;)Ljava/io/File;", false)); + it.add(new FieldInsnNode(Opcodes.PUTSTATIC, gameClass.name, runDirectory.name, runDirectory.desc)); + + it = gameMethod.instructions.iterator(); + + if (gameConstructor == gameMethod) { + moveBefore(it, Opcodes.RETURN); + } + + it.add(new FieldInsnNode(Opcodes.GETSTATIC, gameClass.name, runDirectory.name, runDirectory.desc)); + it.add(new VarInsnNode(Opcodes.ALOAD, 0)); + finishEntrypoint(type, it); + } + + patched = true; + } else { + // Client-side: + // - if constructor, identify runDirectory field + location, run immediately + // after + // - if non-constructor (init method), head + + if (gameConstructor == null) { + throw new RuntimeException("Non-applet client-side, but could not find constructor?"); + } + + ListIterator consIt = gameConstructor.instructions.iterator(); + + while (consIt.hasNext()) { + AbstractInsnNode insn = consIt.next(); + if (insn.getOpcode() == Opcodes.PUTFIELD + && ((FieldInsnNode) insn).desc.equals("Ljava/io/File;")) { + Log.debug(LogCategory.GAME_PATCH, "Run directory field is thought to be %s/%s", + ((FieldInsnNode) insn).owner, ((FieldInsnNode) insn).name); + + ListIterator it; + + if (gameMethod == gameConstructor) { + it = consIt; + } else { + it = gameMethod.instructions.iterator(); + } + + // Add the hook just before the Thread.currentThread() call for 1.19.4 or later + // If older 4 method insn's before the lwjgl log + if (currentThreadNode != null && VERSION_1_19_4.test(gameVersion)) { + moveBefore(it, currentThreadNode); + } else if (lwjglLogNode != null) { + moveBefore(it, lwjglLogNode); + + for (int i = 0; i < 4; i++) { + moveBeforeType(it, AbstractInsnNode.METHOD_INSN); + } + } + + it.add(new VarInsnNode(Opcodes.ALOAD, 0)); + it.add(new FieldInsnNode(Opcodes.GETFIELD, ((FieldInsnNode) insn).owner, + ((FieldInsnNode) insn).name, ((FieldInsnNode) insn).desc)); + it.add(new VarInsnNode(Opcodes.ALOAD, 0)); + finishEntrypoint(type, it); + + patched = true; + break; + } + } + } + + if (!patched) { + throw new RuntimeException("Game constructor patch not applied!"); + } + + if (gameClass != mainClass) { + classEmitter.accept(gameClass); + } else { + classEmitter.accept(mainClass); + } + + if (isApplet) { + Hooks.appletMainClass = entrypoint; + } + } + + private boolean hasSuperClass(String cls, String superCls, Function classSource) { + if (cls.contains("$") || (!cls.startsWith("net/minecraft") && cls.contains("/"))) { + return false; + } + + ClassReader reader = classSource.apply(cls); + + return reader != null && reader.getSuperName().equals(superCls); + } + + private boolean hasStrInMethod(String cls, String methodName, String methodDesc, String str, + Function classSource) { + if (cls.contains("$") || (!cls.startsWith("net/minecraft") && cls.contains("/"))) { + return false; + } + + ClassNode node = readClass(classSource.apply(cls)); + if (node == null) + return false; + + for (MethodNode method : node.methods) { + if (method.name.equals(methodName) && method.desc.equals(methodDesc)) { + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof LdcInsnNode) { + Object cst = ((LdcInsnNode) insn).cst; + + if (cst instanceof String) { + if (cst.equals(str)) { + return true; + } + } + } + } + + break; + } + } + + return false; + } + + private Version getGameVersion() { + try { + return VersionParser.parseSemantic(gameProvider.getNormalizedGameVersion()); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + } + + private static VersionPredicate createVersionPredicate(String predicate) { + try { + return VersionPredicateParser.parse(predicate); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameProvider.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameProvider.java new file mode 100644 index 0000000..57b7bd1 --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameProvider.java @@ -0,0 +1,274 @@ +package org.mcphackers.launchwrapper.inject; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.mcphackers.launchwrapper.Launch; +import org.mcphackers.launchwrapper.LaunchConfig; +import org.mcphackers.launchwrapper.MainLaunchTarget; +import org.mcphackers.launchwrapper.fabric.FabricBridge; +import org.mcphackers.launchwrapper.tweak.FabricLoaderTweak; +import org.mcphackers.launchwrapper.tweak.Tweak; +import org.mcphackers.launchwrapper.util.ClassNodeSource; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.ObjectShare; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.impl.FabricLoaderImpl; +import net.fabricmc.loader.impl.FormattedException; +import net.fabricmc.loader.impl.game.GameProvider; +import net.fabricmc.loader.impl.game.GameProviderHelper; +import net.fabricmc.loader.impl.game.LibClassifier; +import net.fabricmc.loader.impl.game.minecraft.McVersion; +import net.fabricmc.loader.impl.game.minecraft.McVersionLookup; +import net.fabricmc.loader.impl.game.patch.GameTransformer; +import net.fabricmc.loader.impl.launch.FabricLauncher; +import net.fabricmc.loader.impl.metadata.BuiltinModMetadata; +import net.fabricmc.loader.impl.metadata.ContactInformationImpl; +import net.fabricmc.loader.impl.metadata.ModDependencyImpl; +import net.fabricmc.loader.impl.util.Arguments; +import net.fabricmc.loader.impl.util.ExceptionUtil; + +public class LWGameProvider implements GameProvider { + + public LaunchConfig config = FabricBridge.getInstance().config; + public MainLaunchTarget target = null; + + private Path gameJar; + private List lwjglJars = new ArrayList<>(); + private Path launchwrapperJar; + private EnvType envType; + private final List miscGameLibraries = new ArrayList<>(); + private final List validParentClassPath = new ArrayList<>(); + private McVersion versionData; + private boolean hasModLoader; + private final GameTransformer transformer = new LWGameTransformer(this); + + public Tweak getTweak(ClassNodeSource source) { + return new FabricLoaderTweak(Tweak.get(source, config), config); + } + + @Override + public void launch(ClassLoader loader) { + String targetClass = target.targetClass.replace("/", "."); + String[] arguments = target.args; + + MethodHandle invoker; + + try { + Class c = loader.loadClass(targetClass); + invoker = MethodHandles.lookup().findStatic(c, "main", MethodType.methodType(void.class, String[].class)); + } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) { + throw FormattedException.ofLocalized("exception.minecraft.invokeFailure", e); + } + + try { + invoker.invokeExact(arguments); + } catch (Throwable t) { + throw FormattedException.ofLocalized("exception.minecraft.generic", t); + } + } + + @Override + public boolean locateGame(FabricLauncher launcher, String[] args) { + this.envType = launcher.getEnvironmentType(); + + try { + LibClassifier classifier = new LibClassifier<>(LWLib.class, envType, this); + LWLib envGameLib = envType == EnvType.CLIENT ? LWLib.MC_CLIENT : LWLib.MC_SERVER; + Path envGameJar = GameProviderHelper.getEnvGameJar(envType); + if (envGameJar != null) { + classifier.process(envGameJar); + } + + classifier.process(launcher.getClassPath()); + + envGameJar = classifier.getOrigin(envGameLib); + if (envGameJar == null) return false; + + gameJar = envGameJar; + launchwrapperJar = classifier.getOrigin(LWLib.LAUNCHWRAPPER); + if(classifier.has(LWLib.LWJGL)) { + lwjglJars.add(classifier.getOrigin(LWLib.LWJGL)); + } + if(classifier.has(LWLib.LWJGL3)) { + lwjglJars.add(classifier.getOrigin(LWLib.LWJGL3)); + } + hasModLoader = classifier.has(LWLib.MODLOADER); + miscGameLibraries.addAll(lwjglJars); + miscGameLibraries.addAll(classifier.getUnmatchedOrigins()); + if(launchwrapperJar != null) { // Java 8 in dev env doesn't detect LW for some reason + validParentClassPath.add(launchwrapperJar); + } + validParentClassPath.addAll(classifier.getSystemLibraries()); + } catch (IOException e) { + throw ExceptionUtil.wrap(e); + } + + ObjectShare share = FabricLoaderImpl.INSTANCE.getObjectShare(); + share.put("fabric-loader:inputGameJar", gameJar); // deprecated + share.put("fabric-loader:inputGameJars", Collections.singleton(gameJar)); + + versionData = McVersionLookup.getVersion(Collections.singletonList(gameJar), "net.minecraft.client.Minecraft", config.version.get()); + return true; + } + + @Override + public String getEntrypoint() { + return target.targetClass.replace("/", "."); + } + + @Override + public Path getLaunchDirectory() { + return FabricBridge.getInstance().config.gameDir.get().toPath(); + } + + @Override + public GameTransformer getEntrypointTransformer() { + return transformer; + } + + @Override + public String getGameId() { + return "minecraft"; + } + + @Override + public String getGameName() { + return "Minecraft"; + } + + @Override + public String getRawGameVersion() { + return versionData.getRaw(); + } + + @Override + public String getNormalizedGameVersion() { + return versionData.getNormalized(); + } + + @Override + public Collection getBuiltinMods() { + List mods = new ArrayList(); + BuiltinModMetadata.Builder metadata = new BuiltinModMetadata.Builder(getGameId(), getNormalizedGameVersion()) + .setName(getGameName()); + + Map contactInfo = new HashMap(); + contactInfo.put("homepage", "https://github.com/MCPHackers/LaunchWrapper"); + contactInfo.put("sources", "https://github.com/MCPHackers/LaunchWrapper"); + contactInfo.put("issues", "https://github.com/MCPHackers/LaunchWrapper/issues"); + BuiltinModMetadata.Builder metadataLW = new BuiltinModMetadata.Builder("launchwrapper", Launch.VERSION) + .setName("LaunchWrapper") + .setEnvironment(ModEnvironment.CLIENT) + .setDescription("Launch wrapper for legacy Minecraft") + .addAuthor("lassebq", Collections.emptyMap()) + .addIcon(0, "icon_256x256.png") + .addIcon(1, "icon_48x48.png") + .addIcon(2, "icon_32x32.png") + .addIcon(3, "icon_16x16.png") + .addLicense("MIT") + .setContact(new ContactInformationImpl(contactInfo)); + + BuiltinModMetadata.Builder metadataLWJGL = new BuiltinModMetadata.Builder("lwjgl", LWJGLVersionLookup.getVersion(lwjglJars)) + .setName("LWJGL") + .setDescription("Lightweight Java Game Library"); + + if (versionData.getClassVersion().isPresent()) { + int version = versionData.getClassVersion().getAsInt() - 44; + + try { + metadataLW.addDependency(new ModDependencyImpl(ModDependency.Kind.DEPENDS, "minecraft", Collections.emptyList())); + metadata.addDependency(new ModDependencyImpl(ModDependency.Kind.DEPENDS, "java", Collections.singletonList(String.format(Locale.ENGLISH, ">=%d", version)))); + metadata.addDependency(new ModDependencyImpl(ModDependency.Kind.DEPENDS, "lwjgl", Collections.emptyList())); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + } + + mods.add(new BuiltinMod(Collections.singletonList(launchwrapperJar), metadataLW.build())); + mods.add(new BuiltinMod(lwjglJars, metadataLWJGL.build())); + mods.add(new BuiltinMod(Collections.singletonList(gameJar), metadata.build())); + return mods; + } + + public Path getGameJar() { + return gameJar; + } + + @Override + public boolean isObfuscated() { + return true; + } + + @Override + public boolean requiresUrlClassLoader() { + return hasModLoader; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean hasAwtSupport() { + return true; + } + + @Override + public void initialize(FabricLauncher launcher) { + launcher.setValidParentClassPath(validParentClassPath); + + if (isObfuscated()) { + Map obfJars = new HashMap<>(1); + String clientSide = envType.name().toLowerCase(Locale.ENGLISH); + obfJars.put(clientSide, gameJar); + + obfJars = GameProviderHelper.deobfuscate(obfJars, + getGameId(), getNormalizedGameVersion(), + getLaunchDirectory(), + launcher); + gameJar = obfJars.get(clientSide); + } + + transformer.locateEntrypoints(launcher, Collections.singletonList(gameJar)); + } + + @Override + public void unlockClassPath(FabricLauncher launcher) { + for (Path lib : miscGameLibraries) { + launcher.addToClassPath(lib); + } + launcher.addToClassPath(gameJar); + } + + @Override + public Arguments getArguments() { + Arguments args = new Arguments(); + args.parse(config.getArgs()); + return args; + } + + @Override + public String[] getLaunchArguments(boolean sanitize) { + return config.getArgs(); + } + + @Override + public boolean canOpenErrorGui() { + return envType == EnvType.CLIENT; + } +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameTransformer.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameTransformer.java new file mode 100644 index 0000000..2963680 --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWGameTransformer.java @@ -0,0 +1,104 @@ +package org.mcphackers.launchwrapper.inject; + +import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.zip.ZipError; + +import org.mcphackers.launchwrapper.MainLaunchTarget; +import org.mcphackers.launchwrapper.tweak.Tweak; +import org.mcphackers.launchwrapper.util.ClassNodeSource; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import net.fabricmc.loader.impl.game.patch.GameTransformer; +import net.fabricmc.loader.impl.launch.FabricLauncher; +import net.fabricmc.loader.impl.util.ExceptionUtil; +import net.fabricmc.loader.impl.util.LoaderUtil; +import net.fabricmc.loader.impl.util.SimpleClassPath; +import net.fabricmc.loader.impl.util.SimpleClassPath.CpEntry; +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; + +public class LWGameTransformer extends GameTransformer implements ClassNodeSource { + private LWGameProvider gameProvider; + private Map modified; + private Function classSource; + private boolean entrypointsLocated = false; + + public LWGameTransformer(LWGameProvider gameProvider) { + this.gameProvider = gameProvider; + } + + public void locateEntrypoints(FabricLauncher launcher, List gameJars) { + if (entrypointsLocated) { + return; + } + + modified = new HashMap<>(); + + try (SimpleClassPath cp = new SimpleClassPath(gameJars)) { + classSource = name -> { + ClassNode node = modified.get(name); + + if (node != null) { + return node; + } + + try { + CpEntry entry = cp.getEntry(LoaderUtil.getClassFileName(name)); + if (entry == null) return null; + + try (InputStream is = entry.getInputStream()) { + node = new ClassNode(); + ClassReader reader = new ClassReader(is); + reader.accept(node, 0); + return node; + } catch (IOException | ZipError e) { + throw new RuntimeException(String.format("error reading %s in %s: %s", name, LoaderUtil.normalizePath(entry.getOrigin()), e), e); + } + } catch (IOException e) { + throw ExceptionUtil.wrap(e); + } + }; + + Tweak tweak = gameProvider.getTweak(this); + tweak.performTransform(); + gameProvider.target = (MainLaunchTarget)tweak.getLaunchTarget(); + + } catch (IOException e) { + throw ExceptionUtil.wrap(e); + } + + Log.debug(LogCategory.GAME_PATCH, "Patched %d class%s", modified.size(), modified.size() != 1 ? "s" : ""); + entrypointsLocated = true; + } + + public ClassNode getClass(String name) { + return classSource.apply(name); + } + + public void overrideClass(ClassNode node) { + modified.put(node.name.replace("/", "."), node); + } + + public byte[] transform(String className) { + ClassNode node = modified.get(className); + if(node == null) { + return null; + } + // Fabric's GameTransformer did not compute max stack and local + // Tweaks rely on writer computing maxes for it + ClassWriter writer = new ClassWriter(COMPUTE_MAXS); + node.accept(writer); + return writer.toByteArray(); + } + +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWJGLVersionLookup.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWJGLVersionLookup.java new file mode 100644 index 0000000..be23eca --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWJGLVersionLookup.java @@ -0,0 +1,113 @@ +package org.mcphackers.launchwrapper.inject; + +import static org.mcphackers.launchwrapper.util.InsnHelper.*; +import static org.mcphackers.rdi.util.InsnHelper.*; +import static org.objectweb.asm.Opcodes.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; + +import org.mcphackers.rdi.util.NodeHelper; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import net.fabricmc.loader.impl.util.ExceptionUtil; +import net.fabricmc.loader.impl.util.LoaderUtil; +import net.fabricmc.loader.impl.util.SimpleClassPath; + +public final class LWJGLVersionLookup { + + private static final String LWJGL2_VER_CLASS = "org/lwjgl/Sys"; + private static final String LWJGL2_VER_FIELD = "VERSION"; + private static final String LWJGL3_VER_CLASS = "org/lwjgl/Version"; + private static final String[] LWJGL3_VER_FIELDS = {"VERSION_MAJOR", "VERSION_MINOR", "VERSION_REVISION"}; + + private static int getInsnValue(AbstractInsnNode insn) { + switch(insn.getOpcode()) { + case ICONST_0: + return 0; + case ICONST_1: + return 1; + case ICONST_2: + return 2; + case ICONST_3: + return 3; + case ICONST_4: + return 4; + case ICONST_5: + return 5; + case BIPUSH: + return ((IntInsnNode)insn).operand; + case LDC: + return (int)((LdcInsnNode)insn).cst; + } + return 0; + } + + public static String getVersion(List lwjgl) { + try (SimpleClassPath cp = new SimpleClassPath(lwjgl)) { + ClassNode node; + MethodNode m; + for(String s : new String[] {LWJGL2_VER_CLASS, LWJGL3_VER_CLASS}) { + int[] version = new int[3]; + try (InputStream is = cp.getInputStream(LoaderUtil.getClassFileName(s))) { + if(is == null) { + continue; + } + ClassReader reader = new ClassReader(is); + node = new ClassNode(); + reader.accept(node, 0); + m = NodeHelper.getMethod(node, "", "()V"); + if(m == null) { + continue; + } + AbstractInsnNode insn = m.instructions.getFirst(); + if(s == LWJGL2_VER_CLASS) { + FieldNode f = NodeHelper.getField(node, LWJGL2_VER_FIELD, "Ljava/lang/String;"); + if(f.value != null) { + return (String)f.value; + } + while(insn != null) { + if(compareInsn(insn, PUTSTATIC, null, LWJGL2_VER_FIELD) + && compareInsn(insn.getPrevious(), LDC)) { + return (String)((LdcInsnNode)insn.getPrevious()).cst; + } + insn = nextInsn(insn); + } + } + if(s == LWJGL3_VER_CLASS) { + for(int i = 0; i < LWJGL3_VER_FIELDS.length; i++) { + FieldNode f = NodeHelper.getField(node, LWJGL3_VER_FIELDS[i], "I"); + if(f.value != null) { + version[i] = (int)f.value; + continue; + } + while(insn != null) { + if(compareInsn(insn, PUTSTATIC, null, LWJGL3_VER_FIELDS[i])) { + version[i] = getInsnValue(insn.getPrevious()); + } + } + insn = nextInsn(insn); + } + } + } + if(s == LWJGL2_VER_CLASS) { + continue; + } + return version[0] + "." + version[1] + "." + version[2]; + } + + } catch (IOException e) { + throw ExceptionUtil.wrap(e); + } + return "0.0.0"; + } + +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWLib.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWLib.java new file mode 100644 index 0000000..e822d64 --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/inject/LWLib.java @@ -0,0 +1,43 @@ +package org.mcphackers.launchwrapper.inject; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.impl.game.LibClassifier.LibraryType; + +enum LWLib implements LibraryType { + + MC_CLIENT(EnvType.CLIENT, "net/minecraft/client/main/Main.class", "net/minecraft/client/Minecraft.class", + "net/minecraft/client/MinecraftApplet.class", "com/mojang/minecraft/MinecraftApplet.class"), + MC_SERVER(EnvType.SERVER, "net/minecraft/server/Main.class", "net/minecraft/server/MinecraftServer.class", + "com/mojang/minecraft/server/MinecraftServer.class"), + MODLOADER("ModLoader"), + LAUNCHWRAPPER("org/mcphackers/launchwrapper/Launch.class"), + LWJGL("org/lwjgl/Sys.class"), + LWJGL3("org/lwjgl/Version.class"); + + private final EnvType env; + private final String[] paths; + + LWLib(String path) { + this(null, new String[] { path }); + } + + LWLib(String... paths) { + this(null, paths); + } + + LWLib(EnvType env, String... paths) { + this.paths = paths; + this.env = env; + } + + @Override + public boolean isApplicable(EnvType env) { + return this.env == null || this.env == env; + } + + @Override + public String[] getPaths() { + return paths; + } + +} diff --git a/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java new file mode 100644 index 0000000..62c3f46 --- /dev/null +++ b/launchwrapper-fabric/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java @@ -0,0 +1,71 @@ +package org.mcphackers.launchwrapper.tweak; + +import static org.mcphackers.launchwrapper.util.InsnHelper.*; +import static org.objectweb.asm.Opcodes.*; + +import org.mcphackers.launchwrapper.LaunchConfig; +import org.mcphackers.launchwrapper.LaunchTarget; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; + +import net.fabricmc.loader.impl.game.minecraft.Hooks; + +public class FabricLoaderTweak extends Tweak { + + protected Tweak baseTweak; + + public FabricLoaderTweak(Tweak baseTweak, LaunchConfig launch) { + super(baseTweak.source, launch); + this.baseTweak = baseTweak; + } + + private InsnList getGameDirectory() { + InsnList insns = new InsnList(); + insns.add(new TypeInsnNode(NEW, "java/io/File")); + insns.add(new InsnNode(DUP)); + insns.add(new LdcInsnNode(launch.gameDir.getString())); + insns.add(new MethodInsnNode(INVOKESPECIAL, "java/io/File", "", "(Ljava/lang/String;)V")); + return insns; + } + + @Override + public boolean transform() { + if(!baseTweak.transform()) { + return false; + } + if(baseTweak instanceof LegacyTweak) { + LegacyTweak tweak = (LegacyTweak)baseTweak; + + for(MethodNode m : tweak.minecraft.methods) { + if(m.name.equals("")) { + InsnList insns = new InsnList(); + insns.add(getGameDirectory()); + insns.add(new IntInsnNode(ALOAD, 0)); + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Hooks.INTERNAL_NAME, "startClient", + "(Ljava/io/File;Ljava/lang/Object;)V", false)); + m.instructions.insertBefore(getLastReturn(m.instructions.getLast()), insns); + source.overrideClass(tweak.minecraft); + tweakInfo("Adding fabric hooks"); + } + } + } + return true; + } + + @Override + public ClassLoaderTweak getLoaderTweak() { + return baseTweak.getLoaderTweak(); + } + + @Override + public LaunchTarget getLaunchTarget() { + return baseTweak.getLaunchTarget(); + } + +} diff --git a/launchwrapper-fabric/src/main/resources/META-INF/services/net.fabricmc.loader.impl.game.GameProvider b/launchwrapper-fabric/src/main/resources/META-INF/services/net.fabricmc.loader.impl.game.GameProvider new file mode 100644 index 0000000..22b2824 --- /dev/null +++ b/launchwrapper-fabric/src/main/resources/META-INF/services/net.fabricmc.loader.impl.game.GameProvider @@ -0,0 +1 @@ +org.mcphackers.launchwrapper.inject.LWGameProvider \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..15f00d9 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'launchwrapper-fabric' \ No newline at end of file diff --git a/src/main/java/org/mcphackers/launchwrapper/Launch.java b/src/main/java/org/mcphackers/launchwrapper/Launch.java index 39e3dc8..7618873 100644 --- a/src/main/java/org/mcphackers/launchwrapper/Launch.java +++ b/src/main/java/org/mcphackers/launchwrapper/Launch.java @@ -8,24 +8,20 @@ public class Launch { /** * Class loader where overwritten classes will be stored */ + public static final String VERSION = "1.0"; public static final LaunchClassLoader CLASS_LOADER = LaunchClassLoader.instantiate(); static { - CLASS_LOADER.addException(Launch.class); - CLASS_LOADER.addException(LaunchConfig.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameter.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterEnum.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterFile.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterFileList.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterNumber.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterString.class); - CLASS_LOADER.addException(LaunchConfig.LaunchParameterSwitch.class); + CLASS_LOADER.addException("org.mcphackers.launchwrapper"); + CLASS_LOADER.addException("org.objectweb.asm"); + CLASS_LOADER.removeException("org.mcphackers.launchwrapper.inject"); } - private static Launch INSTANCE; + protected static Launch INSTANCE; public final LaunchConfig config; - private Launch(LaunchConfig config) { + protected Launch(LaunchConfig config) { this.config = config; + INSTANCE = this; } public static void main(String[] args) { @@ -34,34 +30,36 @@ public static void main(String[] args) { } public void launch() { - Tweak mainTweak = Tweak.get(CLASS_LOADER, config); + Tweak mainTweak = getTweak(); if(mainTweak == null) { System.err.println("Could not find launch target"); return; } - if(mainTweak.transform()) { + if(mainTweak.performTransform()) { if(config.discordRPC.get()) { setupDiscordRPC(); } + CLASS_LOADER.setLoaderTweak(mainTweak.getLoaderTweak()); mainTweak.getLaunchTarget().launch(CLASS_LOADER); } else { System.err.println("Tweak could not be applied"); } } + + protected Tweak getTweak() { + return Tweak.get(CLASS_LOADER, config); + } protected void setupDiscordRPC() { // TODO } - + + @Deprecated public static Launch getInstance() { return INSTANCE; } - - public static LaunchConfig getConfig() { - return INSTANCE.config; - } public static Launch create(LaunchConfig config) { - return INSTANCE = new Launch(config); + return new Launch(config); } } diff --git a/src/main/java/org/mcphackers/launchwrapper/LaunchConfig.java b/src/main/java/org/mcphackers/launchwrapper/LaunchConfig.java index 6181731..b0780b9 100644 --- a/src/main/java/org/mcphackers/launchwrapper/LaunchConfig.java +++ b/src/main/java/org/mcphackers/launchwrapper/LaunchConfig.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import org.mcphackers.launchwrapper.protocol.SkinOption; import org.mcphackers.launchwrapper.protocol.SkinType; import org.mcphackers.launchwrapper.util.OS; diff --git a/src/main/java/org/mcphackers/launchwrapper/inject/Inject.java b/src/main/java/org/mcphackers/launchwrapper/inject/Inject.java index 9e51eb5..afc1f7e 100644 --- a/src/main/java/org/mcphackers/launchwrapper/inject/Inject.java +++ b/src/main/java/org/mcphackers/launchwrapper/inject/Inject.java @@ -30,14 +30,14 @@ private Inject() { } public static AppletWrapper getApplet() { - return new AppletWrapper(Launch.getConfig().getArgsAsMap()); + return new AppletWrapper(Launch.getInstance().config.getArgsAsMap()); } /** * Indev load level injection */ public static File getLevelFile(int index) { - return new File(Launch.getConfig().gameDir.get(), "levels/level" + index + ".dat"); + return new File(Launch.getInstance().config.gameDir.get(), "levels/level" + index + ".dat"); } /** @@ -45,7 +45,7 @@ public static File getLevelFile(int index) { */ public static File saveLevel(int index, String levelName) { final int maxLevels = 5; - File levels = new File(Launch.getConfig().gameDir.get(), "levels"); + File levels = new File(Launch.getInstance().config.gameDir.get(), "levels"); File level = new File(levels, "level" + index + ".dat"); File levelNames = new File(levels, "levels.txt"); String[] lvlNames = new String[maxLevels]; @@ -99,7 +99,7 @@ private static ByteBuffer loadIcon(BufferedImage icon) { } public static ByteBuffer[] loadIcon(boolean favIcon) { - File[] iconPaths = Launch.getConfig().icon.get(); + File[] iconPaths = Launch.getInstance().config.icon.get(); if(iconPaths != null && hasIcon(iconPaths)) { List processedIcons = new ArrayList(); for(File icon : iconPaths) { @@ -139,7 +139,7 @@ private static boolean hasIcon(File[] icons) { } public static BufferedImage getIcon(boolean favIcon) { - File[] iconPaths = Launch.getConfig().icon.get(); + File[] iconPaths = Launch.getInstance().config.icon.get(); if(iconPaths != null && hasIcon(iconPaths)) { for(File icon : iconPaths) { if(!icon.exists()) { diff --git a/src/main/java/org/mcphackers/launchwrapper/loader/ClassLoaderURLHandler.java b/src/main/java/org/mcphackers/launchwrapper/loader/ClassLoaderURLHandler.java index 3162a37..3bd4552 100644 --- a/src/main/java/org/mcphackers/launchwrapper/loader/ClassLoaderURLHandler.java +++ b/src/main/java/org/mcphackers/launchwrapper/loader/ClassLoaderURLHandler.java @@ -35,14 +35,18 @@ public void connect() throws IOException { @Override public InputStream getInputStream() throws IOException { String path = url.getPath(); + byte[] data = classLoader.overridenResources.get(LaunchClassLoader.classNameFromResource(path)); + if(data != null) { + return new ByteArrayInputStream(data); + } ClassNode node = classLoader.overridenClasses.get(path); if(node == null) { throw new FileNotFoundException(); } ClassWriter writer = new SafeClassWriter(classLoader, COMPUTE_MAXS | COMPUTE_FRAMES); node.accept(writer); - byte[] classData = writer.toByteArray(); - return new ByteArrayInputStream(classData); + data = writer.toByteArray(); + return new ByteArrayInputStream(data); } } diff --git a/src/main/java/org/mcphackers/launchwrapper/loader/LaunchClassLoader.java b/src/main/java/org/mcphackers/launchwrapper/loader/LaunchClassLoader.java index 23e8803..1f72f68 100644 --- a/src/main/java/org/mcphackers/launchwrapper/loader/LaunchClassLoader.java +++ b/src/main/java/org/mcphackers/launchwrapper/loader/LaunchClassLoader.java @@ -16,7 +16,9 @@ import java.security.cert.Certificate; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.mcphackers.launchwrapper.tweak.ClassLoaderTweak; import org.mcphackers.launchwrapper.util.ClassNodeSource; @@ -33,10 +35,12 @@ public class LaunchClassLoader extends URLClassLoader implements ClassNodeSource private ClassLoader parent; private ClassLoaderTweak tweak; - private Map> exceptions = new HashMap>(); + private Set exceptions = new HashSet(); + private Set ignoreExceptions = new HashSet(); /** Keys should contain dots */ Map overridenClasses = new HashMap(); - Map overridenResources = new HashMap(); //TODO + Map overridenResources = new HashMap(); + Map overridenSource = new HashMap(); /** Keys should contain slashes */ private Map classNodeCache = new HashMap(); private File debugOutput; @@ -79,12 +83,10 @@ public URL getResource(String name) { } private URL getOverridenResourceURL(String name) { - if(overridenResources.get(name) != null) { - //TODO - } try { - if(overridenClasses.get(classNameFromResource(name)) != null) { - URL url = new URL("jar", "", -1, classNameFromResource(name), new ClassLoaderURLHandler(this)); + if(overridenResources.get(name) != null + || overridenClasses.get(classNameFromResource(name)) != null) { + URL url = new URL("jar", "", -1, name, new ClassLoaderURLHandler(this)); return url; } } catch (MalformedURLException e) { @@ -97,14 +99,28 @@ public Enumeration findResources(String name) throws IOException { } public Class findClass(String name) throws ClassNotFoundException { + name = className(name); if(name.startsWith("java.")) { return parent.loadClass(name); } - name = className(name); - Class cls; - cls = exceptions.get(name); - if(cls != null) { - return cls; + Class cls = null; + + if(overridenSource.get(name) != null) { + return transformedClass(name); + } + outer: + for(String pkg : exceptions) { + for(String pkg2 : ignoreExceptions) { + if(name.startsWith(pkg2)) { + continue outer; + } + } + if(name.startsWith(pkg)) { + cls = parent.loadClass(name); + if(cls != null) { + return cls; + } + } } cls = transformedClass(name); if(cls != null) { @@ -127,6 +143,18 @@ public void invokeMain(String launchTarget, String... args) { } } + public void addException(String pkg) { + exceptions.add(pkg + "."); + } + + public void removeException(String pkg) { + ignoreExceptions.add(pkg + "."); + } + + public void overrideClassSource(String name, String f) { + overridenSource.put(className(name), f); + } + private ProtectionDomain getProtectionDomain(String name) { final URL resource = getResource(classResourceName(name)); if(resource == null) { @@ -142,6 +170,9 @@ private ProtectionDomain getProtectionDomain(String name) { path = path.substring(0, i); } } + if(overridenSource.get(name) != null) { + path = overridenSource.get(name); + } try { URL newResource = new URL("file", "", path); codeSource = new CodeSource(newResource, new Certificate[0]); @@ -251,9 +282,9 @@ private static String classResourceName(String name) { return name.replace('.', '/') + ".class"; } - private static String classNameFromResource(String resource) { + static String classNameFromResource(String resource) { if(resource.endsWith(".class")) { - return resource.substring(resource.length() - 7); + return resource.substring(0, resource.length() - 6); } return resource; } @@ -278,10 +309,6 @@ private Class transformedClass(String name) throws ClassNotFoundException { return redefineClass(name); } - public void addException(Class cls) { - exceptions.put(cls.getName(), cls); - } - public void setLoaderTweak(ClassLoaderTweak classLoaderTweak) { tweak = classLoaderTweak; } diff --git a/src/main/java/org/mcphackers/launchwrapper/loader/SafeClassWriter.java b/src/main/java/org/mcphackers/launchwrapper/loader/SafeClassWriter.java index 83201d7..cac7067 100644 --- a/src/main/java/org/mcphackers/launchwrapper/loader/SafeClassWriter.java +++ b/src/main/java/org/mcphackers/launchwrapper/loader/SafeClassWriter.java @@ -1,13 +1,14 @@ package org.mcphackers.launchwrapper.loader; +import org.mcphackers.launchwrapper.util.ClassNodeProvider; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; public class SafeClassWriter extends ClassWriter { - protected LaunchClassLoader classLoader; + protected ClassNodeProvider classLoader; - public SafeClassWriter(LaunchClassLoader classLoader, int flags) { + public SafeClassWriter(ClassNodeProvider classLoader, int flags) { super(flags); this.classLoader = classLoader; } diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/AssetRequests.java b/src/main/java/org/mcphackers/launchwrapper/protocol/AssetRequests.java index 7599bd2..a8a8609 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/AssetRequests.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/AssetRequests.java @@ -41,13 +41,16 @@ public AssetRequests(File assetsDir, String index) { } String hash = entry.optString("hash"); long size = entry.optLong("size"); - if(hash == null) { + // Only resources in a folder are valid + if(!s.contains("/") || hash == null) { System.out.println("[LaunchWrapper] Invalid resource: " + s); continue; } File object = new File(assetsDir, "objects/" + hash.substring(0, 2) + "/" + hash); if(!object.exists() || object.length() != size) { - System.out.println("[LaunchWrapper] Invalid resource: " + s); + // Download if missing? + // Some sounds in betacraft indexes are downloaded from custom url which isn't handled by other launchers + System.out.println("[LaunchWrapper] Missing resource: " + s); continue; } // A little slow and probably pointless diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/AssetURLConnection.java b/src/main/java/org/mcphackers/launchwrapper/protocol/AssetURLConnection.java index 2d1df65..f32ae43 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/AssetURLConnection.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/AssetURLConnection.java @@ -1,12 +1,10 @@ package org.mcphackers.launchwrapper.protocol; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; @@ -20,44 +18,38 @@ public AssetURLConnection(URL url, AssetRequests assets) { this.assets = assets; } - @SuppressWarnings("resource") - private InputStream getIndex(boolean xml) throws IOException { - PipedInputStream in = new PipedInputStream(); - PrintWriter out = new PrintWriter(new PipedOutputStream(in), true); - - new Thread(() -> { - if(xml) { - out.write(""); - out.write(""); - } - for(AssetObject asset : assets.list()) { - // path,size,last_updated_timestamp(unused) - if(xml) { - out.write(""); - out.write(""); - out.write(asset.path); - out.write(""); - out.write(""); - out.write(Long.toString(asset.size)); - out.write(""); - out.write(""); - } else { - out.write(asset.path + ',' + asset.size + ",0\n"); - } - } + private InputStream getIndex(final boolean xml) throws IOException { + StringBuilder s = new StringBuilder(); + if(xml) { + s.append(""); + s.append(""); + } + for(AssetObject asset : assets.list()) { + // path,size,last_updated_timestamp(unused) if(xml) { - out.write(""); + s.append(""); + s.append(""); + s.append(asset.path); + s.append(""); + s.append(""); + s.append(Long.toString(asset.size)); + s.append(""); + s.append(""); + } else { + s.append(asset.path + ',' + asset.size + ",0\n"); } - out.close(); - }).start(); - return in; + } + if(xml) { + s.append(""); + } + return new ByteArrayInputStream(s.toString().getBytes()); } @Override public InputStream getInputStream() throws IOException { - String key = url.getPath().replaceAll("%20", " ").substring(1); + String key = url.getPath().replace("%20", " ").substring(1); key = key.substring(key.indexOf('/') + 1); - if(key.isEmpty()) { + if(key.length() == 0) { boolean xml = url.getPath().startsWith("/MinecraftResources/"); return getIndex(xml); } diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/LegacyURLStreamHandler.java b/src/main/java/org/mcphackers/launchwrapper/protocol/LegacyURLStreamHandler.java index 553ae55..507c3d5 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/LegacyURLStreamHandler.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/LegacyURLStreamHandler.java @@ -4,7 +4,6 @@ import java.net.URL; import java.net.URLConnection; -import org.mcphackers.launchwrapper.Launch; import org.mcphackers.launchwrapper.LaunchConfig; public class LegacyURLStreamHandler extends URLStreamHandlerProxy { @@ -35,17 +34,17 @@ protected URLConnection openConnection(URL url) throws IOException { if(path.equals("/haspaid.jsp")) return new BasicResponseURLConnection(url, "true"); if(path.contains("/level/save.html")) - return new SaveLevelURLConnection(url); + return new SaveLevelURLConnection(url, config.gameDir.get()); if(path.contains("/level/load.html")) - return new LoadLevelURLConnection(url); + return new LoadLevelURLConnection(url, config.gameDir.get()); if(path.equals("/listmaps.jsp")) - return new ListLevelsURLConnection(url); + return new ListLevelsURLConnection(url, config.gameDir.get()); if(path.startsWith("/MinecraftResources/") || path.startsWith("/resources/")) return new AssetURLConnection(url, assets); if(path.startsWith("/MinecraftSkins/") || path.startsWith("/skin/") || path.startsWith("/MinecraftCloaks/") || path.startsWith("/cloak/")) return new SkinURLConnection(url, config.skinProxy.get()); if(host.equals("assets.minecraft.net") && path.equals("/1_6_has_been_released.flag")) - if(Launch.getConfig().oneSixFlag.get()) + if(config.oneSixFlag.get()) return new BasicResponseURLConnection(url, "https://web.archive.org/web/20130702232237if_/https://mojang.com/2013/07/minecraft-the-horse-update/"); else return new BasicResponseURLConnection(url, ""); diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/ListLevelsURLConnection.java b/src/main/java/org/mcphackers/launchwrapper/protocol/ListLevelsURLConnection.java index ffaf495..066f8c4 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/ListLevelsURLConnection.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/ListLevelsURLConnection.java @@ -9,15 +9,16 @@ import java.net.URL; import java.net.URLConnection; -import org.mcphackers.launchwrapper.Launch; import org.mcphackers.launchwrapper.util.Util; public class ListLevelsURLConnection extends URLConnection { public static final String EMPTY_LEVEL = "-"; + private File gameDir; - public ListLevelsURLConnection(URL url) { + public ListLevelsURLConnection(URL url, File gameDir) { super(url); + this.gameDir = gameDir; } @Override @@ -25,7 +26,7 @@ public void connect() throws IOException { } public InputStream getInputStream() throws IOException { - File levels = new File(Launch.getConfig().gameDir.get(), "levels"); + File levels = new File(gameDir, "levels"); if(!levels.exists()) levels.mkdirs(); File levelNames = new File(levels, "levels.txt"); diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/LoadLevelURLConnection.java b/src/main/java/org/mcphackers/launchwrapper/protocol/LoadLevelURLConnection.java index af102df..5c2f95a 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/LoadLevelURLConnection.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/LoadLevelURLConnection.java @@ -14,15 +14,16 @@ import java.net.URL; import java.util.Map; -import org.mcphackers.launchwrapper.Launch; import org.mcphackers.launchwrapper.util.Util; public class LoadLevelURLConnection extends HttpURLConnection { Exception exception; + private File gameDir; - public LoadLevelURLConnection(URL url) { + public LoadLevelURLConnection(URL url, File gameDir) { super(url); + this.gameDir = gameDir; } @Override @@ -48,7 +49,7 @@ public InputStream getInputStream() throws IOException { throw new MalformedURLException("Query is missing \"id\" parameter"); } int levelId = Integer.parseInt(query.get("id")); - File levels = new File(Launch.getConfig().gameDir.get(), "levels"); + File levels = new File(gameDir, "levels"); File level = new File(levels, "level" + levelId + ".dat"); if(!level.exists()) { throw new FileNotFoundException("Level doesn't exist"); diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/SaveLevelURLConnection.java b/src/main/java/org/mcphackers/launchwrapper/protocol/SaveLevelURLConnection.java index 686c99e..df22b4c 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/SaveLevelURLConnection.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/SaveLevelURLConnection.java @@ -1,5 +1,7 @@ package org.mcphackers.launchwrapper.protocol; +import static org.mcphackers.launchwrapper.protocol.ListLevelsURLConnection.EMPTY_LEVEL; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -12,17 +14,16 @@ import java.net.HttpURLConnection; import java.net.URL; -import org.mcphackers.launchwrapper.Launch; import org.mcphackers.launchwrapper.util.Util; -import static org.mcphackers.launchwrapper.protocol.ListLevelsURLConnection.EMPTY_LEVEL; - public class SaveLevelURLConnection extends HttpURLConnection { ByteArrayOutputStream levelOutput = new ByteArrayOutputStream(); + private File gameDir; - public SaveLevelURLConnection(URL url) { + public SaveLevelURLConnection(URL url, File gameDir) { super(url); + this.gameDir = gameDir; } @Override @@ -43,7 +44,7 @@ public boolean usingProxy() { public InputStream getInputStream() throws IOException { Exception exception = null; try { - File levels = new File(Launch.getConfig().gameDir.get(), "levels"); + File levels = new File(gameDir, "levels"); byte[] data = levelOutput.toByteArray(); DataInputStream in = new DataInputStream(new ByteArrayInputStream(data)); String username = in.readUTF(); diff --git a/src/main/java/org/mcphackers/launchwrapper/protocol/SkinRequests.java b/src/main/java/org/mcphackers/launchwrapper/protocol/SkinRequests.java index bccc52a..0c1b691 100644 --- a/src/main/java/org/mcphackers/launchwrapper/protocol/SkinRequests.java +++ b/src/main/java/org/mcphackers/launchwrapper/protocol/SkinRequests.java @@ -256,9 +256,8 @@ public static void useLeftArm(ImageUtils imgu) { } public static void alexToSteve(ImageUtils imgu) { - imgu.setArea(48, 16, imgu.crop(47, 16, 7, 16).getImage()); - imgu.setArea(47, 16, imgu.crop(45, 16, 1, 16).getImage()); - imgu.setArea(55, 20, imgu.crop(53, 20, 1, 12).getImage()); - imgu.setArea(51, 16, imgu.crop(49, 16, 1, 4).getImage()); + imgu.setArea(46, 16, imgu.crop(45, 16, 9, 16).getImage()); + imgu.setArea(50, 16, imgu.crop(49, 16, 2, 4).getImage()); + imgu.setArea(54, 20, imgu.crop(53, 20, 2, 12).getImage()); } } diff --git a/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java b/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java deleted file mode 100644 index 3d9ebf6..0000000 --- a/src/main/java/org/mcphackers/launchwrapper/tweak/FabricLoaderTweak.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.mcphackers.launchwrapper.tweak; - -import org.mcphackers.launchwrapper.LaunchConfig; -import org.mcphackers.launchwrapper.LaunchTarget; -import org.mcphackers.launchwrapper.MainLaunchTarget; - -public class FabricLoaderTweak extends Tweak { - - public static final String FABRIC_KNOT_CLIENT = "net/fabricmc/loader/impl/launch/knot/KnotClient"; - protected Tweak baseTweak; - - public FabricLoaderTweak(Tweak baseTweak, LaunchConfig launch) { - super(baseTweak.source, launch); - this.baseTweak = baseTweak; - } - - @Override - public boolean transform() { - if(!baseTweak.transform()) { - return false; - } - // TODO - return true; - } - - @Override - public ClassLoaderTweak getLoaderTweak() { - return baseTweak.getLoaderTweak(); - } - - @Override - public LaunchTarget getLaunchTarget() { - baseTweak.getLaunchTarget(); // discard target, but run any pre-launch tweaks from base tweak - MainLaunchTarget main = new MainLaunchTarget(FABRIC_KNOT_CLIENT); - main.args = new String[] {launch.username.get(), launch.session.get()}; - return main; - } - -} diff --git a/src/main/java/org/mcphackers/launchwrapper/tweak/LegacyTweak.java b/src/main/java/org/mcphackers/launchwrapper/tweak/LegacyTweak.java index 5c43b5d..74d08d8 100644 --- a/src/main/java/org/mcphackers/launchwrapper/tweak/LegacyTweak.java +++ b/src/main/java/org/mcphackers/launchwrapper/tweak/LegacyTweak.java @@ -15,7 +15,6 @@ import org.mcphackers.launchwrapper.LaunchTarget; import org.mcphackers.launchwrapper.MainLaunchTarget; import org.mcphackers.launchwrapper.protocol.LegacyURLStreamHandler; -import org.mcphackers.launchwrapper.protocol.SkinType; import org.mcphackers.launchwrapper.protocol.URLStreamHandlerProxy; import org.mcphackers.launchwrapper.util.ClassNodeSource; import org.mcphackers.launchwrapper.util.UnsafeUtils; @@ -53,8 +52,6 @@ public class LegacyTweak extends Tweak { "com/mojang/minecraft/MinecraftApplet" }; - public static final boolean EXPERIMENTAL_INDEV_SAVING = true; - protected ClassNode minecraft; protected ClassNode minecraftApplet; /** Field that determines if Minecraft should exit */ @@ -74,7 +71,6 @@ public class LegacyTweak extends Tweak { private boolean supportsResizing; /** public static main(String[]) */ protected MethodNode main; - protected SkinType skinType = null; protected int port = -1; public LegacyTweak(ClassNodeSource source, LaunchConfig launch) { @@ -157,9 +153,6 @@ public LaunchTarget getLaunchTarget() { } private void addIndevSaving() { - if(!EXPERIMENTAL_INDEV_SAVING) { - return; - } ClassNode saveLevelMenu = null; ClassNode loadLevelMenu = null; methods: @@ -1708,9 +1701,6 @@ && compareInsn(insns2[4], PUTFIELD, minecraft.name, null, "Z")) { if(launch.forceResizable.get()) { supportsResizing = true; } - if(launch.skinProxy.get() != null) { - skinType = launch.skinProxy.get(); - } } public ClassNode getApplet() { diff --git a/src/main/java/org/mcphackers/launchwrapper/tweak/Tweak.java b/src/main/java/org/mcphackers/launchwrapper/tweak/Tweak.java index b9f14fd..b833e5d 100644 --- a/src/main/java/org/mcphackers/launchwrapper/tweak/Tweak.java +++ b/src/main/java/org/mcphackers/launchwrapper/tweak/Tweak.java @@ -6,14 +6,15 @@ import org.mcphackers.launchwrapper.LaunchConfig; import org.mcphackers.launchwrapper.LaunchTarget; -import org.mcphackers.launchwrapper.loader.LaunchClassLoader; import org.mcphackers.launchwrapper.util.ClassNodeSource; public abstract class Tweak { + private static final boolean LOG_TWEAKS = Boolean.parseBoolean(System.getProperty("launchwrapper.log", "false")); protected ClassNodeSource source; protected LaunchConfig launch; private List features = new ArrayList(); + private boolean clean = true; /** * Every tweak must implement this constructor @@ -30,34 +31,27 @@ public Tweak(ClassNodeSource source, LaunchConfig launch) { * This method does return true even if some of the changes weren't applied, even when they should've been * @return true if given ClassNodeSource was modified without fatal errors */ - public abstract boolean transform(); + protected abstract boolean transform(); + + public boolean performTransform() { + if(!clean) { + throw new RuntimeException("Calling tweak transform twice is not allowed. Create a new instance"); + } + clean = false; + return transform(); + } public abstract ClassLoaderTweak getLoaderTweak(); public abstract LaunchTarget getLaunchTarget(); - - public static Tweak get(LaunchClassLoader classLoader, LaunchConfig launch) { - Tweak tweak = getTweak(classLoader, launch); - if(tweak != null) { - classLoader.setLoaderTweak(tweak.getLoaderTweak()); - } - return tweak; - } - - private static Tweak wrapTweak(ClassNodeSource source, Tweak baseTweak, LaunchConfig launch) { - if(source.getClass(FabricLoaderTweak.FABRIC_KNOT_CLIENT) != null) { - return new FabricLoaderTweak(baseTweak, launch); - } - return baseTweak; - } - private static Tweak getTweak(LaunchClassLoader classLoader, LaunchConfig launch) { + public static Tweak get(ClassNodeSource classLoader, LaunchConfig launch) { if(launch.tweakClass.get() != null) { try { // Instantiate custom tweak if it's present on classpath; - return wrapTweak(classLoader, (Tweak)Class.forName(launch.tweakClass.get()) + return (Tweak)Class.forName(launch.tweakClass.get()) .getConstructor(ClassNodeSource.class, LaunchConfig.class) - .newInstance(classLoader, launch), launch); + .newInstance(classLoader, launch); } catch (ClassNotFoundException e) { return null; } catch (Exception e) { @@ -66,19 +60,19 @@ private static Tweak getTweak(LaunchClassLoader classLoader, LaunchConfig launch } } if(launch.isom.get()) { - return wrapTweak(classLoader, new IsomTweak(classLoader, launch), launch); + return new IsomTweak(classLoader, launch); } if(classLoader.getClass(VanillaTweak.MAIN_CLASS) != null) { - return wrapTweak(classLoader, new VanillaTweak(classLoader, launch), launch); + return new VanillaTweak(classLoader, launch); } for(String cls : LegacyTweak.MAIN_CLASSES) { if(classLoader.getClass(cls) != null) { - return wrapTweak(classLoader, new LegacyTweak(classLoader, launch), launch); + return new LegacyTweak(classLoader, launch); } } for(String cls : LegacyTweak.MAIN_APPLETS) { if(classLoader.getClass(cls) != null) { - return wrapTweak(classLoader, new LegacyTweak(classLoader, launch), launch); + return new LegacyTweak(classLoader, launch); } } return null; // Tweak not found @@ -90,6 +84,12 @@ public List getTweakInfo() { protected void tweakInfo(String name, String... extra) { features.add(new FeatureInfo(name)); - System.out.println("[LaunchWrapper] Applying tweak: " + name + " " + String.join(" ", extra)); + StringBuilder other = new StringBuilder(); + for(String s : extra) { + other.append(" ").append(s); + } + if(LOG_TWEAKS) { + System.out.println("[LaunchWrapper] Applying tweak: " + name + other); + } } } diff --git a/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeProvider.java b/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeProvider.java new file mode 100644 index 0000000..5a25dea --- /dev/null +++ b/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeProvider.java @@ -0,0 +1,9 @@ +package org.mcphackers.launchwrapper.util; + +import org.objectweb.asm.tree.ClassNode; + +public interface ClassNodeProvider { + + ClassNode getClass(String name); + +} diff --git a/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeSource.java b/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeSource.java index c2cdf72..6586a7b 100644 --- a/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeSource.java +++ b/src/main/java/org/mcphackers/launchwrapper/util/ClassNodeSource.java @@ -2,9 +2,7 @@ import org.objectweb.asm.tree.ClassNode; -public interface ClassNodeSource { - - ClassNode getClass(String name); +public interface ClassNodeSource extends ClassNodeProvider { void overrideClass(ClassNode node); diff --git a/src/main/resources/icon_256x256.png b/src/main/resources/icon_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..d982fcffa3b5c11e491658b9c1836456af128ea6 GIT binary patch literal 74168 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 N002ovPDHLkV1fjoCvpG) literal 0 HcmV?d00001 diff --git a/src/test/java/org/mcphackers/launchwrapper/test/TweakTest.java b/src/test/java/org/mcphackers/launchwrapper/test/TweakTest.java index 20fcb0a..f0466b8 100644 --- a/src/test/java/org/mcphackers/launchwrapper/test/TweakTest.java +++ b/src/test/java/org/mcphackers/launchwrapper/test/TweakTest.java @@ -30,7 +30,7 @@ public void test() { FileClassNodeSource classNodeSource = new FileClassNodeSource(gameJar); LaunchConfig config = getDefaultConfig(testDir); Tweak tweak = getTweak(classNodeSource, config); - assertTrue(tweak.transform()); + assertTrue(tweak.performTransform()); // for (FeatureInfo info : tweak.getTweakInfo()) { // System.out.println(info.feature); // }