-
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
407 additions
and
10 deletions.
There are no files selected for viewing
219 changes: 219 additions & 0 deletions
219
src/main/java/com/lucasallegri/bootstrap/ProjectXBootstrap.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* 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<String> 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<File> jars = new ArrayList<File>(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<File> 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<File> 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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
src/main/java/com/lucasallegri/launcher/LauncherDigester.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Oops, something went wrong.