com.forerunnergames.peril.client.application.ClientApplicationProperties.java Source code

Java tutorial

Introduction

Here is the source code for com.forerunnergames.peril.client.application.ClientApplicationProperties.java

Source

/*
 * Copyright  2011 - 2013 Aaron Mahan.
 * Copyright  2013 - 2016 Forerunner Games, LLC.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package com.forerunnergames.peril.client.application;

import com.amazonaws.util.IOUtils;

import com.forerunnergames.peril.client.settings.AssetSettings;
import com.forerunnergames.peril.client.settings.GraphicsSettings;
import com.forerunnergames.peril.client.settings.InputSettings;
import com.forerunnergames.peril.client.settings.MusicSettings;
import com.forerunnergames.peril.client.settings.ScreenSettings;
import com.forerunnergames.peril.client.ui.screens.ScreenId;
import com.forerunnergames.peril.common.game.InitialCountryAssignment;
import com.forerunnergames.peril.common.game.rules.ClassicGameRules;
import com.forerunnergames.peril.common.settings.GameSettings;
import com.forerunnergames.peril.common.settings.NetworkSettings;
import com.forerunnergames.tools.common.Classes;
import com.forerunnergames.tools.common.LetterCase;
import com.forerunnergames.tools.common.Result;
import com.forerunnergames.tools.common.Strings;
import com.forerunnergames.tools.common.io.FileUtils;

import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Reads & writes user configurable settings to & from an external properties file.
 *
 * IMPORTANT: Always increment CURRENT_VERSION when making any changes here! It will cause the changes to propagate to
 * any existing properties files out in the wild.
 */
