Various tweaks to the Java component of the launcher

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2022-10-24 18:21:26 +01:00
parent e4e0c27e1c
commit e68dcea6bc
13 changed files with 316 additions and 237 deletions

View File

@ -3,6 +3,7 @@
* PolyMC - Minecraft Launcher * PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -647,7 +648,17 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
{ {
launchScript += "traits " + trait + "\n"; launchScript += "traits " + trait + "\n";
} }
launchScript += "launcher onesix\n";
launchScript += "launcher ";
// use legacy launcher if the traits are set
if (profile->getTraits().contains("legacyLaunch") || profile->getTraits().contains("alphaLaunch"))
launchScript += "legacy";
else
launchScript += "standard";
launchScript += "\n";
// qDebug() << "Generated launch script:" << launchScript; // qDebug() << "Generated launch script:" << launchScript;
return launchScript; return launchScript;
} }

View File

@ -51,10 +51,10 @@ It:
This means the process is essentially idle until the final command is sent. You can, for example, attach a profiler before you send it. This means the process is essentially idle until the final command is sent. You can, for example, attach a profiler before you send it.
A `legacy` and `onesix` launchers are available. A `legacy` and `standard` launchers are available.
- `legacy` is intended for use with Minecraft versions < 1.6 and is deprecated. - `legacy` is intended for use with Minecraft versions < 1.6 and is deprecated.
- `onesix` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title). - `standard` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title).
Example (some parts have been censored): Example (some parts have been censored):
@ -132,7 +132,7 @@ ext /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl-platform/2.9.1/l
ext /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar ext /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar
natives /home/peterix/minecraft/FTB/17ForgeTest/natives natives /home/peterix/minecraft/FTB/17ForgeTest/natives
cp /home/peterix/minecraft/FTB/versions/1.7.10/1.7.10.jar cp /home/peterix/minecraft/FTB/versions/1.7.10/1.7.10.jar
launcher onesix launcher standard
``` ```
Available under `GPL-3.0-only` (with classpath exception), sublicensed from its original `Apache-2.0` codebase Available under `GPL-3.0-only` (with classpath exception), sublicensed from its original `Apache-2.0` codebase

View File

