From 8b38fda98460f570d11ebe76467c885517077971 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Wed, 12 Dec 2018 18:51:04 +0100 Subject: [PATCH 1/4] rwgame: merge argument + configuration file parsing + add tests - definition of arguments an configuration parameters is centralized in rwgame/RWConfig.inc - argument parsing is tested - the try/catch in main is less weird now (imho) --- rwgame/CMakeLists.txt | 7 +- rwgame/GameBase.cpp | 142 ++++----- rwgame/GameBase.hpp | 12 +- rwgame/GameConfig.cpp | 478 ---------------------------- rwgame/GameConfig.hpp | 283 ----------------- rwgame/RWConfig.cpp | 575 ++++++++++++++++++++++++++++++++++ rwgame/RWConfig.hpp | 262 ++++++++++++++++ rwgame/RWConfig.inc | 25 ++ rwgame/RWGame.cpp | 45 +-- rwgame/RWGame.hpp | 11 +- rwgame/main.cpp | 25 +- rwgame/states/DebugState.cpp | 2 +- rwgame/states/IngameState.cpp | 2 +- tests/CMakeLists.txt | 3 +- tests/test_Config.cpp | 426 ++++++++++++++++--------- tests/test_Globals.cpp | 10 +- 16 files changed, 1257 insertions(+), 1051 deletions(-) delete mode 100644 rwgame/GameConfig.cpp delete mode 100644 rwgame/GameConfig.hpp create mode 100644 rwgame/RWConfig.cpp create mode 100644 rwgame/RWConfig.hpp create mode 100644 rwgame/RWConfig.inc diff --git a/rwgame/CMakeLists.txt b/rwgame/CMakeLists.txt index 438772e0..3ec815f7 100644 --- a/rwgame/CMakeLists.txt +++ b/rwgame/CMakeLists.txt @@ -6,12 +6,14 @@ set(RWGAME_SOURCES main.cpp + RWConfig.inc + RWConfig.hpp + RWConfig.cpp + GameBase.hpp GameBase.cpp RWGame.hpp RWGame.cpp - GameConfig.hpp - GameConfig.cpp GameWindow.hpp GameWindow.cpp @@ -42,7 +44,6 @@ set(RWGAME_SOURCES states/DebugState.cpp states/BenchmarkState.hpp states/BenchmarkState.cpp - ) add_executable(rwgame diff --git a/rwgame/GameBase.cpp b/rwgame/GameBase.cpp index bbb0e342..cc369b4a 100644 --- a/rwgame/GameBase.cpp +++ b/rwgame/GameBase.cpp @@ -1,100 +1,23 @@ #include "GameBase.hpp" -//#include - -#include +#include +#include "GitSHA1.h" #include -#include - -#include "GitSHA1.h" - #include // Use first 8 chars of git hash as the build string const std::string kBuildStr(kGitSHA1Hash, 8); const std::string kWindowTitle = "RWGame"; -const std::string kDefaultConfigFileName = "openrw.ini"; -constexpr int kWindowWidth = 800; -constexpr int kWindowHeight = 600; -GameBase::GameBase(Logger &inlog, int argc, char *argv[]) : - log(inlog) { +GameBase::GameBase(Logger &inlog, const std::optional &args) : + log(inlog), + config(buildConfig(args)) { log.info("Game", "Build: " + kBuildStr); - size_t w = kWindowWidth, h = kWindowHeight; - rwfs::path configPath; - bool fullscreen = false; - bool help = false; - - // Define and parse command line options - namespace po = boost::program_options; - po::options_description desc_window("Window options"); - desc_window.add_options()( - "width,w", po::value()->value_name("WIDTH"), "Game resolution width in pixel")( - "height,h", po::value()->value_name("HEIGHT"), "Game resolution height in pixel")( - "fullscreen,f", "Enable fullscreen mode"); - po::options_description desc_game("Game options"); - desc_game.add_options()( - "newgame,n", "Start a new game")( - "load,l", po::value()->value_name("PATH"), "Load save file"); - po::options_description desc_devel("Developer options"); - desc_devel.add_options()( - "test,t", "Starts a new game in a test location")( - "benchmark,b", po::value()->value_name("PATH"), "Run benchmark from file"); - po::options_description desc("Generic options"); - desc.add_options()( - "config,c", po::value()->value_name("PATH"), "Path of configuration file")( - "help", "Show this help message"); - desc.add(desc_window).add(desc_game).add(desc_devel); - - po::variables_map &vm = options; - try { - po::store(po::parse_command_line(argc, argv, desc), vm); - po::notify(vm); - } catch (po::error &ex) { - help = true; - std::cout << "Error parsing arguments: " << ex.what() << std::endl; - } - - if (help || vm.count("help")) { - std::cout << desc; - throw std::invalid_argument(""); - } - if (vm.count("width")) { - w = vm["width"].as(); - } - if (vm.count("height")) { - h = vm["height"].as(); - } - if (vm.count("fullscreen")) { - fullscreen = true; - } - if (vm.count("config")) { - configPath = vm["config"].as(); - } else { - configPath = GameConfig::getDefaultConfigPath() / kDefaultConfigFileName; - } - - config.loadFile(configPath); - if (!config.isValid()) { - log.error("Config", "Invalid INI file at \"" - + config.getConfigPath().string() + "\".\n" - + "Adapt the following default INI to your configuration.\n" - + config.getDefaultINIString()); - throw std::runtime_error(config.getParseResult().what()); - } - - if (!vm.count("width")) { - w = config.getWindowWidth(); - } - if (!vm.count("height")) { - h = config.getWindowHeight(); - } - if (!vm.count("fullscreen")) { - fullscreen = config.getWindowFullscreen(); - } + bool fullscreen = config.fullscreen(); + size_t w = config.width(), h = config.height(); if (SDL_Init(SDL_INIT_VIDEO) < 0) throw std::runtime_error("Failed to initialize SDL2!"); @@ -105,6 +28,57 @@ GameBase::GameBase(Logger &inlog, int argc, char *argv[]) : [this]() {window.hideCursor();}); } +RWConfig GameBase::buildConfig(const std::optional &args) { + RWConfig config; + if (args.has_value()) { + config.setLayer(RWConfig::LAYER_ARGUMENT, *args); + } + auto defaultLayer = buildDefaultConfigLayer(); + config.setLayer(RWConfig::LAYER_DEFAULT, defaultLayer); + + rwfs::path configPath; + if (args.has_value() && args->configPath.has_value()) { + configPath = *args->configPath; + } else { + configPath = RWConfigParser::getDefaultConfigPath() / "openrw.ini"; + } + + if ((!args) || (args && !args->noconfig)) { + RWConfigParser configParser{}; + auto [configLayer, parseResult] = configParser.loadFile(configPath); + + if (!parseResult.isValid()) { + log.error("Config", "Could not read configuation file at " + configPath.string()); + throw std::runtime_error(parseResult.what()); + } + config.unknown = parseResult.getUnknownData(); + config.setLayer(RWConfig::LAYER_CONFIGFILE, configLayer); + } + + auto missingKeys = config.missingKeys(); + if (!missingKeys.empty()) { + std::ostringstream oss; + oss << "Configuration is incomplete. The following configuration parameters are missing:"; + for (const auto &missingKey : missingKeys) { + oss << "\n- " << missingKey << '\n'; + } + defaultLayer.gamedataPath = "/path/to/gta3/data"; + RWConfigParser configParser{}; + auto [default_ini_string, parseResult] = configParser.layerToString(defaultLayer); + log.error("Config", "Configuration is incomplete. INI file at \"" + configPath.string() + "\""); + if (parseResult.isValid()) { + log.error("Config", "Adapt the following default INI to your configuration."); + log.error("Config", default_ini_string); + } else { + log.error("Config", "Default INI creation failed."); + oss << "\n On top, an internal error occured while creating the default INI string."; + } + + throw std::runtime_error(oss.str()); + } + return config; +} + GameBase::~GameBase() { SDL_Quit(); diff --git a/rwgame/GameBase.hpp b/rwgame/GameBase.hpp index f3c827e6..7d24a004 100644 --- a/rwgame/GameBase.hpp +++ b/rwgame/GameBase.hpp @@ -1,18 +1,18 @@ #ifndef RWGAME_GAMEBASE_HPP #define RWGAME_GAMEBASE_HPP -#include "GameConfig.hpp" #include "GameWindow.hpp" +#include "RWConfig.hpp" #include -#include +#include /** * @brief Handles basic window and setup */ class GameBase { public: - GameBase(Logger& inlog, int argc, char* argv[]); + GameBase(Logger& inlog, const std::optional &args); virtual ~GameBase() = 0; @@ -20,15 +20,15 @@ public: return window; } - const GameConfig& getConfig() const { + const RWConfig& getConfig() const { return config; } protected: + RWConfig buildConfig(const std::optional &args); Logger& log; - GameConfig config{}; GameWindow window{}; - boost::program_options::variables_map options{}; + RWConfig config{}; }; #endif diff --git a/rwgame/GameConfig.cpp b/rwgame/GameConfig.cpp deleted file mode 100644 index fcc987d1..00000000 --- a/rwgame/GameConfig.cpp +++ /dev/null @@ -1,478 +0,0 @@ -#include "GameConfig.hpp" - -#include -#include - -#include -#include -#include -namespace pt = boost::property_tree; - -#ifdef RW_WINDOWS -#include -#include - -#include -#endif - -const std::string kConfigDirectoryName("OpenRW"); - -void GameConfig::loadFile(const rwfs::path &path) { - m_configPath = path; - std::string dummy; - m_parseResult = - parseConfig(ParseType::FILE, path.string(), ParseType::CONFIG, dummy); -} - -rwfs::path GameConfig::getConfigPath() const { - return m_configPath; -} - -bool GameConfig::isValid() const { - return m_parseResult.isValid(); -} - -const GameConfig::ParseResult &GameConfig::getParseResult() const { - return m_parseResult; -} - -rwfs::path GameConfig::getDefaultConfigPath() { -#if defined(RW_LINUX) || defined(RW_FREEBSD) || defined(RW_NETBSD) || \ - defined(RW_OPENBSD) - char *config_home = getenv("XDG_CONFIG_HOME"); - if (config_home != nullptr) { - return rwfs::path(config_home) / kConfigDirectoryName; - } - char *home = getenv("HOME"); - if (home != nullptr) { - return rwfs::path(home) / ".config/" / kConfigDirectoryName; - } - -#elif defined(RW_OSX) - char *home = getenv("HOME"); - if (home) - return rwfs::path(home) / "Library/Preferences/" / kConfigDirectoryName; - -#elif defined(RW_WINDOWS) - wchar_t *widePath; - auto res = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, - nullptr, &widePath); - if (SUCCEEDED(res)) { - auto utf8Path = wideStringToACP(widePath); - return rwfs::path(utf8Path) / kConfigDirectoryName; - } -#else - return rwfs::path(); -#endif - - // Well now we're stuck. - RW_ERROR("No default config path found."); - return rwfs::path(); -} - -std::string stripComments(const std::string &str) { - auto s = std::string(str, 0, str.find_first_of(";#")); - return s.erase(s.find_last_not_of(" \n\r\t") + 1); -} - -struct PathTranslator { - typedef std::string internal_type; - typedef rwfs::path external_type; - boost::optional get_value(const internal_type &str) { - return rwfs::path(str); - } - boost::optional put_value(const external_type &path) { - return path.string(); - } -}; - -struct StringTranslator { - typedef std::string internal_type; - typedef std::string external_type; - boost::optional get_value(const internal_type &str) { - return stripComments(str); - } - boost::optional put_value(const external_type &str) { - return str; - } -}; - -struct BoolTranslator { - typedef std::string internal_type; - typedef bool external_type; - boost::optional get_value(const internal_type &str) { - boost::optional res; - try { - res = std::stoi(stripComments(str)) != 0; - } catch (std::invalid_argument &) { - } - return res; - } - boost::optional put_value(const external_type &b) { - return internal_type(b ? "1" : "0"); - } -}; - -struct IntTranslator { - typedef std::string internal_type; - typedef int external_type; - boost::optional get_value(const internal_type &str) { - boost::optional res; - try { - res = std::stoi(stripComments(str)); - } catch (std::invalid_argument &) { - } - return res; - } - boost::optional put_value(const external_type &i) { - return std::to_string(i); - } -}; - -struct FloatTranslator { - typedef std::string internal_type; - typedef float external_type; - boost::optional get_value(const internal_type &str) { - boost::optional res; - try { - res = std::stof(stripComments(str)); - } catch (std::invalid_argument &) { - } - return res; - } - boost::optional put_value(const external_type &f) { - return std::to_string(f); - } -}; - -GameConfig::ParseResult GameConfig::saveConfig() { - auto configPath = getConfigPath().string(); - return parseConfig(ParseType::CONFIG, "", ParseType::FILE, configPath); -} - -std::string GameConfig::getDefaultINIString() { - std::string result; - parseConfig(ParseType::DEFAULT, "", ParseType::STRING, result); - return result; -} - -GameConfig::ParseResult GameConfig::parseConfig(GameConfig::ParseType srcType, - const std::string &source, - ParseType destType, - std::string &destination) { - // srcTree: holds all key/value pairs - pt::ptree srcTree; - ParseResult parseResult(srcType, source, destType, destination); - - try { - if (srcType == ParseType::STRING) { - std::istringstream iss(source); - pt::read_ini(iss, srcTree); - } else if (srcType == ParseType::FILE) { - pt::read_ini(source, srcTree); - } - } catch (pt::ini_parser_error &e) { - // Catches illegal input files (nonsensical input, duplicate keys) - parseResult.failInputFile(e.line(), e.message()); - RW_MESSAGE(e.what()); - return parseResult; - } - - if (destType == ParseType::DEFAULT) { - parseResult.failArgument(); - RW_ERROR("Target cannot be DEFAULT."); - return parseResult; - } - - // knownKeys: holds all known keys - std::vector knownKeys; - - auto read_config = [&](const std::string &key, auto &target, - const auto &defaultValue, auto &translator, - bool optional = true) { - typedef typename std::remove_reference::type config_t; - - config_t sourceValue; - knownKeys.push_back(key); - - switch (srcType) { - case ParseType::DEFAULT: - sourceValue = defaultValue; - break; - case ParseType::CONFIG: - sourceValue = target; - break; - case ParseType::FILE: - case ParseType::STRING: - try { - sourceValue = srcTree.get(key, translator); - } catch (pt::ptree_bad_path &e) { - RW_UNUSED(e); - // Catches missing key-value pairs: fail when required - if (!optional) { - parseResult.failRequiredMissing(key); - RW_MESSAGE(e.what()); - return; - } - sourceValue = defaultValue; - } catch (pt::ptree_bad_data &e) { - RW_UNUSED(e); - // Catches illegal value data: always fail - parseResult.failInvalidData(key); - RW_MESSAGE(e.what()); - return; - } - break; - } - srcTree.put(key, sourceValue, translator); - - switch (destType) { - case ParseType::DEFAULT: - // Target cannot be DEFAULT (case already handled) - parseResult.failArgument(); - break; - case ParseType::CONFIG: - // Don't care if success == false - target = sourceValue; - break; - case ParseType::FILE: - case ParseType::STRING: - break; - } - }; - - auto deft = StringTranslator(); - auto boolt = BoolTranslator(); - auto patht = PathTranslator(); - auto intt = IntTranslator(); - auto floatt = FloatTranslator(); - - // Add new configuration parameters here. - // Additionally, add them to the unit test. - - // @todo Don't allow path separators and relative directories - read_config("game.path", this->m_gamePath, "/opt/games/Grand Theft Auto 3", - patht, false); - read_config("game.language", this->m_gameLanguage, "american", deft); - read_config("game.hud_scale", this->m_HUDscale, 1.f, floatt); - - read_config("input.invert_y", this->m_inputInvertY, false, boolt); - - read_config("window.width", this->m_windowWidth, 800, intt); - read_config("window.height", this->m_windowHeight, 600, intt); - read_config("window.fullscreen", this->m_windowFullscreen, false, boolt); - - // Build the unknown key/value map from the correct source - switch (srcType) { - case ParseType::FILE: - case ParseType::STRING: - for (const auto §ion : srcTree) { - for (const auto &subKey : section.second) { - std::string key = section.first + "." + subKey.first; - if (std::find(knownKeys.begin(), knownKeys.end(), key) == - knownKeys.end()) { - RW_MESSAGE("Unknown configuration key: " << key); - parseResult.addUnknownData(key, subKey.second.data()); - } - } - } - break; - case ParseType::CONFIG: - parseResult.setUnknownData(m_parseResult.getUnknownData()); - break; - case ParseType::DEFAULT: - break; - } - - // Store the unknown key/value map to the correct destination - switch (destType) { - case ParseType::CONFIG: - m_parseResult.setUnknownData(parseResult.getUnknownData()); - break; - case ParseType::STRING: - case ParseType::FILE: - for (const auto &keyvalue : parseResult.getUnknownData()) { - srcTree.put(keyvalue.first, keyvalue.second); - } - break; - default: - break; - } - - if (!parseResult.isValid()) return parseResult; - - try { - if (destType == ParseType::STRING) { - std::ostringstream ostream; - pt::write_ini(ostream, srcTree); - destination = ostream.str(); - } else if (destType == ParseType::FILE) { - pt::write_ini(destination, srcTree); - } - } catch (pt::ini_parser_error &e) { - parseResult.failOutputFile(e.line(), e.message()); - RW_MESSAGE(e.what()); - } - - if (parseResult.type() == ParseResult::ErrorType::UNINITIALIZED) { - parseResult.markGood(); - } - - return parseResult; -} - -std::string GameConfig::extractFilenameParseTypeData(ParseType type, - const std::string &data) { - switch (type) { - case ParseType::CONFIG: - return ""; - case ParseType::FILE: - return data; - case ParseType::STRING: - return ""; - case ParseType::DEFAULT: - default: - return ""; - } -} - -GameConfig::ParseResult::ParseResult(GameConfig::ParseType srcType, - const std::string &source, - GameConfig::ParseType destType, - const std::string &destination) - : m_result(ErrorType::GOOD) - , m_inputfilename(GameConfig::extractFilenameParseTypeData(srcType, source)) - , m_outputfilename( - GameConfig::extractFilenameParseTypeData(destType, destination)) - , m_line(0) - , m_message() - , m_keys_requiredMissing() - , m_keys_invalidData() - , m_unknownData() { -} - -GameConfig::ParseResult::ParseResult() - : m_result(ErrorType::UNINITIALIZED) - , m_inputfilename() - , m_outputfilename() - , m_line(0) - , m_message() - , m_keys_requiredMissing() - , m_keys_invalidData() { -} - -GameConfig::ParseResult::ErrorType GameConfig::ParseResult::type() const { - return this->m_result; -} - -bool GameConfig::ParseResult::isValid() const { - return this->type() == ErrorType::GOOD; -} - -void GameConfig::ParseResult::failInputFile(size_t line, - const std::string &message) { - this->m_result = ParseResult::ErrorType::INVALIDINPUTFILE; - this->m_line = line; - this->m_message = message; -} - -void GameConfig::ParseResult::markGood() { - this->m_result = ParseResult::ErrorType::GOOD; -} - -void GameConfig::ParseResult::failArgument() { - this->m_result = ParseResult::ErrorType::INVALIDARGUMENT; -} - -void GameConfig::ParseResult::failRequiredMissing(const std::string &key) { - this->m_result = ParseResult::ErrorType::INVALIDCONTENT; - this->m_keys_requiredMissing.push_back(key); -} - -void GameConfig::ParseResult::failInvalidData(const std::string &key) { - this->m_result = ParseResult::ErrorType::INVALIDCONTENT; - this->m_keys_invalidData.push_back(key); -} - -void GameConfig::ParseResult::failOutputFile(size_t line, - const std::string &message) { - this->m_result = ParseResult::ErrorType::INVALIDOUTPUTFILE; - this->m_line = line; - this->m_message = message; -} - -const std::vector - &GameConfig::ParseResult::getKeysRequiredMissing() const { - return this->m_keys_requiredMissing; -} - -const std::vector &GameConfig::ParseResult::getKeysInvalidData() - const { - return this->m_keys_invalidData; -} - -std::string GameConfig::ParseResult::what() const { - std::ostringstream oss; - switch (this->m_result) { - case ErrorType::UNINITIALIZED: - oss << "Parsing was skipped or did not finish."; - break; - case ErrorType::GOOD: - oss << "Parsing completed without errors."; - break; - case ErrorType::INVALIDARGUMENT: - oss << "Invalid argument: destination cannot be the default " - "config."; - break; - case ErrorType::INVALIDINPUTFILE: - oss << "Error while reading \"" << this->m_inputfilename - << "\":" << this->m_line << ":\n" - << this->m_message << "."; - break; - case ErrorType::INVALIDOUTPUTFILE: - oss << "Error while writing \"" << this->m_inputfilename - << "\":" << this->m_line << ":\n" - << this->m_message << "."; - break; - case ErrorType::INVALIDCONTENT: - oss << "Error while parsing \"" << this->m_inputfilename << "\"."; - if (!this->m_keys_requiredMissing.empty()) { - oss << "\nRequired keys that are missing:"; - for (auto &key : this->m_keys_requiredMissing) { - oss << "\n - " << key; - } - } - if (!this->m_keys_invalidData.empty()) { - oss << "\nKeys that contain invalid data:"; - for (auto &key : this->m_keys_invalidData) { - oss << "\n - " << key; - } - } - break; - default: - oss << "Unknown error."; - break; - } - if (!this->m_unknownData.empty()) { - oss << "\nUnknown configuration keys:"; - for (const auto &[key, value] : m_unknownData) { - oss << "\n - " << key; - } - } - return oss.str(); -} - -void GameConfig::ParseResult::addUnknownData(const std::string &key, - const std::string &value) { - this->m_unknownData[key] = value; -} - -const std::map - &GameConfig::ParseResult::getUnknownData() const { - return this->m_unknownData; -} - -void GameConfig::ParseResult::setUnknownData( - const std::map &unknownData) { - this->m_unknownData = unknownData; -} diff --git a/rwgame/GameConfig.hpp b/rwgame/GameConfig.hpp deleted file mode 100644 index 682f68d0..00000000 --- a/rwgame/GameConfig.hpp +++ /dev/null @@ -1,283 +0,0 @@ -#ifndef RWGAME_GAMECONFIG_HPP -#define RWGAME_GAMECONFIG_HPP -#include -#include -#include - -#include - -class GameConfig { -private: - enum ParseType { DEFAULT, CONFIG, FILE, STRING }; - - /** - * @brief extractFilenameParseTypeData Get a human readable filename string - * @return file path or a description of the data type - */ - static std::string extractFilenameParseTypeData(ParseType type, - const std::string &data); - -public: - class ParseResult { - public: - enum ErrorType { - /// UNINITIALIZED: The config was not initialized - UNINITIALIZED, - /// GOOD: Input file/string was good - GOOD, - /// INVALIDINPUTFILE: There was some error while reading from a file - /// or string or the input was ambiguous (e.g. duplicate keys) - INVALIDINPUTFILE, - /// INVALIDARGUMENT: The parser received impossible arguments - INVALIDARGUMENT, - /// INVALIDCONTENT: Some required keys were missing or some values - /// were of incorrect type - INVALIDCONTENT, - /// INVALIDOUTPUTFILE: There was some error while writing to a file - /// or string - INVALIDOUTPUTFILE - }; - - private: - /** - * @brief ParseResult Holds the issues occurred while parsing of a - * config file. - * @param srcType Type of the source - * @param source The source of the parser - * @param destType Type of the destination - * @param destination The destination - */ - ParseResult(ParseType srcType, const std::string &source, - ParseType destType, const std::string &destination); - - /** - * @brief ParseResult Create empty ParseResult - */ - ParseResult(); - - public: - /** - * @brief type Get the type of error - * @return Type of error or GOOD if there was no error - */ - ErrorType type() const; - - /** - * @brief getKeysRequiredMissing Get the keys that were missing - * @return A vector with all the keys - */ - const std::vector &getKeysRequiredMissing() const; - - /** - * @brief getKeysInvalidData Get the keys that contained invalid data - * @return A vector with all the keys - */ - const std::vector &getKeysInvalidData() const; - - /** - * @brief Mark this result as valid - */ - void markGood(); - - /** - * @brief failInputFile Fail because the input file was invalid - * @param line Line number where the error is located - * @param message Description of the error - */ - void failInputFile(size_t line, const std::string &message); - - /** - * @brief failArgument Fail because an argument was invalid - * @param srcType type of the source - * @param destType type of the destination - */ - void failArgument(); - - /** - * @brief failRequiredMissing Fail because a required key is missing - * @param key The key that is missing - */ - void failRequiredMissing(const std::string &key); - - /** - * @brief failInvalidData Fail because a key contains invalid data - * @param key The key that contains invalid data - */ - void failInvalidData(const std::string &key); - - /** - * @brief failOutputFile Fail because an error occurred while while - * writing to the output - * @param line Line number where the error is located - * @param message Description of the error - */ - void failOutputFile(size_t line, const std::string &message); - - /** - * @brief isValid - * @return True if the loaded configuration is valid - */ - bool isValid() const; - - /** - * @brief what Get a string representing the error - * @return String with the error description - */ - std::string what() const; - - /** - * @brief addUnknownData Add unknown key value pairs - * @param key The unknown key - * @param value The associated data - */ - void addUnknownData(const std::string &key, const std::string &value); - - /** - * @brief addUnknownData Get all the unknown key value pairs - * @return Mapping of the unknown keys with associated data - */ - const std::map &getUnknownData() const; - - private: - /// Type of the failure - ErrorType m_result; - - /// Filename of the input file - std::string m_inputfilename; - - /// Filename of the output file - std::string m_outputfilename; - - /// Line number where the failure occurred (on invalid input or output - /// file) - size_t m_line; - - /// Description of the failure (on invalid input or output file) - std::string m_message; - - /// All required keys that are missing - std::vector m_keys_requiredMissing; - - /// All keys that contain invalid data - std::vector m_keys_invalidData; - - // Mapping of unknown keys and associated data - std::map m_unknownData; - - /** - * @brief setUnknownData Replace the the unknown key value pairs - */ - void setUnknownData( - const std::map &unknownData); - - friend class GameConfig; - }; - - /** - * @brief GameConfig Create a game configuration (initially invalid) - */ - GameConfig() = default; - - /** - * @brief Initialize this object using the config file at path - * @param path Path of the configuration file - */ - void loadFile(const rwfs::path &path); - - /** - * @brief getConfigPath Returns the path for the configuration - */ - rwfs::path getConfigPath() const; - - /** - * @brief writeConfig Save the game configuration - */ - ParseResult saveConfig(); - - /** - * @brief isValid - * @return True if the loaded configuration is valid - */ - bool isValid() const; - - /** - * @brief getParseResult Get more information on parsing failures - * @return A ParseResult object containing more information - */ - const ParseResult &getParseResult() const; - - /** - * @brief getConfigString Returns the content of the default INI - * configuration. - * @return INI string - */ - std::string getDefaultINIString(); - - const rwfs::path &getGameDataPath() const { - return m_gamePath; - } - const std::string &getGameLanguage() const { - return m_gameLanguage; - } - bool getInputInvertY() const { - return m_inputInvertY; - } - int getWindowWidth() const { - return m_windowWidth; - } - int getWindowHeight() const { - return m_windowHeight; - } - bool getWindowFullscreen() const { - return m_windowFullscreen; - } - float getHUDScale() const { - return m_HUDscale; - } - - static rwfs::path getDefaultConfigPath(); -private: - - /** - * @brief parseConfig Load data from source and write it to destination. - * Whitespace will be stripped from unknown data. - * @param srcType Can be DEFAULT | CONFIG | FILE | STRING - * @param source don't care if srcType == (DEFAULT | CONFIG), - * path of INI file if srcType == FILE - * INI string if srcType == STRING - * @param destType Can be CONFIG | FILE | STRING (DEFAULT is invalid) - * @param destination don't care if srcType == CONFIG - * path of INI file if destType == FILE - * INI string if srcType == STRING - * @return True if the parsing succeeded - */ - ParseResult parseConfig(ParseType srcType, const std::string &source, - ParseType destType, std::string &destination); - - /* Config State */ - rwfs::path m_configPath{}; - ParseResult m_parseResult{}; - - /* Actual Configuration */ - - /// Path to the game data - rwfs::path m_gamePath; - - /// Language for game - std::string m_gameLanguage = "american"; - - /// Invert the y axis for camera control. - bool m_inputInvertY = false; - - /// Size of the window - int m_windowWidth{800}; - int m_windowHeight{600}; - - /// Set the window to fullscreen - bool m_windowFullscreen = false; - - /// HUD scale parameter - float m_HUDscale = 1.f; -}; - -#endif diff --git a/rwgame/RWConfig.cpp b/rwgame/RWConfig.cpp new file mode 100644 index 00000000..3c0e3884 --- /dev/null +++ b/rwgame/RWConfig.cpp @@ -0,0 +1,575 @@ +#include "RWConfig.hpp" + +#include + +#include + +#include +#include + +#ifdef RW_WINDOWS +#include +#include + +#include +#endif + +namespace po = boost::program_options; +namespace pt = boost::property_tree; + +namespace { + +po::options_description build_options() { + std::array descriptions = + {{ + po::options_description{"Configuration options"}, + po::options_description{"Game actions"}, + po::options_description{"Input options"}, + po::options_description{"Window options"}, + po::options_description{"Developer options"}, + po::options_description{"General options"}, + }}; +#define RWARG(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + do { \ + auto option_builder = descriptions[RWArgumentParser::Category::_RW_CATEGORY].add_options(); \ + if constexpr (std::is_same_v) { \ + option_builder(_RW_ARGMASK, _RW_HELP); \ + } else { \ + option_builder(_RW_ARGMASK, po::value<_RW_TYPE>()->value_name(_RW_ARGMETA), _RW_HELP); \ + } \ + } while (0); +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + do { \ + if constexpr (std::is_same_v) { \ + descriptions[RWArgumentParser::Category::_RW_CATEGORY].add_options()( \ + _RW_ARGMASK, _RW_HELP); \ + } else { \ + descriptions[RWArgumentParser::Category::_RW_CATEGORY].add_options()( \ + _RW_ARGMASK, po::value<_RW_TYPE>()->value_name(_RW_ARGMETA), _RW_HELP); \ + } \ + } while (0); +#define RWARG_OPT(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + RWCONFIGARG(_RW_TYPE, _RW_NAME, std::nullopt, nullptr, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) + +#include "RWConfig.inc" + +#undef RWARG_OPT +#undef RWCONFIGARG +#undef RWARG + + auto& description = descriptions[0]; + for (auto i = 1u; i < descriptions.size(); ++i) { + description.add(descriptions[i]); + } + return description; +} + +} + +RWArgumentParser::RWArgumentParser() : _desc(build_options()) { +} + +namespace { + +constexpr std::string_view arg_mask_to_key(std::string_view v) { + size_t maxstart = 0u, maxsize = 0u; + size_t start = 0u; + size_t end = 0u; + while (start < v.size()) { + end = v.find(",", start); + if (end == std::string_view::npos) { + end = v.size(); + } + if ((end - start) > maxsize) { + maxstart = start; + maxsize = end - start; + } + start = end + 1; + } + return v.substr(maxstart, maxsize); +} + +} + +std::optional RWArgumentParser::parseArguments(int argc, const char* argv[]) const { + po::variables_map vm; + try { + if (argc != 0) { + po::store(po::command_line_parser(argc, argv).options(_desc).positional(po::positional_options_description{}).run(), vm); + } + po::notify(vm); + } catch (po::error &ex) { + std::cerr << "Error parsing arguments: " << ex.what() << std::endl; + return std::nullopt; + } + RWArgConfigLayer layer; + +#define RWARG(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + do { \ + const std::string key{arg_mask_to_key(_RW_ARGMASK)}; \ + layer._RW_NAME = vm.count(key) != 0u; \ + } while (0); +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + do { \ + const std::string key{arg_mask_to_key(_RW_ARGMASK)}; \ + if (vm.count(key)) { \ + layer._RW_NAME = vm[key].as<_RW_TYPE>(); \ + } \ + } while (0); +#define RWARG_OPT(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + do { \ + const std::string key{arg_mask_to_key(_RW_ARGMASK)}; \ + if (vm.count(key)) { \ + layer._RW_NAME = vm[key].as<_RW_TYPE>(); \ + } \ + } while (0); + +#include "RWConfig.inc" + +#undef RWARG_OPT +#undef RWCONFIGARG +#undef RWARG + + if (layer.noconfig && layer.configPath.has_value()) { + std::cerr << "Cannot set config path and ask noconfig at the sametime.\n"; + return std::nullopt; + } + + return layer; +} + +std::ostream& RWArgumentParser::printHelp(std::ostream &os) const { + return os << _desc; +} + +RWConfigLayer buildDefaultConfigLayer() { + RWConfigLayer layer; + +#define RWARG(...) +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + layer._RW_NAME = _RW_DEFAULT; +#define RWARG_OPT(...) +#include "RWConfig.inc" +#undef RWARG_OPT +#undef RWCONFIGARG +#undef RWARG + + return layer; +} + +static constexpr auto kConfigDirectoryName = "OpenRW"; + +rwfs::path RWConfigParser::getDefaultConfigPath() { +#if defined(RW_LINUX) || defined(RW_FREEBSD) || defined(RW_NETBSD) || \ + defined(RW_OPENBSD) + char *config_home = getenv("XDG_CONFIG_HOME"); + if (config_home != nullptr) { + return rwfs::path(config_home) / kConfigDirectoryName; + } + char *home = getenv("HOME"); + if (home != nullptr) { + return rwfs::path(home) / ".config/" / kConfigDirectoryName; + } + +#elif defined(RW_OSX) + char *home = getenv("HOME"); + if (home) + return rwfs::path(home) / "Library/Preferences/" / kConfigDirectoryName; + +#elif defined(RW_WINDOWS) + wchar_t *widePath; + auto res = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, + nullptr, &widePath); + if (SUCCEEDED(res)) { + auto utf8Path = wideStringToACP(widePath); + return rwfs::path(utf8Path) / kConfigDirectoryName; + } +#else + return rwfs::path(); +#endif + + // Well now we're stuck. + RW_ERROR("No default config path found."); + return rwfs::path(); +} + +namespace { + +void flatten_ptree_recursive(std::map &map, const std::string &subkey, const pt::ptree &ptree) { + for (const auto &[name, content] : ptree) { + auto key = subkey + "." + name; + map[key] = content.data(); + flatten_ptree_recursive(map, subkey + "." + name, content); + } +} +std::map flatten_ptree(const pt::ptree &ptree) { + std::map result; + for (const auto &[name, content] : ptree) { + flatten_ptree_recursive(result, name, content); + } + return result; +} + +std::string stripComments(const std::string &str) { + auto s = std::string(str, 0, str.find_first_of(";#")); + return s.erase(s.find_last_not_of(" \n\r\t") + 1); +} + +template +struct Translator { +}; + +template <> +struct Translator { + using internal_type = std::string; + using external_type = std::string; + boost::optional get_value(const internal_type &str) { + return stripComments(str); + } + boost::optional put_value(const external_type &str) { + return str; + } +}; + +template <> +struct Translator { + using internal_type = std::string; + using external_type = bool; + boost::optional get_value(const internal_type &str) { + boost::optional res; + try { + res = std::stoi(stripComments(str)) != 0; + } catch (std::invalid_argument &) { + } + return res; + } + boost::optional put_value(const external_type &b) { + return internal_type(b ? "1" : "0"); + } +}; + +template <> +struct Translator { + using internal_type = std::string; + using external_type = int; + boost::optional get_value(const internal_type &str) { + boost::optional res; + try { + res = std::stoi(stripComments(str)); + } catch (std::invalid_argument &) { + } + return res; + } + boost::optional put_value(const external_type &i) { + return std::to_string(i); + } +}; + +template <> +struct Translator { + using internal_type = std::string; + using external_type = float; + boost::optional get_value(const internal_type &str) { + boost::optional res; + try { + res = std::stof(stripComments(str)); + } catch (std::invalid_argument &) { + } + return res; + } + boost::optional put_value(const external_type &f) { + return std::to_string(f); + } +}; + +class TreeParser { + template + using TGetFunction = std::function(const RWConfigLayer &)>; + template + using TSetFunction = std::function &)>; +public: + TreeParser() = default; + template , typename TSetFunc = TSetFunction> + void add_option(TString &&key, TGetFunc &&getFunc, TSetFunc &&setFunc) { + _itemParsers.emplace_back( + std::make_unique>( + std::forward(key), std::forward(getFunc), std::forward(setFunc))); + } + RWConfigLayer to_layer(const pt::ptree &ptree, RWConfigParser::ParseResult &parseResult) const { + RWConfigLayer layer; + auto flattened_ptree = flatten_ptree(ptree); + for (const auto &itemParser : _itemParsers) { + try { + itemParser->to_layer(layer, ptree); + flattened_ptree.erase(itemParser->key()); + } catch (pt::ptree_bad_path &) { + // bad path -> not found -> no-op / std::nullopt + } catch (pt::ptree_bad_data &) { + parseResult.failInvalidData(itemParser->key()); + } + } + parseResult.setUnknownData(flattened_ptree); + return layer; + } + +pt::ptree to_ptree(const RWConfigLayer &layer, RWConfigParser::ParseResult &parseResult) const { + pt::ptree ptree; + for (const auto &itemParser : _itemParsers) { + try { + itemParser->to_ptree(ptree, layer); + } catch (pt::ptree_bad_path &) { + // bad path -> path has wrong format + parseResult.failInvalidData(itemParser->key()); + } catch (pt::ptree_bad_data &) { + parseResult.failInvalidData(itemParser->key()); + } + } + return ptree; + } +private: + class TreeItemParser { + protected: + const std::string _key; + template + TreeItemParser(TString &&key) : _key(key) {} + public: + virtual ~TreeItemParser() = default; + virtual void to_layer(RWConfigLayer &layer, const pt::ptree &ptree) const = 0; + virtual void to_ptree(pt::ptree &ptree, const RWConfigLayer &layer) const = 0; + const std::string &key() const { + return _key; + } + }; + template + class TreeItemParserImpl : public TreeItemParser { + public: + template + TreeItemParserImpl(TString &&key, TGetFunction &&getFunction, TSetFunction &&setFunction) + : TreeItemParser(std::forward(key)) + , _getFunction(getFunction) + , _setFunction(setFunction) { + } + ~TreeItemParserImpl() override = default; + TGetFunction _getFunction; + TSetFunction _setFunction; + void to_layer(RWConfigLayer &layer, const pt::ptree &ptree) const override { + Translator translator{}; + auto value = ptree.get(_key, translator); + auto stl_optional = std::optional(value); + _setFunction(layer, stl_optional); + } + void to_ptree(pt::ptree &ptree, const RWConfigLayer &layer) const override { + Translator translator{}; + const auto stl_optional = _getFunction(layer); + if (stl_optional.has_value()) { + T value = *stl_optional; + ptree.put(_key, value, translator); + } + } + }; + std::vector> _itemParsers; +}; + +TreeParser buildTreeParser() { + TreeParser treeParser; +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + treeParser.add_option<_RW_TYPE>( \ + _RW_CONFPATH, \ + [](const RWConfigLayer &layer) {return layer._RW_NAME;}, \ + [](RWConfigLayer &layer, const std::optional<_RW_TYPE> &value) {layer._RW_NAME = value;} \ + ); +#define RWARG(...) +#define RWARG_OPT(...) + +#include "RWConfig.inc" + +#undef RWCONFIGARG +#undef RWARG +#undef RWARG_OPT + return treeParser; +} + +} + +std::tuple RWConfigParser::loadFile(const rwfs::path &path) const { + ParseResult parseResult(path.string(), ""); + + auto treeParser = buildTreeParser(); + + RWConfigLayer layer; + try { + pt::ptree ptree; + pt::read_ini(path.string(), ptree); + layer = treeParser.to_layer(ptree, parseResult); + } catch (pt::ini_parser_error &e) { + // Catches illegal input files (nonsensical input, duplicate keys) + parseResult.failInputFile(e.line(), e.message()); + RW_MESSAGE(e.what()); + } + + if (parseResult.type() == ParseResult::UNINITIALIZED) { + parseResult.markGood(); + } + + return std::make_tuple(layer, parseResult); +} + +RWConfigParser::ParseResult RWConfigParser::saveFile(const rwfs::path &path, const RWConfigLayer &layer, const std::map &extra) const { + ParseResult parseResult("", path.string()); + + auto treeParser = buildTreeParser(); + + try { + auto ptree = treeParser.to_ptree(layer, parseResult); + for (const auto &[key, value] : extra) { + if (ptree.count(key) != 0u) { + parseResult.failInvalidData(key); + } + ptree.put(key, value); + } + pt::write_ini(path.string(), ptree); + } catch (pt::ini_parser_error &e) { + parseResult.failOutputFile(e.line(), e.message()); + } + + if (parseResult.type() == ParseResult::UNINITIALIZED) { + parseResult.markGood(); + } + + return parseResult; +} + +RWConfigParser::ParseResult RWConfigParser::saveFile(const rwfs::path &path, const RWConfigLayer &layer) const { + ParseResult parseResult("", path.string()); + + auto treeParser = buildTreeParser(); + + try { + auto ptree = treeParser.to_ptree(layer, parseResult); + pt::write_ini(path.string(), ptree); + } catch (pt::ini_parser_error &e) { + parseResult.failOutputFile(e.line(), e.message()); + } + + if (parseResult.type() == ParseResult::UNINITIALIZED) { + parseResult.markGood(); + } + + return parseResult; +} + +std::tuple RWConfigParser::layerToString(const RWConfigLayer &layer) const { + ParseResult parseResult("", ""); + + auto treeParser = buildTreeParser(); + + std::string result; + try { + auto ptree = treeParser.to_ptree(layer, parseResult); + std::ostringstream oss; + pt::write_ini(oss, ptree); + result = oss.str(); + } catch (pt::ini_parser_error &e) { + parseResult.failOutputFile(e.line(), e.message()); + } + + if (parseResult.type() == ParseResult::UNINITIALIZED) { + parseResult.markGood(); + } + + return std::make_tuple(result, parseResult); +} + +RWConfigParser::ParseResult::ParseResult(const std::string &source, const std::string &destination) + : m_result(ErrorType::UNINITIALIZED) + , m_inputfilename(source) + , m_outputfilename(destination) { +} + +RWConfigParser::ParseResult::ErrorType RWConfigParser::ParseResult::type() const { + return this->m_result; +} + +bool RWConfigParser::ParseResult::isValid() const { + return this->type() == ErrorType::GOOD; +} + +void RWConfigParser::ParseResult::failInputFile(size_t line, + const std::string &message) { + this->m_result = ParseResult::ErrorType::INVALIDINPUTFILE; + this->m_line = line; + this->m_message = message; +} + +void RWConfigParser::ParseResult::markGood() { + this->m_result = ParseResult::ErrorType::GOOD; +} + +void RWConfigParser::ParseResult::failInvalidData(const std::string &key) { + this->m_result = ParseResult::ErrorType::INVALIDCONTENT; + this->m_keys_invalidData.push_back(key); +} + +void RWConfigParser::ParseResult::failOutputFile(size_t line, + const std::string &message) { + this->m_result = ParseResult::ErrorType::INVALIDOUTPUTFILE; + this->m_line = line; + this->m_message = message; +} + +const std::vector &RWConfigParser::ParseResult::getKeysInvalidData() + const { + return this->m_keys_invalidData; +} + +std::string RWConfigParser::ParseResult::what() const { + std::ostringstream oss; + switch (this->m_result) { + case ErrorType::UNINITIALIZED: + oss << "Parsing was skipped or did not finish."; + break; + case ErrorType::GOOD: + oss << "Parsing completed without errors."; + break; + case ErrorType::INVALIDINPUTFILE: + oss << "Error while reading \"" << this->m_inputfilename + << "\":" << this->m_line << ":\n" + << this->m_message << "."; + break; + case ErrorType::INVALIDOUTPUTFILE: + oss << "Error while writing \"" << this->m_inputfilename + << "\":" << this->m_line << ":\n" + << this->m_message << "."; + break; + case ErrorType::INVALIDCONTENT: + oss << "Error while parsing \"" << this->m_inputfilename << "\"."; + if (!this->m_keys_invalidData.empty()) { + oss << "\nKeys that contain invalid data:"; + for (auto &key : this->m_keys_invalidData) { + oss << "\n - " << key; + } + } + break; + default: + oss << "Unknown error."; + break; + } + if (!this->m_unknownData.empty()) { + oss << "\nUnknown configuration keys:"; + for (const auto &[key, value] : m_unknownData) { + RW_UNUSED(value); + oss << "\n - " << key; + } + } + return oss.str(); +} + +const std::map + &RWConfigParser::ParseResult::getUnknownData() const { + return this->m_unknownData; +} + +void RWConfigParser::ParseResult::setUnknownData( + const std::map &unknownData) { + this->m_unknownData = unknownData; +} diff --git a/rwgame/RWConfig.hpp b/rwgame/RWConfig.hpp new file mode 100644 index 00000000..2d498280 --- /dev/null +++ b/rwgame/RWConfig.hpp @@ -0,0 +1,262 @@ +#ifndef RWGAME_RWCONFIG_HPP +#define RWGAME_RWCONFIG_HPP + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + + +struct RWConfigLayer { +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + std::optional<_RW_TYPE> _RW_NAME; +#define RWARG(...) +#define RWARG_OPT(...) +#include "RWConfig.inc" +#undef RWARG_OPT +#undef RWARG +#undef RWCONFIGARG +}; + +struct RWArgConfigLayer : public RWConfigLayer { +#define RWCONFIGARG(...) +#define RWARG(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + _RW_TYPE _RW_NAME; +#define RWARG_OPT(_RW_TYPE, _RW_NAME, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + std::optional<_RW_TYPE> _RW_NAME; +#include "RWConfig.inc" +#undef RWARG_OPT +#undef RWARG +#undef RWCONFIGARG +}; + +RWConfigLayer buildDefaultConfigLayer(); + +template +class RWConfigLayers { + template + std::optional get(F &&func) const { + for (const auto & layer : layers) { + std::optional optValue = func(layer); + if (optValue.has_value()) { + return optValue; + } + } + return std::nullopt; + } +public: + std::array layers; + template + void setLayer(size_t i, Layer&& layer) { + layers[i] = std::forward(layer); + } +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + _RW_TYPE _RW_NAME() const { \ + return *get<_RW_TYPE>([](auto && l) { return l._RW_NAME;}); \ + } +#define RWARG(...) +#define RWARG_OPT(...) +#include "RWConfig.inc" +#undef RWARG_OPT +#undef RWARG +#undef RWCONFIGARG + std::vector missingKeys() const { + std::vector missing; +#define RWCONFIGARG(_RW_TYPE, _RW_NAME, _RW_DEFAULT, _RW_CONFPATH, _RW_CATEGORY, _RW_ARGMASK, _RW_ARGMETA, _RW_HELP) \ + if (!get<_RW_TYPE>([](auto && l) { return l._RW_NAME;}).has_value()) { \ + missing.push_back(_RW_CONFPATH); \ + } +#define RWARG(...) +#define RWARG_OPT(...) +#include "RWConfig.inc" +#undef RWARG_OPT +#undef RWARG +#undef RWCONFIGARG + return missing; + } +}; + +class RWConfig : public RWConfigLayers<4> { +public: + enum { + LAYER_USER = 0, + LAYER_ARGUMENT = 1, + LAYER_CONFIGFILE = 2, + LAYER_DEFAULT = 3, + }; + std::map unknown; +}; + +class RWArgumentParser { + boost::program_options::options_description _desc; +public: + enum Category { + CONFIG, + GAME, + INPUT, + WINDOW, + DEVELOP, + GENERAL, + COUNT_, + }; + RWArgumentParser(); + RWArgumentParser(const RWArgumentParser& parser) = default; + RWArgumentParser(RWArgumentParser&& parser) = default; + std::ostream &printHelp(std::ostream &os) const; + std::optional parseArguments(int argc, const char* argv[]) const; // FIXME(madebr): change to const char? +}; + +class RWConfigParser { + +public: + class ParseResult { + public: + enum ErrorType { + /// UNINITIALIZED: The config was not initialized + UNINITIALIZED, + /// GOOD: Input file/string was good + GOOD, + /// INVALIDINPUTFILE: There was some error while reading from a file + /// or string or the input was ambiguous (e.g. duplicate keys) + INVALIDINPUTFILE, + /// INVALIDCONTENT: Some required keys were missing or some values + /// were of incorrect type + INVALIDCONTENT, + /// INVALIDOUTPUTFILE: There was some error while writing to a file + /// or string + INVALIDOUTPUTFILE + }; + + /** + * @brief ParseResult Create empty ParseResult + */ + ParseResult() = default; + + private: + /** + * @brief ParseResult holds the issues occurred while parsing of a + * config file. + * @param from Source of the parsing + * @param to Destination of the parsing + */ + ParseResult(const std::string &source, const std::string &destination); + + public: + /** + * @brief type Get the type of error + * @return Type of error or GOOD if there was no error + */ + ErrorType type() const; + + /** + * @brief getKeysInvalidData Get the keys that contained invalid data + * @return A vector with all the keys + */ + const std::vector &getKeysInvalidData() const; + + /** + * @brief Mark this result as valid + */ + void markGood(); + + /** + * @brief failInputFile Fail because the input file was invalid + * @param line Line number where the error is located + * @param message Description of the error + */ + void failInputFile(size_t line, const std::string &message); + + /** + * @brief failInvalidData Fail because a key contains invalid data + * @param key The key that contains invalid data + */ + void failInvalidData(const std::string &key); + + /** + * @brief failOutputFile Fail because an error occurred while while + * writing to the output + * @param line Line number where the error is located + * @param message Description of the error + */ + void failOutputFile(size_t line, const std::string &message); + + /** + * @brief isValid + * @return True if the loaded configuration is valid + */ + bool isValid() const; + + /** + * @brief what Get a string representing the error + * @return String with the error description + */ + std::string what() const; + + + /** + * @brief setUnknownData Replace the the unknown key value pairs + */ + void setUnknownData( + const std::map &unknownData); + + /** + * @brief addUnknownData Get all the unknown key value pairs + * @return Mapping of the unknown keys with associated data + */ + const std::map &getUnknownData() const; + + private: + /// Type of the failure + ErrorType m_result = ErrorType::UNINITIALIZED; + + /// Filename of the input file + std::string m_inputfilename; + + /// Filename of the output file + std::string m_outputfilename; + + /// Line number where the failure occurred (on invalid input or output + /// file) + size_t m_line = 0u; + + /// Description of the failure (on invalid input or output file) + std::string m_message; + + /// All keys that contain invalid data + std::vector m_keys_invalidData; + + // Mapping of unknown keys and associated data + std::map m_unknownData; + + friend class RWConfigParser; + }; + + /** + * @brief RWConfigParser Create a game configuration (initially invalid) + */ + RWConfigParser() = default; + + static rwfs::path getDefaultConfigPath(); + + std::tuple loadFile(const rwfs::path &path) const; + + ParseResult saveFile(const rwfs::path &path, const RWConfigLayer &layer) const; + + ParseResult saveFile(const rwfs::path &path, const RWConfigLayer &layer, const std::map &extra) const; + + /** + * @brief layer_to_string Convert the layer to a INI string string + * @param layer The RWConfigLayer to convert + * @return INI string + */ + std::tuple layerToString(const RWConfigLayer &layer) const; +}; + +#endif // RWGAME_RWCONFIG_HPP diff --git a/rwgame/RWConfig.inc b/rwgame/RWConfig.inc new file mode 100644 index 00000000..17494a91 --- /dev/null +++ b/rwgame/RWConfig.inc @@ -0,0 +1,25 @@ +// RWConfig: Category: WINDOW, INPUT, DEVELOP, GAME, GENERAL + +// RWCONFIGARG: option available in argument parser and configuration file: ALWAYS std::optional +// RWARG_OPT: option only available in argument parser: ALWAYS std::optional +// RWARG: option only available in argument parser: NEVER std::optional + +RWCONFIGARG(std::string, gamedataPath, std::nullopt, "game.path", CONFIG, "gamedata", "PATH", "Path of gamedata") +RWARG_OPT( std::string, configPath, CONFIG, "config,c", "PATH", "Path of configuration file") +RWARG( bool, noconfig, CONFIG, "noconfig", nullptr, "Don't load configuration file") + +RWCONFIGARG(bool, invertY, false, "input.invert_y", INPUT, "invert_y", nullptr, "Invert the y-axis of the mouse") + +RWCONFIGARG(int, width, 800, "window.width", WINDOW, "width,w", "WIDTH", "Game resolution width in pixels") +RWCONFIGARG(int, height, 600, "window.height", WINDOW, "height,h", "HEIGHT", "Game resolution height in pixels") +RWCONFIGARG(bool, fullscreen, false, "window.fullscreen", WINDOW, "fullscreen,f", nullptr, "Enable fullscreen mode") +RWCONFIGARG(float, hudScale, 1.f, "game.hud_scale", WINDOW, "hud_scale", "FACTOR", "Scaling factor of the HUD") + +RWARG( bool, test, DEVELOP, "test,t", nullptr, "Start a new game in a test location") +RWARG_OPT( std::string, benchmarkPath, DEVELOP, "benchmark,b", "PATH", "Run benchmark from file") + +RWARG( bool, newGame, GAME, "newgame,n", nullptr, "Start a new game") +RWARG_OPT( std::string, loadGamePath, GAME, "load,l", "PATH", "Load save file") +RWCONFIGARG(std::string, gameLanguage, "american", "game.language", GAME, "language", "LANGUAGE", "Language") + +RWARG( bool, help, GENERAL, "help", nullptr, "Show this help message") diff --git a/rwgame/RWGame.cpp b/rwgame/RWGame.cpp index e9d2b7d9..905b1f0f 100644 --- a/rwgame/RWGame.cpp +++ b/rwgame/RWGame.cpp @@ -37,26 +37,29 @@ constexpr float kMaxPhysicsSubSteps = 2; #define MOUSE_SENSITIVITY_SCALE 2.5f -RWGame::RWGame(Logger& log, int argc, char* argv[]) - : GameBase(log, argc, argv) - , data(&log, config.getGameDataPath()) +RWGame::RWGame(Logger& log, const std::optional &args) + : GameBase(log, args) + , data(&log, config.gamedataPath()) , renderer(&log, &data) { RW_PROFILE_THREAD("Main"); RW_TIMELINE_ENTER("Startup", MP_YELLOW); - bool newgame = options.count("newgame"); - bool test = options.count("test"); - std::string startSave( - options.count("load") ? options["load"].as() : ""); - std::string benchFile(options.count("benchmark") - ? options["benchmark"].as() - : ""); + bool newgame = false; + bool test = false; + std::optional startSave; + std::optional benchFile; + if (args.has_value()) { + newgame = args->newGame; + test = args->test; + startSave = args->loadGamePath; + benchFile = args->benchmarkPath; + } - log.info("Game", "Game directory: " + config.getGameDataPath().string()); + log.info("Game", "Game directory: " + config.gamedataPath()); - if (!GameData::isValidGameDirectory(config.getGameDataPath())) { + if (!GameData::isValidGameDirectory(config.gamedataPath())) { throw std::runtime_error("Invalid game directory path: " + - config.getGameDataPath().string()); + config.gamedataPath()); } data.load(); @@ -71,18 +74,18 @@ RWGame::RWGame(Logger& log, int argc, char* argv[]) renderer.text.setFontTexture(FONT_PRICEDOWN, "font1"); renderer.text.setFontTexture(FONT_ARIAL, "font2"); - hudDrawer.applyHUDScale(config.getHUDScale()); - renderer.map.scaleHUD(config.getHUDScale()); + hudDrawer.applyHUDScale(config.hudScale()); + renderer.map.scaleHUD(config.hudScale()); debug.setDebugMode(btIDebugDraw::DBG_DrawWireframe | btIDebugDraw::DBG_DrawConstraints | btIDebugDraw::DBG_DrawConstraintLimits); debug.setShaderProgram(renderer.worldProg.get()); - data.loadDynamicObjects((config.getGameDataPath() / "data/object.dat") + data.loadDynamicObjects((rwfs::path{config.gamedataPath()} / "data/object.dat") .string()); // FIXME: use path - data.loadGXT("text/" + config.getGameLanguage() + ".gxt"); + data.loadGXT("text/" + config.gameLanguage() + ".gxt"); getRenderer().water.setWaterTable(data.waterHeights, 48, data.realWater, 128 * 128); @@ -94,14 +97,14 @@ RWGame::RWGame(Logger& log, int argc, char* argv[]) } StateManager::get().enter(this, [=]() { - if (!benchFile.empty()) { - StateManager::get().enter(this, benchFile); + if (benchFile.has_value()) { + StateManager::get().enter(this, *benchFile); } else if (test) { StateManager::get().enter(this, true, "test"); } else if (newgame) { StateManager::get().enter(this, true); - } else if (!startSave.empty()) { - StateManager::get().enter(this, true, startSave); + } else if (startSave.has_value()) { + StateManager::get().enter(this, true, *startSave); } else { StateManager::get().enter(this); } diff --git a/rwgame/RWGame.hpp b/rwgame/RWGame.hpp index 41454e98..b3870c04 100644 --- a/rwgame/RWGame.hpp +++ b/rwgame/RWGame.hpp @@ -1,7 +1,10 @@ #ifndef RWGAME_RWGAME_HPP #define RWGAME_RWGAME_HPP -#include +#include "game.hpp" +#include "GameBase.hpp" +#include "HUDDrawer.hpp" +#include "RWConfig.hpp" #ifdef _MSC_VER #pragma warning(disable : 4305 5033) @@ -20,10 +23,8 @@ #include