public final class ClientApplicationProperties {
    public static final int CURRENT_VERSION = 9;
    public static final String PROPERTIES_FILE_SUBDIR = "peril" + File.separator + "settings";
    public static final String PROPERTIES_FILE_NAME = "settings.txt";
    public static final String VERSION_FILE_NAME = ".version";
    public static final String PROPERTIES_FILE_PATH = System.getProperty("user.home") + File.separator
            + PROPERTIES_FILE_SUBDIR;
    public static final String VERSION_FILE_PATH = PROPERTIES_FILE_PATH;
    public static final String PROPERTIES_FILE_PATH_AND_NAME = PROPERTIES_FILE_PATH + File.separator
            + PROPERTIES_FILE_NAME;
    public static final String VERSION_FILE_PATH_AND_NAME = VERSION_FILE_PATH + File.separator + VERSION_FILE_NAME;
    public static final String WINDOW_WIDTH_PROPERTY_KEY = "window-width";
    public static final String WINDOW_HEIGHT_PROPERTY_KEY = "window-height";
    public static final String WINDOW_RESIZABLE_PROPERTY_KEY = "window-resizable";
    public static final String WINDOW_DECORATED_PROPERTY_KEY = "window-decorated";
    public static final String WINDOW_TITLE_PROPERTY_KEY = "window-title";
    public static final String FULLSCREEN_PROPERTY_KEY = "fullscreen";
    public static final String VSYNC_PROPERTY_KEY = "vsync";
    public static final String HIGH_DPI_PROPERTY_KEY = "high-dpi";
    public static final String OPENGL_CORE_PROFILE_PROPERTY_KEY = "opengl-core-profile";
    public static final String MUSIC_ENABLED_PROPERTY_KEY = "music-enabled";
    public static final String MUSIC_VOLUME_PROPERTY_KEY = "music-volume";
    public static final String START_SCREEN_PROPERTY_KEY = "start-screen";
    public static final String UPDATE_ASSETS_KEY = "update-assets";
    public static final String UPDATED_ASSETS_LOCATION_KEY = "updated-assets-location";
    public static final String PLAYER_NAME_KEY = "player-name";
    public static final String CLAN_ACRONYM_KEY = "clan-tag";
    public static final String SERVER_NAME_KEY = "server-title";
    public static final String SERVER_ADDRESS_KEY = "server-address";
    public static final String CLASSIC_MODE_HUMAN_PLAYER_LIMIT_KEY = "human-players-classic-mode";
    public static final String CLASSIC_MODE_AI_PLAYER_LIMIT_KEY = "ai-players-classic-mode";
    public static final String SPECTATOR_LIMIT_KEY = "spectators";
    public static final String CLASSIC_MODE_PLAY_MAP_NAME_KEY = "map-name-classic-mode";
    public static final String CLASSIC_MODE_WIN_PERCENT_KEY = "win-percent-classic-mode";
    public static final String CLASSIC_MODE_INITIAL_COUNTRY_ASSIGNMENT_KEY = "initial-country-assignment-classic-mode";
    public static final String AUTO_JOIN_GAME_KEY = "auto-join-game";
    public static final String AUTO_CREATE_GAME_KEY = "auto-create-game";
    // @formatter:off
    private static final Logger log = LoggerFactory.getLogger(ClientApplicationProperties.class);
    private static final String PROPERTIES_FILE_COMMENTS = " Player-Configurable Peril Settings\n\n"
            + " IMPORTANT: All lines starting with a # don't do anything. Look near the bottom of the file for the actual settings!\n\n"
            + " To reset this file, simply delete it, restart Peril, and it will be recreated with default values.\n"
            + " You can delete any setting in here if you don't care about it, which will just cause it to use the default value.\n"
            + " To delete a setting, just delete the entire line containing the setting.\n\n"
            + " Valid values:\n\n " + WINDOW_WIDTH_PROPERTY_KEY + ": any whole number >= "
            + GraphicsSettings.MIN_INITIAL_WINDOW_WIDTH + " and <= the max resolution width "
            + "of your monitor(s)\n\n " + WINDOW_HEIGHT_PROPERTY_KEY + ": any whole number >= "
            + GraphicsSettings.MIN_INITIAL_WINDOW_HEIGHT
            + " and <= the max resolution height of your monitor(s)\n\n " + WINDOW_RESIZABLE_PROPERTY_KEY
            + ": true, false\n\n " + WINDOW_DECORATED_PROPERTY_KEY + ": true, false\n\n "
            + WINDOW_TITLE_PROPERTY_KEY + ": anything\n\n " + FULLSCREEN_PROPERTY_KEY + ": true, false\n\n "
            + VSYNC_PROPERTY_KEY + ": true, false\n\n " + HIGH_DPI_PROPERTY_KEY
            + ": true, false ('true' enables high DPI mode for Mac OS X retina displays ONLY, 'false' disables it.)\n\n "
            + OPENGL_CORE_PROFILE_PROPERTY_KEY + ": true, false\n\n " + MUSIC_ENABLED_PROPERTY_KEY
            + ": true, false\n\n " + MUSIC_VOLUME_PROPERTY_KEY + ": any decimal number >= "
            + MusicSettings.MIN_VOLUME + " and <= " + MusicSettings.MAX_VOLUME + "\n\n " + START_SCREEN_PROPERTY_KEY
            + ": " + Strings.toStringList(ScreenSettings.VALID_START_SCREENS, ", ", LetterCase.NONE, false)
            + "\n\n " + UPDATE_ASSETS_KEY
            + ": true, false ('true' will overwrite any asset customizations when running the game, "
            + "'false' will preserve any asset customizations, but your assets won't be updated - "
            + "which could crash the game - until you run the game with this set to 'true')\n\n "
            + UPDATED_ASSETS_LOCATION_KEY
            + ": Absolute location where updated assets can be found, either a local directory "
            + "(NOT surrounded by quotes, NO trailing slash) or an Amazon S3 bucket path ("
            + AssetSettings.VALID_S3_BUCKET_PATH_DESCRIPTION.replace("\n", " ") + ")\n\n " + PLAYER_NAME_KEY
            + ": The name of your player, optional, can be left blank. "
            + GameSettings.VALID_PLAYER_NAME_DESCRIPTION.replace("\n", " ") + "\n\n " + CLAN_ACRONYM_KEY
            + ": Your clan's acronym, optional, can be left blank. "
            + GameSettings.VALID_CLAN_ACRONYM_DESCRIPTION.replace("\n", " ") + "\n\n " + SERVER_NAME_KEY
            + ": Your server title, optional, can be left blank. "
            + NetworkSettings.VALID_SERVER_NAME_DESCRIPTION.replace("\n", " ") + "\n\n " + SERVER_ADDRESS_KEY
            + ": Your server address, optional, can be left blank. "
            + NetworkSettings.VALID_SERVER_ADDRESS_DESCRIPTION.replace("\n", " ") + "\n\n "
            + CLASSIC_MODE_HUMAN_PLAYER_LIMIT_KEY
            + ": Number of human players allowed in your classic game mode server, " + "any whole number, "
            + ClassicGameRules.MIN_HUMAN_PLAYERS + " to " + ClassicGameRules.MAX_HUMAN_PLAYERS
            + ", total players (human + AI) must be in the range " + ClassicGameRules.MIN_TOTAL_PLAYERS + " to "
            + ClassicGameRules.MAX_TOTAL_PLAYERS + "\n\n " + CLASSIC_MODE_AI_PLAYER_LIMIT_KEY
            + ": Number of AI players allowed in your classic game mode server, " + "any whole number, "
            + ClassicGameRules.MIN_AI_PLAYERS + " to " + ClassicGameRules.MAX_AI_PLAYERS
            + ", total players (human + AI) must be in the range " + ClassicGameRules.MIN_TOTAL_PLAYERS + " to "
            + ClassicGameRules.MAX_TOTAL_PLAYERS + "\n\n " + SPECTATOR_LIMIT_KEY
            + ": Number of spectators allowed in your server in any game mode, any whole number, "
            + ClassicGameRules.MIN_SPECTATORS + " to " + ClassicGameRules.MAX_SPECTATORS + "\n\n "
            + CLASSIC_MODE_PLAY_MAP_NAME_KEY + ": Name of the map for your classic game mode server. "
            + GameSettings.VALID_PLAY_MAP_NAME_DESCRIPTION.replace("\n", " ") + "\n\n "
            + CLASSIC_MODE_WIN_PERCENT_KEY
            + ": % of the map that must be conquered to win the game in your classic game mode server, any whole "
            + "number, must be a multiple of 5, " + "5 (?) to " + ClassicGameRules.MAX_WIN_PERCENTAGE
            + ", the real " + "minimum valid win % cannot be determined until the map is loaded.\n\n "
            + CLASSIC_MODE_INITIAL_COUNTRY_ASSIGNMENT_KEY
            + ": Method of assigning initial countries to players in your classic game mode server: "
            + Strings.toStringList(", ", LetterCase.NONE, false, InitialCountryAssignment.values()) + "\n\n "
            + AUTO_JOIN_GAME_KEY
            + ": true, false ('true' will automatically join a game using the settings in this file, ignoring the "
            + START_SCREEN_PROPERTY_KEY + " setting. 'false' will disable auto-joining.)\n\n "
            + AUTO_CREATE_GAME_KEY
            + ": true, false ('true' will automatically create a game using the settings in this file, ignoring the "
            + START_SCREEN_PROPERTY_KEY + " setting. 'false' will disable auto-creation.)\n\n " + "\n"
            + " If you've done your best to read & follow these instructions, and you're still having problems:\n\n"
            + " 1) Ask for help on our forums at https://forerunner.games/forums\n"
            + " 2) Email us at support@forerunner.games. Please attach this file in the email, as well as any crash files.\n";
    // @formatter:on

