/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol 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 2 of the License, or * (at your option) any later version. * * FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol; import java.awt.Dimension; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Locale; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.events.XMLEvent; import net.sf.freecol.client.ClientOptions; import net.sf.freecol.client.FreeColClient; import net.sf.freecol.client.gui.i18n.Messages; import net.sf.freecol.common.FreeColException; import net.sf.freecol.common.FreeColSeed; import net.sf.freecol.common.debug.FreeColDebugger; import net.sf.freecol.common.io.FreeColDirectories; import net.sf.freecol.common.io.FreeColSavegameFile; import net.sf.freecol.common.io.FreeColTcFile; import net.sf.freecol.common.logging.DefaultHandler; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.StringTemplate; import net.sf.freecol.common.networking.NoRouteToServerException; import net.sf.freecol.common.option.LanguageOption; import net.sf.freecol.common.util.XMLStream; import net.sf.freecol.server.FreeColServer; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; /** * This class is responsible for handling the command-line arguments * and starting either the stand-alone server or the client-GUI. * * @see net.sf.freecol.client.FreeColClient FreeColClient * @see net.sf.freecol.server.FreeColServer FreeColServer */ public final class FreeCol { private static final Logger logger = Logger.getLogger(FreeCol.class.getName()); public static final String META_SERVER_ADDRESS = "meta.freecol.org"; public static final int META_SERVER_PORT = 3540; public static final int DEFAULT_PORT = 3541; public static final int DEFAULT_TIMEOUT = 60; // 1 minute public static final int TIMEOUT_MIN = 10; // 10s public static final String CLIENT_THREAD = "FreeColClient:"; public static final String SERVER_THREAD = "FreeColServer:"; public static final String METASERVER_THREAD = "FreeColMetaServer:"; private static final String FREECOL_VERSION = "0.10.0-trunk"; private static String FREECOL_REVISION; private static final String MIN_JDK_VERSION = "1.5"; private static final String DEFAULT_SPLASH_FILE = "splash.jpg"; private static boolean sound = true, javaCheck = true, memoryCheck = true, consoleLogging = false, introVideo = true; private static String logFile = null; private static boolean standAloneServer = false; private static boolean publicServer = true; private static String fontName = null; private static int serverPort = -1; private static String serverName = null; private static Level logLevel = Level.INFO; private static boolean checkIntegrity = false; private static final Options options = new Options(); private static String splashFilename = DEFAULT_SPLASH_FILE; private static Dimension windowSize; private static int freeColTimeout = -1; private FreeCol() { // Hide constructor } /** * The entrypoint. * * @param args The command-line arguments. */ public static void main(String[] args) { try { Manifest manifest = new Manifest(FreeCol.class.getResourceAsStream("/META-INF/MANIFEST.MF")); Attributes attribs = manifest.getMainAttributes(); String revision = attribs.getValue("Revision"); FREECOL_REVISION = FREECOL_VERSION + " (Revision: " + revision + ")"; } catch (Exception e) { System.out.println("Unable to load Manifest."); FREECOL_REVISION = FREECOL_VERSION; } // parse command line arguments handleArgs(args); FreeColDirectories.createAndSetDirectories(); initLogging(); Locale locale = getLocale(); Locale.setDefault(locale); Messages.setMessageBundle(locale); if (javaCheck && !checkJavaVersion()) { System.err.println("Java version " + MIN_JDK_VERSION + " or better is recommended in order to run FreeCol." + " Use --no-java-check to skip this check."); System.exit(1); } int minMemory = 128; // million bytes if (memoryCheck && Runtime.getRuntime().maxMemory() < minMemory * 1000000) { System.out.println("You need to assign more memory to the JVM. Restart FreeCol with:"); System.out.println("java -Xmx" + minMemory + "M -jar FreeCol.jar"); System.exit(1); } if (standAloneServer) { startServer(); } else { FreeColClient freeColClient = new FreeColClient(FreeColDirectories.getSavegameFile(), windowSize, sound, splashFilename, introVideo, fontName); } } /** * Initialize loggers. */ private static void initLogging() { final Logger baseLogger = Logger.getLogger(""); final Handler[] handlers = baseLogger.getHandlers(); for (int i = 0; i < handlers.length; i++) { baseLogger.removeHandler(handlers[i]); } if (logFile == null) { logFile = FreeColDirectories.getMainUserDirectory().getPath() + File.separator + "FreeCol.log"; } try { baseLogger.addHandler(new DefaultHandler(consoleLogging, logFile)); Logger freecolLogger = Logger.getLogger("net.sf.freecol"); freecolLogger.setLevel(logLevel); } catch (FreeColException e) { e.printStackTrace(); } Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { public void uncaughtException(Thread thread, Throwable e) { baseLogger.log(Level.WARNING, "Uncaught exception from thread: " + thread, e); } }); } /** * Determines the <code>Locale</code> to be used. * @return Currently this method returns the locale set by * the ClientOptions (read directly from "options.xml"). * This behavior will probably be changed. */ public static Locale getLocale() { XMLInputFactory xif = XMLInputFactory.newInstance(); XMLStreamReader in = null; File options = FreeColDirectories.getClientOptionsFile(); if (options.canRead()) { try { in = xif.createXMLStreamReader(new FileInputStream(options), "UTF-8"); in.nextTag(); /** * The following code was contributed by armcode to fix * bug #[ 2045521 ] "Exception in Freecol.log on starting * game". I was never able to reproduce the bug, but the * patch did no harm either. */ for(int eventid = in.getEventType();eventid != XMLEvent.END_DOCUMENT; eventid = in.getEventType()) { //TODO: Is checking for XMLEvent.ATTRIBUTE needed? if (eventid == XMLEvent.START_ELEMENT) { if (ClientOptions.LANGUAGE.equals(in.getAttributeValue(null, "id"))) { return LanguageOption.getLocale(in.getAttributeValue(null, "value")); } } in.nextTag(); } //We don't have a language option in our file, it is either not there or the file is corrupt logger.log(Level.WARNING, "Language setting not found in client options file. Using default."); } catch (Exception e) { logger.log(Level.WARNING, "Exception while loading options.", e); } finally { try { if (in != null) { in.close(); } } catch (Exception e) { logger.log(Level.WARNING, "Exception while closing stream.", e); } } } return Locale.getDefault(); } /** * Returns the default server network port. * @return The port number. */ public static int getDefaultPort() { return DEFAULT_PORT; } /** * Set up the save file and directory * @param name the name of the save file to use */ private static void setSavegame(String name) { if(name == null){ System.out.println("No savegame given with --load-savegame parameter"); System.exit(1); } FreeColDirectories.setSaveGameFile(name); } /** * Ensure that the Java version is good enough. JDK 1.4 or better is * required. * * @return true if Java version is at least 1.5.0. */ private static boolean checkJavaVersion() { // Must use string comparison because some JVM's provide // versions like "1.4.1" String version = System.getProperty("java.version"); boolean success = (version.compareTo(MIN_JDK_VERSION) >= 0); return success; } /** * Checks the command-line arguments and takes appropriate actions * for each of them. * * @param args The command-line arguments. */ @SuppressWarnings("static-access") private static void handleArgs(String[] args) { // create the command line parser CommandLineParser parser = new PosixParser(); /** * Ugly hack: try to determine language first, so that usage, * etc. will be localized. */ String localeArg = null; String locationArg = null; for (int index = 0; index < args.length - 1; index++) { if ("--default-locale".equals(args[index])) { localeArg = args[++index]; } else if ("--freecol-data".equals(args[index])) { locationArg = args[++index]; } } if (locationArg != null) { FreeColDirectories.setDataFolder(locationArg); } if (localeArg == null) { Messages.setMessageBundle(Locale.getDefault()); } else { Locale locale = LanguageOption.getLocale(localeArg); Locale.setDefault(locale); Messages.setMessageBundle(locale); } // create the Options options.addOption(OptionBuilder.withLongOpt("freecol-data") .withDescription(Messages.message("cli.freecol-data")) .withArgName(Messages.message("cli.arg.directory")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("tc") .withDescription(Messages.message("cli.tc")) .withArgName(Messages.message("cli.arg.name")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("home-directory") .withDescription(Messages.message("cli.home-directory")) .withArgName(Messages.message("cli.arg.directory")) .withType(new File("dummy")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("log-console") .withDescription(Messages.message("cli.log-console")) .create()); options.addOption(OptionBuilder.withLongOpt("log-file") .withDescription(Messages.message("cli.log-file")) .withArgName(Messages.message("cli.arg.name")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("log-level") .withDescription(Messages.message("cli.log-level")) .withArgName(Messages.message("cli.arg.loglevel")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("no-java-check") .withDescription(Messages.message("cli.no-java-check")) .create()); options.addOption(OptionBuilder.withLongOpt("windowed") .withDescription(Messages.message("cli.windowed")) .withArgName(Messages.message("cli.arg.dimensions")) .hasOptionalArg() .create()); options.addOption(OptionBuilder.withLongOpt("default-locale") .withDescription(Messages.message("cli.default-locale")) .withArgName(Messages.message("cli.arg.locale")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("no-memory-check") .withDescription(Messages.message("cli.no-memory-check")) .create()); options.addOption(OptionBuilder.withLongOpt("no-intro") .withDescription(Messages.message("cli.no-intro")) .create()); options.addOption(OptionBuilder.withLongOpt("no-sound") .withDescription(Messages.message("cli.no-sound")) .create()); options.addOption(OptionBuilder.withLongOpt("usage") .withDescription(Messages.message("cli.help")) .create()); options.addOption(OptionBuilder.withLongOpt("help") .withDescription(Messages.message("cli.help")) .create()); options.addOption(OptionBuilder.withLongOpt("version") .withDescription(Messages.message("cli.version")) .create()); options.addOption(OptionBuilder.withLongOpt("debug") .withDescription(Messages.message("cli.debug")) .withArgName(Messages.message("cli.arg.debuglevel")) .hasOptionalArg() .create()); options.addOption(OptionBuilder.withLongOpt("debug-run") .withDescription(Messages.message("cli.debug-run")) .withArgName(Messages.message("cli.arg.debugRun")) .hasOptionalArg() .create()); options.addOption(OptionBuilder.withLongOpt("private") .withDescription(Messages.message("cli.private")) .create()); options.addOption(OptionBuilder.withLongOpt("server") .withDescription(Messages.message("cli.server")) .withArgName(Messages.message("cli.arg.port")) .hasOptionalArg() .create()); options.addOption(OptionBuilder.withLongOpt("load-savegame") .withDescription(Messages.message("cli.load-savegame")) .withArgName(Messages.message("cli.arg.file")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("server-name") .withDescription(Messages.message("cli.server-name")) .withArgName(Messages.message("cli.arg.name")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("splash") .withDescription(Messages.message("cli.splash")) .withArgName(Messages.message("cli.arg.file")) .hasOptionalArg() .create()); options.addOption(OptionBuilder.withLongOpt("check-savegame") .withDescription(Messages.message("cli.check-savegame")) .create()); options.addOption(OptionBuilder.withLongOpt("font") .withDescription(Messages.message("cli.font")) .withArgName(Messages.message("cli.arg.font")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("seed") .withDescription(Messages.message("cli.seed")) .withArgName(Messages.message("cli.arg.seed")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("timeout") .withDescription(Messages.message("cli.timeout")) .withArgName(Messages.message("cli.arg.timeout")) .hasArg() .create()); options.addOption(OptionBuilder.withLongOpt("clientOptions") .withDescription(Messages.message("cli.clientOptions")) .withArgName(Messages.message("cli.arg.clientOptions")) .hasArg() .create()); try { // parse the command line arguments CommandLine line = parser.parse(options, args); if (line.hasOption("default-locale")) { // slightly ugly: strip encoding from LC_MESSAGES String languageID = line.getOptionValue("default-locale"); int index = languageID.indexOf('.'); if (index > 0) { languageID = languageID.substring(0, index); } Locale newLocale = LanguageOption.getLocale(languageID); Locale.setDefault(newLocale); Messages.setMessageBundle(newLocale); } if (line.hasOption("splash")) { final String str = line.getOptionValue("splash"); if (str != null) { splashFilename = str; } } if (line.hasOption("freecol-data")) { FreeColDirectories.setDataFolder(line.getOptionValue("freecol-data")); } if (line.hasOption("tc")) { FreeColDirectories.setTc(line.getOptionValue("tc")); } if (line.hasOption("home-directory")) { String arg = line.getOptionValue("home-directory"); FreeColDirectories.setMainUserDirectory(new File(arg)); String errMsg = null; if(!FreeColDirectories.getMainUserDirectory().exists()){ errMsg = "cli.error.home.notExists"; } if(!FreeColDirectories.getMainUserDirectory().canRead()){ errMsg = "cli.error.home.noRead"; } if(!FreeColDirectories.getMainUserDirectory().canWrite()){ errMsg = "cli.error.home.noWrite"; } if(errMsg != null){ System.out.println(Messages.message(StringTemplate.template(errMsg) .addName("%string%", arg))); System.exit(1); } } if (line.hasOption("log-console")) { consoleLogging = true; } if (line.hasOption("log-file")) { logFile = line.getOptionValue("log-file"); } if (line.hasOption("log-level")) { String logLevelString = line.getOptionValue("log-level").toUpperCase(); try { logLevel = Level.parse(logLevelString); } catch (IllegalArgumentException e) { printUsage(); System.exit(1); } } if (line.hasOption("no-java-check")) { javaCheck = false; } if (line.hasOption("windowed")) { String dimensions = line.getOptionValue("windowed"); if (dimensions == null) { windowSize = new Dimension(-1, -1); } else { String[] xy = dimensions.split("[^0-9]"); if (xy.length == 2) { windowSize = new Dimension(Integer.parseInt(xy[0]), Integer.parseInt(xy[1])); } else { printUsage(); System.exit(1); } } } if (line.hasOption("no-sound")) { sound = false; } if (line.hasOption("no-intro")) { introVideo = false; } if (line.hasOption("no-memory-check")) { memoryCheck = false; } if (line.hasOption("help") || line.hasOption("usage")) { printUsage(); System.exit(0); } if (line.hasOption("version")) { System.out.println("FreeCol " + getVersion()); System.exit(0); } if (line.hasOption("debug")) { // If the optional argument is supplied use limited mode. FreeColDebugger.configureDebugLevel(line.getOptionValue("debug")); // user set log level has precedence if (!line.hasOption("log-level")) { logLevel = Level.FINEST; } if (line.hasOption("debug-run")) { FreeColDebugger.configureDebugRun(line.getOptionValue("debug-run")); } } if (line.hasOption("server")) { standAloneServer = true; String arg = line.getOptionValue("server"); if (arg != null) { try { serverPort = Integer.parseInt(arg); } catch (NumberFormatException nfe) { System.out.println(Messages.message(StringTemplate.template("cli.error.port") .addName("%string%", arg))); System.exit(1); } } } if (line.hasOption("private")) { publicServer = false; } if (line.hasOption("check-savegame")) { setSavegame(line.getOptionValue("load-savegame")); checkIntegrity = true; standAloneServer = true; } if (line.hasOption("load-savegame")) { setSavegame(line.getOptionValue("load-savegame")); } if (line.hasOption("server-name")) { serverName = line.getOptionValue("server-name"); } if (line.hasOption("font")) { fontName = line.getOptionValue("font"); } if (line.hasOption("seed")) { String seedStr = line.getOptionValue("seed"); try { FreeColSeed.initialize(Long.parseLong(seedStr)); } catch (NumberFormatException e) { System.err.println("Ignoring bad seed: " + seedStr); } } if (line.hasOption("timeout")) { String timeoutStr = line.getOptionValue("timeout"); int result; try { result = Integer.parseInt(timeoutStr); } catch (NumberFormatException nfe) { result = -1; } if (result < TIMEOUT_MIN) { System.err.println("Ignoring bad timeout: " + timeoutStr); } else { freeColTimeout = result; } } if (line.hasOption("clientOptions")) { String fileName = line.getOptionValue("clientOptions"); File file = new File(fileName); if (file.exists() && file.isFile() && file.canRead()) { FreeColDirectories.setClientOptionsFile(file); } else { String err = Messages.message(StringTemplate .template("cli.error.clientOptions") .addName("%string%", fileName)); System.err.println(err); } } } catch (ParseException e) { System.err.println("\n" + e.getMessage() + "\n"); printUsage(); System.exit(1); } } private static void printUsage() { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("java -Xmx 128M -jar freecol.jar [OPTIONS]", options); } /** * Gets the current version of game. * * @return The current version of the game using the format "x.y.z", * where "x" is major, "y" is minor and "z" is revision. */ public static String getVersion() { return FREECOL_VERSION; } /** * Gets the current revision of game. * * @return The current version and SVN Revision of the game. */ public static String getRevision() { return FREECOL_REVISION; } /** * Returns the name of the log file. * * @return a <code>String</code> value */ public static String getLogFile() { return logFile; } /** * Gets the timeout. * Use the command line specified one if any, otherwise default * to `infinite' in single player and the DEFAULT_TIMEOUT for * multiplayer. * * @param singlePlayer True if this is a single player game. * @return A suitable timeout value. */ public static int getFreeColTimeout(boolean singlePlayer) { return (freeColTimeout >= TIMEOUT_MIN) ? freeColTimeout : (singlePlayer) ? Integer.MAX_VALUE : DEFAULT_TIMEOUT; } private static void startServer() { logger.info("Starting stand-alone server."); try { final FreeColServer freeColServer; if (FreeColDirectories.getSavegameFile() != null) { XMLStream xs = null; try { // Get suggestions for "singlePlayer" and "public // game" settings from the file: final FreeColSavegameFile fis = new FreeColSavegameFile(FreeColDirectories.getSavegameFile()); xs = FreeColServer.createXMLStreamReader(fis); final XMLStreamReader in = xs.getXMLStreamReader(); in.nextTag(); xs.close(); freeColServer = new FreeColServer(fis, serverPort, serverName); if (checkIntegrity) { boolean integrityOK = freeColServer.getIntegrity(); System.out.println(Messages.message((integrityOK) ? "cli.check-savegame.success" : "cli.check-savegame.failure")); System.exit((integrityOK) ? 0 : 1); } } catch (Exception e) { if (checkIntegrity) { System.out.println(Messages.message("cli.check-savegame.failure")); } System.out.println("Could not load savegame."); System.exit(1); return; } finally { xs.close(); } } else { try { FreeColTcFile tcData = new FreeColTcFile(FreeColDirectories.getTc()); Specification specification = tcData.getSpecification(); freeColServer = new FreeColServer(specification, publicServer, false, serverPort, serverName); } catch (NoRouteToServerException e) { System.out.println(Messages.message("server.noRouteToServer")); System.exit(1); return; } } Runtime runtime = Runtime.getRuntime(); runtime.addShutdownHook(new Thread() { public void run() { freeColServer.getController().shutdown(); } }); } catch (IOException e) { System.err.println("Error while loading server: " + e); System.exit(1); } } }