/** * Copyright (C) 2002-2022 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 . */ package net.sf.freecol; import static net.sf.freecol.common.util.CollectionUtils.alwaysTrue; import static net.sf.freecol.common.util.CollectionUtils.find; import static net.sf.freecol.common.util.CollectionUtils.map; import static net.sf.freecol.common.util.CollectionUtils.transform; import static net.sf.freecol.common.util.StringUtils.upCase; import java.awt.Dimension; import java.awt.GraphicsDevice; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.Inet4Address; import java.net.InetAddress; import java.net.JarURLConnection; import java.net.URL; import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import javax.swing.SwingUtilities; import javax.xml.stream.XMLStreamException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.sf.freecol.client.ClientOptions; import net.sf.freecol.client.FreeColClient; import net.sf.freecol.client.gui.SplashScreen; import net.sf.freecol.common.FreeColException; import net.sf.freecol.common.FreeColSeed; import net.sf.freecol.common.debug.FreeColDebugger; import net.sf.freecol.common.i18n.Messages; import net.sf.freecol.common.io.FreeColDirectories; import net.sf.freecol.common.io.FreeColModFile; import net.sf.freecol.common.io.FreeColRules; 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.Constants.IntegrityType; import net.sf.freecol.common.model.NationOptions.Advantages; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.StringTemplate; import net.sf.freecol.common.option.OptionGroup; import net.sf.freecol.common.util.LogBuilder; import net.sf.freecol.common.util.OSUtils; import net.sf.freecol.common.util.Utils; import net.sf.freecol.server.FreeColServer; import net.sf.freecol.server.control.Controller; /** * 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 { static { /* * Deactivate automatic UI scaling since we are solving it * manually instead. * * This needs to be done before any Swing/AWT class gets * loaded. Please keep this static block right here at the top. */ System.setProperty("sun.java2d.uiScale", "1.0"); System.setProperty("sun.java2d.uiScale.enabled", "false"); } private static final Logger logger = Logger.getLogger(FreeCol.class.getName()); /** The FreeCol release version number. */ private static final String FREECOL_VERSION = "0.13.0+dev"; /** The FreeCol protocol version number. */ private static final String FREECOL_PROTOCOL_VERSION = "0.1.6"; /** The difficulty levels. */ private static final String[] DIFFICULTIES = { "veryEasy", "easy", "medium", "hard", "veryHard" }; /** The extension for FreeCol saved games. */ public static final String FREECOL_SAVE_EXTENSION = "fsg"; /** The extension for FreeCol maps. */ public static final String FREECOL_MAP_EXTENSION = "fsm"; /** The Java version. */ private static final String JAVA_VERSION = System.getProperty("java.version"); /** The maximum available memory. */ private static final long MEMORY_MAX = Runtime.getRuntime().maxMemory(); private static final long MEGA = 1000000; // Metric public static final String CLIENT_THREAD = "FreeColClient:"; public static final String SERVER_THREAD = "FreeColServer:"; public static final String METASERVER_THREAD = "FreeColMetaServer:"; /** Specific revision number (currently the git tag of trunk at release) */ private static String freeColRevision = null; /** The locale, either default or command-line specified. */ private static Locale locale = null; // Cli defaults. private static final Advantages ADVANTAGES_DEFAULT = Advantages.SELECTABLE; private static final String DIFFICULTY_DEFAULT = "model.difficulty.medium"; private static final int EUROPEANS_DEFAULT = 4; private static final int EUROPEANS_MIN = 1; public static final float GUI_SCALE_DEFAULT = 1.0f; private static final int GUI_SCALE_MIN_PCT = 100; private static final int GUI_SCALE_MAX_PCT = 200; public static final float GUI_SCALE_MIN = GUI_SCALE_MIN_PCT / 100.0f; public static final float GUI_SCALE_MAX = GUI_SCALE_MAX_PCT / 100.0f; private static final int GUI_SCALE_STEP_PCT = 25; public static final float GUI_SCALE_STEP = GUI_SCALE_STEP_PCT / 100.0f; private static final Level LOGLEVEL_DEFAULT = Level.INFO; private static final String JAVA_VERSION_MIN = "11"; private static final long MEMORY_MIN = 2000000000; // a bit less than 2GB private static final String META_SERVER_ADDRESS = "meta.freecol.org"; private static final int META_SERVER_PORT = 3540; private static final int PORT_DEFAULT = 3541; private static final String SPLASH_DEFAULT = "splash.jpg"; private static final String TC_DEFAULT = "default"; private static final String RULES_DEFAULT = "freecol"; public static final long TIMEOUT_DEFAULT = 60L; // 1 minute public static final long TIMEOUT_MIN = 10L; // 10s public static final long TIMEOUT_MAX = 3600000L; // 1000hours:-) private static final Dimension WINDOWSIZE_FALLBACK = new Dimension(-1, -1); // Cli values. Often set to null so the default can be applied in // the accessor function. private static boolean checkIntegrity = false, consoleLogging = false, debugStart = false, fastStart = false, headless = false, introVideo = true, javaCheck = true, memoryCheck = true, publicServer = true, sound = true, standAloneServer = false; /** The type of advantages. */ private static Advantages advantages = null; /** The difficulty level id. */ private static String difficulty = null; /** The number of European nations to enable by default. */ private static int europeanCount = EUROPEANS_DEFAULT; /** A font override. */ private static String fontName = null; /** The levels of logging in this game. */ private static class LogLevel { public final String name; public final Level level; // We need to keep a hard reference to the instantiated logger, as // Logger only uses weak references. public Logger logger; public LogLevel(String name, Level level) { this.name = name; this.level = level; this.logger = null; } public void buildLogger() { this.logger = Logger.getLogger("net.sf.freecol" + ((this.name.isEmpty()) ? "" : "." + this.name)); this.logger.setLevel(this.level); } } private static final List logLevels = new ArrayList<>(); static { logLevels.add(new LogLevel("", LOGLEVEL_DEFAULT)); } /** Meta-server location. */ private static String metaServerAddress = META_SERVER_ADDRESS; private static int metaServerPort = META_SERVER_PORT; /** The client player name. */ private static String name = null; /** How to name and configure the server. */ private static InetAddress serverAddress = null; private static int serverPort = -1; private static String serverName = null; /** A stream to get the splash image from. */ private static InputStream splashStream; /** The TotalConversion in play, defaults to "default". */ private static String tc = null; /** The ruleset in play, defaults to "freecol". */ private static String rules = null; /** The time out (seconds) for otherwise blocking commands. */ private static long timeout = -1L; /** * The size of window to create, defaults to impossible dimensions * to require windowed mode with best determined screen size. */ private static Dimension windowSize = WINDOWSIZE_FALLBACK; /** The special client options that must be processed early. */ private static Map specialOptions = null; private FreeCol() {} // Hide constructor /** * The entrypoint. * * @param args The command-line arguments. */ public static void main(String[] args) { freeColRevision = FREECOL_VERSION; JarURLConnection juc; try { juc = getJarURLConnection(FreeCol.class); } catch (ClassCastException cce) { juc = null; System.err.println("Unable to cast class properly: " + cce.getMessage()); } catch (IOException ioe) { juc = null; System.err.println("Unable to open class jar: " + ioe.getMessage()); } if (juc != null) { try { String revision = readVersion(juc); if (revision != null) { freeColRevision += " (Revision: " + revision + ")"; } } catch (Exception e) { System.err.println("Unable to load Manifest: " + e.getMessage()); } try { splashStream = getDefaultSplashStream(juc); } catch (Exception e) { System.err.println("Unable to open default splash: " + e.getMessage()); } } // We can not even emit localized error messages until we find // the data directory, which might have been specified on the // command line. String dataDirectoryArg = findArg("--freecol-data", args); String err = FreeColDirectories.setDataDirectory(dataDirectoryArg); if (err != null) fatal(err); // This must not fail. // Now we have the data directory, establish the base locale. // Beware, the locale may change! String localeArg = findArg("--default-locale", args); setLocale(localeArg); Messages.loadMessageBundle(getLocale()); // Now that we can emit error messages, parse the other // command line arguments. handleArgs(args); // Do the potentially fatal system checks as early as possible. if (javaCheck && JAVA_VERSION_MIN.compareTo(JAVA_VERSION) > 0) { fatal(StringTemplate.template("main.javaVersion") .addName("%version%", JAVA_VERSION) .addName("%minVersion%", JAVA_VERSION_MIN)); } if (memoryCheck && MEMORY_MAX < MEMORY_MIN) { // Memory message is in Mbytes, hence division by MEGA. fatal(StringTemplate.template("main.memory") .addAmount("%memory%", MEMORY_MAX) .addAmount("%minMemory%", MEMORY_MIN / MEGA)); } // Having parsed the command line args, we know where the user // directories should be, so we can set up the rest of the // file/directory structure. Exit on failure here. StringTemplate key = FreeColDirectories.setUserDirectories(); if (key != null) fatal(key); // Now we have the log file path, start logging. final Logger baseLogger = Logger.getLogger(""); for (Handler handler : baseLogger.getHandlers()) { baseLogger.removeHandler(handler); } try { Writer writer = FreeColDirectories.getLogWriter(); baseLogger.addHandler(new DefaultHandler(consoleLogging, writer)); for (LogLevel ll : logLevels) ll.buildLogger(); } catch (FreeColException e) { System.err.println("Logging initialization failure: " + e); } // This is overridden by FreeColClient. Thread.setDefaultUncaughtExceptionHandler((Thread thread, Throwable e) -> { baseLogger.log(Level.WARNING, "Uncaught exception from thread: " + thread, e); if (e instanceof Error) { e.printStackTrace(); System.exit(1); } }); // Now we can find the client options, allow the options // setting to override the locale, but only if no command line // option had been specified. This works for our users who, // say, have machines that default to Finnish but play FreeCol in // English. // // If the user has selected automatic language selection, do // nothing, since we have already set up the default locale. try { specialOptions = ClientOptions.getSpecialOptions(); } catch (FreeColException fce) { specialOptions = new HashMap<>(); logger.log(Level.WARNING, "Special options unavailable", fce); } String cLang; if (localeArg == null && (cLang = specialOptions.get(ClientOptions.LANGUAGE)) != null && !Messages.AUTOMATIC.equalsIgnoreCase(cLang) && setLocale(cLang)) { Locale loc = getLocale(); Messages.loadMessageBundle(loc); logger.info("Loaded messages for " + loc); } // Now we have the user mods directory and the locale is now // stable, load the rules, the mods and their messages. FreeColRules.loadRules(); FreeColModFile.loadMods(); Messages.loadModMessageBundle(getLocale()); // Sort out the special graphics options before touching the GUI // (which is initialized by FreeColClient). These options control // graphics pipeline initialization, and are ineffective if twiddled // after the first Java2D call is made. headless |= Utils.isHeadless(); processSpecialOptions(); // Report on where we are. logger.info(getConfiguration().toString()); // Ready to specialize into client or server. if (standAloneServer) { startServer(); } else { if (headless) { if (!FreeColDebugger.isInDebugMode() || FreeColDebugger.getDebugRunTurns() <= 0) { fatal(logger, Messages.message("client.headlessDebug")); } } startClient(); } } /** * Handle the special options that need to be processed early. */ private static void processSpecialOptions() { LogBuilder lb = new LogBuilder(64); // Work around a Java 2D bug that seems to be X11 specific. // According to: // http://www.oracle.com/technetwork/java/javase/index-142560.html // // ``The use of pixmaps typically results in better // performance. However, in certain cases, the opposite is true.'' // // The standard workaround is to use -Dsun.java2d.pmoffscreen=false, // but this is too hard for some users, so provide an option to // do it easily. However respect the initial value if present. // // Remove this if Java 2D is ever fixed. DHYB. // final String pmoffscreen = "sun.java2d.pmoffscreen"; final String pmoValue = System.getProperty(pmoffscreen); if (pmoValue == null) { String usePixmaps = specialOptions.get(ClientOptions.USE_PIXMAPS); if (usePixmaps != null) { System.setProperty(pmoffscreen, usePixmaps); lb.add(pmoffscreen, " using client option: ", usePixmaps); } else { lb.add(pmoffscreen, " unset/ignored: "); } } else { lb.add(pmoffscreen, " overrides client option: ", pmoValue); } // There is also this option, BR#3102. final String openGL = "sun.java2d.opengl"; final String openGLValue = System.getProperty(openGL); if (openGLValue == null) { String useOpenGL = specialOptions.get(ClientOptions.USE_OPENGL); if (useOpenGL != null) { System.setProperty(openGL, useOpenGL); lb.add(", ", openGL, " using client option: ", useOpenGL); } else { lb.add(", ", openGL, " unset/ignored"); } } else { lb.add(", ", openGL, " overrides client option: ", openGLValue); } // XRender is available for most unix (not MacOS?) if (OSUtils.onUnix()) { final String xrender = "sun.java2d.xrender"; final String xrValue = System.getProperty(xrender); if (xrValue == null) { String useXR = specialOptions.get(ClientOptions.USE_XRENDER); if (useXR != null) { System.setProperty(xrender, useXR); lb.add(", ", xrender, " using client option: ", useXR); } else { lb.add(", ", xrender, " unset/ignored"); } } else { lb.add(", ", xrender, " overrides client option: ", xrValue); } } lb.log(logger, Level.INFO); } /** * Get the JarURLConnection from a class. * * @param c The {@code Class} to get the connection for. * @return The {@code JarURLConnection}. * @exception IOException if the connection fails to open. */ private static JarURLConnection getJarURLConnection(Class c) throws IOException { String resourceName = "/" + c.getName().replace('.', '/') + ".class"; URL url = c.getResource(resourceName); return (JarURLConnection)url.openConnection(); } /** * Extract the package version from the class. * * @param juc The {@code JarURLConnection} to extract from. * @return A value of the package version attribute. * @exception IOException if the manifest is not available. */ private static String readVersion(JarURLConnection juc) throws IOException { Manifest mf = juc.getManifest(); return (mf == null) ? null : mf.getMainAttributes().getValue("Package-Version"); } /** * Get a stream for the default splash file. * * Note: Not bothering to check for nulls as this is called in try * block that ignores all exceptions. * * @param juc The {@code JarURLConnection} to extract from. * @return A suitable {@code InputStream}, or null on error. * @exception IOException if the connection fails to open. */ private static InputStream getDefaultSplashStream(JarURLConnection juc) throws IOException { JarFile jf = juc.getJarFile(); ZipEntry ze = jf.getEntry(SPLASH_DEFAULT); return jf.getInputStream(ze); } /** * Quit FreeCol. Route all exits through here. * * @param status Exit status. */ @SuppressFBWarnings(value="DM_EXIT", justification="Deliberate") public static void quit(int status) { System.exit(status); } /** * Quit and exit with an error. * * @param logger An optional {@code Logger} to log to. * @param err The error message. */ public static void fatal(Logger logger, String err) { if (logger != null) logger.log(Level.SEVERE, err); FreeCol.fatal(err); } /** * Exit printing fatal error message. * * @param template A {@code StringTemplate} to print. */ private static void fatal(StringTemplate template) { fatal(Messages.message(template)); } /** * Exit printing fatal error message. * * @param err The error message to print. */ private static void fatal(String err) { if (err == null || err.isEmpty()) { err = "Bogus null fatal error message"; Thread.dumpStack(); } System.err.println(err); quit(1); } /** * Just gripe to System.err. * * @param template A {@code StringTemplate} to print. */ private static void gripe(StringTemplate template) { System.err.println(Messages.message(template)); } /** * Just gripe to System.err. * * @param key A message key. */ private static void gripe(String key) { System.err.println(Messages.message(key)); } /** * Log a warning with a stack trace. * * @param logger The {@code Logger} to log to. * @param warn The warning message. */ public static void trace(Logger logger, String warn) { FreeColDebugger.trace(logger, warn); } /** * Find an option before the real option handling can get started. * Takes care to use the *last* instance. * * @param option The option to find. * @param args The command-line arguments. * @return The option's parameter. */ private static String findArg(String option, String[] args) { for (int i = args.length - 2; i >= 0; i--) { if (option.equals(args[i])) { return args[i+1]; } } return null; } /** Definitions for all the options. */ private static String argDir = "cli.arg.directory"; private static String argFile = "cli.arg.file"; private static String[][] optionsTable = { // Help options { "?", "usage", "cli.help", null }, { "@", "help", "cli.help", null }, // Special early options { "d", "freecol-data", "cli.freecol-data", argDir }, { "L", "default-locale", "cli.default-locale", "cli.arg.locale" }, // Ordinary options { "a", "advantages", getAdvantagesDescription(), "cli.arg.advantages" }, { null, "check-savegame", "cli.check-savegame", argFile }, { "O", "clientOptions", "cli.clientOptions", "cli.arg.clientOptions" }, { null, "debug", getDebugDescription(), "cli.arg.debug" }, { "R", "debug-run", "cli.debug-run", "cli.arg.debugRun" }, { "S", "debug-start", "cli.debug-start", null }, { "D", "difficulty", "cli.difficulty", "cli.arg.difficulty" }, { "e", "europeans", "cli.european-count", "cli.arg.europeans" }, { null, "fast", "cli.fast", null }, { "f", "font", "cli.font", "cli.arg.font" }, { "F", "full-screen", "cli.full-screen", null }, { "H", "headless", "cli.headless", null }, { "l", "load-savegame", "cli.load-savegame", argFile }, { null, "log-console", "cli.log-console", null }, { null, "log-file", "cli.log-file", "cli.arg.name" }, { null, "log-level", "cli.log-level", "cli.arg.loglevel" }, { "m", "meta-server", "cli.meta-server", "cli.arg.metaServer" }, { "n", "name", "cli.name", "cli.arg.name" }, { null, "no-intro", "cli.no-intro", null }, { null, "no-java-check", "cli.no-java-check", null }, { null, "no-memory-check", "cli.no-memory-check", null }, { null, "no-sound", "cli.no-sound", null }, { null, "no-splash", "cli.no-splash", null }, { "p", "private", "cli.private", null }, { "r", "rules", "cli.rules", "cli.arg.name" }, { "Z", "seed", "cli.seed", "cli.arg.seed" }, { null, "server", "cli.server", null }, { null, "server-name", "cli.server-name", "cli.arg.name" }, { null, "server-ip", "cli.server-ip", "cli.arg.serverIp" }, { null, "server-port", "cli.server-port", "cli.arg.port" }, { "s", "splash", "cli.splash", "!" + argFile }, { "t", "tc", "cli.tc", "cli.arg.name" }, { "T", "timeout", "cli.timeout", "cli.arg.timeout" }, { "C", "user-cache-directory", "cli.user-cache-directory", argDir }, { "c", "user-config-directory", "cli.user-config-directory", argDir }, { "u", "user-data-directory", "cli.user-data-directory", argDir }, { "v", "version", "cli.version", null }, { "w", "windowed", "cli.windowed", "!cli.arg.dimensions" }, }; /** * Processes the command-line arguments and takes appropriate * actions for each of them. * * @param args The command-line arguments. */ private static void handleArgs(String[] args) { Options options = new Options(); for (String[] o : optionsTable) { String arg = o[3]; Option op = new Option(o[0], o[1], arg != null, ((o[2].startsWith("cli.")) ? Messages.message(o[2]) : o[2])); if (arg != null) { boolean optional = false; if (arg.startsWith("!")) { optional = true; arg = arg.substring(1, arg.length()); } if (arg.startsWith(argDir) || arg.startsWith(argFile)) op.setType(File.class); if (arg.startsWith("cli.")) arg = Messages.message(arg); op.setArgName(arg); op.setOptionalArg(optional); } options.addOption(op); } CommandLineParser parser = new DefaultParser(); boolean usageError = false; try { CommandLine line = parser.parse(options, args); if (line.hasOption("help") || line.hasOption("usage")) { printUsage(options, 0); } // Ignore "default-locale", "freecol-data", which are // already handled in main() if (line.hasOption("advantages")) { String arg = line.getOptionValue("advantages"); Advantages a = selectAdvantages(arg); if (a == null) { fatal(StringTemplate.template("cli.error.advantages") .addName("%advantages%", getValidAdvantages()) .addName("%arg%", arg)); } } if (line.hasOption("check-savegame")) { String arg = line.getOptionValue("check-savegame"); if (!FreeColDirectories.setSavegameFile(arg)) { fatal(StringTemplate.template("cli.err.save") .addName("%string%", arg)); } checkIntegrity = true; standAloneServer = true; } if (line.hasOption("clientOptions")) { String fileName = line.getOptionValue("clientOptions"); if (!FreeColDirectories.setClientOptionsFile(fileName)) { // Not fatal. gripe(StringTemplate.template("cli.error.clientOptions") .addName("%string%", fileName)); } } if (line.hasOption("debug")) { // If the optional argument is supplied use limited mode. String arg = line.getOptionValue("debug"); if (arg == null || arg.isEmpty()) { // Let empty argument default to menus functionality. arg = FreeColDebugger.DebugMode.MENUS.toString(); } if (!FreeColDebugger.setDebugModes(arg)) { // Not fatal. gripe(StringTemplate.template("cli.error.debug") .addName("%modes%", FreeColDebugger.getDebugModes())); } // Keep doing this before checking log-level option! logLevels.add(new LogLevel("", Level.FINEST)); } if (line.hasOption("debug-run")) { FreeColDebugger.enableDebugMode(FreeColDebugger.DebugMode.MENUS); FreeColDebugger.configureDebugRun(line.getOptionValue("debug-run")); } if (line.hasOption("debug-start")) { debugStart = true; FreeColDebugger.enableDebugMode(FreeColDebugger.DebugMode.MENUS); } if (line.hasOption("difficulty")) { String arg = line.getOptionValue("difficulty"); String difficulty = selectDifficulty(arg); if (difficulty == null) { fatal(StringTemplate.template("cli.error.difficulties") .addName("%difficulties%", getValidDifficulties()) .addName("%arg%", arg)); } } if (line.hasOption("europeans")) { int e = selectEuropeanCount(line.getOptionValue("europeans")); if (e < 0) { gripe(StringTemplate.template("cli.error.europeans") .addAmount("%min%", EUROPEANS_MIN)); } } if (line.hasOption("fast")) { fastStart = true; introVideo = false; } if (line.hasOption("font")) { fontName = line.getOptionValue("font"); } if (line.hasOption("full-screen")) { windowSize = null; } if (line.hasOption("headless")) { headless = true; } if (line.hasOption("load-savegame")) { String arg = line.getOptionValue("load-savegame"); if (!FreeColDirectories.setSavegameFile(arg)) { fatal(StringTemplate.template("cli.error.save") .addName("%string%", arg)); } } if (line.hasOption("log-console")) { consoleLogging = true; } if (line.hasOption("log-file")) { FreeColDirectories.setLogFilePath(line.getOptionValue("log-file")); } if (line.hasOption("log-level")) { for (String value : line.getOptionValues("log-level")) { String[] s = value.split(":"); logLevels.add((s.length == 1) ? new LogLevel("", Level.parse(upCase(s[0]))) : new LogLevel(s[0], Level.parse(upCase(s[1])))); } } if (line.hasOption("meta-server")) { String arg = line.getOptionValue("meta-server"); if (!setMetaServer(arg)) { gripe(StringTemplate.template("cli.error.meta-server") .addName("%arg%", arg)); } } if (line.hasOption("name")) { setName(line.getOptionValue("name")); } if (line.hasOption("no-intro")) { introVideo = false; } if (line.hasOption("no-java-check")) { javaCheck = false; } if (line.hasOption("no-memory-check")) { memoryCheck = false; } if (line.hasOption("no-sound")) { sound = false; } if (line.hasOption("no-splash")) { splashStream = null; } if (line.hasOption("private")) { publicServer = false; } if (line.hasOption("server")) { standAloneServer = true; } if (line.hasOption("server-name")) { serverName = line.getOptionValue("server-name"); } if (line.hasOption("server-port")) { String arg = line.getOptionValue("server-port"); if (!setServerPort(arg)) { fatal(StringTemplate.template("cli.error.serverPort") .addName("%string%", arg)); } } if (line.hasOption("server-ip")) { String arg = line.getOptionValue("server-ip"); if (!setServerAddress(arg)) { fatal(StringTemplate.template("cli.error.serverIp") .addName("%string%", arg)); } } boolean seeded = (line.hasOption("seed") && FreeColSeed.setFreeColSeed(line.getOptionValue("seed"))); if (!seeded) FreeColSeed.generateFreeColSeed(); if (line.hasOption("splash")) { String splash = line.getOptionValue("splash"); try { InputStream fis = Files.newInputStream(Paths.get(splash)); splashStream = fis; } catch (IOException ioe) { gripe(StringTemplate.template("cli.error.splash") .addName("%name%", splash)); } } if (line.hasOption("tc")) { setTc(line.getOptionValue("tc")); // Failure is deferred. } if (line.hasOption("rules")) { setRules(line.getOptionValue("rules")); // Failure is deferred. } if (line.hasOption("timeout")) { String arg = line.getOptionValue("timeout"); try { setTimeout(arg); // Not fatal } catch (NumberFormatException nfe) { gripe(StringTemplate.template("cli.error.timeout") .addName("%string%", arg) .addName("%minimum%", Long.toString(TIMEOUT_MIN))); } } if (line.hasOption("user-cache-directory")) { String arg = line.getOptionValue("user-cache-directory"); String errMsg = FreeColDirectories.setUserCacheDirectory(arg); if (errMsg != null) { // Not fatal. gripe(StringTemplate.template(errMsg) .addName("%string%", arg)); } } if (line.hasOption("user-config-directory")) { String arg = line.getOptionValue("user-config-directory"); String errMsg = FreeColDirectories.setUserConfigDirectory(arg); if (errMsg != null) { // Not fatal. gripe(StringTemplate.template(errMsg) .addName("%string%", arg)); } } if (line.hasOption("user-data-directory")) { String arg = line.getOptionValue("user-data-directory"); String errMsg = FreeColDirectories.setUserDataDirectory(arg); if (errMsg != null) { // Fatal, unable to save. fatal(StringTemplate.template(errMsg) .addName("%string%", arg)); } } if (line.hasOption("version")) { System.out.println("FreeCol " + getVersion()); quit(0); } if (line.hasOption("windowed")) { String arg = line.getOptionValue("windowed"); setWindowSize(arg); // Does not fail } } catch (ParseException e) { System.err.println("\n" + e.getMessage() + "\n"); usageError = true; } if (usageError) printUsage(options, 1); } /** * Prints the usage message and exits. * * @param options The command line {@code Options}. * @param status The status to exit with. */ private static void printUsage(Options options, int status) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("java -Xmx2G -jar freecol.jar [OPTIONS]", options); quit(status); } /** * Get the specification from a given TC file. * * @param rulesFile The {@code FreeColModFile} to load. * @param advantages An optional {@code Advantages} setting. * @param difficulty An optional difficulty level. * @return A {@code Specification}. */ public static Specification loadSpecification(FreeColModFile rulesFile, Advantages advantages, String difficulty) { Specification spec = null; try { if (rulesFile != null) spec = rulesFile.getSpecification(); } catch (IOException|XMLStreamException ex) { System.err.println("Spec read failed in " + rulesFile.getId() + ": " + ex.getMessage() + "\n"); } if (spec != null) spec.prepare(advantages, difficulty); return spec; } /** * Get the specification from the currently selected rules. * * @return A {@code Specification}, quits on error. */ private static Specification getRulesSpecification() { Specification spec = loadSpecification(getRulesFile(), getAdvantages(), getDifficulty()); if (spec == null) { fatal(StringTemplate.template("cli.error.badTC") .addName("%tc%", getRules())); } return spec; } // Accessors, mutators and support for the cli variables. /** * Are we in headless mode, either externally true or explicitly * from the command line. * * @return True if in headless mode. */ public static boolean getHeadless() { return headless; } /** * Set the headless state. * * Used by the test suite. * * @param newHeadless The new headless state. */ public static void setHeadless(boolean newHeadless) { headless = newHeadless; } /** * Gets the default advantages type. * * @return Usually Advantages.SELECTABLE, but can be overridden at the * command line. */ public static Advantages getAdvantages() { return (advantages == null) ? ADVANTAGES_DEFAULT : advantages; } /** * Sets the advantages type. * * Called from NewPanel when a selection is made. * * @param as The name of the new advantages type. * @return The type of advantages set, or null if none. */ private static Advantages selectAdvantages(String as) { Advantages a = find(Advantages.values(), Messages.matchesNamed(as)); if (a != null) setAdvantages(a); return a; } /** * Sets the advantages type. * * @param advantages The new {@code Advantages} type. */ public static void setAdvantages(Advantages advantages) { FreeCol.advantages = advantages; } /** * Gets a comma separated list of localized advantage type names. * * @return A list of advantage types. */ private static String getValidAdvantages() { return transform(Advantages.values(), alwaysTrue(), a -> Messages.getName(a), Collectors.joining(",")); } /** * Get a description for the advantages argument. * * @return A suitable description. */ private static String getAdvantagesDescription() { return Messages.message(StringTemplate.template("cli.advantages") .addName("%advantages%", getValidAdvantages())); } /** * Get a description for the debug argument. * * @return A suitable description. */ private static String getDebugDescription() { return Messages.message(StringTemplate.template("cli.debug") .addName("%modes%", FreeColDebugger.getDebugModes())); } /** * Gets the difficulty level. * * @return The name of a difficulty level. */ public static String getDifficulty() { return (difficulty == null) ? DIFFICULTY_DEFAULT : difficulty; } /** * Selects a difficulty level. * * @param arg The supplied difficulty argument. * @return The name of the selected difficulty, or null if none. */ private static String selectDifficulty(String arg) { String difficulty = find(map(DIFFICULTIES, d -> "model.difficulty." + d), Messages.matchesName(arg)); if (difficulty != null) setDifficulty(difficulty); return difficulty; } /** * Sets the difficulty level. * * @param difficulty The actual {@code OptionGroup} * containing the difficulty level. */ public static void setDifficulty(OptionGroup difficulty) { setDifficulty(difficulty.getId()); } /** * Sets the difficulty level. * * @param difficulty The new difficulty. */ public static void setDifficulty(String difficulty) { FreeCol.difficulty = difficulty; } /** * Gets the names of the valid difficulty levels. * * @return The valid difficulty levels, comma separated. */ public static String getValidDifficulties() { return transform(DIFFICULTIES, alwaysTrue(), d -> Messages.getName("model.difficulty." + d), Collectors.joining(",")); } /** * Get the number of European nations to enable by default. * * @return The default European nation count. */ public static int getEuropeanCount() { return europeanCount; } /** * Sets the number of enabled European nations. * * @param n The number of nations to enable. */ public static void setEuropeanCount(int n) { europeanCount = n; } /** * Gets the valid scale factors for the GUI. * * @return A string containing these. */ public static String getValidGUIScales() { StringBuilder sb = new StringBuilder(64); for (int i = GUI_SCALE_MIN_PCT; i <= GUI_SCALE_MAX_PCT; i += GUI_SCALE_STEP_PCT) sb.append(i).append(','); sb.setLength(sb.length()-1); return sb.toString(); } /** * Selects a European nation count. * * @param arg The supplied count argument. * @return A valid nation number, or negative on error. */ private static int selectEuropeanCount(String arg) { try { int n = Integer.parseInt(arg); if (n >= EUROPEANS_MIN) { setEuropeanCount(n); return n; } } catch (NumberFormatException nfe) {} return -1; } /** * Get the meta-server address. * * @return The current meta-server address. */ public static String getMetaServerAddress() { return metaServerAddress; } /** * Get the meta-server port. * * @return The current meta-server port. */ public static int getMetaServerPort() { return metaServerPort; } /** * Set the meta-server location. * * @param arg The new meta-server location in HOST:PORT format. * @return True if the location was set. */ private static boolean setMetaServer(String arg) { String[] s = arg.split(":"); int port = -1; try { port = (s.length == 2) ? Integer.parseInt(s[1]) : -1; } catch (NumberFormatException nfe) {} if (s.length != 2 || s[0] == null || "".equals(s[0])) return false; metaServerAddress = s[0]; metaServerPort = port; return true; } /** * Gets the user name. * * @return The user name, defaults to the user.name property, then to * the "main.defaultPlayerName" message value. */ public static String getName() { return (name != null) ? name : System.getProperty("user.name", Messages.message("main.defaultPlayerName")); } /** * Sets the user name. * * @param name The new user name. */ public static void setName(String name) { FreeCol.name = name; logger.info("Set FreeCol.name = " + name); } /** * Get the selected locale. * * @return The {@code Locale} currently in use. */ public static Locale getLocale() { return (FreeCol.locale == null) ? Locale.getDefault() : FreeCol.locale; } /** * Set the locale. * * Public for the test suite. * * @param localeArg The locale specification, null implies the * default locale. * @return True if the {@code Locale} changed. */ @SuppressFBWarnings(value="MDM_SETDEFAULTLOCALE", justification="Locale can be reset by user") public static boolean setLocale(String localeArg) { Locale newLocale = null; if (localeArg == null) { newLocale = Locale.getDefault(); } else { int index = localeArg.indexOf('.'); // Strip encoding if present if (index > 0) localeArg = localeArg.substring(0, index); newLocale = Messages.getLocale(localeArg); } if (newLocale != FreeCol.locale) { FreeCol.locale = newLocale; Locale.setDefault(newLocale); return true; } return false; } /** * Gets the current revision of game. * * @return The current version and SVN Revision of the game. */ public static String getRevision() { return freeColRevision; } /** * Get the default server host name. * * @return The host name. */ public static String getServerHost() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) {} return InetAddress.getLoopbackAddress().getHostAddress(); } /** * Gets the server network port. * * @return The port number the server will be listening on. */ public static int getServerPort() { return (serverPort < 0) ? PORT_DEFAULT : serverPort; } /** * Gets the server address. * * @return The {@code InetAddress} the server will be listening on. */ public static InetAddress getServerName() { try { return (serverAddress == null) ? InetAddress.getByName("0.0.0.0") : serverAddress; } catch (UnknownHostException e) { return Inet4Address.getLoopbackAddress(); } } /** * Sets the server port. * * @param arg The server port number. * @return True if the port was set. */ private static boolean setServerPort(String arg) { if (arg == null) return false; try { serverPort = Integer.parseInt(arg); } catch (NumberFormatException nfe) { return false; } return true; } /** * Sets the server address. * * @param arg The server address. * @return True if the address was set. */ private static boolean setServerAddress(String arg) { if (arg == null) return false; try { serverAddress = InetAddress.getByName(arg); } catch (UnknownHostException e) { return false; } return true; } /** * Gets the current Total-Conversion. * * @return Usually TC_DEFAULT, but can be overridden at the command line. */ public static String getTc() { return (tc == null) ? TC_DEFAULT : tc; } /** * Sets the Total-Conversion. * * Called from NewPanel when a selection is made. * * @param tc The name of the new total conversion. */ public static void setTc(String tc) { FreeCol.tc = tc; } /** * Gets the current rules * * @return Usually RULES_DEFAULT, but can be overridden at the command line. */ public static String getRules() { return (rules == null) ? RULES_DEFAULT : rules; } /** * Sets the rules. * * Called from NewPanel when a selection is made. * * @param tc The name of the new total conversion. */ public static void setRules(String tc) { FreeCol.rules = tc; } /** * Gets the FreeColModFile for the current rules. * * @return The {@code FreeColModFile}. */ public static FreeColModFile getRulesFile() { return FreeColRules.getFreeColRulesFile(getRules()); } /** * Gets the timeout. * Use the command line specified one if any, otherwise default * to `infinite' in single player and the TIMEOUT_DEFAULT for * multiplayer. * * @param singlePlayer True if this is a single player game. * @return A suitable timeout value. */ public static long getTimeout(boolean singlePlayer) { if (timeout < 0L) { timeout = (singlePlayer) ? TIMEOUT_MAX : TIMEOUT_DEFAULT; } return timeout; } /** * Sets the timeout. * * @param timeout A string containing the new timeout. */ public static void setTimeout(String timeout) { long result = Long.parseLong(timeout); if (TIMEOUT_MIN <= result && result <= TIMEOUT_MAX) { FreeCol.timeout = result; } } /** * 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 version of the FreeCol protocol. * * @return The version of the FreeCol protocol. */ public static String getFreeColProtocolVersion() { return FREECOL_PROTOCOL_VERSION; } /** * Sets the window size. * * Does not fail because any empty or invalid value is interpreted as * `windowed but use as much screen as possible'. * * @param arg The window size specification. */ private static void setWindowSize(String arg) { windowSize = WINDOWSIZE_FALLBACK; // Set fallback up front if (arg != null) { String[] xy = arg.split("[^0-9]"); if (xy.length == 2) { try { windowSize = new Dimension(Integer.parseInt(xy[0]), Integer.parseInt(xy[1])); } catch (NumberFormatException nfe) {} } } } /** * Generate a failure message depending on a file parameter. * * @param messageId The failure message identifier. * @param file The {@code File} that caused the failure. * @return A {@code StringTemplate} with the error message. */ public static StringTemplate badFile(String messageId, File file) { return StringTemplate.template(messageId) .addName("%name%", (file == null) ? "-" : file.getPath()); } /** * Build an error template from an exception. * * @param ex The {@code Exception} to make an error from. * @param fallbackKey A message key to use to make a fallback message * if the exception is unsuitable. * @return An error {@code StringTemplate}. */ public static StringTemplate errorFromException(Exception ex, String fallbackKey) { return errorFromException(ex, StringTemplate.template(fallbackKey)); } /** * Build an error template from an exception. * * @param ex The {@code Exception} to make an error from. * @param fallback A {@code StringTemplate} to use as a fall * back if the exception is unsuitable. * @return An error {@code StringTemplate}. */ public static StringTemplate errorFromException(Exception ex, StringTemplate fallback) { String msg; return (ex == null || (msg = ex.getMessage()) == null) ? fallback : (Messages.containsKey(msg)) ? StringTemplate.template(msg) : (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)) ? StringTemplate.name(msg) : fallback; } /** * We get a lot of lame bug reports with insufficient configuration * information. Get a buffer containing as much information as we can * to embed in the log file and saved games. * * @return A {@code StringBuilder} full of configuration information. */ public static StringBuilder getConfiguration() { File autosave = FreeColDirectories.getAutosaveDirectory(); File clientOptionsFile = FreeColDirectories.getClientOptionsFile(); File save = FreeColDirectories.getSaveDirectory(); File userConfig = FreeColDirectories.getUserConfigDirectory(); File userData = FreeColDirectories.getUserDataDirectory(); File userMods = FreeColDirectories.getUserModsDirectory(); StringBuilder sb = new StringBuilder(256); sb.append("Configuration:") .append("\n version ").append(getRevision()) .append("\n java: ").append(JAVA_VERSION) .append("\n memory: ").append(MEMORY_MAX) .append("\n locale: ").append(getLocale()) .append("\n data: ") .append(FreeColDirectories.getDataDirectory().getPath()) .append("\n userConfig: ") .append((userConfig == null) ? "NONE" : userConfig.getPath()) .append("\n userData: ") .append((userData == null) ? "NONE" : userData.getPath()) .append("\n autosave: ") .append((autosave == null) ? "NONE" : autosave.getPath()) .append("\n logFile: ") .append(FreeColDirectories.getLogFilePath()) .append("\n options: ") .append((clientOptionsFile == null) ? "NONE" : clientOptionsFile.getPath()) .append("\n save: ") .append((save == null) ? "NONE" : save.getPath()) .append("\n userMods: ") .append((userMods == null) ? "NONE" : userMods.getPath()) .append("\n seed: ").append(FreeColSeed.getFreeColSeed()) .append("\n debug: ") .append(FreeColDebugger.getDebugModes()); return sb; } // The major final actions. /** * Start a client. */ private static void startClient() { SwingUtilities.invokeLater(() -> { /* * Please do NOT move the splash screen into SwingGUI again. Make a separate * interface if this needs to be pluggable. * * The Swing-classes need to be constructed in the EDT -- and the splash screen * is never displayed correctly if SwingGUI is loaded correctly in the EDT. * * In addition, the splash screen should not be displayed at the same time * as a fullscreen frame. */ final SplashScreen splashScreen = createSplashScreen(); SwingUtilities.invokeLater(() -> { Specification spec = null; File savegame = FreeColDirectories.getSavegameFile(); if (debugStart) { spec = FreeCol.getRulesSpecification(); } else if (fastStart) { if (savegame == null) { // continue last saved game if possible, // otherwise start a new one savegame = FreeColDirectories.getLastSaveGameFile(); if (savegame == null) { spec = FreeCol.getRulesSpecification(); } } // savegame was specified on command line } new FreeColClient(splashScreen, fontName, windowSize, (String)null, sound, introVideo, savegame, spec); }); }); } private static SplashScreen createSplashScreen() { SplashScreen splashScreen = null; if (splashStream != null) { try { final GraphicsDevice defaultScreenDevice = Utils.getGoodGraphicsDevice(); splashScreen = new SplashScreen(defaultScreenDevice, splashStream); splashScreen.setVisible(true); } catch (Exception e) { logger.log(Level.WARNING, "Splash screen failure", e); } } return splashScreen; } /** * Wrapper for the test suite to start a test client. * * @param spec The {@code Specification} to use in the new client. * @return The new {@code FreeColClient}. */ public static FreeColClient startTestClient(Specification spec) { FreeCol.setHeadless(true); return new FreeColClient(null, (String)null, (Dimension)null, (String)null, false, false, (File)null, spec); } /** * Check the integrity of a FreeCol server. * * @param freeColServer The server to check. */ private static void checkServerIntegrity(FreeColServer freeColServer) { String key; int ret; IntegrityType integ = IntegrityType.INTEGRITY_GOOD; if (freeColServer == null) { logger.warning("Integrity test blocked"); integ = integ.fail(); } else { integ = freeColServer.getIntegrity(); } switch (integ) { case INTEGRITY_GOOD: key = "cli.check-savegame.success"; ret = 0; break; case INTEGRITY_FIXED: key = "cli.check-savegame.fixed"; ret = 2; break; default: key = "cli.check-savegame.failed"; ret = 3; break; } gripe(StringTemplate.template(key) .add("%log%", FreeColDirectories.getLogFilePath())); quit(ret); } /** * Start the server. */ private static void startServer() { logger.info("Starting stand-alone server."); FreeColServer freeColServer; File saveGame = FreeColDirectories.getSavegameFile(); if (saveGame != null) { try { final FreeColSavegameFile fis = new FreeColSavegameFile(saveGame); freeColServer = new FreeColServer(fis, (Specification)null, serverAddress, serverPort, serverName); freeColServer.setPublicServer(publicServer); if (!freeColServer.registerWithMetaServer()) { fatal(Messages.message("server.noRouteToServer")); } } catch (Exception e) { logger.log(Level.SEVERE, "Load fail", e); fatal(Messages.message(badFile("error.couldNotLoad", saveGame)) + ": " + e); freeColServer = null; } if (checkIntegrity) checkServerIntegrity(freeColServer); if (freeColServer == null) return; } else { Specification spec = FreeCol.getRulesSpecification(); try { freeColServer = new FreeColServer(publicServer, false, spec, serverAddress, serverPort, serverName); if (!freeColServer.registerWithMetaServer()) { fatal(Messages.message("server.noRouteToServer")); } } catch (Exception e) { fatal(Messages.message("server.initialize") + ": " + e.getMessage()); return; } if (publicServer && !freeColServer.getPublicServer()) { gripe(Messages.message("server.noRouteToServer")); } } String quit = FreeCol.SERVER_THREAD + "Quit Game"; final Controller controller = freeColServer.getController(); Runtime.getRuntime().addShutdownHook(new Thread(quit) { @Override public void run() { controller.shutdown(); } }); } }