    public static void set() {
        final Properties properties = new Properties();

        setDefaultValuesFor(properties);

        if (versionIsCurrent()) {
            if (loadPropertiesFromExistingFileInto(properties).failed())
                createNewPropertiesFileWith(properties);
        } else {
            updateVersion();
            mergeOldPropertiesInto(properties);
            deleteOldPropertiesFile();
            createNewPropertiesFileWith(properties);
        }

        configureApplicationSettingsFromProperties(properties);
    }

    private static boolean versionIsCurrent() {
        final Optional<Integer> version = readVersion();

        return version.isPresent() && version.get() == CURRENT_VERSION;
    }

    private static Optional<Integer> readVersion() {
        try {
            String versionString = FileUtils.loadFile(VERSION_FILE_PATH_AND_NAME, StandardCharsets.UTF_8);
            versionString = versionString.trim();

            return Optional.of(Integer.valueOf(versionString));
        } catch (final IOException | NumberFormatException e) {
            log.warn("Failed to read settings version from \"{}\".\n\nDetails:\n\n", VERSION_FILE_PATH_AND_NAME, e);
            return Optional.absent();
        }
    }

    private static Properties intersection(final Properties propertiesUseValues,
            final Properties propertiesDontUseValues) {
        final Properties intersection = new Properties();

        final Collection<Object> keys = new HashSet<>(propertiesUseValues.keySet());
        keys.retainAll(propertiesDontUseValues.keySet());

        for (final Object key : keys) {
            intersection.setProperty((String) key, propertiesUseValues.getProperty((String) key));
        }

        return intersection;
    }

