package net.kdt.pojavlaunch; import android.util.ArrayMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.InputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import net.kdt.pojavlaunch.uikit.UIKit; import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.utils.JSONUtils; import net.kdt.pojavlaunch.value.DependentLibrary; import net.kdt.pojavlaunch.value.MinecraftAccount; import org.lwjgl.glfw.GLFW; /* import org.robovm.apple.foundation.*; import org.robovm.apple.uikit.*; import org.robovm.pods.dialog.*; import org.robovm.pods.*; */ public final class Tools { public static final boolean ENABLE_DEV_FEATURES = true; // BuildConfig.DEBUG; public static String APP_NAME = "PojavLauncher"; public static final Gson GLOBAL_GSON = new GsonBuilder().setPrettyPrinting().create(); public static final String URL_HOME = "https://pojav.ml"; public static String DIR_BUNDLE = System.getenv("BUNDLE_PATH"); // path to "PojavLauncher.app" public static String CURRENT_ARCHITECTURE; public static final String DIR_GAME_HOME = System.getenv("POJAV_HOME"); public static final String DIR_GAME_NEW = System.getenv("POJAV_GAME_DIR"); // path to "Library/Application Support/minecraft" public static final String DIR_APP_DATA = System.getenv("POJAV_HOME"); public static final String DIR_ACCOUNT_NEW = DIR_APP_DATA + "/accounts"; // New since 3.0.0 public static String DIR_HOME_JRE = System.getProperty("java.home"); public static String DIRNAME_HOME_JRE = "lib"; // New since 2.4.2 public static final String DIR_HOME_VERSION = DIR_GAME_NEW + "/versions"; public static final String DIR_HOME_LIBRARY = DIR_GAME_NEW + "/libraries"; public static final String DIR_HOME_CRASH = DIR_GAME_NEW + "/crash-reports"; public static final String ASSETS_PATH = DIR_GAME_NEW + "/assets"; public static final String OBSOLETE_RESOURCES_PATH=DIR_GAME_NEW + "/resources"; public static final String CTRLMAP_PATH = DIR_GAME_NEW + "/controlmap"; public static final String CTRLDEF_FILE = DIR_GAME_NEW + "/controlmap/default.json"; public static final String LIBNAME_OPTIFINE = "optifine:OptiFine"; public static void launchMinecraft(MinecraftAccount profile, final JMinecraftVersionList.Version versionInfo) throws Throwable { String javaVersion = System.getProperty("java.version"); if (javaVersion.startsWith("1.")) { javaVersion = javaVersion.substring(2); } int splitIndex = javaVersion.indexOf('.'); if (splitIndex == -1) { splitIndex = javaVersion.indexOf('-'); } if (splitIndex != -1) { javaVersion = javaVersion.substring(0, splitIndex); } if (versionInfo.javaVersion != null && versionInfo.javaVersion.majorVersion > Integer.parseInt(javaVersion)) { throw new UnsupportedOperationException("Minecraft " + versionInfo.id + " requires Java " + versionInfo.javaVersion.majorVersion + " in order to run. Please switch to Java " + versionInfo.javaVersion.majorVersion + " in launcher settings."); } String[] launchArgs = getMinecraftArgs(profile, versionInfo); // System.out.println("Minecraft Args: " + Arrays.toString(launchArgs)); final String launchClassPath = generateLaunchClassPath(profile.selectedVersion); List javaArgList = new ArrayList(); //javaArgList.add(versionInfo.logging.client.argument.replace("${path}", DIR_GAME_NEW.getAbsolutePath() + "/" + mVersion.logging.client.file.id)); javaArgList.add("-cp"); javaArgList.add(launchClassPath); javaArgList.add(versionInfo.mainClass); javaArgList.addAll(Arrays.asList(launchArgs)); // Debug /* BufferedWriter bw = new BufferedWriter(new FileWriter(new File(Tools.DIR_GAME_HOME, "currargs_generated.txt"))); for (String s : javaArgList) { bw.write(s, 0, s.length()); bw.write(" ", 0, 1); } bw.close(); */ /* final List urlList = new ArrayList<>(); for (String s : launchClassPath.split(":")) { if (!s.isEmpty()) { urlList.add(new File(s).toURI().toURL()); } } */ new Thread(() -> { try { System.out.println("Args init finished. Now starting game"); // URLClassLoader loader = new URLClassLoader(urlList.toArray(new URL[0]), ClassLoader.getSystemClassLoader()); PojavClassLoader loader = (PojavClassLoader) ClassLoader.getSystemClassLoader(); // add launcher.jar itself loader.addURL(Tools.class.getProtectionDomain().getCodeSource().getLocation().toURI().toURL()); for (String s : launchClassPath.split(":")) { if (!s.isEmpty()) { loader.addURL(new File(s).toURI().toURL()); } } Class clazz = loader.loadClass(versionInfo.mainClass); Method method = clazz.getMethod("main", String[].class); method.invoke(null, new Object[]{launchArgs}); // throw new RuntimeException("Game exited. Check latestlog.txt for more details."); } catch (Throwable th) { showError(th); } }).start(); // JREUtils.launchJavaVM(javaArgList); } public static String[] getMinecraftArgs(MinecraftAccount profile, JMinecraftVersionList.Version versionInfo) { String username = profile.username; String versionName = versionInfo.id; if (versionInfo.inheritsFrom != null) { versionName = versionInfo.inheritsFrom; } File gameDir = new File(Tools.DIR_GAME_NEW); gameDir.mkdirs(); Map varArgMap = new ArrayMap(); varArgMap.put("auth_access_token", profile.accessToken); varArgMap.put("auth_player_name", username); varArgMap.put("auth_uuid", profile.profileId); // varArgMap.put("auth_xuid", profile.profileId); // TODO!!! varArgMap.put("assets_root", Tools.ASSETS_PATH); varArgMap.put("assets_index_name", versionInfo.assets); varArgMap.put("clientid", profile.clientToken); varArgMap.put("game_assets", Tools.ASSETS_PATH); varArgMap.put("game_directory", gameDir.getAbsolutePath()); varArgMap.put("user_properties", "{}"); varArgMap.put("user_type", "msa"); varArgMap.put("version_name", versionName); varArgMap.put("version_type", versionInfo.type); List minecraftArgs = new ArrayList(); if (versionInfo.arguments != null) { // Support Minecraft 1.13+ for (Object arg : versionInfo.arguments.game) { if (arg instanceof String) { minecraftArgs.add((String) arg); } else { /* JMinecraftVersionList.Arguments.ArgValue argv = (JMinecraftVersionList.Arguments.ArgValue) arg; if (argv.values != null) { minecraftArgs.add(argv.values[0]); } else { for (JMinecraftVersionList.Arguments.ArgValue.ArgRules rule : arg.rules) { // rule.action = allow // TODO implement this } } */ } } } minecraftArgs.add("--width"); minecraftArgs.add(Integer.toString(GLFW.mGLFWWindowWidth)); minecraftArgs.add("--height"); minecraftArgs.add(Integer.toString(GLFW.mGLFWWindowHeight)); minecraftArgs.add("--fullscreenWidth"); minecraftArgs.add(Integer.toString(GLFW.mGLFWWindowWidth)); minecraftArgs.add("--fullscreenHeight"); minecraftArgs.add(Integer.toString(GLFW.mGLFWWindowHeight)); String[] argsFromJson = JSONUtils.insertJSONValueList( splitAndFilterEmpty( versionInfo.minecraftArguments == null ? fromStringArray(minecraftArgs.toArray(new String[0])): versionInfo.minecraftArguments, profile ), varArgMap ); // Tools.dialogOnUiThread(this, "Result args", Arrays.asList(argsFromJson).toString()); return argsFromJson; } public static String fromStringArray(String[] strArr) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < strArr.length; i++) { if (i > 0) builder.append(" "); builder.append(strArr[i]); } return builder.toString(); } private static String[] splitAndFilterEmpty(String argStr, MinecraftAccount profile) { List strList = new ArrayList(); if(profile.username.equals("demo_user") && profile.accessToken.equals("-1") && profile.profileId.equals("25e00594-cf57-3f87-b3af-3f06591be252")) { strList.add("--demo"); // if statement is this complex to prevent tampering with the .json } for (String arg : argStr.split(" ")) { if (!arg.isEmpty()) { strList.add(arg); } } strList.add("--fullscreen"); return strList.toArray(new String[0]); } public static String artifactToPath(String group, String artifact, String version) { return group.replaceAll("\\.", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar"; } public static String getPatchedFile(String version) { return DIR_HOME_VERSION + "/" + version + "/" + version + ".jar"; } /* private static String getLWJGL3ClassPath() { StringBuilder libStr = new StringBuilder(); File lwjgl3Folder = new File(Tools.DIR_GAME_NEW, "lwjgl3"); if (/* info.arguments != null && @lwjgl3Folder.exists()) { for (File file: lwjgl3Folder.listFiles()) { if (file.getName().endsWith(".jar")) { libStr.append(file.getAbsolutePath() + ":"); } } // Remove the ':' at the end libStr.setLength(libStr.length() - 1); } return libStr.toString(); } */ private static boolean isClientFirst = false; public static String generateLaunchClassPath(String version) { StringBuilder libStr = new StringBuilder(); //versnDir + "/" + version + "/" + version + ".jar:"; JMinecraftVersionList.Version info = getVersionInfo(version); String[] classpath = generateLibClasspath(info); // Debug: LWJGL 3 override // File lwjgl2Folder = new File(Tools.MAIN_PATH, "lwjgl2"); /* File lwjgl3Folder = new File(Tools.MAIN_PATH, "lwjgl3"); if (lwjgl3Folder.exists()) { for (File file: lwjgl3Folder.listFiles()) { if (file.getName().endsWith(".jar")) { libStr.append(file.getAbsolutePath() + ":"); } } } else if (lwjgl2Folder.exists()) { for (File file: lwjgl2Folder.listFiles()) { if (file.getName().endsWith(".jar")) { libStr.append(file.getAbsolutePath() + ":"); } } } */ if (isClientFirst) { libStr.append(getPatchedFile(version)); } for (String perJar : classpath) { if (!new File(perJar).exists()) { System.out.println("Ignored non-exists file: " + perJar); continue; } libStr.append((isClientFirst ? ":" : "") + perJar + (!isClientFirst ? ":" : "")); } if (!isClientFirst) { libStr.append(getPatchedFile(version)); } return libStr.toString(); } public static void showError(Throwable e) { showError(e, false); } public static void showError(final Throwable e, final boolean exitIfOk) { showError("Error", e, exitIfOk, false); } public static void showError(final String title, final Throwable e, final boolean exitIfOk) { showError(title, e, exitIfOk, false); } private static void showError(final String title, final Throwable e, final boolean exitIfOk, final boolean showMore) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); pw.flush(); System.err.println(sw.toString()); UIKit.showError(title, sw.toString(), exitIfOk); /* Platform.getPlatform().runOnUIThread(() -> { WindowAlertController alertController = new WindowAlertController(title, sw.toString(), UIAlertControllerStyle.Alert); alertController.addAction(new UIAlertAction("OK", UIAlertActionStyle.Default, (action) -> { alertController.dismissViewController(true, null); if (exitIfOk) { System.exit(0); } }) ); alertController.show(); }); */ } /* public static void dialogOnUiThread(final Activity ctx, final CharSequence title, final CharSequence message) { ctx.runOnUiThread(new Runnable(){ @Override public void run() { new AlertDialog.Builder(ctx) .setTitle(title) .setMessage(message) .setPositiveButton(android.R.string.ok, null) .show(); } }); } */ public static void moveInside(String from, String to) { File fromFile = new File(from); for (File fromInside : fromFile.listFiles()) { moveRecursive(fromInside.getAbsolutePath(), to); } fromFile.delete(); } public static void moveRecursive(String from, String to) { moveRecursive(new File(from), new File(to)); } public static void moveRecursive(File from, File to) { File toFrom = new File(to, from.getName()); try { if (from.isDirectory()) { for (File child : from.listFiles()) { moveRecursive(child, toFrom); } } } finally { from.getParentFile().mkdirs(); from.renameTo(toFrom); } } /* public static void openURL(Activity act, String url) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); act.startActivity(browserIntent); } */ public static void preProcessLibraries(DependentLibrary[] libraries) { // Ignore some libraries since they are unsupported (jinput, text2speech) or unused (LWJGL) // Support for text2speech is not planned, so skip it for now. for (int i = 0; i < libraries.length; i++) { DependentLibrary libItem = libraries[i]; if (libItem.name.startsWith("com.mojang:text2speech") || libItem.name.startsWith("net.java.jinput") || libItem.name.startsWith("net.java.dev.jna:platform:") || libItem.name.startsWith("org.lwjgl") || libItem.name.startsWith("tv.twitch")) { libItem._skip = true; } else if (libItem.name.startsWith("net.java.dev.jna:jna:")) { // Special handling for LabyMod 1.8.9 and Forge 1.12.2(?) // we have libjnidispatch 5.8.0 in Frameworks directory System.out.println("Library " + libItem.name + " has been changed to version 5.8.0"); libItem.name = "net.java.dev.jna:jna:5.8.0"; libItem.downloads.artifact.path = "net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar"; libItem.downloads.artifact.url = "https://libraries.minecraft.net/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar"; } } } public static String[] generateLibClasspath(JMinecraftVersionList.Version info) { List libDir = new ArrayList(); preProcessLibraries(info.libraries); for (DependentLibrary libItem : info.libraries) { if (libItem._skip) continue; String[] libInfos = libItem.name.split(":"); libDir.add(Tools.DIR_HOME_LIBRARY + "/" + Tools.artifactToPath(libInfos[0], libInfos[1], libInfos[2])); } return libDir.toArray(new String[0]); } public static JMinecraftVersionList.Version getVersionInfo(String versionName) { try { JMinecraftVersionList.Version customVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + versionName + "/" + versionName + ".json"), JMinecraftVersionList.Version.class); for (DependentLibrary lib : customVer.libraries) { if (lib.name.startsWith(LIBNAME_OPTIFINE)) { customVer.optifineLib = lib; } } if (customVer.inheritsFrom == null || customVer.inheritsFrom.equals(customVer.id)) { return customVer; } else { JMinecraftVersionList.Version inheritsVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + customVer.inheritsFrom + "/" + customVer.inheritsFrom + ".json"), JMinecraftVersionList.Version.class); inheritsVer.inheritsFrom = inheritsVer.id; insertSafety(inheritsVer, customVer, "assetIndex", "assets", "id", "mainClass", "minecraftArguments", "optifineLib", "releaseTime", "time", "type" ); List libList = new ArrayList(Arrays.asList(inheritsVer.libraries)); try { loop_1: for (DependentLibrary lib : customVer.libraries) { String libName = lib.name.substring(0, lib.name.lastIndexOf(":")); for (int i = 0; i < libList.size(); i++) { DependentLibrary libAdded = libList.get(i); String libAddedName = libAdded.name.substring(0, libAdded.name.lastIndexOf(":")); if (!libAdded.name.equals(lib.name) && libAddedName.equals(libName)) { System.out.println("Library " + libName + ": Replaced version " + libAdded.name.substring(libAddedName.length() + 1) + " with " + lib.name.substring(libName.length() + 1)); libList.set(i, lib); continue loop_1; } } libList.add(lib); } } finally { inheritsVer.libraries = libList.toArray(new DependentLibrary[0]); } // Inheriting Minecraft 1.13+ with append custom args if (inheritsVer.arguments != null && customVer.arguments != null) { List totalArgList = new ArrayList(); totalArgList.addAll(Arrays.asList(inheritsVer.arguments.game)); int nskip = 0; for (int i = 0; i < customVer.arguments.game.length; i++) { if (nskip > 0) { nskip--; continue; } Object perCustomArg = customVer.arguments.game[i]; if (perCustomArg instanceof String) { String perCustomArgStr = (String) perCustomArg; // Check if there is a duplicate argument on combine if (perCustomArgStr.startsWith("--") && totalArgList.contains(perCustomArgStr)) { perCustomArg = customVer.arguments.game[i + 1]; if (perCustomArg instanceof String) { perCustomArgStr = (String) perCustomArg; // If the next is argument value, skip it if (!perCustomArgStr.startsWith("--")) { nskip++; } } } else { totalArgList.add(perCustomArgStr); } } else if (!totalArgList.contains(perCustomArg)) { totalArgList.add(perCustomArg); } } inheritsVer.arguments.game = totalArgList.toArray(new Object[0]); } return inheritsVer; } } catch (Exception e) { throw new RuntimeException(e); } } // Prevent NullPointerException private static void insertSafety(JMinecraftVersionList.Version targetVer, JMinecraftVersionList.Version fromVer, String... keyArr) { for (String key : keyArr) { Object value = null; try { Field fieldA = fromVer.getClass().getField(key); value = fieldA.get(fromVer); if (((value instanceof String) && !((String) value).isEmpty()) || value != null) { Field fieldB = targetVer.getClass().getField(key); fieldB.set(targetVer, value); } } catch (Throwable th) { System.err.println("Unable to insert " + key + "=" + value); th.printStackTrace(); } } } public static String convertStream(InputStream inputStream) throws IOException { return convertStream(inputStream, Charset.forName("UTF-8")); } public static String convertStream(InputStream inputStream, Charset charset) throws IOException { String out = ""; int len; byte[] buf = new byte[512]; while((len = inputStream.read(buf))!=-1) { out += new String(buf,0,len,charset); } return out; } public static void copy(final InputStream input, final OutputStream output) throws IOException { final byte[] buffer = new byte[8192]; int n = 0; while ((n = input.read(buffer)) != -1) { output.write(buffer, 0, n); } } public static File lastFileModified(String dir) { File fl = new File(dir); File[] files = fl.listFiles(new FileFilter() { public boolean accept(File file) { return file.isFile(); } }); long lastMod = Long.MIN_VALUE; File choice = null; for (File file : files) { if (file.lastModified() > lastMod) { choice = file; lastMod = file.lastModified(); } } return choice; } public static String read(InputStream is) throws IOException { String out = ""; int len; byte[] buf = new byte[512]; while((len = is.read(buf))!=-1) { out += new String(buf,0,len); } return out; } public static String read(String path) throws IOException { return read(new FileInputStream(path)); } public static void write(String path, byte[] content) throws IOException { File outPath = new File(path); outPath.getParentFile().mkdirs(); outPath.createNewFile(); BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(path)); fos.write(content, 0, content.length); fos.close(); } public static void write(String path, String content) throws IOException { write(path, content.getBytes()); } /* public static byte[] loadFromAssetToByte(Context ctx, String inFile) { byte[] buffer = null; try { InputStream stream = ctx.getAssets().open(inFile); int size = stream.available(); buffer = new byte[size]; stream.read(buffer); stream.close(); } catch (IOException e) { // Handle exceptions here e.printStackTrace(); } return buffer; } */ public static void downloadFile(String urlInput, String nameOutput) throws IOException { File file = new File(nameOutput); DownloadUtils.downloadFile(urlInput, file); } public abstract static class DownloaderFeedback { public abstract void updateProgress(int curr, int max); } public static void downloadFileMonitored(String urlInput,String nameOutput, DownloaderFeedback monitor) throws IOException { if(!new File(nameOutput).exists()){ new File(nameOutput).getParentFile().mkdirs(); } HttpURLConnection conn = (HttpURLConnection) new URL(urlInput).openConnection(); InputStream readStr = conn.getInputStream(); FileOutputStream fos = new FileOutputStream(new File(nameOutput)); int cur = 0; int oval=0; int len = conn.getContentLength(); byte[] buf = new byte[65535]; while((cur = readStr.read(buf)) != -1) { oval += cur; fos.write(buf,0,cur); monitor.updateProgress(oval,len); } fos.close(); conn.disconnect(); } public static class ZipTool { private ZipTool(){} public static void zip(List files, File zipFile) throws IOException { final int BUFFER_SIZE = 2048; BufferedInputStream origin = null; ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile))); try { byte data[] = new byte[BUFFER_SIZE]; for (File file : files) { FileInputStream fileInputStream = new FileInputStream( file ); origin = new BufferedInputStream(fileInputStream, BUFFER_SIZE); try { ZipEntry entry = new ZipEntry(file.getName()); out.putNextEntry(entry); int count; while ((count = origin.read(data, 0, BUFFER_SIZE)) != -1) { out.write(data, 0, count); } } finally { origin.close(); } } } finally { out.close(); } } public static void unzip(File zipFile, File targetDirectory) throws IOException { final int BUFFER_SIZE = 1024; ZipInputStream zis = new ZipInputStream( new BufferedInputStream(new FileInputStream(zipFile))); try { ZipEntry ze; int count; byte[] buffer = new byte[BUFFER_SIZE]; while ((ze = zis.getNextEntry()) != null) { File file = new File(targetDirectory, ze.getName()); File dir = ze.isDirectory() ? file : file.getParentFile(); if (!dir.isDirectory() && !dir.mkdirs()) throw new FileNotFoundException("Failed to ensure directory: " + dir.getAbsolutePath()); if (ze.isDirectory()) continue; FileOutputStream fout = new FileOutputStream(file); try { while ((count = zis.read(buffer)) != -1) fout.write(buffer, 0, count); } finally { fout.close(); } /* if time should be restored as well long time = ze.getTime(); if (time > 0) file.setLastModified(time); */ } } finally { zis.close(); } } } }