@ -10,7 +10,9 @@ set(SRC
org/prismlauncher/EntryPoint.java org/prismlauncher/EntryPoint.java
org/prismlauncher/Launcher.java org/prismlauncher/Launcher.java
org/prismlauncher/LauncherFactory.java org/prismlauncher/LauncherFactory.java
org/prismlauncher/impl/OneSixLauncher.java org/prismlauncher/impl/AbstractLauncher.java
org/prismlauncher/impl/LegacyLauncher.java
org/prismlauncher/impl/StandardLauncher.java
org/prismlauncher/applet/LegacyFrame.java org/prismlauncher/applet/LegacyFrame.java
org/prismlauncher/exception/ParameterNotFoundException.java org/prismlauncher/exception/ParameterNotFoundException.java
org/prismlauncher/exception/ParseException.java org/prismlauncher/exception/ParseException.java

View File

@ -81,33 +81,35 @@ public final class EntryPoint {
} }
private Action parseLine(String inData) throws ParseException { private Action parseLine(String inData) throws ParseException {
String[] tokens = inData.split("\\s+", 2); if (inData.length() == 0)
if (tokens.length == 0)
throw new ParseException("Unexpected empty string!"); throw new ParseException("Unexpected empty string!");
switch (tokens[0]) { String first = inData;
case "launch": { String second = null;
return Action.Launch; int splitPoint = inData.indexOf(' ');
if (splitPoint != -1) {
first = first.substring(0, splitPoint);
second = inData.substring(splitPoint + 1);
} }
case "abort": { switch (first) {
return Action.Abort; case "launch":
} return Action.LAUNCH;
case "abort":
default: { return Action.ABORT;
if (tokens.length != 2) default:
if (second == null || second.isEmpty())
throw new ParseException("Error while parsing:" + inData); throw new ParseException("Error while parsing:" + inData);
params.add(tokens[0], tokens[1]); params.add(first, second);
return Action.Proceed; return Action.PROCEED;
}
} }
} }
public int listen() { public int listen() {
Action action = Action.Proceed; Action action = Action.PROCEED;
try (BufferedReader reader = new BufferedReader(new InputStreamReader( try (BufferedReader reader = new BufferedReader(new InputStreamReader(
System.in, System.in,
@ -115,21 +117,21 @@ public final class EntryPoint {
))) { ))) {
String line; String line;
while (action == Action.Proceed) { while (action == Action.PROCEED) {
if ((line = reader.readLine()) != null) { if ((line = reader.readLine()) != null) {
action = parseLine(line); action = parseLine(line);
} else { } else {
action = Action.Abort; action = Action.ABORT;
} }
} }
} catch (IOException | ParseException e) { } catch (IOException | ParseException e) {
LOGGER.log(Level.SEVERE, "Launcher ABORT due to exception:", e); LOGGER.log(Level.SEVERE, "Launcher abort due to exception:", e);
return 1; return 1;
} }
// Main loop // Main loop
if (action == Action.Abort) { if (action == Action.ABORT) {
LOGGER.info("Launch aborted by the launcher."); LOGGER.info("Launch aborted by the launcher.");
return 1; return 1;
@ -138,7 +140,7 @@ public final class EntryPoint {
try { try {
Launcher launcher = Launcher launcher =
LauncherFactory LauncherFactory
.getInstance() .INSTANCE
.createLauncher(params); .createLauncher(params);
launcher.launch(); launcher.launch();
@ -148,7 +150,7 @@ public final class EntryPoint {
LOGGER.log(Level.SEVERE, "Wrong argument.", e); LOGGER.log(Level.SEVERE, "Wrong argument.", e);
return 1; return 1;
} catch (Exception e) { } catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e);
return 1; return 1;
@ -156,9 +158,9 @@ public final class EntryPoint {
} }
private enum Action { private enum Action {
Proceed, PROCEED,
Launch, LAUNCH,
Abort ABORT
} }
} }

View File

@ -18,6 +18,6 @@ package org.prismlauncher;
public interface Launcher { public interface Launcher {
void launch() throws Exception; void launch() throws Throwable;
} }

View File

@ -35,7 +35,8 @@
package org.prismlauncher; package org.prismlauncher;
import org.prismlauncher.impl.OneSixLauncher; import org.prismlauncher.impl.LegacyLauncher;
import org.prismlauncher.impl.StandardLauncher;
import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.Parameters;
import java.util.HashMap; import java.util.HashMap;
@ -43,15 +44,21 @@ import java.util.Map;
public final class LauncherFactory { public final class LauncherFactory {
private static final LauncherFactory INSTANCE = new LauncherFactory(); public static final LauncherFactory INSTANCE = new LauncherFactory();
private final Map<String, LauncherProvider> launcherRegistry = new HashMap<>(); private final Map<String, LauncherProvider> launcherRegistry = new HashMap<>();
private LauncherFactory() { private LauncherFactory() {
launcherRegistry.put("onesix", new LauncherProvider() { launcherRegistry.put("standard", new LauncherProvider() {
@Override @Override
public Launcher provide(Parameters parameters) { public Launcher provide(Parameters parameters) {
return new OneSixLauncher(parameters); return new StandardLauncher(parameters);
}
});
launcherRegistry.put("legacy", new LauncherProvider() {
@Override
public Launcher provide(Parameters parameters) {
return new LegacyLauncher(parameters);
} }
}); });
} }
@ -67,10 +74,6 @@ public final class LauncherFactory {
return launcherProvider.provide(parameters); return launcherProvider.provide(parameters);
} }
public static LauncherFactory getInstance() {
return INSTANCE;
}
public interface LauncherProvider { public interface LauncherProvider {
Launcher provide(Parameters parameters); Launcher provide(Parameters parameters);

View File

@ -34,6 +34,7 @@ import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@SuppressWarnings("removal")
public final class LegacyFrame extends Frame { public final class LegacyFrame extends Frame {
private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); private static final Logger LOGGER = Logger.getLogger("LegacyFrame");
@ -105,7 +106,7 @@ public final class LegacyFrame extends Frame {
appletWrap.setParameter("username", user); appletWrap.setParameter("username", user);
appletWrap.setParameter("sessionid", session); appletWrap.setParameter("sessionid", session);
appletWrap.setParameter("stand-alone", "true"); // Show the quit button. appletWrap.setParameter("stand-alone", "true"); // Show the quit button. TODO: why won't this work?
appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work. appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work.
appletWrap.setParameter("demo", isDemo ? "true" : "false"); appletWrap.setParameter("demo", isDemo ? "true" : "false");
appletWrap.setParameter("fullscreen", "false"); appletWrap.setParameter("fullscreen", "false");

View File

@ -0,0 +1,95 @@
/* Copyright 2012-2021 MultiMC Contributors
*
* 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.prismlauncher.impl;
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;
import org.prismlauncher.Launcher;
import org.prismlauncher.exception.ParseException;
import org.prismlauncher.utils.Parameters;
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<String> 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 ClassLoader classLoader;
public AbstractLauncher(Parameters params) {
classLoader = ClassLoader.getSystemClassLoader();
mcParams = params.allSafe("param", new ArrayList<String>());
mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft");
serverAddress = params.firstSafe("serverAddress", null);
serverPort = params.firstSafe("serverPort", null);
String windowParams = params.firstSafe("windowParams", null);
if ("max".equals(windowParams) || windowParams == null) {
maximize = windowParams != null;
width = DEFAULT_WINDOW_WIDTH;
height = DEFAULT_WINDOW_HEIGHT;
} else {
maximize = false;
int byIndex = windowParams.indexOf('x');
if (byIndex != -1) {
try {
width = Integer.parseInt(windowParams.substring(0, byIndex));
height = Integer.parseInt(windowParams.substring(byIndex + 1));
return;
} catch(NumberFormatException pass) {
}
}
throw new ParseException("Invalid window size parameter value: " + windowParams);
}
}
protected Class<?> loadMain() throws ClassNotFoundException {
return classLoader.loadClass(mainClass);
}
protected void loadAndInvokeMain() throws Throwable, ClassNotFoundException {
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]));
}
}

View File

@ -0,0 +1,104 @@
/* Copyright 2012-2021 MultiMC Contributors
*
* 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.prismlauncher.impl;
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.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.prismlauncher.applet.LegacyFrame;
import org.prismlauncher.utils.Parameters;
import org.prismlauncher.utils.Utils;
@SuppressWarnings("removal")
public final class LegacyLauncher extends AbstractLauncher {
private static final Logger LOGGER = Logger.getLogger("LegacyLauncher");
private final String user, session;
private final String title;
private final String appletClass;
private final boolean noApplet;
private final String cwd;
public LegacyLauncher(Parameters params) {
super(params);
user = params.first("userName");
session = params.first("sessionId");
title = params.firstSafe("windowTitle", "Minecraft");
appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet");
List<String> traits = params.allSafe("traits", Collections.<String>emptyList());
noApplet = traits.contains("noapplet");
cwd = System.getProperty("user.dir");
}
@Override
public void launch() throws Throwable {
Class<?> main = loadMain();
Field gameDirField = Utils.getMinecraftGameDirField(main);
if (gameDirField == null) {
LOGGER.warning("Could not find Mineraft path field.");
} else {
gameDirField.setAccessible(true);
gameDirField.set(null, new File(cwd));
}
if (!noApplet) {
LOGGER.info("Launching with 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);
window.start(
user,
session,
width,
height,
maximize,
serverAddress,
serverPort,
mcParams.contains("--demo")
);
return;
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e);
LOGGER.warning("Falling back to using main class.");
}
}
invokeMain(main);
}
}

View File

@ -1,190 +0,0 @@
/* Copyright 2012-2021 MultiMC Contributors
*
* 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.prismlauncher.impl;
import org.prismlauncher.Launcher;
import org.prismlauncher.applet.LegacyFrame;
import org.prismlauncher.utils.Parameters;
import org.prismlauncher.utils.Utils;
import java.applet.Applet;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class OneSixLauncher implements Launcher {
private static final int DEFAULT_WINDOW_WIDTH = 854;
private static final int DEFAULT_WINDOW_HEIGHT = 480;
private static final Logger LOGGER = Logger.getLogger("OneSixLauncher");
// parameters, separated from ParamBucket
private final List<String> mcParams;
private final List<String> traits;
private final String appletClass;
private final String mainClass;
private final String userName, sessionId;
private final String windowTitle;
// secondary parameters
private final int winSizeW;
private final int winSizeH;
private final boolean maximize;
private final String cwd;
private final String serverAddress;
private final String serverPort;
private final ClassLoader classLoader;
public OneSixLauncher(Parameters params) {
classLoader = ClassLoader.getSystemClassLoader();
mcParams = params.allSafe("param", new ArrayList<String>());
mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft");
appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet");
traits = params.allSafe("traits", new ArrayList<String>());
userName = params.first("userName");
sessionId = params.first("sessionId");
windowTitle = params.firstSafe("windowTitle", "Minecraft");
serverAddress = params.firstSafe("serverAddress", null);
serverPort = params.firstSafe("serverPort", null);
cwd = System.getProperty("user.dir");
String windowParams = params.firstSafe("windowParams", null);
if (windowParams != null) {
String[] dimStrings = windowParams.split("x");
if (windowParams.equalsIgnoreCase("max")) {
maximize = true;
winSizeW = DEFAULT_WINDOW_WIDTH;
winSizeH = DEFAULT_WINDOW_HEIGHT;
} else if (dimStrings.length == 2) {
maximize = false;
winSizeW = Integer.parseInt(dimStrings[0]);
winSizeH = Integer.parseInt(dimStrings[1]);
} else {
throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams);
}
} else {
maximize = false;
winSizeW = DEFAULT_WINDOW_WIDTH;
winSizeH = DEFAULT_WINDOW_HEIGHT;
}
}
private void invokeMain(Class<?> mainClass) throws Exception {
Method method = mainClass.getMethod("main", String[].class);
method.invoke(null, (Object) mcParams.toArray(new String[0]));
}
private void legacyLaunch() throws Exception {
// Get the Minecraft Class and set the base folder
Class<?> minecraftClass = classLoader.loadClass(mainClass);
Field baseDirField = Utils.getMinecraftBaseDirField(minecraftClass);
if (baseDirField == null) {
LOGGER.warning("Could not find Minecraft path field.");
} else {
baseDirField.setAccessible(true);
baseDirField.set(null, new File(cwd));
}
System.setProperty("minecraft.applet.TargetDirectory", cwd);
if (!traits.contains("noapplet")) {
LOGGER.info("Launching with applet wrapper...");
try {
Class<?> mcAppletClass = classLoader.loadClass(appletClass);
Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance();
LegacyFrame mcWindow = new LegacyFrame(windowTitle, mcApplet);
mcWindow.start(
userName,
sessionId,
winSizeW,
winSizeH,
maximize,
serverAddress,
serverPort,
mcParams.contains("--demo")
);
return;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Applet wrapper failed: ", e);
LOGGER.warning("Falling back to using main class.");
}
}
invokeMain(minecraftClass);
}
private void launchWithMainClass() throws Exception {
// window size, title and state, onesix
// FIXME: there is no good way to maximize the minecraft window in onesix.
// the following often breaks linux screen setups
// mcparams.add("--fullscreen");
if (!maximize) {
mcParams.add("--width");
mcParams.add(Integer.toString(winSizeW));
mcParams.add("--height");
mcParams.add(Integer.toString(winSizeH));
}
if (serverAddress != null) {
mcParams.add("--server");
mcParams.add(serverAddress);
mcParams.add("--port");
mcParams.add(serverPort);
}
invokeMain(classLoader.loadClass(mainClass));
}
@Override
public void launch() throws Exception {
if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch")) {
// legacy launch uses the applet wrapper
legacyLaunch();
} else {
// normal launch just calls main()
launchWithMainClass();
}
}
}

View File

@ -0,0 +1,51 @@
/* Copyright 2012-2021 MultiMC Contributors
*
* 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.prismlauncher.impl;
import org.prismlauncher.utils.Parameters;
public final class StandardLauncher extends AbstractLauncher {
public StandardLauncher(Parameters params) {
super(params);
}
@Override
public void launch() throws Throwable {
// window size, title and state
// FIXME: there is no good way to maximize the minecraft window from here.
// 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));
}
if (serverAddress != null) {
mcParams.add("--server");
mcParams.add(serverAddress);
mcParams.add("--port");
mcParams.add(serverPort);
}
loadAndInvokeMain();
}
}

View File

@ -25,22 +25,22 @@ import java.util.Map;
public final class Parameters { public final class Parameters {
private final Map<String, List<String>> paramsMap = new HashMap<>(); private final Map<String, List<String>> map = new HashMap<>();
public void add(String key, String value) { public void add(String key, String value) {
List<String> params = paramsMap.get(key); List<String> params = map.get(key);
if (params == null) { if (params == null) {
params = new ArrayList<>(); params = new ArrayList<>();
paramsMap.put(key, params); map.put(key, params);
} }
params.add(value); params.add(value);
} }
public List<String> all(String key) throws ParameterNotFoundException { public List<String> all(String key) throws ParameterNotFoundException {
List<String> params = paramsMap.get(key); List<String> params = map.get(key);
if (params == null) if (params == null)
throw new ParameterNotFoundException(key); throw new ParameterNotFoundException(key);
@ -49,7 +49,7 @@ public final class Parameters {
} }
public List<String> allSafe(String key, List<String> def) { public List<String> allSafe(String key, List<String> def) {
List<String> params = paramsMap.get(key); List<String> params = map.get(key);
if (params == null || params.isEmpty()) if (params == null || params.isEmpty())
return def; return def;
@ -67,7 +67,7 @@ public final class Parameters {
} }
public String firstSafe(String key, String def) { public String firstSafe(String key, String def) {
List<String> params = paramsMap.get(key); List<String> params = map.get(key);
if (params == null || params.isEmpty()) if (params == null || params.isEmpty())
return def; return def;

View File

@ -29,7 +29,7 @@ public final class Utils {
* *
* @param clazz the class to scan * @param clazz the class to scan
*/ */
public static Field getMinecraftBaseDirField(Class<?> clazz) { public static Field getMinecraftGameDirField(Class<?> clazz) {
for (Field f : clazz.getDeclaredFields()) { for (Field f : clazz.getDeclaredFields()) {
// Has to be File // Has to be File
if (f.getType() != File.class) if (f.getType() != File.class)