    private static String getParseErrorMessageFor(final String propertyKey, final String propertyValue)
            throws RuntimeException {
        return "Oops! Looks like your " + PROPERTIES_FILE_NAME + " (located in: \"" + PROPERTIES_FILE_PATH
                + "\") has an invalid setting:\n\n" + propertyKey + "=" + propertyValue
                + " <-- HINT: The part after the = sign (" + propertyValue + ") ain't right!\n\n"
                + "Time to go back and actually read the instructions in your " + PROPERTIES_FILE_NAME
                + " and then try again... ;-)\n\nNerdy developer details:\n";
    }

    private static void createNewPropertiesFileWith(final Properties properties) {
        log.info("Attempting to create new settings file \"{}\"...", PROPERTIES_FILE_PATH_AND_NAME);

        createPropertiesFileDirectory();

        try (final FileOutputStream out = new FileOutputStream(PROPERTIES_FILE_PATH_AND_NAME)) {
            properties.store(out, PROPERTIES_FILE_COMMENTS);

            log.info("Successfully created new settings file \"{}\".", PROPERTIES_FILE_PATH_AND_NAME);
        } catch (final IOException e) {
            log.error("Failed to create \"{}\". Falling back to internal default values.\n\nDetails:\n\n",
                    PROPERTIES_FILE_PATH_AND_NAME, e);
        }

        fixUrisInPropertiesFile();
    }

    private static void createPropertiesFileDirectory() {
        new File(PROPERTIES_FILE_PATH).mkdirs();
    }

    private static void fixUrisInPropertiesFile() {
        String propertiesFile;

        try (final FileInputStream in = new FileInputStream(PROPERTIES_FILE_PATH_AND_NAME)) {
            propertiesFile = IOUtils.toString(in);

            // Replace escaped colon characters in property value URI's, e.g., http\://example.com => http://example.com
            propertiesFile = propertiesFile.replace("\\://", "://");
        } catch (final IOException e) {
            log.error("Failed to fix URI's in \"{}\".\n\nDetails:\n\n", PROPERTIES_FILE_PATH_AND_NAME, e);
            return;
        }

        // Re-write the properties file with the URI fix in place.
        try (final FileOutputStream out = new FileOutputStream(PROPERTIES_FILE_PATH_AND_NAME)) {
            out.write(propertiesFile.getBytes(Charsets.UTF_8));
        } catch (final IOException e) {
            log.error("Failed to fix URI's in \"{}\".\n\nDetails:\n\n", PROPERTIES_FILE_PATH_AND_NAME, e);
        }
    }

    private static void deleteOldPropertiesFile() {
        try {
            log.info("Attempting to delete your old settings file \"{}\"...", PROPERTIES_FILE_PATH_AND_NAME);

            Files.delete(Paths.get(PROPERTIES_FILE_PATH_AND_NAME));

            log.info("Successfully deleted your old settings file \"{}\".", PROPERTIES_FILE_PATH_AND_NAME);
        } catch (final IOException e) {
            log.warn("Failed to delete your old settings file \"{}\".\n\nDetails:\n\n",
                    PROPERTIES_FILE_PATH_AND_NAME, e);
        }
    }

