/* * ATLauncher - https://github.com/ATLauncher/ATLauncher * Copyright (C) 2013 ATLauncher * * 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.atlauncher; import java.awt.Dimension; import java.awt.Image; import java.awt.SystemTray; import java.awt.TrayIcon; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.Enumeration; import java.util.Locale; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.swing.InputMap; import javax.swing.JOptionPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.UIManager; import javax.swing.text.DefaultEditorKit; import com.atlauncher.data.Constants; import com.atlauncher.data.Instance; import com.atlauncher.data.Pack; import com.atlauncher.data.Settings; import com.atlauncher.gui.LauncherFrame; import com.atlauncher.gui.SplashScreen; import com.atlauncher.gui.TrayMenu; import com.atlauncher.gui.dialogs.SetupDialog; import com.atlauncher.gui.theme.Theme; import com.atlauncher.utils.HTMLUtils; import com.atlauncher.utils.Utils; import io.github.asyncronous.toast.Toaster; /** * Main entry point for the application, Java runs the main method here when the application is launched. */ public class App { /** * The taskpool used to quickly add in tasks to do in the background. */ public static final ExecutorService TASKPOOL = Executors.newFixedThreadPool(2); /** * The instance of toaster to show popups in the bottom right. */ public static final Toaster TOASTER = Toaster.instance(); /** * The tray menu shown in the notification area or whatever it's called in non Windows OS. */ public static TrayMenu TRAY_MENU = new TrayMenu(); /** * If the launcher was just updated and this is it's first time loading after the update. This is used to check for * when there are possible issues in which the user may have to download the update manually. */ public static boolean wasUpdated = false; /** * This controls if GZIP is used when downloading files through the launcher. It's used as a debugging tool and is * enabled with the command line argument shown below. * <p/> * --usegzip=false */ public static boolean useGzipForDownloads = true; /** * This allows skipping the system tray integration so that the launcher doesn't even try to show the icon and menu * etc, in the users system tray. It can be skipped with the below command line argument. * <p/> * --skip-tray-integration */ public static boolean skipTrayIntegration = false; /** * This removes writing the launchers location to AppData/Application Support. It can be enabled with the below * command line argument. * <p/> * --skip-integration */ public static boolean skipIntegration = false; /** * This allows skipping the hash checking when downloading files. It can be skipped with the below command line * argument. * <p/> * --skip-hash-checking */ public static boolean skipHashChecking = false; /** * This forces the launcher to start in offline mode. It can be enabled with the below command line argument. * <p/> * --force-offline-mode */ public static boolean forceOfflineMode = false; /** * This forces the working directory for the launcher. It can be changed with the below command line argument. * <p/> * --working-dir=C:/Games/ATLauncher */ public static File workingDir = null; /** * This forces the launcher to not check for a launcher update. It can be enabled with the below command line * argument. * <p/> * --no-launcher-update */ public static boolean noLauncherUpdate = false; /** * This sets a pack code to be added to the launcher on startup. */ public static String packCodeToAdd = null; /** * This sets a pack to install on startup (no share code so just prompt). */ public static String packToInstall = null; /** * This sets a pack to install on startup (with share code). */ public static String packShareCodeToInstall = null; /** * This sets a pack to auto launch on startup */ public static String autoLaunch = null; /** * This is the Settings instance which holds all the users settings and alot of methods relating to getting things * done. * * @TODO This should probably be switched to be less large and have less responsibility. */ public static Settings settings; /** * This is the theme used by the launcher. By default it uses the default theme until the theme can be created and * loaded. * <p/> * For more information on themeing, please see https://atl.pw/theme */ public static Theme THEME = Theme.DEFAULT_THEME; static { /** * Sets up where all uncaught exceptions go to. */ Thread.setDefaultUncaughtExceptionHandler(new ExceptionStrainer()); } /** * Where the magic happens. * * @param args all the arguments passed in from the command line */ public static void main(String[] args) { // Set English as the default locale. CodeChickenLib(?) has some issues when not using this on some systems. Locale.setDefault(Locale.ENGLISH); // Prefer to use IPv4 System.setProperty("java.net.preferIPv4Stack", "true"); if (args != null) { for (String arg : args) { String[] parts = arg.split("="); if (parts[0].equalsIgnoreCase("--launch")) { autoLaunch = parts[1]; } else if (parts[0].equalsIgnoreCase("--updated")) { wasUpdated = true; } else if (parts[0].equalsIgnoreCase("--debug")) { LogManager.showDebug = true; LogManager.debugLevel = 1; LogManager.debug("Debug logging is enabled! Please note that this will remove any censoring of " + "user data!"); } else if (parts[0].equalsIgnoreCase("--debug-level") && parts.length == 2) { int debugLevel; try { debugLevel = Integer.parseInt(parts[1]); } catch (NumberFormatException e) { LogManager.error("Error converting given debug level string to an integer. The specified " + "debug level given was '" + parts[1] + "'"); continue; } if (debugLevel < 1 || debugLevel > 3) { LogManager.error("Invalid debug level of '" + parts[1] + "' given!"); continue; } LogManager.debugLevel = debugLevel; LogManager.debug("Debug level has been set to " + debugLevel + "!"); } else if (parts[0].equalsIgnoreCase("--usegzip") && parts[1].equalsIgnoreCase("false")) { useGzipForDownloads = false; LogManager.debug("GZip has been turned off for downloads! Don't ask for support with this " + "disabled!", true); } else if (parts[0].equalsIgnoreCase("--skip-tray-integration")) { skipTrayIntegration = true; LogManager.debug("Skipping tray integration!", true); } else if (parts[0].equalsIgnoreCase("--skip-integration")) { skipIntegration = true; LogManager.debug("Skipping integration!", true); } else if (parts[0].equalsIgnoreCase("--skip-hash-checking")) { skipHashChecking = true; LogManager.debug("Skipping hash checking! Don't ask for support with this enabled!", true); } else if (parts[0].equalsIgnoreCase("--force-offline-mode")) { forceOfflineMode = true; LogManager.debug("Forcing offline mode!", true); } else if (parts[0].equalsIgnoreCase("--no-launcher-update")) { noLauncherUpdate = true; LogManager.debug("Not checking for launcher updates! Don't ask for support with this enabled", true); } else if (parts[0].equalsIgnoreCase("--working-dir")) { File wDir = new File(parts[1]); if (wDir.exists() && !wDir.isDirectory()) { LogManager.error("Working directory not set as it references a file!"); } if (!wDir.exists()) { wDir.mkdirs(); } workingDir = wDir; } } } File config = new File(Utils.getCoreGracefully(), "Configs"); if (!config.exists()) { int files = config.getParentFile().list().length; if (files > 1) { String[] options = {"Yes It's Fine", "Whoops. I'll Change That Now"}; int ret = JOptionPane.showOptionDialog(null, HTMLUtils.centerParagraph("I've detected that you may " + "not have installed this in the right location.<br/><br/>The exe or jar file should " + "be placed in it's own folder with nothing else in it.<br/><br/>Are you 100% sure " + "that's what you've done?"), "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane .ERROR_MESSAGE, null, options, options[0]); if (ret != 0) { System.exit(0); } } } // Setup the Settings and wait for it to finish. settings = new Settings(); final SplashScreen ss = new SplashScreen(); // Load and show the splash screen while we load other things. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { ss.setVisible(true); } }); // Load the theme and style everything. loadTheme(); // Load the console, making sure it's after the theme and L&F has been loaded otherwise bad results may occur. settings.loadConsole(); if (settings.enableTrayIcon() && !skipTrayIntegration) { try { // Try to enable the tray icon. trySystemTrayIntegration(); } catch (Exception e) { LogManager.logStackTrace(e); } } LogManager.info(Constants.LAUNCHER_NAME + " Version: " + Constants.VERSION); LogManager.info("Operating System: " + System.getProperty("os.name")); LogManager.info("RAM Available: " + Utils.getMaximumRam() + "MB"); if (settings.isUsingCustomJavaPath()) { LogManager.warn("Custom Java Path Set!"); settings.checkForValidJavaPath(false); } else if (settings.isUsingMacApp()) { // If the user is using the Mac Application, then we forcibly set the java path if they have none set. File oracleJava = new File("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); if (oracleJava.exists() && oracleJava.canExecute()) { settings.setJavaPath("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home"); LogManager.warn("Launcher Forced Custom Java Path Set!"); } } LogManager.info("Java Version: " + Utils.getActualJavaVersion()); LogManager.info("Java Path: " + settings.getJavaPath()); LogManager.info("64 Bit Java: " + Utils.is64Bit()); LogManager.info("Launcher Directory: " + settings.getBaseDir()); LogManager.info("Using Theme: " + THEME); // Now for some Mac specific stuff, mainly just setting the name of the application and icon. if (Utils.isMac()) { System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("com.apple.mrj.application.apple.menu.about.name", Constants.LAUNCHER_NAME + " " + Constants.VERSION); try { Class<?> util = Class.forName("com.apple.eawt.Application"); Method getApplication = util.getMethod("getApplication"); Object application = getApplication.invoke(util); Method setDockIconImage = util.getMethod("setDockIconImage", Image.class); setDockIconImage.invoke(application, Utils.getImage("/assets/image/Icon.png")); } catch (Exception ex) { LogManager.logStackTrace("Failed to set dock icon", ex); } } if (settings.enableConsole()) { // Show the console if enabled. settings.getConsole().setVisible(true); } LogManager.info("Showing splash screen and loading everything"); settings.loadEverything(); // Loads everything that needs to be loaded LogManager.info("Launcher finished loading everything"); if (settings.isFirstTimeRun()) { LogManager.warn("Launcher not setup. Loading Setup Dialog"); new SetupDialog(); } boolean open = true; if (autoLaunch != null && settings.isInstanceBySafeName(autoLaunch)) { Instance instance = settings.getInstanceBySafeName(autoLaunch); LogManager.info("Opening Instance " + instance.getName()); if (instance.launch()) { open = false; } else { LogManager.error("Error Opening Instance " + instance.getName()); } } TRAY_MENU.localize(); if (!skipIntegration) { integrate(); } ss.close(); if (packCodeToAdd != null) { if (settings.addPack(packCodeToAdd)) { Pack packAdded = settings.getSemiPublicPackByCode(packCodeToAdd); if (packAdded != null) { LogManager.info("The pack " + packAdded.getName() + " was automatically added to the launcher!"); } else { LogManager.error("Error automatically adding semi public pack with code of " + packCodeToAdd + "!"); } } else { LogManager.error("Error automatically adding semi public pack with code of " + packCodeToAdd + "!"); } } new LauncherFrame(open); // Open the Launcher } /** * Loads the theme and applies the theme's settings to the look and feel. */ public static void loadTheme() { File themeFile = settings.getThemeFile(); if (themeFile != null) { try { InputStream stream = null; ZipFile zipFile = new ZipFile(themeFile); Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.getName().equals("theme.json")) { stream = zipFile.getInputStream(entry); break; } } if (stream != null) { THEME = Gsons.THEMES.fromJson(new InputStreamReader(stream), Theme.class); stream.close(); } zipFile.close(); } catch (Exception ex) { THEME = Theme.DEFAULT_THEME; } } try { setLAF(); modifyLAF(); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Sets the look and feel to be that of nimbus which is the base. * * @throws Exception */ private static void setLAF() throws Exception { for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { if (info.getName().equalsIgnoreCase("nimbus")) { UIManager.setLookAndFeel(info.getClassName()); } } } /** * This modifies the look and feel based upon the theme loaded. * * @throws Exception */ private static void modifyLAF() throws Exception { THEME.apply(); ToolTipManager.sharedInstance().setDismissDelay(15000); ToolTipManager.sharedInstance().setInitialDelay(50); UIManager.put("FileChooser.readOnly", Boolean.TRUE); UIManager.put("ScrollBar.minimumThumbSize", new Dimension(50, 50)); if (Utils.isMac()) { InputMap im = (InputMap) UIManager.get("TextField.focusInputMap"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.META_DOWN_MASK), DefaultEditorKit.copyAction); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.META_DOWN_MASK), DefaultEditorKit.pasteAction); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.META_DOWN_MASK), DefaultEditorKit.cutAction); } } /** * This tries to create the system tray menu. * * @throws Exception */ private static void trySystemTrayIntegration() throws Exception { if (SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); TrayIcon trayIcon = new TrayIcon(Utils.getImage("/assets/image/Icon.png")); trayIcon.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON3) { TRAY_MENU.setInvoker(TRAY_MENU); TRAY_MENU.setLocation(e.getX(), e.getY()); TRAY_MENU.setVisible(true); } } }); trayIcon.setToolTip(Constants.LAUNCHER_NAME); trayIcon.setImageAutoSize(true); tray.add(trayIcon); } } /** * This creates some integration files so the launcher can work with other applications by storing some properties * about itself and it's location in a set location. */ public static void integrate() { if (!Utils.getOSStorageDir().exists()) { boolean success = Utils.getOSStorageDir().mkdirs(); if (!success) { LogManager.error("Failed to create OS storage directory"); return; } } File f = new File(Utils.getOSStorageDir(), "atlauncher.conf"); try { f.createNewFile(); } catch (IOException e) { LogManager.logStackTrace("Failed to create atlauncher.conf", e); return; } Properties props = new Properties(); InputStream is = null; try { is = new FileInputStream(f); props.load(is); } catch (FileNotFoundException e) { LogManager.logStackTrace("Failed to open atlauncher.conf for reading", e); return; } catch (IOException e) { LogManager.logStackTrace("Failed to read from atlauncher.conf", e); return; } finally { if (is != null) { try { is.close(); } catch (IOException e) { LogManager.logStackTrace("Failed to close atlauncher.conf FileInputStream", e); } } } props.setProperty("java_version", Utils.getLauncherJavaVersion()); props.setProperty("location", App.settings.getBaseDir().toString()); props.setProperty("executable", new File(Update.class.getProtectionDomain().getCodeSource().getLocation() .getPath()).getAbsolutePath()); packCodeToAdd = props.getProperty("pack_code_to_add", null); props.remove("pack_code_to_add"); packToInstall = props.getProperty("pack_to_install", null); props.remove("pack_to_install"); packShareCodeToInstall = props.getProperty("pack_share_code_to_install", null); props.remove("pack_share_code_to_install"); OutputStream os = null; try { os = new FileOutputStream(f); props.store(os, ""); } catch (FileNotFoundException e) { LogManager.logStackTrace("Failed to open atlauncher.conf for writing", e); } catch (IOException e) { LogManager.logStackTrace("Failed to write to atlauncher.conf", e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { LogManager.logStackTrace("Failed to close atlauncher.conf FileOutputStream", e); } } } } }