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:
parent
c49b4bbd50
commit
8b38fda984
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 §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 "<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;
|
|
||||||
}
|
|
@ -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
575
rwgame/RWConfig.cpp
Normal 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
262
rwgame/RWConfig.hpp
Normal 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
25
rwgame/RWConfig.inc
Normal 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")
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user