    private static void updateVersion() {
        try {
            log.info("Attempting to create (or update) \"{}\"...", VERSION_FILE_PATH_AND_NAME);

            createVersionFileDirectory();

            Files.write(Paths.get(VERSION_FILE_PATH_AND_NAME), ImmutableList.of(String.valueOf(CURRENT_VERSION)),
                    StandardCharsets.UTF_8);

            log.info("Successfully created (or updated) \"{}\".", VERSION_FILE_PATH_AND_NAME);
        } catch (final IOException e) {
            log.error(
                    "Failed to create (or update) \"{}\".\nIf \"{}\" exists, it will be upgraded "
                            + "because we couldn't determine its version!\n\nDetails:\n\n",
                    VERSION_FILE_PATH_AND_NAME, PROPERTIES_FILE_PATH_AND_NAME, e);
        }
    }

    private static void createVersionFileDirectory() {
        new File(VERSION_FILE_PATH).mkdirs();
    }

    private static void mergeOldPropertiesInto(final Properties properties) {
        properties.putAll(recoverOldPropertiesHavingKeysMatching(properties));
    }

    private static Result<String> loadPropertiesFromExistingFileInto(final Properties properties) {
        try {
            final String propertiesFileContents = FileUtils.loadFile(PROPERTIES_FILE_PATH_AND_NAME,
                    StandardCharsets.UTF_8);

            // Deal with Windows path single backslashes being erased because it's a reserved character for continuing lines
            // by the Properties class.
            properties.load(new StringReader(propertiesFileContents.replace("\\", "\\\\")));
        } catch (final IOException e) {
            final String failureReason = Throwables.getStackTraceAsString(e);
            log.warn("Failed to load \"{}\".\n\nDetails:\n\n{}", PROPERTIES_FILE_PATH_AND_NAME, failureReason);
            return Result.failure(failureReason);
        }

        return Result.success();
    }

    private static void setDefaultValuesFor(final Properties properties) {
        // @formatter:off
        properties.setProperty(WINDOW_WIDTH_PROPERTY_KEY, String.valueOf(GraphicsSettings.INITIAL_WINDOW_WIDTH));
        properties.setProperty(WINDOW_HEIGHT_PROPERTY_KEY, String.valueOf(GraphicsSettings.INITIAL_WINDOW_HEIGHT));
        properties.setProperty(WINDOW_RESIZABLE_PROPERTY_KEY, String.valueOf(GraphicsSettings.IS_WINDOW_RESIZABLE));
        properties.setProperty(WINDOW_DECORATED_PROPERTY_KEY, String.valueOf(GraphicsSettings.IS_WINDOW_DECORATED));
        properties.setProperty(WINDOW_TITLE_PROPERTY_KEY, String.valueOf(GraphicsSettings.WINDOW_TITLE));
        properties.setProperty(FULLSCREEN_PROPERTY_KEY, String.valueOf(GraphicsSettings.IS_FULLSCREEN));
        properties.setProperty(VSYNC_PROPERTY_KEY, String.valueOf(GraphicsSettings.IS_VSYNC_ENABLED));
        properties.setProperty(HIGH_DPI_PROPERTY_KEY, String.valueOf(GraphicsSettings.USE_HIGH_DPI));
        properties.setProperty(OPENGL_CORE_PROFILE_PROPERTY_KEY,
                String.valueOf(GraphicsSettings.USE_OPENGL_CORE_PROFILE));
        properties.setProperty(MUSIC_ENABLED_PROPERTY_KEY, String.valueOf(MusicSettings.IS_ENABLED));
        properties.setProperty(MUSIC_VOLUME_PROPERTY_KEY, String.valueOf(MusicSettings.INITIAL_VOLUME));
        properties.setProperty(START_SCREEN_PROPERTY_KEY, String.valueOf(ScreenSettings.START_SCREEN));
        properties.setProperty(UPDATE_ASSETS_KEY, String.valueOf(AssetSettings.UPDATE_ASSETS));
        properties.setProperty(UPDATED_ASSETS_LOCATION_KEY, AssetSettings.ABSOLUTE_UPDATED_ASSETS_LOCATION);
        properties.setProperty(PLAYER_NAME_KEY, InputSettings.INITIAL_PLAYER_NAME);
        properties.setProperty(CLAN_ACRONYM_KEY, InputSettings.INITIAL_CLAN_ACRONYM);
        properties.setProperty(SERVER_NAME_KEY, InputSettings.INITIAL_SERVER_NAME);
        properties.setProperty(SERVER_ADDRESS_KEY, InputSettings.INITIAL_SERVER_ADDRESS);
        properties.setProperty(CLASSIC_MODE_HUMAN_PLAYER_LIMIT_KEY,
                String.valueOf(InputSettings.INITIAL_CLASSIC_MODE_HUMAN_PLAYER_LIMIT));
        properties.setProperty(CLASSIC_MODE_AI_PLAYER_LIMIT_KEY,
                String.valueOf(InputSettings.INITIAL_CLASSIC_MODE_AI_PLAYER_LIMIT));
        properties.setProperty(SPECTATOR_LIMIT_KEY, String.valueOf(InputSettings.INITIAL_SPECTATOR_LIMIT));
        properties.setProperty(CLASSIC_MODE_PLAY_MAP_NAME_KEY,
                Strings.toProperCase(String.valueOf(InputSettings.INITIAL_CLASSIC_MODE_PLAY_MAP_NAME)));
        properties.setProperty(CLASSIC_MODE_WIN_PERCENT_KEY,
                String.valueOf(InputSettings.INITIAL_CLASSIC_MODE_WIN_PERCENT));
        properties.setProperty(CLASSIC_MODE_INITIAL_COUNTRY_ASSIGNMENT_KEY,
                String.valueOf(InputSettings.INITIAL_CLASSIC_MODE_COUNTRY_ASSIGNMENT));
        properties.setProperty(AUTO_JOIN_GAME_KEY, String.valueOf(InputSettings.AUTO_JOIN_GAME));
        properties.setProperty(AUTO_CREATE_GAME_KEY, String.valueOf(InputSettings.AUTO_CREATE_GAME));
        // @formatter:on
    }

