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 0000000..d982fcf Binary files /dev/null and b/src/main/resources/icon_256x256.png differ diff --git a/src/main/resources/icon_48x48.png b/src/main/resources/icon_48x48.png new file mode 100644 index 0000000..119ca1f Binary files /dev/null and b/src/main/resources/icon_48x48.png differ 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); // }