package com.kartoflane.superluminal2; import java.awt.Desktop; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import net.vhati.ftldat.FTLDat.FTLPack; import net.vhati.modmanager.core.ComparableVersion; import net.vhati.modmanager.core.FTLUtilities; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MessageBox; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.JDOMParseException; import com.kartoflane.common.selfpatch.SelfPatcher; import com.kartoflane.superluminal2.components.Hotkey; import com.kartoflane.superluminal2.components.enums.Hotkeys; import com.kartoflane.superluminal2.components.enums.OS; import com.kartoflane.superluminal2.components.interfaces.Action; import com.kartoflane.superluminal2.core.Database; import com.kartoflane.superluminal2.core.DatabaseEntry; import com.kartoflane.superluminal2.core.KeyboardInputDispatcher; import com.kartoflane.superluminal2.core.Manager; import com.kartoflane.superluminal2.core.SuperluminalConfig; import com.kartoflane.superluminal2.selfpatch.SPSLGetTask; import com.kartoflane.superluminal2.selfpatch.SPSLPatchTask; import com.kartoflane.superluminal2.selfpatch.SPSLRunTask; import com.kartoflane.superluminal2.ui.DownloadDialog; import com.kartoflane.superluminal2.ui.EditorWindow; import com.kartoflane.superluminal2.ui.ShipContainer; import com.kartoflane.superluminal2.utils.IOUtils; import com.kartoflane.superluminal2.utils.UIUtils; import com.kartoflane.superluminal2.utils.Utils; public class Superluminal { public static final Logger log = LogManager.getLogger(Superluminal.class); public static final String APP_NAME = "Superluminal"; public static final ComparableVersion APP_VERSION = new ComparableVersion("2.1.2b"); public static final String APP_UPDATE_FETCH_URL = "https://raw.github.com/kartoFlane/superluminal2/master/skels/common/auto_update.xml"; public static final String APP_FORUM_URL = "http://www.ftlgame.com/forum/viewtopic.php?f=12&t=24901&p=78738#p78738"; public static final String APP_AUTHOR = "kartoFlane"; public static final String HOTKEYS_FILE = "hotkeys.xml"; public static final String CONFIG_FILE = "editor.cfg"; /** Length of a single row of text */ public static final int WRAP_WIDTH = 55; /** * How many characters beyond the limit set by WRAP_WIDTH can be * accepted before splitting up the word / moving it to next line */ public static final int WRAP_TOLERANCE = 9; /** * TODO: * ============================================================================================= * * IMMEDIATE: * - work out correction from center-relative to top-left-corner-relative * * MEDIUM: * - room tool doesn't work sometimes? -- as per SleeperService * - ship overview sometimes messes up invisible object detection (grays out items incorrectly) * * LOW: * - Rework highlight to be cursor based? --> allows to show mounts/rooms that are hidden * beneath hull/other rooms * - rework the layered painter to allow more freedom in arranging stuff's ordering -> single views * consisting of primitive subviews placed on different layers? * * Suggestions: * - detachable toolbar? * - detachable sidebar elements? */ public static void main(String[] args) { log.debug(String.format("%s v%s", APP_NAME, APP_VERSION)); log.debug(String.format("%s %s", System.getProperty("os.name"), System.getProperty("os.version"))); log.debug(String.format("%s, %s, %s", System.getProperty("java.vm.name"), System.getProperty("java.version"), System.getProperty("os.arch"))); log.debug(String.format("SWT v%s", SWT.getVersion())); System.out.println(); try { // Display#setAppName() allows to set the name of the application on OSX // However, in order to work, it has to be called before any instance of // Display is created (via Display#getCurrent() or #getDefault()) // Also tests whether the correct version of the editor is installed (since SWT code is platform-specific) Display.setAppName(APP_NAME); Display.setAppVersion(APP_VERSION.toString()); } catch (Throwable t) { log.error("Failed to retrieve display - wrong version of the editor has been downloaded."); OS os = OS.identifyOS(); if (os.isUnknown()) log.error(String.format("Your system (%s %s) was not recognized, or is not supported :(", System.getProperty("os.name"), System.getProperty("sun.arch.data.model"))); else log.error("You should download version for " + os.toString()); String msg = ""; msg += "You have downloaded a wrong version of the editor for your system.\n"; msg += "\n"; if (os.isUnknown()) { msg += String.format("Your system (%s %s) was not recognized, or is not supported :(", System.getProperty("os.name"), System.getProperty("sun.arch.data.model")); } else { msg += "You should download version for: " + os.toString(); } UIUtils.showSwingDialog(APP_NAME + " - Wrong version", msg); System.exit(1); } Display display = Display.getDefault(); File configFile = new File(CONFIG_FILE); SuperluminalConfig appConfig = new SuperluminalConfig(configFile); // Read the config file InputStreamReader reader = null; try { if (configFile.exists()) { log.trace("Loading properties from config file..."); reader = new InputStreamReader(new FileInputStream(configFile), "UTF-8"); appConfig.load(reader); } } catch (IOException e) { log.error("Error loading config.", e); UIUtils.showErrorDialog(null, null, "Error loading config from " + configFile.getPath()); } finally { try { if (reader != null) reader.close(); } catch (IOException e) { } } // Read config values Manager.sidebarOnRightSide = appConfig.getPropertyAsBoolean(SuperluminalConfig.SIDEBAR_SIDE, false); Manager.rememberGeometry = appConfig.getPropertyAsBoolean(SuperluminalConfig.SAVE_GEOMETRY, true); Manager.checkUpdates = appConfig.getPropertyAsBoolean(SuperluminalConfig.CHECK_UPDATES, true); Manager.startMaximised = appConfig.getPropertyAsBoolean(SuperluminalConfig.START_MAX, false); Manager.closeLoader = appConfig.getPropertyAsBoolean(SuperluminalConfig.CLOSE_LOADER, false); Manager.allowRoomOverlap = appConfig.getPropertyAsBoolean(SuperluminalConfig.ALLOW_OVERLAP, false); Manager.allowDoorOverlap = appConfig.getPropertyAsBoolean(SuperluminalConfig.ALLOW_OVERLAP_DOOR, false); Manager.resetDoorLinksOnMove = appConfig.getPropertyAsBoolean(SuperluminalConfig.RESET_LINKS, true); Manager.mouseShipRelative = appConfig.getPropertyAsBoolean(SuperluminalConfig.MOUSE_SHIP_RELATIVE, false); Manager.shownSlotWarning = appConfig.getPropertyAsBoolean(SuperluminalConfig.SLOT_WARNING, false); Manager.shownArtilleryWarning = appConfig.getPropertyAsBoolean(SuperluminalConfig.ARTILLERY_WARNING, false); Manager.windowSize = appConfig.getPropertyAsPoint(SuperluminalConfig.GEOMETRY, 0, 0); initHotkeys(); Manager.loadDefaultHotkeys(); File hotkeysFile = new File(HOTKEYS_FILE); if (hotkeysFile.exists()) loadHotkeys(hotkeysFile); // Read FTL resources path File datsDir = null; Manager.resourcePath = appConfig.getProperty(SuperluminalConfig.FTL_RESOURCE, ""); if (Manager.resourcePath.length() > 0) { log.info("Using FTL dats path from config: " + Manager.resourcePath); datsDir = new File(Manager.resourcePath); if (FTLUtilities.isDatsDirValid(datsDir) == false) { log.error("The config's ftlResourcePath does not exist, or it lacks data.dat."); datsDir = null; } } else { log.trace("No FTL dats path previously set."); } // Create the main window instance EditorWindow editorWindow = null; try { editorWindow = new EditorWindow(display); } catch (Exception e) { log.error("Exception occured while creating EditorWindow: ", e); String msg = "An error has occured while creating the editor's GUI:\n" + e.getClass().getSimpleName() + ": " + e.getMessage() + "\n\n" + "Check the log for details."; UIUtils.showErrorDialog(null, null, Utils.wrap(msg, 50, 5)); System.exit(1); } // Find / prompt for the path to set in the config if (datsDir == null) { datsDir = FTLUtilities.findDatsDir(); if (datsDir != null) { MessageBox box = new MessageBox(editorWindow.getShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO); box.setText("Confirm"); box.setMessage("FTL resources were found in:\n" + datsDir.getPath() + "\nIs this correct?"); int response = box.open(); if (response == SWT.NO) datsDir = null; } if (datsDir == null) { log.debug("FTL dats path was not located automatically. Prompting user for location."); datsDir = UIUtils.promptForDatsDir(editorWindow.getShell()); } if (datsDir != null) { Manager.resourcePath = datsDir.getAbsolutePath(); appConfig.setProperty(SuperluminalConfig.FTL_RESOURCE, Manager.resourcePath); log.info("FTL dats located at: " + Manager.resourcePath); } } // Show a warning if no dats were selected, or load them if they were if (datsDir == null) { UIUtils.showWarningDialog(editorWindow.getShell(), null, "FTL resources were not found.\n" + "The editor will not be able to load any data from the game,\n" + "and may crash unexpectedly."); log.debug("No FTL dats path found - creating empty Database."); new Database(); } else { try { log.trace("Loading dat archives..."); File dataFile = new File(datsDir + "/data.dat"); File resourceFile = new File(datsDir + "/resource.dat"); FTLPack data = new FTLPack(dataFile, "r"); FTLPack resource = new FTLPack(resourceFile, "r"); final Database db = new Database(data, resource); final List<String> argsList = Arrays.asList(args); log.trace("Loading database..."); UIUtils.showLoadDialog(editorWindow.getShell(), null, null, new Action() { public void execute() { db.getCore().load(); for (String arg : argsList) { File f = new File(arg); if (!f.exists()) { log.warn(String.format("'%s' was not loaded because the specified file could not be found.", arg)); continue; } if (!arg.endsWith(".ftl") && !arg.endsWith(".zip")) { log.warn(String.format("'%s' was not loaded because the specified file is not a .zip or .ftl.", f.getName())); continue; } try { DatabaseEntry de = new DatabaseEntry(f); DatabaseEntry[] dbEntries = db.getEntries(); if (de == db.getCore()) continue; if (!Utils.contains(dbEntries, de)) db.addEntry(de); } catch (Exception e) { log.warn(String.format("Could not create a database entry for file '%s': ", arg), e); } } db.cacheAnimations(); } }); } catch (Exception e) { log.error("An error occured while loading dat archives:", e); String msg = "An error has occured while loading the game's resources.\n\n" + "Please check editor-log.txt in the editor's directory, and post\n" + "it in the editor's thread at the FTL forums."; UIUtils.showErrorDialog(editorWindow.getShell(), null, msg); System.exit(1); } } KeyboardInputDispatcher kid = new KeyboardInputDispatcher(); display.addFilter(SWT.KeyDown, kid); display.addFilter(SWT.KeyUp, kid); // Open the main window's shell, making it visible editorWindow.open(); if (Manager.checkUpdates) { checkForUpdates(false); // Automatic update check } log.info("Running..."); try { while (!editorWindow.getShell().isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } } catch (Throwable t) { String msg = APP_NAME + " has encountered a problem and needs to close.\n\n" + "Please check editor-log.txt in the editor's directory, and post " + "it in the editor's thread at the FTL forums."; ShipContainer ship = Manager.getCurrentShip(); if (ship != null) { String name = "crash_" + System.currentTimeMillis() + ".ftl"; msg += "\n\nThe ship you had loaded has been saved in the editor's directory as '" + name + "'."; ship.save(new File(name)); } log.error("An error has occured and the editor was forced to terminate.", t); UIUtils.showErrorDialog(editorWindow.getShell(), null, msg); } log.info("Exiting..."); saveHotkeys(hotkeysFile); // Save config try { appConfig.setCurrent(); appConfig.writeConfig(); } catch (IOException e) { String errorMsg = String.format("Error writing config to \"%s\".", configFile.getPath()); log.error(errorMsg, e); UIUtils.showErrorDialog(editorWindow.getShell(), null, errorMsg); } editorWindow.dispose(); display.dispose(); log.info("Bye"); // Prevents the application from lingering on MacOSX when the window is closed, // makes no difference on Windows/Linux System.exit(0); } /** * * @param manual * if true, an information dialog will pop up even if the program is up to date */ public static void checkForUpdates(boolean manual) { log.info("Checking for updates..."); final String[] downloadLink = new String[1]; final ComparableVersion[] remoteVersion = new ComparableVersion[1]; final ArrayList<String> changes = new ArrayList<String>(); final Exception[] lastException = new Exception[1]; String msg = "Checking for updates, please wait..." + (manual ? "" : "\n(You can disable this in Edit > Settings)"); UIUtils.showLoadDialog(EditorWindow.getInstance().getShell(), "Checking Updates...", msg, new Action() { public void execute() { InputStream is = null; try { URL url = new URL(APP_UPDATE_FETCH_URL); is = url.openStream(); Document updateDoc = IOUtils.readStreamXML(is, "auto-update"); Element root = updateDoc.getRootElement(); Element latest = root.getChild("latest"); String id = latest.getAttributeValue("id"); downloadLink[0] = latest.getAttributeValue("url"); remoteVersion[0] = new ComparableVersion(id); Element changelog = root.getChild("changelog"); for (Element version : changelog.getChildren("version")) { ComparableVersion vId = new ComparableVersion(version.getAttributeValue("id")); if (vId.compareTo(APP_VERSION) > 0) { for (Element change : version.getChildren("change")) { changes.add(change.getValue()); } } } } catch (UnknownHostException e) { log.error("Update check failed -- connection to the repository could not be estabilished."); lastException[0] = e; } catch (JDOMException e) { log.error("Udpate check failed -- an error has occured while parsing update file.", e); lastException[0] = e; } catch (Exception e) { log.error("An error occured while checking for updates.", e); lastException[0] = e; } finally { try { if (is != null) is.close(); } catch (IOException e) { } } } }); if (lastException[0] != null) { UIUtils.showWarningDialog(EditorWindow.getInstance().getShell(), null, String.format("An error occurred while checking for updates:%n%n%s: %s", lastException[0].getClass().getSimpleName(), lastException[0].getMessage())); } if (remoteVersion[0] == null) { // Version check failed, already logged by previous catches } else if (APP_VERSION.compareTo(remoteVersion[0]) < 0) { try { log.info("Update is available, user version: " + APP_VERSION + ", remote version: " + remoteVersion[0]); MessageBox box = new MessageBox(EditorWindow.getInstance().getShell(), SWT.ICON_INFORMATION | SWT.YES | SWT.NO); box.setText(APP_NAME + " - Update Available"); StringBuilder buf = new StringBuilder(); buf.append("A new version of the editor is available: v."); buf.append(remoteVersion[0].toString()); buf.append("\n\n"); if (changes.size() > 0) { int count = 0; for (String change : changes) { if (count < 5) { buf.append(" - "); buf.append(change); buf.append("\n"); count++; } else { buf.append("...and "); buf.append(changes.size() - count); buf.append(" more - check changelog for details.\n"); break; } } buf.append("\n"); } buf.append("Would you like to download it now?"); box.setMessage(buf.toString()); if (box.open() == SWT.YES) { try { SelfPatcher sp = new SelfPatcher(new SPSLGetTask(), new SPSLPatchTask(), new SPSLRunTask()); DownloadDialog dd = new DownloadDialog(EditorWindow.getInstance().getShell()); sp.patch(dd); } catch (Exception e) { log.error("Self-patching failed!", e); box = new MessageBox(EditorWindow.getInstance().getShell(), SWT.ICON_ERROR | SWT.YES | SWT.NO); box.setText(APP_NAME + " - Auto-Update Failed"); box.setMessage("Whoops! Something went terribly wrong, and the editor was unable to patch itself.\n" + "Do you want to download and update the editor manually?"); if (box.open() == SWT.YES) { URL url = new URL(downloadLink[0] == null ? APP_FORUM_URL : downloadLink[0]); Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { try { desktop.browse(url.toURI()); } catch (Exception ex) { log.error("An error has occured while opening web browser.", ex); } } } } } } catch (Exception e) { log.error("An error has occured while displaying update result.", e); UIUtils.showWarningDialog(EditorWindow.getInstance().getShell(), null, String.format("An error has occured while displaying update result:%n%n%s: %s", e.getClass().getSimpleName(), e.getMessage())); } } else { if (APP_VERSION.compareTo(remoteVersion[0]) == 0) { log.info("Program is up to date."); } else { log.info("Program is up to date. (actually ahead)"); } if (manual) { // The user manually initiated the version check, so probably expects some kind of // response in either case. UIUtils.showInfoDialog(EditorWindow.getInstance().getShell(), null, APP_NAME + " is up to date."); } } } /** Create a dummy Hotkey object for each hotkey, and store them in the hotkey map. */ private static void initHotkeys() { for (Hotkeys keyId : Hotkeys.values()) Manager.putHotkey(keyId, new Hotkey()); } /** * Loads hotkey configuration from the given file. * * @param f * File from which hotkey config will be read. */ private static void loadHotkeys(File f) { try { Document keyDoc = IOUtils.readFileXML(f); Element root = keyDoc.getRootElement(); for (Element bind : root.getChildren("bind")) { String actionName = bind.getValue(); if (actionName == null) { log.warn(HOTKEYS_FILE + " contained a bind without an assigned action."); continue; } Hotkeys action = null; String loading = null; String attr = null; try { action = Hotkeys.valueOf(actionName); } catch (IllegalArgumentException e) { log.warn("Action '" + actionName + "' was not recognised, and was not loaded."); continue; } try { loading = "shift"; attr = bind.getAttributeValue(loading); boolean shift = attr != null && Boolean.valueOf(attr); loading = "ctrl"; attr = bind.getAttributeValue(loading); boolean ctrl = attr != null && Boolean.valueOf(attr); loading = "alt"; attr = bind.getAttributeValue(loading); boolean alt = attr != null && Boolean.valueOf(attr); loading = "cmd"; attr = bind.getAttributeValue(loading); boolean cmd = attr != null && Boolean.valueOf(attr); loading = "char"; attr = bind.getAttributeValue(loading); if (attr == null) throw new NullPointerException(); if (attr.length() > 1) throw new IllegalArgumentException(action + " keybind has invalid 'char' attribute: " + attr); boolean enabled = attr.length() == 1; int ch = '\0'; if (enabled) ch = attr.charAt(0); Hotkey h = Manager.getHotkey(action); h.setEnabled(enabled); h.setShift(shift); h.setCtrl(ctrl); h.setAlt(alt); h.setCommand(cmd); h.setKey(ch); } catch (IllegalArgumentException e) { log.warn(String.format("Keybind for action %s had invalid '%s' attribute and was not loaded.", action.name(), loading)); } catch (NullPointerException e) { log.warn(String.format("Keybind for action %s was missing '%s' attribute and was not loaded.", action.name(), loading)); } } } catch (FileNotFoundException ex) { log.error("Keybind file could not be found: " + f.getAbsolutePath()); } catch (IOException ex) { log.error("An error has occured while loading keybind file: ", ex); } catch (JDOMParseException ex) { log.error("JDOM exception occured while loading file " + f.getAbsolutePath(), ex); } } /** * Saves the current hotkey configuration in the given file. * * @param f * File in which hotkey config will be saved. */ private static void saveHotkeys(File f) { Document keyDoc = new Document(); Element wrapper = new Element("wrapper"); Element root = new Element("keybinds"); for (Hotkeys action : Hotkeys.values()) { Hotkey h = Manager.getHotkey(action); Element bind = new Element("bind"); bind.setText(action.name()); bind.setAttribute("shift", "" + h.getShift()); bind.setAttribute("ctrl", "" + h.getCtrl()); bind.setAttribute("alt", "" + h.getAlt()); bind.setAttribute("cmd", "" + h.getCommand()); bind.setAttribute("char", "" + (h.isEnabled() ? h.getKeyString() : "")); root.addContent(bind); } wrapper.addContent(root); keyDoc.setRootElement(wrapper); try { IOUtils.writeFileXML(keyDoc, f); } catch (IOException e) { log.error("An error occured while saving hotkeys file: ", e); } } }