    private static Properties recoverOldPropertiesHavingKeysMatching(final Properties properties) {
        log.info("Attempting to save your old settings...");

        final Properties allOldProperties = new Properties();
        final Properties recoveredOldProperties = new Properties();

        final Result<String> result = loadPropertiesFromExistingFileInto(allOldProperties);

        if (result.failed()) {
            log.error("Failed to save your old settings.\n\nDetails:\n\n{}", result.getFailureReason());
            return recoveredOldProperties;
        }

        recoveredOldProperties.putAll(intersection(allOldProperties, properties));

        log.info("Successfully saved your old settings.");

        return recoveredOldProperties;
    }

    private static void configureApplicationSettingsFromProperties(final Properties properties) {
        // @formatter:off
        GraphicsSettings.INITIAL_WINDOW_WIDTH = parseInteger(WINDOW_WIDTH_PROPERTY_KEY,
                GraphicsSettings.MIN_INITIAL_WINDOW_WIDTH, properties);
        GraphicsSettings.INITIAL_WINDOW_HEIGHT = parseInteger(WINDOW_HEIGHT_PROPERTY_KEY,
                GraphicsSettings.MIN_INITIAL_WINDOW_HEIGHT, properties);
        GraphicsSettings.IS_WINDOW_RESIZABLE = parseBoolean(WINDOW_RESIZABLE_PROPERTY_KEY, properties);
        GraphicsSettings.IS_WINDOW_DECORATED = parseBoolean(WINDOW_DECORATED_PROPERTY_KEY, properties);
        GraphicsSettings.WINDOW_TITLE = properties.getProperty(WINDOW_TITLE_PROPERTY_KEY);
        GraphicsSettings.IS_FULLSCREEN = parseBoolean(FULLSCREEN_PROPERTY_KEY, properties);
        GraphicsSettings.IS_VSYNC_ENABLED = parseBoolean(VSYNC_PROPERTY_KEY, properties);
        GraphicsSettings.USE_HIGH_DPI = parseBoolean(HIGH_DPI_PROPERTY_KEY, properties);
        GraphicsSettings.USE_OPENGL_CORE_PROFILE = parseBoolean(OPENGL_CORE_PROFILE_PROPERTY_KEY, properties);
        MusicSettings.IS_ENABLED = parseBoolean(MUSIC_ENABLED_PROPERTY_KEY, properties);
        MusicSettings.INITIAL_VOLUME = parseFloat(MUSIC_VOLUME_PROPERTY_KEY, MusicSettings.MIN_VOLUME,
                MusicSettings.MAX_VOLUME, properties);
        ScreenSettings.START_SCREEN = parseStartScreen(START_SCREEN_PROPERTY_KEY, properties);
        AssetSettings.UPDATE_ASSETS = parseBoolean(UPDATE_ASSETS_KEY, properties);
        AssetSettings.ABSOLUTE_UPDATED_ASSETS_LOCATION = properties.getProperty(UPDATED_ASSETS_LOCATION_KEY);
        InputSettings.INITIAL_PLAYER_NAME = parsePlayerName(PLAYER_NAME_KEY, properties);
        InputSettings.INITIAL_CLAN_ACRONYM = parseClanAcronym(CLAN_ACRONYM_KEY, properties);
        InputSettings.INITIAL_SERVER_NAME = parseServerName(SERVER_NAME_KEY, properties);
        InputSettings.INITIAL_SERVER_ADDRESS = parseServerAddress(SERVER_ADDRESS_KEY, properties);
        InputSettings.INITIAL_CLASSIC_MODE_HUMAN_PLAYER_LIMIT = parseInteger(CLASSIC_MODE_HUMAN_PLAYER_LIMIT_KEY,
                ClassicGameRules.MIN_HUMAN_PLAYER_LIMIT, ClassicGameRules.MAX_HUMAN_PLAYER_LIMIT, properties);
        InputSettings.INITIAL_CLASSIC_MODE_AI_PLAYER_LIMIT = parseInteger(CLASSIC_MODE_AI_PLAYER_LIMIT_KEY,
                ClassicGameRules.getMinAiPlayerLimit(InputSettings.INITIAL_CLASSIC_MODE_HUMAN_PLAYER_LIMIT),
                ClassicGameRules.getMaxAiPlayerLimit(InputSettings.INITIAL_CLASSIC_MODE_HUMAN_PLAYER_LIMIT),
                properties);
        InputSettings.INITIAL_SPECTATOR_LIMIT = parseInteger(SPECTATOR_LIMIT_KEY,
                ClassicGameRules.MIN_SPECTATOR_LIMIT, ClassicGameRules.MAX_SPECTATOR_LIMIT, properties);
        InputSettings.INITIAL_CLASSIC_MODE_PLAY_MAP_NAME = parseClassicModePlayMapName(
                CLASSIC_MODE_PLAY_MAP_NAME_KEY, properties);
        InputSettings.INITIAL_CLASSIC_MODE_WIN_PERCENT = parseInteger(CLASSIC_MODE_WIN_PERCENT_KEY, 5,
                ClassicGameRules.MAX_WIN_PERCENTAGE, 5, properties);
        InputSettings.INITIAL_CLASSIC_MODE_COUNTRY_ASSIGNMENT = parseEnum(
                CLASSIC_MODE_INITIAL_COUNTRY_ASSIGNMENT_KEY, InitialCountryAssignment.class, properties);
        InputSettings.AUTO_JOIN_GAME = parseBoolean(AUTO_JOIN_GAME_KEY, properties);
        InputSettings.AUTO_CREATE_GAME = parseBoolean(AUTO_CREATE_GAME_KEY, properties);
        // @formatter:on
    }

