net.sf.freecol.FreeCol.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.freecol.FreeCol.java

Source

/**
 *  Copyright (C) 2002-2015   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.Button;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Frame;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.JarURLConnection;
import java.util.Arrays;
import java.util.Locale;
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 net.sf.freecol.client.ClientOptions;
import net.sf.freecol.client.FreeColClient;
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.FreeColSavegameFile;
import net.sf.freecol.common.io.FreeColTcFile;
import net.sf.freecol.common.io.Mods;
import net.sf.freecol.common.logging.DefaultHandler;
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 static net.sf.freecol.common.util.CollectionUtils.*;
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());

    /** The FreeCol release version number. */
    private static final String FREECOL_VERSION = "0.11.6";

    /** The difficulty levels. */
    public static final String[] DIFFICULTIES = { "veryEasy", "easy", "medium", "hard", "veryHard" };

    /** The extension for FreeCol saved games. */
    public static final String FREECOL_SAVE_EXTENSION = "fsg";

    /** 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();

    public static final String CLIENT_THREAD = "FreeColClient:";
    public static final String SERVER_THREAD = "FreeColServer:";
    public static final String METASERVER_THREAD = "FreeColMetaServer:";

    public static final String META_SERVER_ADDRESS = "meta.freecol.org";
    public static final int META_SERVER_PORT = 3540;

    /** 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;
    private static final Level LOGLEVEL_DEFAULT = Level.INFO;
    private static final String JAVA_VERSION_MIN = "1.8";
    private static final int MEMORY_MIN = 128; // Mbytes
    private static final int PORT_DEFAULT = 3541;
    private static final String SPLASH_DEFAULT = "splash.jpg";
    private static final String TC_DEFAULT = "freecol";
    public static final int TIMEOUT_DEFAULT = 60; // 1 minute
    public static final int TIMEOUT_MIN = 10; // 10s
    private static final int GUI_SCALE_MIN_PCT = 100;
    private static final int GUI_SCALE_MAX_PCT = 200;
    private static final int GUI_SCALE_STEP_PCT = 25;
    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;
    public static final float GUI_SCALE_STEP = GUI_SCALE_STEP_PCT / 100.0f;
    public static final float GUI_SCALE_DEFAULT = 1.0f;

    // 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 level of logging in this game. */
    private static Level logLevel = LOGLEVEL_DEFAULT;

    /** The client player name. */
    private static String name = null;

    /** How to name and configure the server. */
    private static int serverPort = -1;
    private static String serverName = null;

    /** A stream to get the splash image from. */
    private static InputStream splashStream;

    /** The TotalConversion / ruleset in play, defaults to "freecol". */
    private static String tc = null;

    /** The time out (seconds) for otherwise blocking commands. */
    private static int timeout = -1;

    /**
     * The size of window to create, defaults to impossible dimensions
     * to require windowed mode with best determined screen size.
     */
    private static Dimension windowSize = new Dimension(-1, -1);

    /** How much gui elements get scaled. */
    private static float guiScale = GUI_SCALE_DEFAULT;

    private FreeCol() {
    } // Hide constructor

    //Your variables

    public static void startYourAddition() throws FontFormatException, IOException {
        Frame mainFrame;
        Frame mainFrame2;
        TextField t1;
        TextField t2;
        TextField t3;
        TextField t4;
        Frame mainFrame3 = new Frame("Haha, am i middle?");
        mainFrame = new Frame("Haha, am I right?!");
        mainFrame.setSize(400, 400);
        mainFrame.setLayout(null);

        t1 = new TextField("Enter here");
        t1.setBounds(160, 200, 150, 50);

        t2 = new TextField("What grade do we deserve?");
        t3 = new TextField("Enter here");
        t3.setBounds(160, 200, 150, 50);

        t4 = new TextField("What letter grade do we deserve?");
        Button b = new Button("click ----->");
        b.setBounds(30, 250, 130, 30);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                t2.setText("I think you meant 100");
            }
        });
        Button c = new Button("Submit");
        c.setBounds(150, 250, 80, 30);
        c.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                String in = t1.getText();
                if (in.equals("100")) {
                    t1.setText("Correct");
                    t1.setEditable(false);
                    t1.setBackground(new Color(95, 216, 109));

                    if (t3.getText().equals("Correct")) {
                        mainFrame3.setVisible(true);
                    }
                } else {
                    t1.setText("Wrong");
                    t1.setBackground(new Color(214, 81, 96));
                }
            }
        });

        t2.setBounds(160, 0, 175, 100);
        t2.setEditable(false);

        mainFrame.add(b);
        mainFrame.add(c);
        mainFrame.add(t1);
        mainFrame.add(t2);

        mainFrame.setLocation(1280, 0);

        mainFrame.setVisible(true);

        ///////////////The left area below and above is right

        mainFrame2 = new Frame("Haha, am i left?");
        mainFrame2.setSize(400, 400);
        mainFrame2.setLayout(null);

        Button b2 = new Button("click ----->");
        b2.setBounds(30, 250, 130, 30);
        b2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                t4.setText("I think you meant A");
            }
        });
        Button c2 = new Button("Submit");
        c2.setBounds(150, 250, 80, 30);
        c2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                String in = t3.getText();
                if (in.equals("A")) {
                    t3.setText("Correct");
                    t3.setEditable(false);
                    t3.setBackground(new Color(95, 216, 109));

                    if (t1.getText().equals("Correct")) {
                        mainFrame3.setVisible(true);
                    }

                } else {
                    t3.setText("Wrong");
                    t3.setBackground(new Color(214, 81, 96));
                }
            }
        });

        t4.setBounds(120, 0, 220, 100);
        t4.setEditable(false);

        mainFrame2.add(b2);
        mainFrame2.add(c2);
        mainFrame2.add(t3);
        mainFrame2.add(t4);

        mainFrame2.setVisible(true);

        //Overall correct

        mainFrame3.setSize(400, 400);
        mainFrame3.setLayout(null);
        mainFrame3.setLocation(640, 420);
        mainFrame3.setBackground(new Color(95, 216, 109));
        TextField t6 = new TextField("Soooooo give us an A!!!");
        t6.setBounds(160, 200, 200, 50);
        t6.setBackground(new Color(102, 136, 232));

        mainFrame3.add(t6);
    }

    /**
     * The entrypoint.
     *
     * @param args The command-line arguments.
     * @throws IOException 
     * @throws FontFormatException 
     */
    public static void main(String[] args) throws FontFormatException, IOException {
        freeColRevision = FREECOL_VERSION;
        JarURLConnection juc;
        try {
            juc = getJarURLConnection(FreeCol.class);
        } 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());
            }
        }

        // Java bug #7075600 causes BR#2554.  The workaround is to set
        // the following property.  Remove if/when they fix Java.
        System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

        // 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);
        if (localeArg == null) {
            locale = Locale.getDefault();
        } else {
            int index = localeArg.indexOf('.'); // Strip encoding if present
            if (index > 0)
                localeArg = localeArg.substring(0, index);
            locale = Messages.getLocale(localeArg);
        }
        Messages.loadMessageBundle(locale);

        // 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 * 1000000) {
            fatal(StringTemplate.template("main.memory").addAmount("%memory%", MEMORY_MAX).addAmount("%minMemory%",
                    MEMORY_MIN));
        }

        // 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.
        String userMsg = FreeColDirectories.setUserDirectories();

        // Now we have the log file path, start logging.
        final Logger baseLogger = Logger.getLogger("");
        final Handler[] handlers = baseLogger.getHandlers();
        for (Handler handler : handlers) {
            baseLogger.removeHandler(handler);
        }
        String logFile = FreeColDirectories.getLogFilePath();
        try {
            baseLogger.addHandler(new DefaultHandler(consoleLogging, logFile));
            Logger freecolLogger = Logger.getLogger("net.sf.freecol");
            freecolLogger.setLevel(logLevel);
        } catch (FreeColException e) {
            System.err.println("Logging initialization failure: " + e.getMessage());
            e.printStackTrace();
        }
        Thread.setDefaultUncaughtExceptionHandler((Thread thread, Throwable e) -> {
            baseLogger.log(Level.WARNING, "Uncaught exception from thread: " + thread, e);
        });

        // Now we can find the client options, allow the options
        // setting to override the locale, if no command line option
        // had been specified.
        // We have users whose machines 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.
        if (localeArg == null) {
            String clientLanguage = ClientOptions.getLanguageOption();
            Locale clientLocale;
            if (clientLanguage != null && !Messages.AUTOMATIC.equalsIgnoreCase(clientLanguage)
                    && (clientLocale = Messages.getLocale(clientLanguage)) != locale) {
                locale = clientLocale;
                Messages.loadMessageBundle(locale);
                logger.info("Loaded messages for " + locale);
            }
        }

        // Now we have the user mods directory and the locale is now
        // stable, load the mods and their messages.
        Mods.loadMods();
        Messages.loadModMessageBundle(locale);

        // Report on where we are.
        if (userMsg != null)
            logger.info(Messages.message(userMsg));
        logger.info(getConfiguration().toString());

        // Ready to specialize into client or server.
        if (standAloneServer) {
            startServer();
        } else {
            startClient(userMsg);
        }
        startYourAddition();
    }

    /**
     * Get the JarURLConnection from a class.
     *
     * @return The <code>JarURLConnection</code>.
     */
    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</code> to extract from.
     * @return A value of the package version attribute.
     */
    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</code> to extract from.
     * @return A suitable <code>InputStream</code>, or null on error.
     */
    private static InputStream getDefaultSplashStream(JarURLConnection juc) throws IOException {
        JarFile jf = juc.getJarFile();
        ZipEntry ze = jf.getEntry(SPLASH_DEFAULT);
        return jf.getInputStream(ze);
    }

    /**
     * Exit printing fatal error message.
     *
     * @param template A <code>StringTemplate</code> to print.
     */
    public static void fatal(StringTemplate template) {
        fatal(Messages.message(template));
    }

    /**
     * Exit printing fatal error message.
     *
     * @param err The error message to print.
     */
    public static void fatal(String err) {
        if (err == null || err.isEmpty()) {
            err = "Bogus null fatal error message";
            Thread.dumpStack();
        }
        System.err.println(err);
        System.exit(1);
    }

    /**
     * Just gripe to System.err.
     *
     * @param template A <code>StringTemplate</code> to print.
     */
    public static void gripe(StringTemplate template) {
        System.err.println(Messages.message(template));
    }

    /**
     * Just gripe to System.err.
     *
     * @param key A message key.
     */
    public static void gripe(String key) {
        System.err.println(Messages.message(key));
    }

    /**
     * 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;
    }

    /**
     * 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();
        final String help = Messages.message("cli.help");
        final File dummy = new File("dummy");
        final String argDirectory = Messages.message("cli.arg.directory");

        // Help options.
        options.addOption(OptionBuilder.withLongOpt("usage").withDescription(help).create());
        options.addOption(OptionBuilder.withLongOpt("help").withDescription(help).create());

        // Special options handled early.
        options.addOption(OptionBuilder.withLongOpt("freecol-data")
                .withDescription(Messages.message("cli.freecol-data")).withArgName(argDirectory).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("default-locale").withDescription(Messages.message("cli.default-locale"))
                        .withArgName(Messages.message("cli.arg.locale")).hasArg().create());

        // Ordinary options, handled here.
        options.addOption(OptionBuilder.withLongOpt("advantages")
                .withDescription(Messages.message(
                        StringTemplate.template("cli.advantages").addName("%advantages%", getValidAdvantages())))
                .withArgName(Messages.message("cli.arg.advantages")).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("check-savegame").withDescription(Messages.message("cli.check-savegame"))
                        .withArgName(Messages.message("cli.arg.file")).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("clientOptions").withDescription(Messages.message("cli.clientOptions"))
                        .withArgName(Messages.message("cli.arg.clientOptions")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("debug")
                .withDescription(Messages.message(
                        StringTemplate.template("cli.debug").addName("%modes%", FreeColDebugger.getDebugModes())))
                .withArgName(Messages.message("cli.arg.debug")).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("debug-start")
                .withDescription(Messages.message("cli.debug-start")).create());
        options.addOption(
                OptionBuilder.withLongOpt("difficulty").withDescription(Messages.message("cli.difficulty"))
                        .withArgName(Messages.message("cli.arg.difficulty")).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("europeans").withDescription(Messages.message("cli.european-count"))
                        .withArgName(Messages.message("cli.arg.europeans")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("fast").withDescription(Messages.message("cli.fast")).create());
        options.addOption(OptionBuilder.withLongOpt("font").withDescription(Messages.message("cli.font"))
                .withArgName(Messages.message("cli.arg.font")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("full-screen")
                .withDescription(Messages.message("cli.full-screen")).create());
        options.addOption(OptionBuilder.withLongOpt("gui-scale")
                .withDescription(Messages
                        .message(StringTemplate.template("cli.gui-scale").addName("%scales%", getValidGUIScales())))
                .withArgName(Messages.message("cli.arg.gui-scale")).hasOptionalArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("headless").withDescription(Messages.message("cli.headless")).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("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("name").withDescription(Messages.message("cli.name"))
                .withArgName(Messages.message("cli.arg.name")).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("no-intro").withDescription(Messages.message("cli.no-intro")).create());
        options.addOption(OptionBuilder.withLongOpt("no-java-check")
                .withDescription(Messages.message("cli.no-java-check")).create());
        options.addOption(OptionBuilder.withLongOpt("no-memory-check")
                .withDescription(Messages.message("cli.no-memory-check")).create());
        options.addOption(
                OptionBuilder.withLongOpt("no-sound").withDescription(Messages.message("cli.no-sound")).create());
        options.addOption(
                OptionBuilder.withLongOpt("no-splash").withDescription(Messages.message("cli.no-splash")).create());
        options.addOption(
                OptionBuilder.withLongOpt("private").withDescription(Messages.message("cli.private")).create());
        options.addOption(OptionBuilder.withLongOpt("seed").withDescription(Messages.message("cli.seed"))
                .withArgName(Messages.message("cli.arg.seed")).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("server").withDescription(Messages.message("cli.server")).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("server-port").withDescription(Messages.message("cli.server-port"))
                        .withArgName(Messages.message("cli.arg.port")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("splash").withDescription(Messages.message("cli.splash"))
                .withArgName(Messages.message("cli.arg.file")).hasOptionalArg().create());
        options.addOption(OptionBuilder.withLongOpt("tc").withDescription(Messages.message("cli.tc"))
                .withArgName(Messages.message("cli.arg.name")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("timeout").withDescription(Messages.message("cli.timeout"))
                .withArgName(Messages.message("cli.arg.timeout")).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("user-cache-directory")
                .withDescription(Messages.message("cli.user-cache-directory")).withArgName(argDirectory)
                .withType(dummy).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("user-config-directory")
                .withDescription(Messages.message("cli.user-config-directory")).withArgName(argDirectory)
                .withType(dummy).hasArg().create());
        options.addOption(OptionBuilder.withLongOpt("user-data-directory")
                .withDescription(Messages.message("cli.user-data-directory")).withArgName(argDirectory)
                .withType(dummy).hasArg().create());
        options.addOption(
                OptionBuilder.withLongOpt("version").withDescription(Messages.message("cli.version")).create());
        options.addOption(OptionBuilder.withLongOpt("windowed").withDescription(Messages.message("cli.windowed"))
                .withArgName(Messages.message("cli.arg.dimensions")).hasOptionalArg().create());

        CommandLineParser parser = new PosixParser();
        boolean usageError = false;
        try {
            CommandLine line = parser.parse(options, args);
            if (line.hasOption("help") || line.hasOption("usage")) {
                printUsage(options, 0);
            }

            if (line.hasOption("default-locale")) {
                ; // Do nothing, already handled in main().
            }
            if (line.hasOption("freecol-data")) {
                ; // Do nothing, 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()));
                }
                // user set log level has precedence
                if (!line.hasOption("log-level"))
                    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("gui-scale")) {
                String arg = line.getOptionValue("gui-scale");
                if (!setGUIScale(arg)) {
                    gripe(StringTemplate.template("cli.error.gui-scale").addName("%scales%", getValidGUIScales())
                            .addName("%arg%", arg));
                }
            }

            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")) {
                setLogLevel(line.getOptionValue("log-level"));
            }

            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("seed")) {
                FreeColSeed.setFreeColSeed(line.getOptionValue("seed"));
            }

            if (line.hasOption("splash")) {
                String splash = line.getOptionValue("splash");
                try {
                    FileInputStream fis = new FileInputStream(splash);
                    splashStream = fis;
                } catch (FileNotFoundException fnfe) {
                    gripe(StringTemplate.template("cli.error.splash").addName("%name%", splash));
                }
            }

            if (line.hasOption("tc")) {
                setTC(line.getOptionValue("tc")); // Failure is deferred.
            }

            if (line.hasOption("timeout")) {
                String arg = line.getOptionValue("timeout");
                if (!setTimeout(arg)) { // Not fatal
                    gripe(StringTemplate.template("cli.error.timeout").addName("%string%", arg).addName("%minimum%",
                            Integer.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());
                System.exit(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</code>.
     * @param status The status to exit with.
     */
    private static void printUsage(Options options, int status) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("java -Xmx 256M -jar freecol.jar [OPTIONS]", options);
        System.exit(status);
    }

    /**
     * Get the specification from a given TC file.
     *
     * @param tcf The <code>FreeColTcFile</code> to load.
     * @param advantages An optional <code>Advantages</code> setting.
     * @param difficulty An optional difficulty level.
     * @return A <code>Specification</code>.
     */
    public static Specification loadSpecification(FreeColTcFile tcf, Advantages advantages, String difficulty) {
        Specification spec = null;
        try {
            if (tcf != null)
                spec = tcf.getSpecification();
        } catch (IOException ioe) {
            System.err.println("Spec read failed in " + tcf.getId() + ": " + ioe.getMessage() + "\n");
        }
        if (spec != null)
            spec.prepare(advantages, difficulty);
        return spec;
    }

    /**
     * Get the specification from the specified TC.
     *
     * @return A <code>Specification</code>, quits on error.
     */
    private static Specification getTCSpecification() {
        Specification spec = loadSpecification(getTCFile(), getAdvantages(), getDifficulty());
        if (spec == null) {
            fatal(StringTemplate.template("cli.error.badTC").addName("%tc%", getTC()));
        }
        return spec;
    }

    // Accessors, mutators and support for the cli variables.

    /**
     * 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 advantages The name of the new advantages type.
     * @return The type of advantages set, or null if none.
     */
    private static Advantages selectAdvantages(String advantages) {
        Advantages adv = find(Advantages.values(), a -> Messages.getName(a).equals(advantages), null);
        if (adv != null)
            setAdvantages(adv);
        return adv;
    }

    /**
     * Sets the advantages type.
     *
     * @param advantages The new <code>Advantages</code> 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 Arrays.stream(Advantages.values()).map(a -> Messages.getName(a)).collect(Collectors.joining(","));
    }

    /**
     * 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.
     */
    public static String selectDifficulty(String arg) {
        String difficulty = find(map(DIFFICULTIES, d -> "model.difficulty." + d),
                k -> Messages.getName(k).equals(arg), null);
        if (difficulty != null)
            setDifficulty(difficulty);
        return difficulty;
    }

    /**
     * Sets the difficulty level.
     *
     * @param difficulty The actual <code>OptionGroup</code>
     *     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 Arrays.stream(DIFFICULTIES).map(d -> Messages.getName("model.difficulty." + d))
                .collect(Collectors.joining(","));
    }

    /**
     * Get the number of European nations to enable by default.
     */
    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;
    }

    /**
     * Sets the scale for GUI elements.
     * 
     * @param arg The optional command line argument to be parsed.
     * @return If the argument was correctly formatted.
     */
    public static boolean setGUIScale(String arg) {
        boolean valid = true;
        if (arg == null) {
            guiScale = GUI_SCALE_MAX;
        } else {
            try {
                int n = Integer.parseInt(arg);
                if (n < GUI_SCALE_MIN_PCT) {
                    valid = false;
                    n = GUI_SCALE_MIN_PCT;
                } else if (n > GUI_SCALE_MAX_PCT) {
                    valid = false;
                    n = GUI_SCALE_MAX_PCT;
                } else if (n % GUI_SCALE_STEP_PCT != 0) {
                    valid = false;
                }
                guiScale = ((float) (n / GUI_SCALE_STEP_PCT)) * GUI_SCALE_STEP;
            } catch (NumberFormatException nfe) {
                valid = false;
                guiScale = GUI_SCALE_MAX;
            }
        }
        return valid;
    }

    /**
     * Gets the valid scale factors for the GUI.
     * 
     * @return A string containing these.
     */
    public static String getValidGUIScales() {
        String result = "";
        for (int i = GUI_SCALE_MIN_PCT; i < GUI_SCALE_MAX_PCT; i += GUI_SCALE_STEP_PCT)
            result += i + ",";
        result += GUI_SCALE_MAX_PCT;
        return result;
    }

    /**
     * Selects a European nation count.
     *
     * @param arg The supplied count argument.
     * @return A valid nation number, or negative on error.
     */
    public static int selectEuropeanCount(String arg) {
        try {
            int n = Integer.parseInt(arg);
            if (n >= EUROPEANS_MIN) {
                setEuropeanCount(n);
                return n;
            }
        } catch (NumberFormatException nfe) {
        }
        return -1;
    }

    /**
     * Sets the log level.
     *
     * @param arg The log level to set.
     */
    private static void setLogLevel(String arg) {
        logLevel = Level.parse(arg.toUpperCase());
    }

    /**
     * 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</code> currently in use.
     */
    public static Locale getLocale() {
        return FreeCol.locale;
    }

    /**
     * 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() {
        return InetAddress.getLoopbackAddress().getHostAddress();
    }

    /**
     * Gets the server network port.
     *
     * @return The port number.
     */
    public static int getServerPort() {
        return (serverPort < 0) ? PORT_DEFAULT : serverPort;
    }

    /**
     * Sets the server port.
     *
     * @param arg The server port number.
     * @return True if the port was set.
     */
    public static boolean setServerPort(String arg) {
        if (arg == null)
            return false;
        try {
            serverPort = Integer.parseInt(arg);
        } catch (NumberFormatException nfe) {
            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 FreeColTcFile for the current TC.
     *
     * @return The <code>FreeColTcFile</code>.
     */
    public static FreeColTcFile getTCFile() {
        try {
            return new FreeColTcFile(getTC());
        } catch (IOException ioe) {
        }
        return null;
    }

    /**
     * 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 int getTimeout(boolean singlePlayer) {
        return (timeout >= TIMEOUT_MIN) ? timeout : (singlePlayer) ? Integer.MAX_VALUE : TIMEOUT_DEFAULT;
    }

    /**
     * Sets the timeout.
     *
     * @param timeout A string containing the new timeout.
     * @return True if the timeout was set.
     */
    public static boolean setTimeout(String timeout) {
        try {
            int result = Integer.parseInt(timeout);
            if (result >= TIMEOUT_MIN) {
                FreeCol.timeout = result;
                return true;
            }
        } catch (NumberFormatException nfe) {
        }
        return false;
    }

    /**
     * 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;
    }

    /**
     * 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) {
        String[] xy;
        if (arg != null && (xy = arg.split("[^0-9]")) != null && xy.length == 2) {
            try {
                windowSize = new Dimension(Integer.parseInt(xy[0]), Integer.parseInt(xy[1]));
            } catch (NumberFormatException nfe) {
            }
        }
        if (windowSize == null)
            windowSize = new Dimension(-1, -1);
    }

    /**
     * Utility to make a load failure message.
     *
     * @param file The <code>File</code> that failed to load.
     * @return A <code>StringTemplate</code> with the error message.
     */
    public static StringTemplate badLoad(File file) {
        return StringTemplate.template("error.couldNotLoad").addName("%name%", file.getPath());
    }

    /**
     * Utility to make a save failure message.
     *
     * @param file The <code>File</code> that failed to save.
     * @return A <code>StringTemplate</code> with the error message.
     */
    public static StringTemplate badSave(File file) {
        return StringTemplate.template("error.couldNotSave").addName("%name%", file.getPath());
    }

    /**
     * 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</code> 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(locale).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());
        return sb;
    }

    // The major final actions.

    /**
     * Start a client.
     *
     * @param userMsg An optional user message key.
     */
    private static void startClient(String userMsg) {
        Specification spec = null;
        File savegame = FreeColDirectories.getSavegameFile();
        if (debugStart) {
            spec = FreeCol.getTCSpecification();
        } 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.getTCSpecification();
                }
            }
            // savegame was specified on command line
        }
        final FreeColClient freeColClient = new FreeColClient(splashStream, fontName, guiScale, headless);
        freeColClient.startClient(windowSize, userMsg, sound, false, savegame, spec);
    }

    /**
     * Start the server.
     */
    private static void startServer() {
        logger.info("Starting stand-alone server.");
        final FreeColServer freeColServer;
        File saveGame = FreeColDirectories.getSavegameFile();
        if (saveGame != null) {
            try {
                final FreeColSavegameFile fis = new FreeColSavegameFile(saveGame);
                freeColServer = new FreeColServer(fis, (Specification) null, serverPort, serverName);
                if (checkIntegrity) {
                    boolean integrityOK = freeColServer.getIntegrity() > 0;
                    gripe((integrityOK) ? "cli.check-savegame.success" : "cli.check-savegame.failure");
                    System.exit((integrityOK) ? 0 : 2);
                }
            } catch (Exception e) {
                if (checkIntegrity)
                    gripe("cli.check-savegame.failure");
                fatal(Messages.message(badLoad(saveGame)) + ": " + e.getMessage());
                return;
            }
        } else {
            Specification spec = FreeCol.getTCSpecification();
            try {
                freeColServer = new FreeColServer(publicServer, false, spec, serverPort, serverName);
            } catch (Exception e) {
                fatal(Messages.message("server.initialize") + ": " + e.getMessage());
                return;
            }
            if (publicServer && freeColServer != null && !freeColServer.getPublicServer()) {
                gripe(Messages.message("server.noRouteToServer"));
            }
        }

        String quit = FreeCol.SERVER_THREAD + "Quit Game";
        Runtime.getRuntime().addShutdownHook(new Thread(quit) {
            @Override
            public void run() {
                freeColServer.getController().shutdown();
            }
        });
    }
}