From 627a17ce717cc5bd8c52436907deecb58d91c5ac Mon Sep 17 00:00:00 2001 From: leego Date: Wed, 8 Feb 2023 16:02:27 +0800 Subject: [PATCH 1/6] Support jar mods --- .../bootstrap/ProjectXBootstrap.java | 219 ++++++++++++++++++ .../lucasallegri/launcher/LauncherApp.java | 1 + .../launcher/LauncherDigester.java | 160 +++++++++++++ .../lucasallegri/launcher/mods/ModLoader.java | 22 +- .../java/com/lucasallegri/util/FileUtil.java | 15 ++ 5 files changed, 407 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java create mode 100644 src/main/java/com/lucasallegri/launcher/LauncherDigester.java diff --git a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java new file mode 100644 index 00000000..12382687 --- /dev/null +++ b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java @@ -0,0 +1,219 @@ +package com.lucasallegri.bootstrap; + +import javax.swing.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.zip.ZipFile; + +/** + * Bootstraps instead of {@literal com.threerings.projectx.client.ProjectXApp} for loading mods and language packs. + *

+ * Makes sure the "META-INF/MANIFEST.MF" is included in each mod jars, + * and the main class must be specified. + * + * @author Leego Yih + */ +public class ProjectXBootstrap { + private static final String USER_DIR = System.getProperty("user.dir"); + private static final String CODE_MODS_DIR = USER_DIR + "/code-mods/"; + private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; + private static final String MAIN_CLASS_KEY = "Main-Class:"; + private static final String NAME_KEY = "Name:"; + + public static void main(String[] args) throws Exception { + System.setProperty("com.threerings.io.enumPolicy", "ORDINAL"); + // ak.gm() + if ((boolean) invokeMethod("com.samskivert.util.ak", "gm", null, new Object[0])) { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } + // X.dM("projectx.log"); + invokeMethod("com.threerings.util.X", "dM", null, new Object[]{"projectx.log"}); + + loadJarMods(); + + String ticket = null; + String password; + for (int i = 0; i < args.length; ++i) { + if ((password = args[i]).startsWith("+connect=")) { + ticket = password; + // com.samskivert.util.c.b(args, i, 1); + args = (String[]) invokeMethod("com.samskivert.util.c", "b", null, new Object[]{args, i, 1}); + break; + } + } + String username = args.length > 0 ? args[0] : System.getProperty("username"); + password = args.length > 1 ? args[1] : System.getProperty("password"); + boolean encrypted = Boolean.getBoolean("encrypted"); + String knight = args.length > 2 ? args[2] : System.getProperty("knight"); + String action = args.length > 3 ? args[3] : System.getProperty("action"); + String arg = args.length > 4 ? args[4] : System.getProperty("arg"); + String sessionKey = System.getProperty("sessionKey"); + + Constructor constructor = Class.forName("com.threerings.projectx.client.ProjectXApp") + .getDeclaredConstructor(String.class, String.class, boolean.class, String.class, String.class, String.class, String.class, String.class); + constructor.setAccessible(true); + Object app = constructor.newInstance(username, password, encrypted, knight, action, arg, sessionKey, ticket); + invokeMethod("com.threerings.projectx.client.ProjectXApp", "startup", app, new Object[0]); + } + + static void loadJarMods() { + // Read disabled jar mods from KnightLauncher.properties + Set disabledJarMods = new HashSet<>(); + String disabledJarModsString = getConfigValue("modloader.disabledMods"); + if (disabledJarModsString != null && disabledJarModsString.length() > 0) { + for (String disabledJarMod : disabledJarModsString.split(",")) { + disabledJarMod = disabledJarMod.trim(); + if (disabledJarMod.length() > 0) { + disabledJarMods.add(disabledJarMod); + } + } + } + // Obtain the mod files in the "/code-mods/" directory + File codeModsDir = new File(CODE_MODS_DIR); + if (!codeModsDir.exists()) { + return; + } + File[] files = codeModsDir.listFiles(); + if (files == null || files.length == 0) { + return; + } + List jars = new ArrayList(files.length); + for (File file : files) { + String filename = file.getName(); + if (filename.endsWith(".jar") + && !disabledJarMods.contains(filename)) { + jars.add(file); + } + } + if (jars.isEmpty()) { + return; + } + loadJars(jars); + loadClasses(jars); + } + + static void loadJars(List jars) { + // TODO Compatible with more versions of the JDK + Method method; + try { + method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + } catch (Exception e) { + e.printStackTrace(); + return; + } + URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + boolean accessible = method.isAccessible(); + method.setAccessible(true); + for (File jar : jars) { + try { + method.invoke(classLoader, jar.toURI().toURL()); + System.out.println("Loaded jar '" + jar.getName() + "'"); + } catch (Exception e) { + System.out.println("Failed to load jar '" + jar.getName() + "'"); + e.printStackTrace(); + } + } + method.setAccessible(accessible); + } + + static void loadClasses(List jars) { + for (File jar : jars) { + String manifest = readZip(jar, MANIFEST_PATH); + if (manifest == null || manifest.length() == 0) { + System.out.println("Failed to read '" + MANIFEST_PATH + "' from '" + jar.getName() + "'"); + continue; + } + String className = null; + String modName = null; + for (String item : manifest.split("\n")) { + if (item.startsWith(MAIN_CLASS_KEY)) { + className = item.replace(MAIN_CLASS_KEY, "").trim(); + } else if (item.startsWith(NAME_KEY)) { + modName = item.replace(NAME_KEY, "").trim(); + } + } + if (className == null || className.length() == 0) { + System.out.println("Failed to read 'Main-Class' from '" + jar.getName() + "'"); + continue; + } + if (modName == null) { + modName = jar.getName(); + } + System.out.println("Mod '" + modName + "' initializing"); + try { + Class.forName(className); + System.out.println("Mod '" + modName + "' initialized"); + } catch (Exception e) { + System.out.println("Failed to load mod '" + modName + "'"); + e.printStackTrace(); + } + } + } + + static String readZip(File file, String entry) { + StringBuilder sb = new StringBuilder(); + try { + ZipFile zip = new ZipFile(file); + InputStream is = zip.getInputStream(zip.getEntry(entry)); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String s; + while ((s = reader.readLine()) != null) { + sb.append(s).append("\n"); + } + reader.close(); + zip.close(); + return sb.toString(); + } catch (Exception e) { + System.out.println("Failed to read '" + file.getName() + "'"); + e.printStackTrace(); + return null; + } + } + + static String getConfigValue(String key) { + Properties _prop = new Properties(); + String value; + try (InputStream is = Files.newInputStream(Paths.get(System.getProperty("user.dir") + File.separator + "KnightLauncher.properties"))) { + _prop.load(is); + value = _prop.getProperty(key); + return value; + } catch (IOException ignored) { + } + return null; + } + + static Object invokeMethod(String className, String methodName, Object object, Object[] args) throws Exception { + Class clazz = Class.forName(className); + Method[] methods = clazz.getDeclaredMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(methodName)) { + method.setAccessible(true); + return method.invoke(object, args); + } + } + methods = clazz.getMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(methodName)) { + method.setAccessible(true); + return method.invoke(object, args); + } + } + throw new NoSuchMethodException(methodName); + } +} diff --git a/src/main/java/com/lucasallegri/launcher/LauncherApp.java b/src/main/java/com/lucasallegri/launcher/LauncherApp.java index e85be3f9..bc9568fe 100644 --- a/src/main/java/com/lucasallegri/launcher/LauncherApp.java +++ b/src/main/java/com/lucasallegri/launcher/LauncherApp.java @@ -64,6 +64,7 @@ public LauncherApp () { DiscordRPC.getInstance().start(); KeyboardController.start(); checkDirectories(); + LauncherDigester.doDigest(); if (SystemUtil.isWindows()) checkShortcut(); } diff --git a/src/main/java/com/lucasallegri/launcher/LauncherDigester.java b/src/main/java/com/lucasallegri/launcher/LauncherDigester.java new file mode 100644 index 00000000..d4802496 --- /dev/null +++ b/src/main/java/com/lucasallegri/launcher/LauncherDigester.java @@ -0,0 +1,160 @@ +package com.lucasallegri.launcher; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.security.MessageDigest; + +/** + * @author Leego Yih + */ +public class LauncherDigester { + public static final String KL_JAR_PATH = "/KnightLauncher.jar"; + public static final String KL_JARV_PATH = "/KnightLauncher.jarv"; + public static final String GETDOWN_PATH = "/getdown.txt"; + public static final String DIGEST_PATH = "/digest.txt"; + public static final String MAGIC_HEAD = "# Customized by KnightLauncher"; + public static final String GETDOWN_PROJECTXAPP_CLASS = "class = com.threerings.projectx.client.ProjectXApp"; + public static final String GETDOWN_PROJECTXAPP_CLIENT_CLASS = "client.class = com.threerings.projectx.client.ProjectXApp"; + public static final String GETDOWN_BOOTSTRAP_CLASS = "class = com.lucasallegri.bootstrap.ProjectXBootstrap"; + public static final String GETDOWN_BOOTSTRAP_CLIENT_CLASS = "client.class = com.lucasallegri.bootstrap.ProjectXBootstrap"; + public static final String GETDOWN_KL_JAR = "code = KnightLauncher.jar"; + + public static void doDigest() { + try { + // Guarantee that the files exists + File klJarFile = new File(LauncherGlobals.USER_DIR + KL_JAR_PATH); + File klJarvFile = new File(LauncherGlobals.USER_DIR + KL_JARV_PATH); + File getdownFile = new File(LauncherGlobals.USER_DIR + GETDOWN_PATH); + File digestFile = new File(LauncherGlobals.USER_DIR + DIGEST_PATH); + String getdownContent = readFile(getdownFile).trim(); + String digestContent = readFile(digestFile).trim(); + // Build a new "getdown.txt" file if it has not been modified by KL + if (!getdownContent.startsWith(MAGIC_HEAD)) { + getdownFile.renameTo(new File(getdownFile.getAbsoluteFile() + ".bak")); + getdownContent = getdownContent + .replace("\n"+GETDOWN_PROJECTXAPP_CLIENT_CLASS, "\n#" + GETDOWN_PROJECTXAPP_CLIENT_CLASS) + .replace("\n"+GETDOWN_PROJECTXAPP_CLASS, "\n#" + GETDOWN_PROJECTXAPP_CLASS); + StringBuilder sb = new StringBuilder() + .append(MAGIC_HEAD).append("\n") + .append(getdownContent).append("\n\n") + .append("# KnightLauncher resources").append("\n") + .append(GETDOWN_KL_JAR).append("\n") + .append(GETDOWN_BOOTSTRAP_CLASS).append("\n") + .append(GETDOWN_BOOTSTRAP_CLIENT_CLASS).append("\n"); + writeFile(getdownFile, sb.toString()); + } + // For Windows + if (!klJarvFile.exists()) { + klJarvFile.createNewFile(); + } + // Calculate the MD5 of the files + String klMD5 = md5(klJarFile.getAbsolutePath()); + String getdownMD5 = md5(getdownFile.getAbsolutePath()); + // Build a new "digest.txt" file from the original one + digestContent = digestContent.trim(); + digestContent = digestContent.replaceFirst("digest\\.txt = \\S+", ""); + digestContent = digestContent.replaceFirst("getdown\\.txt = \\S+\n", "getdown.txt = " + getdownMD5 + "\n"); + if (digestContent.indexOf("KnightLauncher.jar") > 0) { + digestContent = digestContent.replaceFirst("KnightLauncher\\.jar = \\S+\n", "KnightLauncher.jar = " + klMD5 + "\n"); + } else { + digestContent = digestContent + "KnightLauncher.jar = " + klMD5 + "\n"; + } + // Append the final MD5 to the end + String digestMD5 = md5(digestContent.getBytes("UTF-8")); + digestContent = digestContent + "digest.txt = " + digestMD5 + "\n"; + writeFile(digestFile, digestContent); + Log.log.info(String.format("\nKnightLauncher.jar: %s\ngetdown.txt: %s\ndigest.txt: %s\n", klMD5, getdownMD5, digestMD5)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static String readFile(File file) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(file)); + String s; + while ((s = reader.readLine()) != null) { + sb.append(s).append("\n"); + } + reader.close(); + return sb.toString(); + } + + static void writeFile(File file, String s) throws IOException { + FileWriter writer = new FileWriter(file); + writer.write(s); + writer.flush(); + writer.close(); + } + + static String md5(String path) throws Exception { + File file = new File(path); + byte[] data = new byte[(int) file.length()]; + FileInputStream fis = new FileInputStream(file); + fis.read(data); + fis.close(); + return md5(data); + } + + static String md5(byte[] data) throws Exception { + byte[] hash = MessageDigest.getInstance("MD5").digest(data); + return encodeHex(hash); + } + + /** Table for byte to hex string translation. */ + static final char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + /** Table for HEX to DEC byte translation. */ + static final int[] DEC = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15}; + + static int getDec(int index) { + // Fast for correct values, slower for incorrect ones + try { + return DEC[index - 48]; + } catch (ArrayIndexOutOfBoundsException e) { + return -1; + } + } + + static byte getHex(int index) { + return (byte) HEX[index]; + } + + static String encodeHex(byte[] bytes) { + if (null == bytes) { + return null; + } + int i = 0; + char[] chars = new char[bytes.length << 1]; + for (byte b : bytes) { + chars[i++] = HEX[(b & 0xf0) >> 4]; + chars[i++] = HEX[b & 0x0f]; + } + return new String(chars); + } + + static byte[] decodeHex(String input) { + if (input == null) { + return null; + } + if ((input.length() & 1) == 1) { + // Odd number of characters + throw new IllegalArgumentException("Odd digits"); + } + char[] inputChars = input.toCharArray(); + byte[] result = new byte[input.length() >> 1]; + for (int i = 0; i < result.length; i++) { + int upperNibble = getDec(inputChars[2 * i]); + int lowerNibble = getDec(inputChars[2 * i + 1]); + if (upperNibble < 0 || lowerNibble < 0) { + // Non hex character + throw new IllegalArgumentException("Non hex"); + } + result[i] = (byte) ((upperNibble << 4) + lowerNibble); + } + return result; + } +} diff --git a/src/main/java/com/lucasallegri/launcher/mods/ModLoader.java b/src/main/java/com/lucasallegri/launcher/mods/ModLoader.java index 65ff2cbd..d4d84081 100644 --- a/src/main/java/com/lucasallegri/launcher/mods/ModLoader.java +++ b/src/main/java/com/lucasallegri/launcher/mods/ModLoader.java @@ -34,22 +34,23 @@ public static void checkInstalled() { if (getModCount() > 0) clearModList(); // Append all .zip and .jar files inside the mod folder into an ArrayList. - List rawFiles = FileUtil.fileNamesInDirectory(LauncherGlobals.USER_DIR + "/mods/", ".zip"); - rawFiles.addAll(FileUtil.fileNamesInDirectory(LauncherGlobals.USER_DIR + "/code-mods/", ".jar")); + List rawFiles = FileUtil.filesInDirectory(LauncherGlobals.USER_DIR + "/mods/", ".zip"); + rawFiles.addAll(FileUtil.filesInDirectory(LauncherGlobals.USER_DIR + "/code-mods/", ".jar")); - for (String file : rawFiles) { + for (File file : rawFiles) { JSONObject modJson; try { - modJson = new JSONObject(Compressor.readFileInsideZip(LauncherGlobals.USER_DIR + "/mods/" + file, "mod.json")).getJSONObject("mod"); + modJson = new JSONObject(Compressor.readFileInsideZip(file.getAbsolutePath(), "mod.json")).getJSONObject("mod"); } catch (Exception e) { modJson = null; } + String fileName = file.getName(); Mod mod = null; - if (file.endsWith("zip")) { - mod = new ZipMod(file); - } else if (file.endsWith("jar")) { - mod = new JarMod(file); + if (fileName.endsWith("zip")) { + mod = new ZipMod(fileName); + } else if (fileName.endsWith("jar")) { + mod = new JarMod(fileName); } if (mod != null && modJson != null) { @@ -63,8 +64,8 @@ public static void checkInstalled() { mod.wasAdded(); // Compute a hash for each mod file and check that it matches on every execution, if it doesn't, then rebuild. - String hash = Compressor.getZipHash(LauncherGlobals.USER_DIR + "/mods/" + file); - String hashFilePath = LauncherGlobals.USER_DIR + "/mods/" + mod.getFileName() + ".hash"; + String hash = Compressor.getZipHash(file.getAbsolutePath()); + String hashFilePath = file.getAbsolutePath() + ".hash"; if (FileUtil.fileExists(hashFilePath)) { try { @@ -124,6 +125,7 @@ public static void mount() { mountRequired = false; ProgressBar.finishTask(); + LauncherDigester.doDigest(); LauncherGUI.launchButton.setEnabled(true); } diff --git a/src/main/java/com/lucasallegri/util/FileUtil.java b/src/main/java/com/lucasallegri/util/FileUtil.java index cabf9ba7..00881693 100644 --- a/src/main/java/com/lucasallegri/util/FileUtil.java +++ b/src/main/java/com/lucasallegri/util/FileUtil.java @@ -92,6 +92,21 @@ public static List fileNamesInDirectory(String dir, String ext) { return fileNames; } + public static List filesInDirectory(String dir, String ext) { + + File folder = new File(dir); + File[] fileList = folder.listFiles(); + List files = new ArrayList(); + + for (int i = 0; i < fileList.length; i++) { + if (fileList[i].isDirectory() == false && fileList[i].toString().endsWith(ext)) { + files.add(fileList[i]); + } + } + + return files; + } + public static boolean fileExists(String path) { File file = new File(path); return file.exists(); From c8f89ad085b3fa0ad481314861ad0341f779acf0 Mon Sep 17 00:00:00 2001 From: leego Date: Thu, 9 Feb 2023 02:40:36 +0800 Subject: [PATCH 2/6] Use `ProjectXBootstrap#loadConnectionSettings` instead of `GameSettings#loadConnectionSettings` --- .../bootstrap/ProjectXBootstrap.java | 62 ++++++++++++++----- .../launcher/settings/GameSettings.java | 7 ++- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java index 12382687..6bd32b98 100644 --- a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java +++ b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java @@ -3,18 +3,20 @@ import javax.swing.*; import java.io.BufferedReader; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.zip.ZipFile; @@ -33,6 +35,7 @@ public class ProjectXBootstrap { private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; private static final String MAIN_CLASS_KEY = "Main-Class:"; private static final String NAME_KEY = "Name:"; + private static final Properties configs = new Properties(); public static void main(String[] args) throws Exception { System.setProperty("com.threerings.io.enumPolicy", "ORDINAL"); @@ -43,6 +46,10 @@ public static void main(String[] args) throws Exception { // X.dM("projectx.log"); invokeMethod("com.threerings.util.X", "dM", null, new Object[]{"projectx.log"}); + loadConfigs(); + + loadConnectionSettings(); + loadJarMods(); String ticket = null; @@ -70,10 +77,49 @@ public static void main(String[] args) throws Exception { invokeMethod("com.threerings.projectx.client.ProjectXApp", "startup", app, new Object[0]); } + static void loadConfigs() throws Exception { + InputStream is = Files.newInputStream(Paths.get(USER_DIR + File.separator + "KnightLauncher.properties")); + configs.load(is); + is.close(); + } + + static void loadConnectionSettings() throws Exception { + // com.threerings.projectx.util.DeploymentConfig + Field configField = Class.forName("com.threerings.projectx.util.a").getDeclaredField("akf"); + configField.setAccessible(true); + Object config = configField.get(null); + // com.samskivert.util.Config + Field propsField = Class.forName("com.samskivert.util.m").getDeclaredField("AQ"); + propsField.setAccessible(true); + // deployment.properties + Properties properties = (Properties) propsField.get(config); + // Replace connection settings + Map mapping = new HashMap<>(); + mapping.put("server_host", "game.endpoint"); + mapping.put("server_ports", "game.port"); + mapping.put("datagram_ports", "game.port"); + mapping.put("key.public", "game.publicKey"); + mapping.put("client_root_url", "game.getdownURL"); + for (Map.Entry e : mapping.entrySet()) { + String newConf = configs.getProperty(e.getValue()); + if (newConf == null) { + continue; + } + newConf = newConf.trim(); + String oldConf = properties.getProperty(e.getKey()); + if (newConf.length() > 0 && !newConf.equals(oldConf)) { + properties.setProperty(e.getKey(), newConf); + System.out.println("[deployment.properties] Replace [" + e.getKey() + "] '" + oldConf + "' -> '" + newConf + "'"); + } else { + System.out.println("[deployment.properties] No change [" + e.getKey() + "] '" + oldConf + "'"); + } + } + } + static void loadJarMods() { // Read disabled jar mods from KnightLauncher.properties Set disabledJarMods = new HashSet<>(); - String disabledJarModsString = getConfigValue("modloader.disabledMods"); + String disabledJarModsString = configs.getProperty("modloader.disabledMods"); if (disabledJarModsString != null && disabledJarModsString.length() > 0) { for (String disabledJarMod : disabledJarModsString.split(",")) { disabledJarMod = disabledJarMod.trim(); @@ -184,18 +230,6 @@ static String readZip(File file, String entry) { } } - static String getConfigValue(String key) { - Properties _prop = new Properties(); - String value; - try (InputStream is = Files.newInputStream(Paths.get(System.getProperty("user.dir") + File.separator + "KnightLauncher.properties"))) { - _prop.load(is); - value = _prop.getProperty(key); - return value; - } catch (IOException ignored) { - } - return null; - } - static Object invokeMethod(String className, String methodName, Object object, Object[] args) throws Exception { Class clazz = Class.forName(className); Method[] methods = clazz.getDeclaredMethods(); diff --git a/src/main/java/com/lucasallegri/launcher/settings/GameSettings.java b/src/main/java/com/lucasallegri/launcher/settings/GameSettings.java index b06bb31e..829036c9 100644 --- a/src/main/java/com/lucasallegri/launcher/settings/GameSettings.java +++ b/src/main/java/com/lucasallegri/launcher/settings/GameSettings.java @@ -58,8 +58,6 @@ public static void load() { writer.println(Settings.gameAdditionalArgs); writer.close(); - loadConnectionSettings(); - ProgressBar.setBarValue(1); ProgressBar.finishTask(); } catch (FileNotFoundException | UnsupportedEncodingException e) { @@ -67,6 +65,11 @@ public static void load() { } } + /** + * @see com.lucasallegri.bootstrap.ProjectXBootstrap + * @deprecated No longer use the way of modifying files. + */ + @Deprecated private static void loadConnectionSettings() { try { FileUtil.extractFileWithinJar("/config/deployment.properties", LauncherGlobals.USER_DIR + "\\deployment.properties"); From 8354d066277dcb738a2141859e55e9b96be0a16d Mon Sep 17 00:00:00 2001 From: leego Date: Thu, 9 Feb 2023 10:36:28 +0800 Subject: [PATCH 3/6] Detect jvm path --- .../launcher/LauncherGlobals.java | 161 ++++++++++-------- .../launcher/mods/ModListGUI.java | 3 + 2 files changed, 89 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/lucasallegri/launcher/LauncherGlobals.java b/src/main/java/com/lucasallegri/launcher/LauncherGlobals.java index b0ce6d6d..a2a419cc 100644 --- a/src/main/java/com/lucasallegri/launcher/LauncherGlobals.java +++ b/src/main/java/com/lucasallegri/launcher/LauncherGlobals.java @@ -25,81 +25,92 @@ public class LauncherGlobals { public static final String RPC_CLIENT_ID = "626524043209867274"; + public static final String[] GETDOWN_ARGS; + public static final String[] GETDOWN_ARGS_WIN; + public static final String[] ALT_CLIENT_ARGS; + public static final String[] ALT_CLIENT_ARGS_WIN; - public static final String[] GETDOWN_ARGS = { - "./java/bin/java", - "-Dsun.java2d.d3d=false", - "-Dcheck_unpacked=true", - "-jar", - "./getdown-pro.jar", - ".", - "client" - }; - - public static final String[] GETDOWN_ARGS_WIN = { - ".\\java_vm\\bin\\java.exe", - "-Dsun.java2d.d3d=false", - "-Dcheck_unpacked=true", - "-jar", - USER_DIR + File.separator + "getdown-pro.jar", - ".", - "client" - }; - - public static final String[] ALT_CLIENT_ARGS = { - "./java/bin/java", - "-classpath", - USER_DIR + File.separator + "./code/config.jar:" + - USER_DIR + File.separator + "./code/projectx-config.jar:" + - USER_DIR + File.separator + "./code/projectx-pcode.jar:" + - USER_DIR + File.separator + "./code/lwjgl.jar:" + - USER_DIR + File.separator + "./code/lwjgl_util.jar:" + - USER_DIR + File.separator + "./code/jinput.jar:" + - USER_DIR + File.separator + "./code/jutils.jar:" + - USER_DIR + File.separator + "./code/jshortcut.jar:" + - USER_DIR + File.separator + "./code/commons-beanutils.jar:" + - USER_DIR + File.separator + "./code/commons-digester.jar:" + - USER_DIR + File.separator + "./code/commons-logging.jar:", - "-Dcom.threerings.getdown=false", - "-Xms256M", - "-Xmx512M", - "-XX:+AggressiveOpts", - "-XX:SoftRefLRUPolicyMSPerMB=10", - "-Djava.library.path=" + USER_DIR + File.separator + "./native", - "-Dorg.lwjgl.util.NoChecks=true", - "-Dsun.java2d.d3d=false", - "-Dappdir=" + USER_DIR + File.separator + ".", - "-Dresource_dir=" + USER_DIR + File.separator + "./rsrc", - "-XX:+UseStringDeduplication", - "com.threerings.projectx.client.ProjectXApp", - }; - - public static final String[] ALT_CLIENT_ARGS_WIN = { - "./java_vm/bin/java", - "-classpath", - USER_DIR + File.separator + "./code/config.jar;" + - USER_DIR + File.separator + "./code/projectx-config.jar;" + - USER_DIR + File.separator + "./code/projectx-pcode.jar;" + - USER_DIR + File.separator + "./code/lwjgl.jar;" + - USER_DIR + File.separator + "./code/lwjgl_util.jar;" + - USER_DIR + File.separator + "./code/jinput.jar;" + - USER_DIR + File.separator + "./code/jutils.jar;" + - USER_DIR + File.separator + "./code/jshortcut.jar;" + - USER_DIR + File.separator + "./code/commons-beanutils.jar;" + - USER_DIR + File.separator + "./code/commons-digester.jar;" + - USER_DIR + File.separator + "./code/commons-logging.jar;", - "-Dcom.threerings.getdown=false", - "-Xms256M", - "-Xmx512M", - "-XX:+AggressiveOpts", - "-XX:SoftRefLRUPolicyMSPerMB=10", - "-Djava.library.path=" + USER_DIR + File.separator + "./native", - "-Dorg.lwjgl.util.NoChecks=true", - "-Dsun.java2d.d3d=false", - "-Dappdir=" + USER_DIR + File.separator + ".", - "-Dresource_dir=" + USER_DIR + File.separator + "./rsrc", - "-XX:+UseStringDeduplication", - "com.threerings.projectx.client.ProjectXApp", - }; + static { + String javaPath; + if (new File(USER_DIR + File.separator + "./java/bin/java").exists()) { + javaPath = "./java/bin/java"; + } else { + javaPath = "./java_vm/bin/java"; + } + GETDOWN_ARGS = new String[]{ + javaPath, + "-Dsun.java2d.d3d=false", + "-Dcheck_unpacked=true", + "-jar", + "./getdown-pro.jar", + ".", + "client" + }; + GETDOWN_ARGS_WIN = new String[]{ + javaPath, + "-Dsun.java2d.d3d=false", + "-Dcheck_unpacked=true", + "-jar", + USER_DIR + File.separator + "getdown-pro.jar", + ".", + "client" + }; + ALT_CLIENT_ARGS = new String[]{ + javaPath, + "-classpath", + USER_DIR + File.separator + "./code/config.jar:" + + USER_DIR + File.separator + "./code/projectx-config.jar:" + + USER_DIR + File.separator + "./code/projectx-pcode.jar:" + + USER_DIR + File.separator + "./code/lwjgl.jar:" + + USER_DIR + File.separator + "./code/lwjgl_util.jar:" + + USER_DIR + File.separator + "./code/jinput.jar:" + + USER_DIR + File.separator + "./code/jutils.jar:" + + USER_DIR + File.separator + "./code/jshortcut.jar:" + + USER_DIR + File.separator + "./code/commons-beanutils.jar:" + + USER_DIR + File.separator + "./code/commons-digester.jar:" + + USER_DIR + File.separator + "./code/commons-logging.jar:" + + USER_DIR + File.separator + "./KnightLauncher.jar:", + "-Dcom.threerings.getdown=false", + "-Xms256M", + "-Xmx512M", + "-XX:+AggressiveOpts", + "-XX:SoftRefLRUPolicyMSPerMB=10", + "-Djava.library.path=" + USER_DIR + File.separator + "./native", + "-Dorg.lwjgl.util.NoChecks=true", + "-Dsun.java2d.d3d=false", + "-Dappdir=" + USER_DIR + File.separator + ".", + "-Dresource_dir=" + USER_DIR + File.separator + "./rsrc", + "-XX:+UseStringDeduplication", + "com.lucasallegri.bootstrap.ProjectXBootstrap", + }; + ALT_CLIENT_ARGS_WIN = new String[]{ + javaPath, + "-classpath", + USER_DIR + File.separator + "./code/config.jar;" + + USER_DIR + File.separator + "./code/projectx-config.jar;" + + USER_DIR + File.separator + "./code/projectx-pcode.jar;" + + USER_DIR + File.separator + "./code/lwjgl.jar;" + + USER_DIR + File.separator + "./code/lwjgl_util.jar;" + + USER_DIR + File.separator + "./code/jinput.jar;" + + USER_DIR + File.separator + "./code/jutils.jar;" + + USER_DIR + File.separator + "./code/jshortcut.jar;" + + USER_DIR + File.separator + "./code/commons-beanutils.jar;" + + USER_DIR + File.separator + "./code/commons-digester.jar;" + + USER_DIR + File.separator + "./code/commons-logging.jar;" + + USER_DIR + File.separator + "./KnightLauncher.jar;", + "-Dcom.threerings.getdown=false", + "-Xms256M", + "-Xmx512M", + "-XX:+AggressiveOpts", + "-XX:SoftRefLRUPolicyMSPerMB=10", + "-Djava.library.path=" + USER_DIR + File.separator + "./native", + "-Dorg.lwjgl.util.NoChecks=true", + "-Dsun.java2d.d3d=false", + "-Dappdir=" + USER_DIR + File.separator + ".", + "-Dresource_dir=" + USER_DIR + File.separator + "./rsrc", + "-XX:+UseStringDeduplication", + "com.lucasallegri.bootstrap.ProjectXBootstrap", + }; + } } diff --git a/src/main/java/com/lucasallegri/launcher/mods/ModListGUI.java b/src/main/java/com/lucasallegri/launcher/mods/ModListGUI.java index 40bf2f71..2c1b666c 100644 --- a/src/main/java/com/lucasallegri/launcher/mods/ModListGUI.java +++ b/src/main/java/com/lucasallegri/launcher/mods/ModListGUI.java @@ -312,6 +312,9 @@ public void windowClosed(WindowEvent windowEvent) { } public static void updateModList() { + if (modListContainer == null) { + return; + } int idx = modListContainer.getSelectedIndex(); modListContainer.removeAll(); for (Mod mod : ModLoader.getModList()) { From d5bdb92a75bd87a9d9f5a39088bff779283a7022 Mon Sep 17 00:00:00 2001 From: leego Date: Fri, 10 Feb 2023 00:58:05 +0800 Subject: [PATCH 4/6] Support for trying to call the `mount` method when loading jars --- .../lucasallegri/bootstrap/ProjectXBootstrap.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java index 6bd32b98..fba3932e 100644 --- a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java +++ b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java @@ -200,13 +200,23 @@ static void loadClasses(List jars) { modName = jar.getName(); } System.out.println("Mod '" + modName + "' initializing"); + Class clazz = null; try { - Class.forName(className); - System.out.println("Mod '" + modName + "' initialized"); + clazz = Class.forName(className); } catch (Exception e) { System.out.println("Failed to load mod '" + modName + "'"); e.printStackTrace(); } + if (clazz != null) { + try { + Method method = clazz.getDeclaredMethod("mount"); + method.setAccessible(true); + method.invoke(null); + } catch (Exception e) { + System.out.println("Mod '" + modName + "' does not define `mount` method"); + } + } + System.out.println("Mod '" + modName + "' initialized"); } } From 993a30e9b060e48e0e507a62f2879fd3f59246a7 Mon Sep 17 00:00:00 2001 From: leego Date: Fri, 10 Feb 2023 11:46:01 +0800 Subject: [PATCH 5/6] Load mods in filename order --- .../java/com/lucasallegri/bootstrap/ProjectXBootstrap.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java index fba3932e..198e0e28 100644 --- a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java +++ b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java @@ -13,6 +13,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -148,6 +149,8 @@ static void loadJarMods() { if (jars.isEmpty()) { return; } + // Load mods in filename order + jars.sort(Comparator.comparing(File::getName)); loadJars(jars); loadClasses(jars); } From 21545c1093b4f9d793a09632a0f704cca5a50946 Mon Sep 17 00:00:00 2001 From: leego Date: Fri, 17 Feb 2023 10:43:03 +0800 Subject: [PATCH 6/6] No longer load mods in sorted order by filename, and polish the logs when loading mods --- .../bootstrap/ProjectXBootstrap.java | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java index 198e0e28..29e49dd9 100644 --- a/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java +++ b/src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java @@ -7,15 +7,16 @@ import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -149,8 +150,6 @@ static void loadJarMods() { if (jars.isEmpty()) { return; } - // Load mods in filename order - jars.sort(Comparator.comparing(File::getName)); loadJars(jars); loadClasses(jars); } @@ -180,6 +179,7 @@ static void loadJars(List jars) { } static void loadClasses(List jars) { + Map> classes = new LinkedHashMap<>(); for (File jar : jars) { String manifest = readZip(jar, MANIFEST_PATH); if (manifest == null || manifest.length() == 0) { @@ -202,24 +202,36 @@ static void loadClasses(List jars) { if (modName == null) { modName = jar.getName(); } - System.out.println("Mod '" + modName + "' initializing"); - Class clazz = null; try { - clazz = Class.forName(className); + Class clazz = Class.forName(className); + classes.put(modName, clazz); + System.out.println("Loaded class '" + className + "' from '" + jar.getName() + "'"); } catch (Exception e) { - System.out.println("Failed to load mod '" + modName + "'"); + System.out.println("Failed to load class '" + className + "' from '" + jar.getName() + "'"); e.printStackTrace(); } - if (clazz != null) { - try { - Method method = clazz.getDeclaredMethod("mount"); - method.setAccessible(true); - method.invoke(null); - } catch (Exception e) { - System.out.println("Mod '" + modName + "' does not define `mount` method"); - } + } + if (!classes.isEmpty()) { + mountMods(classes); + } + } + + static void mountMods(Map> classes) { + for (Map.Entry> entry : classes.entrySet()) { + String modName = entry.getKey(); + Class clazz = entry.getValue(); + try { + System.out.println("Mounting mod '" + modName + "'"); + Method method = clazz.getDeclaredMethod("mount"); + method.setAccessible(true); + method.invoke(null); + System.out.println("Mounted mod '" + modName + "'"); + } catch (NoSuchMethodException e) { + System.out.println("Failed to mount mod '" + modName + "', it does not define `mount` method"); + } catch (IllegalAccessException | InvocationTargetException e) { + System.out.println("Failed to mount mod '" + modName + "': " + e.getMessage()); + e.printStackTrace(); } - System.out.println("Mod '" + modName + "' initialized"); } }