    private static Integer parseInteger(final String propertyKey, final int min, final Properties properties) {
        return parseInteger(propertyKey, min, Integer.MAX_VALUE, properties);
    }

    private static Integer parseInteger(final String propertyKey, final int min, final int max,
            final Properties properties) {
        return parseInteger(propertyKey, min, max, 1, properties);
    }

    private static Integer parseInteger(final String propertyKey, final int min, final int max, final int multiple,
            final Properties properties) {
        String propertyValue = null;

        try {
            propertyValue = properties.getProperty(propertyKey);

            final int value = Integer.valueOf(properties.getProperty(propertyKey));

            if (value < min || value > max || value % multiple != 0) {
                throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue));
            }

            return value;
        } catch (final NumberFormatException e) {
            throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue), e);
        }
    }

    private static Float parseFloat(final String propertyKey, final float min, final float max,
            final Properties properties) {
        String propertyValue = null;

        try {
            propertyValue = properties.getProperty(propertyKey);

            final float value = Float.valueOf(properties.getProperty(propertyKey));

            if (value < min || value > max)
                throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue));

            return value;
        } catch (final NumberFormatException e) {
            throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue), e);
        }
    }

    private static Boolean parseBoolean(final String propertyKey, final Properties properties) {
        final String propertyValue = properties.getProperty(propertyKey);

        if (!propertyValue.equalsIgnoreCase("true") && !propertyValue.equalsIgnoreCase("false")) {
            throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue));
        }

        return Boolean.valueOf(propertyValue);
    }

    private static <E extends Enum<E>> E parseEnum(final String propertyKey, final Class<E> enumType,
            final Properties properties) {
        String propertyValue = null;

        try {
            propertyValue = properties.getProperty(propertyKey);

            return Enum.valueOf(enumType, properties.getProperty(propertyKey).replace("-", "_").toUpperCase());
        } catch (final NullPointerException | IllegalArgumentException e) {
            throw new RuntimeException(getParseErrorMessageFor(propertyKey, propertyValue), e);
        }
    }

    private static ScreenId parseStartScreen(final String startScreenKey, final Properties properties) {
        final ScreenId startScreenId = parseEnum(startScreenKey, ScreenId.class, properties);

        if (!ScreenSettings.isValidStartScreen(startScreenId)) {
            throw new RuntimeException(
                    getParseErrorMessageFor(startScreenKey, properties.getProperty(startScreenKey)));
        }

        return startScreenId;
    }

    private static String parsePlayerName(final String playerNameKey, final Properties properties) {
        final String playerName = properties.getProperty(playerNameKey);

        if (!playerName.isEmpty() && !GameSettings.isValidPlayerNameWithoutClanTag(playerName)) {
            throw new RuntimeException(getParseErrorMessageFor(playerNameKey, playerName));
        }

        return playerName;
    }

    private static String parseClanAcronym(final String clanAcronymKey, final Properties properties) {
        final String clanAcronym = properties.getProperty(clanAcronymKey);

        if (!clanAcronym.isEmpty() && !GameSettings.isValidHumanClanAcronym(clanAcronym)) {
            throw new RuntimeException(getParseErrorMessageFor(clanAcronymKey, clanAcronym));
        }

        return clanAcronym;
    }

    private static String parseServerName(final String serverNameKey, final Properties properties) {
        final String serverName = properties.getProperty(serverNameKey);

        if (!serverName.isEmpty() && !NetworkSettings.isValidServerName(serverName)) {
            throw new RuntimeException(getParseErrorMessageFor(serverNameKey, serverName));
        }

        return serverName;
    }

    private static String parseServerAddress(final String serverAddressKey, final Properties properties) {
        final String serverAddress = properties.getProperty(serverAddressKey);

        if (!serverAddress.isEmpty() && !NetworkSettings.isValidServerAddress(serverAddress)) {
            throw new RuntimeException(getParseErrorMessageFor(serverAddressKey, serverAddress));
        }

        return serverAddress;
    }

    private static String parseClassicModePlayMapName(final String classicModePlayMapNameKey,
            final Properties properties) {
        final String playMapName = properties.getProperty(classicModePlayMapNameKey);

        if (!GameSettings.isValidPlayMapName(playMapName)) {
            throw new RuntimeException(getParseErrorMessageFor(classicModePlayMapNameKey, playMapName));
        }

        return Strings.toProperCase(playMapName);
    }

    private ClientApplicationProperties() {
        Classes.instantiationNotAllowed();
    }
}