diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index f85c2354..45a43b93 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -18,6 +18,7 @@ set(SRC org/prismlauncher/exception/ParameterNotFoundException.java org/prismlauncher/exception/ParseException.java org/prismlauncher/utils/Parameters.java + org/prismlauncher/utils/ReflectionUtils.java net/minecraft/Launcher.java ) add_jar(NewLaunch ${SRC}) diff --git a/libraries/launcher/org/prismlauncher/exception/ParameterNotFoundException.java b/libraries/launcher/org/prismlauncher/exception/ParameterNotFoundException.java index c083e02a..3dd6efc3 100644 --- a/libraries/launcher/org/prismlauncher/exception/ParameterNotFoundException.java +++ b/libraries/launcher/org/prismlauncher/exception/ParameterNotFoundException.java @@ -54,10 +54,28 @@ package org.prismlauncher.exception; + +@SuppressWarnings("serial") public final class ParameterNotFoundException extends IllegalArgumentException { - public ParameterNotFoundException(String key) { - super("Unknown parameter name: " + key); + public ParameterNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ParameterNotFoundException(Throwable cause) { + super(cause); + } + + public ParameterNotFoundException(String message) { + super(message); + } + + public ParameterNotFoundException() { + super(); + } + + public static ParameterNotFoundException forParameterName(String parameterName) { + return new ParameterNotFoundException(String.format("Unknown parameter name '%s'", parameterName)); } } diff --git a/libraries/launcher/org/prismlauncher/exception/ParseException.java b/libraries/launcher/org/prismlauncher/exception/ParseException.java index 8904f9ee..2243f23f 100644 --- a/libraries/launcher/org/prismlauncher/exception/ParseException.java +++ b/libraries/launcher/org/prismlauncher/exception/ParseException.java @@ -54,10 +54,31 @@ package org.prismlauncher.exception; + +@SuppressWarnings({ "serial", "unused" }) public final class ParseException extends IllegalArgumentException { public ParseException(String message) { super(message); } + public ParseException(String message, Throwable cause) { + super(message, cause); + } + + public ParseException(Throwable cause) { + super(cause); + } + + public ParseException() { + super(); + } + + public static ParseException forInputString(String inputString) { + return new ParseException(String.format("Could not parse input string '%s'", inputString)); + } + + public static ParseException forInputString(String inputString, Throwable cause) { + return new ParseException(String.format("Could not parse input string '%s'", inputString), cause); + } } diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java index c572db10..9dd7df10 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java @@ -55,81 +55,66 @@ package org.prismlauncher.launcher.impl; + import org.prismlauncher.exception.ParseException; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.StringUtils; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; import java.util.ArrayList; +import java.util.Collections; import java.util.List; + public abstract class AbstractLauncher implements Launcher { private static final int DEFAULT_WINDOW_WIDTH = 854; + private static final int DEFAULT_WINDOW_HEIGHT = 480; // parameters, separated from ParamBucket protected final List mcParams; - private final String mainClass; // secondary parameters protected final int width; + protected final int height; + protected final boolean maximize; - protected final String serverAddress, serverPort; + protected final String serverAddress; - protected final ClassLoader classLoader; + protected final String serverPort; + + protected final String mainClassName; protected AbstractLauncher(Parameters params) { - classLoader = ClassLoader.getSystemClassLoader(); + this.mcParams = Collections.unmodifiableList(params.getList("param", new ArrayList())); + this.mainClassName = params.getString("mainClass", "net.minecraft.client.Minecraft"); - mcParams = params.getList("param", new ArrayList()); - mainClass = params.getString("mainClass", "net.minecraft.client.Minecraft"); - - serverAddress = params.getString("serverAddress", null); - serverPort = params.getString("serverPort", null); + this.serverAddress = params.getString("serverAddress", null); + this.serverPort = params.getString("serverPort", null); String windowParams = params.getString("windowParams", null); - if ("max".equals(windowParams) || windowParams == null) { - maximize = windowParams != null; + this.maximize = "max".equalsIgnoreCase(windowParams); - width = DEFAULT_WINDOW_WIDTH; - height = DEFAULT_WINDOW_HEIGHT; - } else { - maximize = false; - + if (windowParams != null && !"max".equalsIgnoreCase(windowParams)) { String[] sizePair = StringUtils.splitStringPair('x', windowParams); - + if (sizePair != null) { try { - width = Integer.parseInt(sizePair[0]); - height = Integer.parseInt(sizePair[1]); - return; - } catch (NumberFormatException ignored) { + this.width = Integer.parseInt(sizePair[0]); + this.height = Integer.parseInt(sizePair[1]); + } catch (NumberFormatException e) { + throw new ParseException(String.format("Could not parse window parameters from '%s'", windowParams), e); } + } else { + throw new ParseException(String.format("Invalid window size parameters '%s'. Format: [height]x[width]", windowParams)); } - - throw new ParseException("Invalid window size parameter value: " + windowParams); + } else { + this.width = DEFAULT_WINDOW_WIDTH; + this.height = DEFAULT_WINDOW_HEIGHT; } } - - protected Class loadMain() throws ClassNotFoundException { - return classLoader.loadClass(mainClass); - } - - protected void loadAndInvokeMain() throws Throwable { - invokeMain(loadMain()); - } - - protected void invokeMain(Class mainClass) throws Throwable { - MethodHandle method = MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)); - - method.invokeExact(mcParams.toArray(new String[0])); - } - } diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index 24b12c95..fc0c9823 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -58,9 +58,17 @@ package org.prismlauncher.launcher.impl; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.launcher.LauncherProvider; import org.prismlauncher.utils.Parameters; +import org.prismlauncher.utils.ReflectionUtils; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; public final class StandardLauncher extends AbstractLauncher { + private static final Logger LOGGER = Logger.getLogger("LegacyLauncher"); + public StandardLauncher(Parameters params) { super(params); @@ -78,21 +86,27 @@ public final class StandardLauncher extends AbstractLauncher { // the following often breaks linux screen setups // mcparams.add("--fullscreen"); - if (!maximize) { - mcParams.add("--width"); - mcParams.add(Integer.toString(width)); - mcParams.add("--height"); - mcParams.add(Integer.toString(height)); + List launchParameters = new ArrayList<>(this.mcParams); + + if (!this.maximize) { + launchParameters.add("--width"); + launchParameters.add(Integer.toString(width)); + launchParameters.add("--height"); + launchParameters.add(Integer.toString(height)); } - if (serverAddress != null) { - mcParams.add("--server"); - mcParams.add(serverAddress); - mcParams.add("--port"); - mcParams.add(serverPort); + if (this.serverAddress != null) { + launchParameters.add("--server"); + launchParameters.add(serverAddress); + launchParameters.add("--port"); + launchParameters.add(serverPort); } - loadAndInvokeMain(); + LOGGER.info("Launching minecraft using the main class entrypoint"); + + MethodHandle method = ReflectionUtils.findMainEntrypoint(this.mainClassName); + + method.invokeExact((Object[]) launchParameters.toArray(new String[0])); } diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java index e342e788..0ce3c57b 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java @@ -60,14 +60,11 @@ import org.prismlauncher.launcher.Launcher; import org.prismlauncher.launcher.LauncherProvider; import org.prismlauncher.launcher.impl.AbstractLauncher; import org.prismlauncher.utils.Parameters; +import org.prismlauncher.utils.ReflectionUtils; -import java.applet.Applet; import java.io.File; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.Collections; import java.util.List; import java.util.logging.Level; @@ -87,7 +84,7 @@ public final class LegacyLauncher extends AbstractLauncher { private final String appletClass; - private final boolean noApplet; + private final boolean usesApplet; private final String cwd; @@ -100,8 +97,9 @@ public final class LegacyLauncher extends AbstractLauncher { appletClass = params.getString("appletClass", "net.minecraft.client.MinecraftApplet"); List traits = params.getList("traits", Collections.emptyList()); - noApplet = traits.contains("noapplet"); + usesApplet = !traits.contains("noapplet"); + //noinspection AccessOfSystemProperties cwd = System.getProperty("user.dir"); } @@ -109,74 +107,40 @@ public final class LegacyLauncher extends AbstractLauncher { return new LegacyLauncherProvider(); } - /** - * Finds a field that looks like a Minecraft base folder in a supplied class - * - * @param clazz the class to scan - * - * @return The found field. - */ - private static Field getMinecraftGameDirField(Class clazz) { - // Field we're looking for is always - // private static File obfuscatedName = null; - for (Field field : clazz.getDeclaredFields()) { - // Has to be File - if (field.getType() != File.class) - continue; - - // And Private Static. - if (!Modifier.isStatic(field.getModifiers()) || !Modifier.isPrivate(field.getModifiers())) - continue; - - return field; - } - - return null; - } - @Override public void launch() throws Throwable { - Class main = loadMain(); - Field gameDirField = getMinecraftGameDirField(main); + Class main = ClassLoader.getSystemClassLoader().loadClass(this.mainClassName); + Field gameDirField = ReflectionUtils.getMinecraftGameDirField(main); if (gameDirField == null) { - LOGGER.warning("Could not find Mineraft path field."); + LOGGER.warning("Could not find Minecraft path field"); } else { gameDirField.setAccessible(true); - gameDirField.set(null, new File(cwd)); + gameDirField.set(null /* field is static, so instance is null */, new File(cwd)); } - if (!noApplet) { - LOGGER.info("Launching with applet wrapper..."); + if (this.usesApplet) { + LOGGER.info("Launching legacy minecraft using applet wrapper..."); try { - Class appletClass = classLoader.loadClass(this.appletClass); - - MethodHandle constructor = MethodHandles.lookup().findConstructor(appletClass, MethodType.methodType(void.class)); - Applet applet = (Applet) constructor.invoke(); - - LegacyFrame window = new LegacyFrame(title, applet); + LegacyFrame window = new LegacyFrame(title, ReflectionUtils.createAppletClass(this.appletClass)); window.start( - user, - session, - width, - height, - maximize, - serverAddress, - serverPort, - mcParams.contains("--demo") + this.user, + this.session, + this.width, this.height, this.maximize, + this.serverAddress, this.serverPort, + this.mcParams.contains("--demo") ); - - return; } catch (Throwable e) { - LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e); - - LOGGER.warning("Falling back to using main class."); + LOGGER.log(Level.SEVERE, "Running applet wrapper failed with exception", e); } - } + } else { + LOGGER.info("Launching legacy minecraft using the main class entrypoint"); + MethodHandle method = ReflectionUtils.findMainEntrypoint(main); - invokeMain(main); + method.invokeExact((Object[]) mcParams.toArray(new String[0])); + } } diff --git a/libraries/launcher/org/prismlauncher/utils/Parameters.java b/libraries/launcher/org/prismlauncher/utils/Parameters.java index 5596e88a..6fbd0ef1 100644 --- a/libraries/launcher/org/prismlauncher/utils/Parameters.java +++ b/libraries/launcher/org/prismlauncher/utils/Parameters.java @@ -83,7 +83,7 @@ public final class Parameters { List params = map.get(key); if (params == null) - throw new ParameterNotFoundException(key); + throw ParameterNotFoundException.forParameterName(key); return params; } @@ -101,7 +101,7 @@ public final class Parameters { List list = getList(key); if (list.isEmpty()) - throw new ParameterNotFoundException(key); + throw ParameterNotFoundException.forParameterName(key); return list.get(0); } diff --git a/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java new file mode 100644 index 00000000..484e0d8a --- /dev/null +++ b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java @@ -0,0 +1,131 @@ +package org.prismlauncher.utils; + + +import java.applet.Applet; +import java.io.File; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.logging.Level; +import java.util.logging.Logger; + + +public final class ReflectionUtils { + private static final Logger LOGGER = Logger.getLogger("ReflectionUtils"); + + private ReflectionUtils() { + } + + /** + * Instantiate an applet class by name + * + * @param appletClassName The name of the applet class to resolve + * + * @return The instantiated applet class + * + * @throws ClassNotFoundException if the provided class name cannot be found + * @throws NoSuchMethodException if the no-args constructor cannot be found + * @throws IllegalAccessException if the constructor cannot be accessed via method handles + * @throws Throwable any exceptions from the class's constructor + */ + public static Applet createAppletClass(String appletClassName) throws Throwable { + Class appletClass = ClassLoader.getSystemClassLoader().loadClass(appletClassName); + + MethodHandle appletConstructor = MethodHandles.lookup().findConstructor(appletClass, MethodType.methodType(void.class)); + return (Applet) appletConstructor.invoke(); + } + + /** + * Finds a field that looks like a Minecraft base folder in a supplied class + * + * @param minecraftMainClass the class to scan + * + * @return The found field. + */ + public static Field getMinecraftGameDirField(Class minecraftMainClass) { + LOGGER.fine("Resolving minecraft game directory field"); + // Field we're looking for is always + // private static File obfuscatedName = null; + for (Field field : minecraftMainClass.getDeclaredFields()) { + // Has to be File + if (field.getType() != File.class) { + continue; + } + + int fieldModifiers = field.getModifiers(); + + + // Must be static + if (!Modifier.isStatic(fieldModifiers)) { + LOGGER.log(Level.FINE, "Rejecting field {0} because it is not static", field.getName()); + continue; + } + + // Must be private + if (!Modifier.isPrivate(fieldModifiers)) { + LOGGER.log(Level.FINE, "Rejecting field {0} because it is not private", field.getName()); + continue; + } + + // Must not be final + if (Modifier.isFinal(fieldModifiers)) { + LOGGER.log(Level.FINE, "Rejecting field {0} because it is final", field.getName()); + continue; + } + + LOGGER.log(Level.FINE, "Identified field {0} to match conditions for minecraft game directory field", field.getName()); + + return field; + } + + return null; + } + + /** + * Resolve main entrypoint and returns method handle for it. + *

+ * Resolves a method that matches the following signature + * + * public static void main(String[] args) { + *

+ * } + * + * + * @param entrypointClass The entrypoint class to resolve the method from + * + * @return The method handle for the resolved entrypoint + * + * @throws NoSuchMethodException If no method matching the correct signature can be found + * @throws IllegalAccessException If method handles cannot access the entrypoint + */ + public static MethodHandle findMainEntrypoint(Class entrypointClass) throws NoSuchMethodException, IllegalAccessException { + return MethodHandles.lookup().findStatic(entrypointClass, "main", MethodType.methodType(void.class, String[].class)); + } + + /** + * Resolve main entrypoint and returns method handle for it. + *

+ * Resolves a method that matches the following signature + * + * public static void main(String[] args) { + *

+ * } + * + * + * @param entrypointClassName The name of the entrypoint class to resolve the method from + * + * @return The method handle for the resolved entrypoint + * + * @throws ClassNotFoundException If a class cannot be found with the provided name + * @throws NoSuchMethodException If no method matching the correct signature can be found + * @throws IllegalAccessException If method handles cannot access the entrypoint + */ + public static MethodHandle findMainEntrypoint(String entrypointClassName) throws + ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException { + return findMainEntrypoint(ClassLoader.getSystemClassLoader().loadClass(entrypointClassName)); + } +}