1
0
mirror of https://github.com/rwengine/openrw.git synced 2024-11-25 11:52:40 +01:00

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)
This commit is contained in:
Anonymous Maarten 2018-12-12 18:51:04 +01:00
parent c49b4bbd50
commit 8b38fda984
16 changed files with 1257 additions and 1051 deletions

View File

@ -6,12 +6,14 @@ set(RWGAME_SOURCES
main.cpp main.cpp
RWConfig.inc
RWConfig.hpp
RWConfig.cpp
GameBase.hpp GameBase.hpp
GameBase.cpp GameBase.cpp
RWGame.hpp RWGame.hpp
RWGame.cpp RWGame.cpp
GameConfig.hpp
GameConfig.cpp
GameWindow.hpp GameWindow.hpp
GameWindow.cpp GameWindow.cpp
@ -42,7 +44,6 @@ set(RWGAME_SOURCES
states/DebugState.cpp states/DebugState.cpp
states/BenchmarkState.hpp states/BenchmarkState.hpp
states/BenchmarkState.cpp states/BenchmarkState.cpp
) )
add_executable(rwgame add_executable(rwgame

View File

@ -1,100 +1,23 @@
#include "GameBase.hpp" #include "GameBase.hpp"
//#include <rw/filesystem.hpp> #include <rw/debug.hpp>
#include "GitSHA1.h"
#include <iostream>
#include <SDL.h> #include <SDL.h>
#include <rw/debug.hpp>
#include "GitSHA1.h"
#include <iostream> #include <iostream>
// Use first 8 chars of git hash as the build string // Use first 8 chars of git hash as the build string
const std::string kBuildStr(kGitSHA1Hash, 8); const std::string kBuildStr(kGitSHA1Hash, 8);
const std::string kWindowTitle = "RWGame"; 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[]) : GameBase::GameBase(Logger &inlog, const std::optional<RWArgConfigLayer> &args) :
log(inlog) { log(inlog),
config(buildConfig(args)) {
log.info("Game", "Build: " + kBuildStr); log.info("Game", "Build: " + kBuildStr);
size_t w = kWindowWidth, h = kWindowHeight; bool fullscreen = config.fullscreen();
rwfs::path configPath; size_t w = config.width(), h = config.height();
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<size_t>()->value_name("WIDTH"), "Game resolution width in pixel")(
"height,h", po::value<size_t>()->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<std::string>()->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<std::string>()->value_name("PATH"), "Run benchmark from file");
po::options_description desc("Generic options");
desc.add_options()(
"config,c", po::value<rwfs::path>()->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<size_t>();
}
if (vm.count("height")) {
h = vm["height"].as<size_t>();
}
if (vm.count("fullscreen")) {
fullscreen = true;
}
if (vm.count("config")) {
configPath = vm["config"].as<rwfs::path>();
} 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();
}
if (SDL_Init(SDL_INIT_VIDEO) < 0) if (SDL_Init(SDL_INIT_VIDEO) < 0)
throw std::runtime_error("Failed to initialize SDL2!"); throw std::runtime_error("Failed to initialize SDL2!");
@ -105,6 +28,57 @@ GameBase::GameBase(Logger &inlog, int argc, char *argv[]) :
[this]() {window.hideCursor();}); [this]() {window.hideCursor();});
} }
RWConfig GameBase::buildConfig(const std::optional<RWArgConfigLayer> &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() { GameBase::~GameBase() {
SDL_Quit(); SDL_Quit();

View File

@ -1,18 +1,18 @@
#ifndef RWGAME_GAMEBASE_HPP #ifndef RWGAME_GAMEBASE_HPP
#define RWGAME_GAMEBASE_HPP #define RWGAME_GAMEBASE_HPP
#include "GameConfig.hpp"
#include "GameWindow.hpp" #include "GameWindow.hpp"
#include "RWConfig.hpp"
#include <core/Logger.hpp> #include <core/Logger.hpp>
#include <boost/program_options.hpp> #include <map>
/** /**
* @brief Handles basic window and setup * @brief Handles basic window and setup
*/ */
class GameBase { class GameBase {
public: public:
GameBase(Logger& inlog, int argc, char* argv[]); GameBase(Logger& inlog, const std::optional<RWArgConfigLayer> &args);
virtual ~GameBase() = 0; virtual ~GameBase() = 0;
@ -20,15 +20,15 @@ public:
return window; return window;
} }
const GameConfig& getConfig() const { const RWConfig& getConfig() const {
return config; return config;
} }
protected: protected:
RWConfig buildConfig(const std::optional<RWArgConfigLayer> &args);
Logger& log; Logger& log;
GameConfig config{};
GameWindow window{}; GameWindow window{};
boost::program_options::variables_map options{}; RWConfig config{};
}; };
#endif #endif

View File

@ -1,478 +0,0 @@
#include "GameConfig.hpp"
#include <rw/debug.hpp>
#include <rw/filesystem.hpp>
#include <algorithm>
#include <boost/property_tree/ini_parser.hpp>
#include <boost/property_tree/ptree.hpp>
namespace pt = boost::property_tree;
#ifdef RW_WINDOWS
#include <Shlobj.h>
#include <winerror.h>
#include <platform/RWWindows.hpp>
#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<external_type> get_value(const internal_type &str) {
return rwfs::path(str);
}
boost::optional<internal_type> put_value(const external_type &path) {
return path.string();
}
};
struct StringTranslator {
typedef std::string internal_type;
typedef std::string external_type;
boost::optional<external_type> get_value(const internal_type &str) {
return stripComments(str);
}
boost::optional<internal_type> put_value(const external_type &str) {
return str;
}
};
struct BoolTranslator {
typedef std::string internal_type;
typedef bool external_type;
boost::optional<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stoi(stripComments(str)) != 0;
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> 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<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stoi(stripComments(str));
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> put_value(const external_type &i) {
return std::to_string(i);
}
};
struct FloatTranslator {
typedef std::string internal_type;
typedef float external_type;
boost::optional<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stof(stripComments(str));
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> 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<std::string> knownKeys;
auto read_config = [&](const std::string &key, auto &target,
const auto &defaultValue, auto &translator,
bool optional = true) {
typedef typename std::remove_reference<decltype(target)>::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<config_t>(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 &section : 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 "<configuration>";
case ParseType::FILE:
return data;
case ParseType::STRING:
return "<string>";
case ParseType::DEFAULT:
default:
return "<default>";
}
}
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<std::string>
&GameConfig::ParseResult::getKeysRequiredMissing() const {
return this->m_keys_requiredMissing;
}
const std::vector<std::string> &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<std::string, std::string>
&GameConfig::ParseResult::getUnknownData() const {
return this->m_unknownData;
}
void GameConfig::ParseResult::setUnknownData(
const std::map<std::string, std::string> &unknownData) {
this->m_unknownData = unknownData;
}

View File

@ -1,283 +0,0 @@
#ifndef RWGAME_GAMECONFIG_HPP
#define RWGAME_GAMECONFIG_HPP
#include <map>
#include <string>
#include <vector>
#include <rw/filesystem.hpp>
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<std::string> &getKeysRequiredMissing() const;
/**
* @brief getKeysInvalidData Get the keys that contained invalid data
* @return A vector with all the keys
*/
const std::vector<std::string> &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<std::string, std::string> &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<std::string> m_keys_requiredMissing;
/// All keys that contain invalid data
std::vector<std::string> m_keys_invalidData;
// Mapping of unknown keys and associated data
std::map<std::string, std::string> m_unknownData;
/**
* @brief setUnknownData Replace the the unknown key value pairs
*/
void setUnknownData(
const std::map<std::string, std::string> &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

575
rwgame/RWConfig.cpp Normal file
View File

@ -0,0 +1,575 @@
#include "RWConfig.hpp"
#include <iostream>
#include <rw/debug.hpp>
#include <boost/property_tree/ini_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#ifdef RW_WINDOWS
#include <Shlobj.h>
#include <winerror.h>
#include <platform/RWWindows.hpp>
#endif
namespace po = boost::program_options;
namespace pt = boost::property_tree;
namespace {
po::options_description build_options() {
std::array<po::options_description, RWArgumentParser::Category::COUNT_> 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<bool, _RW_TYPE>) { \
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<bool, _RW_TYPE>) { \
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<RWArgConfigLayer> 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<std::string, std::string> &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<std::string, std::string> flatten_ptree(const pt::ptree &ptree) {
std::map<std::string, std::string> 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 <typename T>
struct Translator {
};
template <>
struct Translator<std::string> {
using internal_type = std::string;
using external_type = std::string;
boost::optional<external_type> get_value(const internal_type &str) {
return stripComments(str);
}
boost::optional<internal_type> put_value(const external_type &str) {
return str;
}
};
template <>
struct Translator<bool> {
using internal_type = std::string;
using external_type = bool;
boost::optional<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stoi(stripComments(str)) != 0;
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> put_value(const external_type &b) {
return internal_type(b ? "1" : "0");
}
};
template <>
struct Translator<int> {
using internal_type = std::string;
using external_type = int;
boost::optional<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stoi(stripComments(str));
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> put_value(const external_type &i) {
return std::to_string(i);
}
};
template <>
struct Translator<float> {
using internal_type = std::string;
using external_type = float;
boost::optional<external_type> get_value(const internal_type &str) {
boost::optional<external_type> res;
try {
res = std::stof(stripComments(str));
} catch (std::invalid_argument &) {
}
return res;
}
boost::optional<internal_type> put_value(const external_type &f) {
return std::to_string(f);
}
};
class TreeParser {
template <typename T>
using TGetFunction = std::function<const std::optional<T>(const RWConfigLayer &)>;
template <typename T>
using TSetFunction = std::function<void(RWConfigLayer &, const std::optional<T> &)>;
public:
TreeParser() = default;
template <typename T, typename TString, typename TGetFunc = TGetFunction<T>, typename TSetFunc = TSetFunction<T>>
void add_option(TString &&key, TGetFunc &&getFunc, TSetFunc &&setFunc) {
_itemParsers.emplace_back(
std::make_unique<TreeItemParserImpl<T>>(
std::forward<TString>(key), std::forward<TGetFunc>(getFunc), std::forward<TSetFunc>(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 <typename TString>
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 <typename T>
class TreeItemParserImpl : public TreeItemParser {
public:
template <typename TString>
TreeItemParserImpl(TString &&key, TGetFunction<T> &&getFunction, TSetFunction<T> &&setFunction)
: TreeItemParser(std::forward<TString>(key))
, _getFunction(getFunction)
, _setFunction(setFunction) {
}
~TreeItemParserImpl() override = default;
TGetFunction<T> _getFunction;
TSetFunction<T> _setFunction;
void to_layer(RWConfigLayer &layer, const pt::ptree &ptree) const override {
Translator<T> translator{};
auto value = ptree.get<T>(_key, translator);
auto stl_optional = std::optional<T>(value);
_setFunction(layer, stl_optional);
}
void to_ptree(pt::ptree &ptree, const RWConfigLayer &layer) const override {
Translator<T> translator{};
const auto stl_optional = _getFunction(layer);
if (stl_optional.has_value()) {
T value = *stl_optional;
ptree.put(_key, value, translator);
}
}
};
std::vector<std::unique_ptr<TreeItemParser>> _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<RWConfigLayer, RWConfigParser::ParseResult> RWConfigParser::loadFile(const rwfs::path &path) const {
ParseResult parseResult(path.string(), "<internal>");
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<std::string, std::string> &extra) const {
ParseResult parseResult("<internal>", 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("<internal>", 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<std::string, RWConfigParser::ParseResult> RWConfigParser::layerToString(const RWConfigLayer &layer) const {
ParseResult parseResult("<internal>", "<string>");
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<std::string> &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<std::string, std::string>
&RWConfigParser::ParseResult::getUnknownData() const {
return this->m_unknownData;
}
void RWConfigParser::ParseResult::setUnknownData(
const std::map<std::string, std::string> &unknownData) {
this->m_unknownData = unknownData;
}

262
rwgame/RWConfig.hpp Normal file
View File

@ -0,0 +1,262 @@
#ifndef RWGAME_RWCONFIG_HPP
#define RWGAME_RWCONFIG_HPP
#include <rw/filesystem.hpp>
#include <boost/program_options.hpp>
#include <array>
#include <iosfwd>
#include <map>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
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 <size_t N>
class RWConfigLayers {
template <typename T, typename F>
std::optional<T> get(F &&func) const {
for (const auto & layer : layers) {
std::optional<T> optValue = func(layer);
if (optValue.has_value()) {
return optValue;
}
}
return std::nullopt;
}
public:
std::array<RWConfigLayer, N> layers;
template <typename Layer>
void setLayer(size_t i, Layer&& layer) {
layers[i] = std::forward<Layer>(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<std::string> missingKeys() const {
std::vector<std::string> 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<std::string, std::string> 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<RWArgConfigLayer> 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<std::string> &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<std::string, std::string> &unknownData);
/**
* @brief addUnknownData Get all the unknown key value pairs
* @return Mapping of the unknown keys with associated data
*/
const std::map<std::string, std::string> &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<std::string> m_keys_invalidData;
// Mapping of unknown keys and associated data
std::map<std::string, std::string> m_unknownData;
friend class RWConfigParser;
};
/**
* @brief RWConfigParser Create a game configuration (initially invalid)
*/
RWConfigParser() = default;
static rwfs::path getDefaultConfigPath();
std::tuple<RWConfigLayer, RWConfigParser::ParseResult> 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<std::string, std::string> &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<std::string, RWConfigParser::ParseResult> layerToString(const RWConfigLayer &layer) const;
};
#endif // RWGAME_RWCONFIG_HPP

25
rwgame/RWConfig.inc Normal file
View File

@ -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")

View File

@ -37,26 +37,29 @@ constexpr float kMaxPhysicsSubSteps = 2;
#define MOUSE_SENSITIVITY_SCALE 2.5f #define MOUSE_SENSITIVITY_SCALE 2.5f
RWGame::RWGame(Logger& log, int argc, char* argv[]) RWGame::RWGame(Logger& log, const std::optional<RWArgConfigLayer> &args)
: GameBase(log, argc, argv) : GameBase(log, args)
, data(&log, config.getGameDataPath()) , data(&log, config.gamedataPath())
, renderer(&log, &data) { , renderer(&log, &data) {
RW_PROFILE_THREAD("Main"); RW_PROFILE_THREAD("Main");
RW_TIMELINE_ENTER("Startup", MP_YELLOW); RW_TIMELINE_ENTER("Startup", MP_YELLOW);
bool newgame = options.count("newgame"); bool newgame = false;
bool test = options.count("test"); bool test = false;
std::string startSave( std::optional<std::string> startSave;
options.count("load") ? options["load"].as<std::string>() : ""); std::optional<std::string> benchFile;
std::string benchFile(options.count("benchmark") if (args.has_value()) {
? options["benchmark"].as<std::string>() 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: " + throw std::runtime_error("Invalid game directory path: " +
config.getGameDataPath().string()); config.gamedataPath());
} }
data.load(); data.load();
@ -71,18 +74,18 @@ RWGame::RWGame(Logger& log, int argc, char* argv[])
renderer.text.setFontTexture(FONT_PRICEDOWN, "font1"); renderer.text.setFontTexture(FONT_PRICEDOWN, "font1");
renderer.text.setFontTexture(FONT_ARIAL, "font2"); renderer.text.setFontTexture(FONT_ARIAL, "font2");
hudDrawer.applyHUDScale(config.getHUDScale()); hudDrawer.applyHUDScale(config.hudScale());
renderer.map.scaleHUD(config.getHUDScale()); renderer.map.scaleHUD(config.hudScale());
debug.setDebugMode(btIDebugDraw::DBG_DrawWireframe | debug.setDebugMode(btIDebugDraw::DBG_DrawWireframe |
btIDebugDraw::DBG_DrawConstraints | btIDebugDraw::DBG_DrawConstraints |
btIDebugDraw::DBG_DrawConstraintLimits); btIDebugDraw::DBG_DrawConstraintLimits);
debug.setShaderProgram(renderer.worldProg.get()); 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 .string()); // FIXME: use path
data.loadGXT("text/" + config.getGameLanguage() + ".gxt"); data.loadGXT("text/" + config.gameLanguage() + ".gxt");
getRenderer().water.setWaterTable(data.waterHeights, 48, data.realWater, getRenderer().water.setWaterTable(data.waterHeights, 48, data.realWater,
128 * 128); 128 * 128);
@ -94,14 +97,14 @@ RWGame::RWGame(Logger& log, int argc, char* argv[])
} }
StateManager::get().enter<LoadingState>(this, [=]() { StateManager::get().enter<LoadingState>(this, [=]() {
if (!benchFile.empty()) { if (benchFile.has_value()) {
StateManager::get().enter<BenchmarkState>(this, benchFile); StateManager::get().enter<BenchmarkState>(this, *benchFile);
} else if (test) { } else if (test) {
StateManager::get().enter<IngameState>(this, true, "test"); StateManager::get().enter<IngameState>(this, true, "test");
} else if (newgame) { } else if (newgame) {
StateManager::get().enter<IngameState>(this, true); StateManager::get().enter<IngameState>(this, true);
} else if (!startSave.empty()) { } else if (startSave.has_value()) {
StateManager::get().enter<IngameState>(this, true, startSave); StateManager::get().enter<IngameState>(this, true, *startSave);
} else { } else {
StateManager::get().enter<MenuState>(this); StateManager::get().enter<MenuState>(this);
} }

View File

@ -1,7 +1,10 @@
#ifndef RWGAME_RWGAME_HPP #ifndef RWGAME_RWGAME_HPP
#define RWGAME_RWGAME_HPP #define RWGAME_RWGAME_HPP
#include <chrono> #include "game.hpp"
#include "GameBase.hpp"
#include "HUDDrawer.hpp"
#include "RWConfig.hpp"
#ifdef _MSC_VER #ifdef _MSC_VER
#pragma warning(disable : 4305 5033) #pragma warning(disable : 4305 5033)
@ -20,10 +23,8 @@
#include <script/SCMFile.hpp> #include <script/SCMFile.hpp>
#include <script/ScriptMachine.hpp> #include <script/ScriptMachine.hpp>
#include <script/modules/GTA3Module.hpp> #include <script/modules/GTA3Module.hpp>
#include "game.hpp"
#include "GameBase.hpp" #include <chrono>
#include "HUDDrawer.hpp"
class PlayerController; class PlayerController;
@ -57,7 +58,7 @@ class RWGame final : public GameBase {
std::string cheatInputWindow = std::string(32, ' '); std::string cheatInputWindow = std::string(32, ' ');
public: public:
RWGame(Logger& log, int argc, char* argv[]); RWGame(Logger& log, const std::optional<RWArgConfigLayer> &args);
~RWGame() override; ~RWGame() override;
int run(); int run();

View File

@ -5,21 +5,30 @@
#include <core/Logger.hpp> #include <core/Logger.hpp>
int main(int argc, char* argv[]) { #include "RWConfig.hpp"
SDL_SetMainReady();
int main(int argc, const char* argv[]) {
// Initialise Logging before anything else happens // Initialise Logging before anything else happens
StdOutReceiver logstdout; StdOutReceiver logstdout;
Logger logger({ &logstdout }); Logger logger({ &logstdout });
RWArgumentParser argParser;
auto argLayerOpt = argParser.parseArguments(argc, argv);
if (!argLayerOpt.has_value()) {
argParser.printHelp(std::cerr);
return 1;
}
if (argLayerOpt->help) {
argParser.printHelp(std::cout);
return 0;
}
SDL_SetMainReady();
try { try {
RWGame game(logger, argc, argv); RWGame game(logger, argLayerOpt);
return game.run(); return game.run();
} catch (std::invalid_argument&) {
// This exception is thrown when either an invalid command line option
// or a --help is found. The RWGame constructor prints a usage message
// in this case and then throws this exception.
return -2;
} catch (std::runtime_error& ex) { } catch (std::runtime_error& ex) {
// Catch runtime_error as these are fatal issues the user may want to // Catch runtime_error as these are fatal issues the user may want to
// know about like corrupted files or GL initialisation failure. // know about like corrupted files or GL initialisation failure.

View File

@ -365,7 +365,7 @@ Menu DebugState::createMissionsMenu() {
} }
DebugState::DebugState(RWGame* game, const glm::vec3& vp, const glm::quat& vd) DebugState::DebugState(RWGame* game, const glm::vec3& vp, const glm::quat& vd)
: State(game), _invertedY(game->getConfig().getInputInvertY()) { : State(game), _invertedY(game->getConfig().invertY()) {
this->setNextMenu(createDebugMenu()); this->setNextMenu(createDebugMenu());
_debugCam.position = vp; _debugCam.position = vp;

View File

@ -42,7 +42,7 @@ IngameState::IngameState(RWGame* game, bool newgame, const std::string& save)
: State(game) : State(game)
, save(save) , save(save)
, newgame(newgame) , newgame(newgame)
, m_invertedY(game->getConfig().getInputInvertY()) { , m_invertedY(game->getConfig().invertY()) {
} }
void IngameState::startTest() { void IngameState::startTest() {

View File

@ -44,7 +44,7 @@ set(TEST_SOURCES
test_Globals.hpp test_Globals.hpp
# Hack in rwgame sources until there's a per-target test suite # Hack in rwgame sources until there's a per-target test suite
"${PROJECT_SOURCE_DIR}/rwgame/GameConfig.cpp" "${PROJECT_SOURCE_DIR}/rwgame/RWConfig.cpp"
"${PROJECT_SOURCE_DIR}/rwgame/GameWindow.cpp" "${PROJECT_SOURCE_DIR}/rwgame/GameWindow.cpp"
"${PROJECT_SOURCE_DIR}/rwgame/GameInput.cpp" "${PROJECT_SOURCE_DIR}/rwgame/GameInput.cpp"
) )
@ -71,6 +71,7 @@ target_include_directories(rwtests
target_link_libraries(rwtests target_link_libraries(rwtests
PRIVATE PRIVATE
Boost::unit_test_framework Boost::unit_test_framework
Boost::program_options
rwengine rwengine
SDL2::SDL2 SDL2::SDL2
Boost::filesystem Boost::filesystem

View File

@ -1,4 +1,4 @@
#include <GameConfig.hpp> #include <RWConfig.hpp>
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
@ -269,36 +269,38 @@ BOOST_AUTO_TEST_CASE(test_TempFile) {
BOOST_CHECK(!rwfs::exists(path)); BOOST_CHECK(!rwfs::exists(path));
} }
BOOST_AUTO_TEST_CASE(test_config_initial) { BOOST_AUTO_TEST_CASE(test_configParser_initial) {
// Test an initial config // Test an initial config
GameConfig cfg; [[maybe_unused]] RWConfigParser cfgParser;
BOOST_CHECK(!cfg.isValid());
} }
BOOST_AUTO_TEST_CASE(test_config_valid) { BOOST_AUTO_TEST_CASE(test_configParser_valid) {
// Test reading a valid configuration file // Test reading a valid configuration file
auto cfg = getValidConfig(); auto cfg = getValidConfig();
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(config.isValid()); BOOST_CHECK(parseResult.isValid());
BOOST_CHECK_EQUAL(config.getParseResult().type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::GOOD); RWConfigParser::ParseResult::GOOD);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(), BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
0);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
BOOST_CHECK_EQUAL(config.getGameDataPath().string(), "/dev/test"); BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
BOOST_CHECK_EQUAL(config.getGameLanguage(), "american"); BOOST_REQUIRE(cfgLayer.gameLanguage.has_value());
BOOST_CHECK(config.getInputInvertY()); BOOST_REQUIRE(cfgLayer.invertY.has_value());
BOOST_CHECK_EQUAL(config.getHUDScale(), 2.f); BOOST_REQUIRE(cfgLayer.hudScale.has_value());
BOOST_CHECK_EQUAL(*cfgLayer.gamedataPath, "/dev/test");
BOOST_CHECK_EQUAL(*cfgLayer.gameLanguage, "american");
BOOST_CHECK(*cfgLayer.invertY);
BOOST_CHECK_EQUAL(*cfgLayer.hudScale, 2.f);
} }
BOOST_AUTO_TEST_CASE(test_config_valid_modified) { BOOST_AUTO_TEST_CASE(test_configParser_valid_modified) {
// Test reading a valid modified configuration file // Test reading a valid modified configuration file
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["game"]["path"] = "Liberty City"; cfg["game"]["path"] = "Liberty City";
@ -307,21 +309,21 @@ BOOST_AUTO_TEST_CASE(test_config_valid_modified) {
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(config.isValid()); BOOST_CHECK(parseResult.isValid());
BOOST_CHECK_EQUAL(config.getParseResult().type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::GOOD); RWConfigParser::ParseResult::GOOD);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(), BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
0);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
BOOST_CHECK(!config.getInputInvertY()); BOOST_REQUIRE(cfgLayer.invertY.has_value());
BOOST_CHECK_EQUAL(config.getGameDataPath().string(), "Liberty City"); BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
BOOST_CHECK(!*cfgLayer.invertY);
BOOST_CHECK_EQUAL(*cfgLayer.gamedataPath, "Liberty City");
} }
BOOST_AUTO_TEST_CASE(test_config_save) { BOOST_AUTO_TEST_CASE(test_configParser_save) {
// Test saving a configuration file // Test saving a configuration file
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["game"]["path"] = "Liberty City"; cfg["game"]["path"] = "Liberty City";
@ -329,41 +331,61 @@ BOOST_AUTO_TEST_CASE(test_config_save) {
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; {
config.loadFile(tempFile.path()); RWConfigLayer cfgLayer;
{
BOOST_CHECK(config.isValid()); RWConfigParser cfgParser;
RWConfigParser::ParseResult parseResult;
std::tie(cfgLayer, parseResult) = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(parseResult.isValid());
BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
}
tempFile.remove(); tempFile.remove();
BOOST_CHECK(!tempFile.exists()); BOOST_CHECK(!tempFile.exists());
auto writeResult = config.saveConfig(); {
BOOST_CHECK(writeResult.isValid()); RWConfigParser cfgParser;
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer);
BOOST_CHECK(parseResult.isValid());
BOOST_CHECK(tempFile.exists()); BOOST_CHECK(tempFile.exists());
}
}
GameConfig config2; {
config2.loadFile(tempFile.path()); RWConfigParser cfgParser;
BOOST_CHECK_EQUAL(config2.getGameDataPath().string(), "Liberty City"); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(parseResult.isValid());
BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
BOOST_CHECK_EQUAL(*cfgLayer.gamedataPath, "Liberty City");
}
simpleConfig_t cfg2 = readConfig(tempFile.path()); simpleConfig_t cfg2 = readConfig(tempFile.path());
BOOST_CHECK_EQUAL(cfg2["game"]["path"], "Liberty City"); BOOST_CHECK_EQUAL(cfg2["game"]["path"], "Liberty City");
} }
BOOST_AUTO_TEST_CASE(test_config_valid_unknown_keys) { BOOST_AUTO_TEST_CASE(test_configParser_valid_unknown_keys) {
// Test reading a valid modified configuration file with unknown data // Test reading a valid modified configuration file with unknown data
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["game"]["unknownkey"] = "descartes"; cfg["game"]["unknownkey"] = "descartes";
cfg["dontknow"]["dontcare"] = "\t$%!$8847 %%$ "; cfg["dontknow"]["dontcare"] = "\t$%!$8847 %%$ ";
std::map<std::string, std::string> globalUnknownData;
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; {
config.loadFile(tempFile.path()); RWConfigParser cfgParser;
RWConfigLayer cfgLayer;
{
RWConfigParser::ParseResult parseResult;
std::tie(cfgLayer, parseResult) = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(config.isValid()); BOOST_CHECK(parseResult.isValid());
const auto &unknownData = config.getParseResult().getUnknownData(); const auto &unknownData = parseResult.getUnknownData();
BOOST_CHECK_EQUAL(unknownData.size(), 2); BOOST_CHECK_EQUAL(unknownData.size(), 2);
@ -376,28 +398,48 @@ BOOST_AUTO_TEST_CASE(test_config_valid_unknown_keys) {
stripWhitespace(cfg["dontknow"]["dontcare"])); stripWhitespace(cfg["dontknow"]["dontcare"]));
BOOST_CHECK_EQUAL(unknownData.count("game.path"), 0); BOOST_CHECK_EQUAL(unknownData.count("game.path"), 0);
globalUnknownData = unknownData;
tempFile.remove();
config.saveConfig();
GameConfig config2;
config2.loadFile(tempFile.path());
const auto &unknownData2 = config2.getParseResult().getUnknownData();
BOOST_CHECK_EQUAL(unknownData2.size(), 2);
BOOST_CHECK_EQUAL(unknownData2.count("game.unknownkey"), 1);
BOOST_CHECK_EQUAL(unknownData2.at("game.unknownkey"),
stripWhitespace(cfg["game"]["unknownkey"]));
BOOST_CHECK_EQUAL(unknownData2.count("dontknow.dontcare"), 1);
BOOST_CHECK_EQUAL(unknownData2.at("dontknow.dontcare"),
stripWhitespace(cfg["dontknow"]["dontcare"]));
BOOST_CHECK_EQUAL(unknownData2.count("game.path"), 0);
} }
BOOST_AUTO_TEST_CASE(test_config_save_readonly) { tempFile.remove();
{
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer, globalUnknownData);
BOOST_CHECK(parseResult.isValid());
}
}
{
RWConfigParser cfgParser;
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(parseResult.isValid());
const auto &unknownData = parseResult.getUnknownData();
BOOST_REQUIRE_EQUAL(unknownData.size(), 2);
BOOST_CHECK_EQUAL(unknownData.count("game.unknownkey"), 1);
BOOST_CHECK_EQUAL(unknownData.at("game.unknownkey"),
stripWhitespace(cfg["game"]["unknownkey"]));
BOOST_CHECK_EQUAL(unknownData.count("dontknow.dontcare"), 1);
BOOST_CHECK_EQUAL(unknownData.at("dontknow.dontcare"),
stripWhitespace(cfg["dontknow"]["dontcare"]));
BOOST_CHECK_EQUAL(unknownData.count("game.path"), 0);
}
}
BOOST_AUTO_TEST_CASE(test_configParser_valid_empty_file) {
// An empty config file is valid
TempFile tempFile;
tempFile.touch();
RWConfigParser cfgParser;
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(parseResult.isValid());
}
BOOST_AUTO_TEST_CASE(test_configParser_save_readonly) {
// Test whether saving to a readonly INI file fails // Test whether saving to a readonly INI file fails
auto cfg = getValidConfig(); auto cfg = getValidConfig();
@ -405,34 +447,48 @@ BOOST_AUTO_TEST_CASE(test_config_save_readonly) {
tempFile.append(cfg); tempFile.append(cfg);
tempFile.change_perms_readonly(); tempFile.change_perms_readonly();
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); RWConfigLayer cfgLayer;
BOOST_CHECK(config.isValid()); {
RWConfigParser::ParseResult parseResult;
auto writeResult = config.saveConfig(); std::tie(cfgLayer, parseResult) = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!writeResult.isValid()); BOOST_CHECK(parseResult.isValid());
BOOST_CHECK_EQUAL(writeResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDOUTPUTFILE);
} }
BOOST_AUTO_TEST_CASE(test_config_valid_default) { {
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer);
BOOST_CHECK(!parseResult.isValid());
BOOST_CHECK_EQUAL(parseResult.type(),
RWConfigParser::ParseResult::INVALIDOUTPUTFILE);
}
}
BOOST_AUTO_TEST_CASE(test_configParser_valid_default) {
// Test whether the default INI string is valid // Test whether the default INI string is valid
TempFile tempFile; TempFile tempFile;
BOOST_CHECK(!tempFile.exists()); BOOST_CHECK(!tempFile.exists());
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); {
BOOST_CHECK(!config.isValid()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!parseResult.isValid());
auto defaultINI = config.getDefaultINIString();
tempFile.append(defaultINI);
BOOST_CHECK(tempFile.exists());
config.loadFile(tempFile.path());
BOOST_CHECK(config.isValid());
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_emptykey) { {
auto defaultLayer = buildDefaultConfigLayer();
auto parseResult = cfgParser.saveFile(tempFile.path(), defaultLayer);
BOOST_CHECK(parseResult.isValid());
}
BOOST_CHECK(tempFile.exists());
{
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(parseResult.isValid());
}
}
BOOST_AUTO_TEST_CASE(test_configParser_invalid_emptykey) {
// Test duplicate keys in invalid configuration file // Test duplicate keys in invalid configuration file
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["game"][""] = "0"; cfg["game"][""] = "0";
@ -440,16 +496,15 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_emptykey) {
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!parseResult.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDINPUTFILE); RWConfigParser::ParseResult::INVALIDINPUTFILE);
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_duplicate) { BOOST_AUTO_TEST_CASE(test_configParser_invalid_duplicate) {
// Test duplicate keys in invalid configuration file // Test duplicate keys in invalid configuration file
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["input"]["invert_y "] = "0"; cfg["input"]["invert_y "] = "0";
@ -457,39 +512,15 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_duplicate) {
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!parseResult.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDINPUTFILE); RWConfigParser::ParseResult::INVALIDINPUTFILE);
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_required_missing) { BOOST_AUTO_TEST_CASE(test_configParser_invalid_wrong_type) {
// Test missing required keys in invalid configuration file
auto cfg = getValidConfig();
cfg["game"].erase("path");
TempFile tempFile;
tempFile.append(cfg);
GameConfig config;
config.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDCONTENT);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing().size(), 1);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing()[0], "game.path");
}
BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) {
// Test wrong data type // Test wrong data type
auto cfg = getValidConfig(); auto cfg = getValidConfig();
cfg["input"]["invert_y"] = "d"; cfg["input"]["invert_y"] = "d";
@ -497,41 +528,20 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) {
TempFile tempFile; TempFile tempFile;
tempFile.append(cfg); tempFile.append(cfg);
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!parseResult.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDCONTENT); RWConfigParser::ParseResult::INVALIDCONTENT);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing().size(), 0); BOOST_REQUIRE_EQUAL(parseResult.getKeysInvalidData().size(), 1);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 1);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData()[0], "input.invert_y"); BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData()[0], "input.invert_y");
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_empty) { BOOST_AUTO_TEST_CASE(test_configParser_invalid_nodir) {
// Test reading empty configuration file
// An empty file has a valid data structure, but has missing keys and is
// thus invalid.
TempFile tempFile;
tempFile.touch();
BOOST_CHECK(tempFile.exists());
GameConfig config;
config.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDCONTENT);
BOOST_CHECK_GE(parseResult.getKeysRequiredMissing().size(), 1);
}
BOOST_AUTO_TEST_CASE(test_config_invalid_nodir) {
// Test reading non-existing configuration file in non-existing directory // Test reading non-existing configuration file in non-existing directory
TempDir tempDir; TempDir tempDir;
TempFile tempFile(tempDir); TempFile tempFile(tempDir);
@ -539,29 +549,133 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_nodir) {
BOOST_CHECK(!tempDir.exists()); BOOST_CHECK(!tempDir.exists());
BOOST_CHECK(!tempFile.exists()); BOOST_CHECK(!tempFile.exists());
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!parseResult.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDINPUTFILE); RWConfigParser::ParseResult::INVALIDINPUTFILE);
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_nonexisting) { BOOST_AUTO_TEST_CASE(test_configParser_invalid_nonexisting) {
// Test reading non-existing configuration file // Test reading non-existing configuration file
TempFile tempFile; TempFile tempFile;
BOOST_CHECK(!tempFile.exists()); BOOST_CHECK(!tempFile.exists());
GameConfig config; RWConfigParser cfgParser;
config.loadFile(tempFile.path()); auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!parseResult.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), BOOST_CHECK_EQUAL(parseResult.type(),
GameConfig::ParseResult::ErrorType::INVALIDINPUTFILE); RWConfigParser::ParseResult::INVALIDINPUTFILE);
}
BOOST_AUTO_TEST_CASE(test_argParser_nullptr) {
RWArgumentParser argParser;
argParser.parseArguments(0, nullptr);
}
BOOST_AUTO_TEST_CASE(test_argParser_one) {
RWArgumentParser argParser;
const char *args[] = {""};
argParser.parseArguments(1, args);
}
BOOST_AUTO_TEST_CASE(test_argParser_optional_nonexisting) {
RWArgumentParser argParser;
const char *args[] = {"", "--nonexistingoptional"};
auto optLayer = argParser.parseArguments(2, args);
BOOST_CHECK(!optLayer.has_value());
}
BOOST_AUTO_TEST_CASE(test_argParser_positional_nonexisting) {
RWArgumentParser argParser;
const char *args[] = {"", "nonexistingpositional"};
auto optLayer = argParser.parseArguments(2, args);
BOOST_CHECK(!optLayer.has_value());
}
BOOST_AUTO_TEST_CASE(test_argParser_bool) {
RWArgumentParser argParser;
const char *args[] = {"", "--help"};
auto optLayer = argParser.parseArguments(2, args);
BOOST_REQUIRE(optLayer.has_value());
BOOST_CHECK(optLayer->help);
}
BOOST_AUTO_TEST_CASE(test_argParser_string) {
RWArgumentParser argParser;
{
const auto path = "/some/path";
const char *args[] = {"", "-c", path};
auto optLayer = argParser.parseArguments(3, args);
BOOST_REQUIRE(optLayer.has_value());
BOOST_REQUIRE(optLayer->configPath.has_value());
BOOST_CHECK_EQUAL(*optLayer->configPath, path);
}
{
const auto path = "/some/path";
const char *args[] = {"", "-b", path};
auto optLayer = argParser.parseArguments(3, args);
BOOST_REQUIRE(optLayer.has_value());
BOOST_REQUIRE(optLayer->benchmarkPath.has_value());
BOOST_CHECK_EQUAL(*optLayer->benchmarkPath, path);
}
}
BOOST_AUTO_TEST_CASE(test_argParser_int) {
RWArgumentParser argParser;
const int width = 1920;
const auto widthStr = std::to_string(width);
const char *args[] = {"", "-w", widthStr.c_str()};
auto optLayer = argParser.parseArguments(3, args);
BOOST_REQUIRE(optLayer.has_value());
BOOST_REQUIRE(optLayer->width.has_value());
BOOST_CHECK_EQUAL(*optLayer->width, width);
}
BOOST_AUTO_TEST_CASE(test_argParser_int_invalid) {
RWArgumentParser argParser;
const auto widthStr = "1920d";
const char *args[] = {"", "-w", widthStr};
auto optLayer = argParser.parseArguments(3, args);
BOOST_CHECK(!optLayer.has_value());
}
BOOST_AUTO_TEST_CASE(test_rwconfig_initial) {
RWConfig config;
auto missingKeys = config.missingKeys();
BOOST_CHECK_NE(missingKeys.size(), 0u);
}
BOOST_AUTO_TEST_CASE(test_rwconfig_defaultLayer) {
auto defaultLayer = buildDefaultConfigLayer();
RWConfig config;
config.setLayer(RWConfig::LAYER_DEFAULT, defaultLayer);
BOOST_CHECK_NE(config.missingKeys().size(), 0u);
BOOST_CHECK_EQUAL(config.missingKeys().size(), 1u);
defaultLayer.gamedataPath = "/path/to/gamedata";
config.setLayer(RWConfig::LAYER_DEFAULT, defaultLayer);
BOOST_REQUIRE(config.layers[RWConfig::LAYER_DEFAULT].gamedataPath.has_value());
BOOST_CHECK_EQUAL(*config.layers[RWConfig::LAYER_DEFAULT].gamedataPath, "/path/to/gamedata");
BOOST_CHECK_EQUAL(config.gamedataPath(), "/path/to/gamedata");
BOOST_CHECK_EQUAL(config.missingKeys().size(), 0u);
config.layers[RWConfig::LAYER_USER].gamedataPath = "/some/other/path/to/gamedata";
BOOST_REQUIRE(config.layers[RWConfig::LAYER_DEFAULT].gamedataPath.has_value());
BOOST_CHECK_EQUAL(*config.layers[RWConfig::LAYER_DEFAULT].gamedataPath, "/path/to/gamedata");
BOOST_REQUIRE(config.layers[RWConfig::LAYER_USER].gamedataPath.has_value());
BOOST_CHECK_EQUAL(*config.layers[RWConfig::LAYER_USER].gamedataPath, "/some/other/path/to/gamedata");
BOOST_CHECK_EQUAL(config.gamedataPath(), "/some/other/path/to/gamedata");
} }
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()

View File

@ -1,11 +1,13 @@
#include "test_Globals.hpp" #include "test_Globals.hpp"
#include <GameConfig.hpp> #include <RWConfig.hpp>
#if RW_TEST_WITH_DATA #if RW_TEST_WITH_DATA
std::string Global::getGamePath() { std::string Global::getGamePath() {
GameConfig config; rwfs::path configPath = RWConfigParser::getDefaultConfigPath() / "openrw.ini";
config.loadFile(GameConfig::getDefaultConfigPath() / "openrw.ini"); RWConfigParser cfgParser;
return config.getGameDataPath().string(); //FIXME: use path auto [cfgLayer, parseResult] = cfgParser.loadFile(configPath);
BOOST_REQUIRE(parseResult.isValid());
return *cfgLayer.gamedataPath;
} }
#endif #endif