mirror of
https://github.com/rwengine/openrw.git
synced 2024-11-21 18:02:43 +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
|
||||
|
||||
RWConfig.inc
|
||||
RWConfig.hpp
|
||||
RWConfig.cpp
|
||||
|
||||
GameBase.hpp
|
||||
GameBase.cpp
|
||||
RWGame.hpp
|
||||
RWGame.cpp
|
||||
GameConfig.hpp
|
||||
GameConfig.cpp
|
||||
GameWindow.hpp
|
||||
GameWindow.cpp
|
||||
|
||||
@ -42,7 +44,6 @@ set(RWGAME_SOURCES
|
||||
states/DebugState.cpp
|
||||
states/BenchmarkState.hpp
|
||||
states/BenchmarkState.cpp
|
||||
|
||||
)
|
||||
|
||||
add_executable(rwgame
|
||||
|
@ -1,100 +1,23 @@
|
||||
#include "GameBase.hpp"
|
||||
|
||||
//#include <rw/filesystem.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <rw/debug.hpp>
|
||||
#include "GitSHA1.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <rw/debug.hpp>
|
||||
|
||||
#include "GitSHA1.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
// Use first 8 chars of git hash as the build string
|
||||
const std::string kBuildStr(kGitSHA1Hash, 8);
|
||||
const std::string kWindowTitle = "RWGame";
|
||||
const std::string kDefaultConfigFileName = "openrw.ini";
|
||||
constexpr int kWindowWidth = 800;
|
||||
constexpr int kWindowHeight = 600;
|
||||
|
||||
GameBase::GameBase(Logger &inlog, int argc, char *argv[]) :
|
||||
log(inlog) {
|
||||
GameBase::GameBase(Logger &inlog, const std::optional<RWArgConfigLayer> &args) :
|
||||
log(inlog),
|
||||
config(buildConfig(args)) {
|
||||
log.info("Game", "Build: " + kBuildStr);
|
||||
|
||||
size_t w = kWindowWidth, h = kWindowHeight;
|
||||
rwfs::path configPath;
|
||||
bool fullscreen = false;
|
||||
bool help = false;
|
||||
|
||||
// Define and parse command line options
|
||||
namespace po = boost::program_options;
|
||||
po::options_description desc_window("Window options");
|
||||
desc_window.add_options()(
|
||||
"width,w", po::value<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();
|
||||
}
|
||||
bool fullscreen = config.fullscreen();
|
||||
size_t w = config.width(), h = config.height();
|
||||
|
||||
if (SDL_Init(SDL_INIT_VIDEO) < 0)
|
||||
throw std::runtime_error("Failed to initialize SDL2!");
|
||||
@ -105,6 +28,57 @@ GameBase::GameBase(Logger &inlog, int argc, char *argv[]) :
|
||||
[this]() {window.hideCursor();});
|
||||
}
|
||||
|
||||
RWConfig GameBase::buildConfig(const std::optional<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() {
|
||||
SDL_Quit();
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
#ifndef RWGAME_GAMEBASE_HPP
|
||||
#define RWGAME_GAMEBASE_HPP
|
||||
#include "GameConfig.hpp"
|
||||
#include "GameWindow.hpp"
|
||||
#include "RWConfig.hpp"
|
||||
|
||||
#include <core/Logger.hpp>
|
||||
|
||||
#include <boost/program_options.hpp>
|
||||
#include <map>
|
||||
|
||||
/**
|
||||
* @brief Handles basic window and setup
|
||||
*/
|
||||
class GameBase {
|
||||
public:
|
||||
GameBase(Logger& inlog, int argc, char* argv[]);
|
||||
GameBase(Logger& inlog, const std::optional<RWArgConfigLayer> &args);
|
||||
|
||||
virtual ~GameBase() = 0;
|
||||
|
||||
@ -20,15 +20,15 @@ public:
|
||||
return window;
|
||||
}
|
||||
|
||||
const GameConfig& getConfig() const {
|
||||
const RWConfig& getConfig() const {
|
||||
return config;
|
||||
}
|
||||
|
||||
protected:
|
||||
RWConfig buildConfig(const std::optional<RWArgConfigLayer> &args);
|
||||
Logger& log;
|
||||
GameConfig config{};
|
||||
GameWindow window{};
|
||||
boost::program_options::variables_map options{};
|
||||
RWConfig config{};
|
||||
};
|
||||
|
||||
#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
|
||||
|
||||
RWGame::RWGame(Logger& log, int argc, char* argv[])
|
||||
: GameBase(log, argc, argv)
|
||||
, data(&log, config.getGameDataPath())
|
||||
RWGame::RWGame(Logger& log, const std::optional<RWArgConfigLayer> &args)
|
||||
: GameBase(log, args)
|
||||
, data(&log, config.gamedataPath())
|
||||
, renderer(&log, &data) {
|
||||
RW_PROFILE_THREAD("Main");
|
||||
RW_TIMELINE_ENTER("Startup", MP_YELLOW);
|
||||
|
||||
bool newgame = options.count("newgame");
|
||||
bool test = options.count("test");
|
||||
std::string startSave(
|
||||
options.count("load") ? options["load"].as<std::string>() : "");
|
||||
std::string benchFile(options.count("benchmark")
|
||||
? options["benchmark"].as<std::string>()
|
||||
: "");
|
||||
bool newgame = false;
|
||||
bool test = false;
|
||||
std::optional<std::string> startSave;
|
||||
std::optional<std::string> benchFile;
|
||||
if (args.has_value()) {
|
||||
newgame = args->newGame;
|
||||
test = args->test;
|
||||
startSave = args->loadGamePath;
|
||||
benchFile = args->benchmarkPath;
|
||||
}
|
||||
|
||||
log.info("Game", "Game directory: " + config.getGameDataPath().string());
|
||||
log.info("Game", "Game directory: " + config.gamedataPath());
|
||||
|
||||
if (!GameData::isValidGameDirectory(config.getGameDataPath())) {
|
||||
if (!GameData::isValidGameDirectory(config.gamedataPath())) {
|
||||
throw std::runtime_error("Invalid game directory path: " +
|
||||
config.getGameDataPath().string());
|
||||
config.gamedataPath());
|
||||
}
|
||||
|
||||
data.load();
|
||||
@ -71,18 +74,18 @@ RWGame::RWGame(Logger& log, int argc, char* argv[])
|
||||
renderer.text.setFontTexture(FONT_PRICEDOWN, "font1");
|
||||
renderer.text.setFontTexture(FONT_ARIAL, "font2");
|
||||
|
||||
hudDrawer.applyHUDScale(config.getHUDScale());
|
||||
renderer.map.scaleHUD(config.getHUDScale());
|
||||
hudDrawer.applyHUDScale(config.hudScale());
|
||||
renderer.map.scaleHUD(config.hudScale());
|
||||
|
||||
debug.setDebugMode(btIDebugDraw::DBG_DrawWireframe |
|
||||
btIDebugDraw::DBG_DrawConstraints |
|
||||
btIDebugDraw::DBG_DrawConstraintLimits);
|
||||
debug.setShaderProgram(renderer.worldProg.get());
|
||||
|
||||
data.loadDynamicObjects((config.getGameDataPath() / "data/object.dat")
|
||||
data.loadDynamicObjects((rwfs::path{config.gamedataPath()} / "data/object.dat")
|
||||
.string()); // FIXME: use path
|
||||
|
||||
data.loadGXT("text/" + config.getGameLanguage() + ".gxt");
|
||||
data.loadGXT("text/" + config.gameLanguage() + ".gxt");
|
||||
|
||||
getRenderer().water.setWaterTable(data.waterHeights, 48, data.realWater,
|
||||
128 * 128);
|
||||
@ -94,14 +97,14 @@ RWGame::RWGame(Logger& log, int argc, char* argv[])
|
||||
}
|
||||
|
||||
StateManager::get().enter<LoadingState>(this, [=]() {
|
||||
if (!benchFile.empty()) {
|
||||
StateManager::get().enter<BenchmarkState>(this, benchFile);
|
||||
if (benchFile.has_value()) {
|
||||
StateManager::get().enter<BenchmarkState>(this, *benchFile);
|
||||
} else if (test) {
|
||||
StateManager::get().enter<IngameState>(this, true, "test");
|
||||
} else if (newgame) {
|
||||
StateManager::get().enter<IngameState>(this, true);
|
||||
} else if (!startSave.empty()) {
|
||||
StateManager::get().enter<IngameState>(this, true, startSave);
|
||||
} else if (startSave.has_value()) {
|
||||
StateManager::get().enter<IngameState>(this, true, *startSave);
|
||||
} else {
|
||||
StateManager::get().enter<MenuState>(this);
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
#ifndef RWGAME_RWGAME_HPP
|
||||
#define RWGAME_RWGAME_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include "game.hpp"
|
||||
#include "GameBase.hpp"
|
||||
#include "HUDDrawer.hpp"
|
||||
#include "RWConfig.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(disable : 4305 5033)
|
||||
@ -20,10 +23,8 @@
|
||||
#include <script/SCMFile.hpp>
|
||||
#include <script/ScriptMachine.hpp>
|
||||
#include <script/modules/GTA3Module.hpp>
|
||||
#include "game.hpp"
|
||||
|
||||
#include "GameBase.hpp"
|
||||
#include "HUDDrawer.hpp"
|
||||
#include <chrono>
|
||||
|
||||
class PlayerController;
|
||||
|
||||
@ -57,7 +58,7 @@ class RWGame final : public GameBase {
|
||||
std::string cheatInputWindow = std::string(32, ' ');
|
||||
|
||||
public:
|
||||
RWGame(Logger& log, int argc, char* argv[]);
|
||||
RWGame(Logger& log, const std::optional<RWArgConfigLayer> &args);
|
||||
~RWGame() override;
|
||||
|
||||
int run();
|
||||
|
@ -5,21 +5,30 @@
|
||||
|
||||
#include <core/Logger.hpp>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
SDL_SetMainReady();
|
||||
#include "RWConfig.hpp"
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
// Initialise Logging before anything else happens
|
||||
StdOutReceiver 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 {
|
||||
RWGame game(logger, argc, argv);
|
||||
RWGame game(logger, argLayerOpt);
|
||||
|
||||
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 runtime_error as these are fatal issues the user may want to
|
||||
// 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)
|
||||
: State(game), _invertedY(game->getConfig().getInputInvertY()) {
|
||||
: State(game), _invertedY(game->getConfig().invertY()) {
|
||||
this->setNextMenu(createDebugMenu());
|
||||
|
||||
_debugCam.position = vp;
|
||||
|
@ -42,7 +42,7 @@ IngameState::IngameState(RWGame* game, bool newgame, const std::string& save)
|
||||
: State(game)
|
||||
, save(save)
|
||||
, newgame(newgame)
|
||||
, m_invertedY(game->getConfig().getInputInvertY()) {
|
||||
, m_invertedY(game->getConfig().invertY()) {
|
||||
}
|
||||
|
||||
void IngameState::startTest() {
|
||||
|
@ -44,7 +44,7 @@ set(TEST_SOURCES
|
||||
test_Globals.hpp
|
||||
|
||||
# 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/GameInput.cpp"
|
||||
)
|
||||
@ -71,6 +71,7 @@ target_include_directories(rwtests
|
||||
target_link_libraries(rwtests
|
||||
PRIVATE
|
||||
Boost::unit_test_framework
|
||||
Boost::program_options
|
||||
rwengine
|
||||
SDL2::SDL2
|
||||
Boost::filesystem
|
||||
|
@ -1,4 +1,4 @@
|
||||
#include <GameConfig.hpp>
|
||||
#include <RWConfig.hpp>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
@ -269,36 +269,38 @@ BOOST_AUTO_TEST_CASE(test_TempFile) {
|
||||
BOOST_CHECK(!rwfs::exists(path));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_initial) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_initial) {
|
||||
// Test an initial config
|
||||
GameConfig cfg;
|
||||
BOOST_CHECK(!cfg.isValid());
|
||||
[[maybe_unused]] RWConfigParser cfgParser;
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_valid) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_valid) {
|
||||
// Test reading a valid configuration file
|
||||
auto cfg = getValidConfig();
|
||||
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
|
||||
BOOST_CHECK(config.isValid());
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().type(),
|
||||
GameConfig::ParseResult::ErrorType::GOOD);
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(),
|
||||
0);
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
BOOST_CHECK_EQUAL(parseResult.type(),
|
||||
RWConfigParser::ParseResult::GOOD);
|
||||
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
|
||||
|
||||
BOOST_CHECK_EQUAL(config.getGameDataPath().string(), "/dev/test");
|
||||
BOOST_CHECK_EQUAL(config.getGameLanguage(), "american");
|
||||
BOOST_CHECK(config.getInputInvertY());
|
||||
BOOST_CHECK_EQUAL(config.getHUDScale(), 2.f);
|
||||
BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
|
||||
BOOST_REQUIRE(cfgLayer.gameLanguage.has_value());
|
||||
BOOST_REQUIRE(cfgLayer.invertY.has_value());
|
||||
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
|
||||
auto cfg = getValidConfig();
|
||||
cfg["game"]["path"] = "Liberty City";
|
||||
@ -307,21 +309,21 @@ BOOST_AUTO_TEST_CASE(test_config_valid_modified) {
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
|
||||
BOOST_CHECK(config.isValid());
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().type(),
|
||||
GameConfig::ParseResult::ErrorType::GOOD);
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(),
|
||||
0);
|
||||
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
BOOST_CHECK_EQUAL(parseResult.type(),
|
||||
RWConfigParser::ParseResult::GOOD);
|
||||
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
|
||||
|
||||
BOOST_CHECK(!config.getInputInvertY());
|
||||
BOOST_CHECK_EQUAL(config.getGameDataPath().string(), "Liberty City");
|
||||
BOOST_REQUIRE(cfgLayer.invertY.has_value());
|
||||
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
|
||||
auto cfg = getValidConfig();
|
||||
cfg["game"]["path"] = "Liberty City";
|
||||
@ -329,75 +331,115 @@ BOOST_AUTO_TEST_CASE(test_config_save) {
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
{
|
||||
RWConfigLayer cfgLayer;
|
||||
{
|
||||
RWConfigParser cfgParser;
|
||||
RWConfigParser::ParseResult parseResult;
|
||||
std::tie(cfgLayer, parseResult) = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
|
||||
}
|
||||
|
||||
BOOST_CHECK(config.isValid());
|
||||
tempFile.remove();
|
||||
BOOST_CHECK(!tempFile.exists());
|
||||
|
||||
tempFile.remove();
|
||||
BOOST_CHECK(!tempFile.exists());
|
||||
{
|
||||
RWConfigParser cfgParser;
|
||||
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer);
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
BOOST_CHECK(tempFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
auto writeResult = config.saveConfig();
|
||||
BOOST_CHECK(writeResult.isValid());
|
||||
BOOST_CHECK(tempFile.exists());
|
||||
{
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
|
||||
GameConfig config2;
|
||||
config2.loadFile(tempFile.path());
|
||||
BOOST_CHECK_EQUAL(config2.getGameDataPath().string(), "Liberty City");
|
||||
BOOST_REQUIRE(cfgLayer.gamedataPath.has_value());
|
||||
BOOST_CHECK_EQUAL(*cfgLayer.gamedataPath, "Liberty City");
|
||||
}
|
||||
|
||||
simpleConfig_t cfg2 = readConfig(tempFile.path());
|
||||
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
|
||||
auto cfg = getValidConfig();
|
||||
cfg["game"]["unknownkey"] = "descartes";
|
||||
cfg["dontknow"]["dontcare"] = "\t$%!$8847 %%$ ";
|
||||
|
||||
std::map<std::string, std::string> globalUnknownData;
|
||||
|
||||
TempFile tempFile;
|
||||
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);
|
||||
|
||||
BOOST_CHECK_EQUAL(unknownData.count("game.unknownkey"), 1);
|
||||
BOOST_CHECK_EQUAL(unknownData.at("game.unknownkey"),
|
||||
stripWhitespace(cfg["game"]["unknownkey"]));
|
||||
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("dontknow.dontcare"), 1);
|
||||
BOOST_CHECK_EQUAL(unknownData.at("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();
|
||||
tempFile.remove();
|
||||
|
||||
GameConfig config2;
|
||||
config2.loadFile(tempFile.path());
|
||||
const auto &unknownData2 = config2.getParseResult().getUnknownData();
|
||||
{
|
||||
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer, globalUnknownData);
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_CHECK_EQUAL(unknownData2.size(), 2);
|
||||
{
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
const auto &unknownData = parseResult.getUnknownData();
|
||||
|
||||
BOOST_CHECK_EQUAL(unknownData2.count("game.unknownkey"), 1);
|
||||
BOOST_CHECK_EQUAL(unknownData2.at("game.unknownkey"),
|
||||
stripWhitespace(cfg["game"]["unknownkey"]));
|
||||
BOOST_REQUIRE_EQUAL(unknownData.size(), 2);
|
||||
|
||||
BOOST_CHECK_EQUAL(unknownData2.count("dontknow.dontcare"), 1);
|
||||
BOOST_CHECK_EQUAL(unknownData2.at("dontknow.dontcare"),
|
||||
stripWhitespace(cfg["dontknow"]["dontcare"]));
|
||||
BOOST_CHECK_EQUAL(unknownData.count("game.unknownkey"), 1);
|
||||
BOOST_CHECK_EQUAL(unknownData.at("game.unknownkey"),
|
||||
stripWhitespace(cfg["game"]["unknownkey"]));
|
||||
|
||||
BOOST_CHECK_EQUAL(unknownData2.count("game.path"), 0);
|
||||
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_config_save_readonly) {
|
||||
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
|
||||
auto cfg = getValidConfig();
|
||||
|
||||
@ -405,34 +447,48 @@ BOOST_AUTO_TEST_CASE(test_config_save_readonly) {
|
||||
tempFile.append(cfg);
|
||||
tempFile.change_perms_readonly();
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
BOOST_CHECK(config.isValid());
|
||||
RWConfigParser cfgParser;
|
||||
RWConfigLayer cfgLayer;
|
||||
{
|
||||
RWConfigParser::ParseResult parseResult;
|
||||
std::tie(cfgLayer, parseResult) = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
}
|
||||
|
||||
auto writeResult = config.saveConfig();
|
||||
BOOST_CHECK(!writeResult.isValid());
|
||||
BOOST_CHECK_EQUAL(writeResult.type(),
|
||||
GameConfig::ParseResult::ErrorType::INVALIDOUTPUTFILE);
|
||||
{
|
||||
auto parseResult = cfgParser.saveFile(tempFile.path(), cfgLayer);
|
||||
BOOST_CHECK(!parseResult.isValid());
|
||||
BOOST_CHECK_EQUAL(parseResult.type(),
|
||||
RWConfigParser::ParseResult::INVALIDOUTPUTFILE);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_valid_default) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_valid_default) {
|
||||
// Test whether the default INI string is valid
|
||||
TempFile tempFile;
|
||||
BOOST_CHECK(!tempFile.exists());
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
BOOST_CHECK(!config.isValid());
|
||||
RWConfigParser cfgParser;
|
||||
{
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(!parseResult.isValid());
|
||||
}
|
||||
|
||||
{
|
||||
auto defaultLayer = buildDefaultConfigLayer();
|
||||
auto parseResult = cfgParser.saveFile(tempFile.path(), defaultLayer);
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
}
|
||||
|
||||
auto defaultINI = config.getDefaultINIString();
|
||||
tempFile.append(defaultINI);
|
||||
BOOST_CHECK(tempFile.exists());
|
||||
|
||||
config.loadFile(tempFile.path());
|
||||
BOOST_CHECK(config.isValid());
|
||||
{
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
BOOST_CHECK(parseResult.isValid());
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_invalid_emptykey) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_invalid_emptykey) {
|
||||
// Test duplicate keys in invalid configuration file
|
||||
auto cfg = getValidConfig();
|
||||
cfg["game"][""] = "0";
|
||||
@ -440,16 +496,15 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_emptykey) {
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
|
||||
BOOST_CHECK(!config.isValid());
|
||||
const auto &parseResult = config.getParseResult();
|
||||
BOOST_CHECK(!parseResult.isValid());
|
||||
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
|
||||
auto cfg = getValidConfig();
|
||||
cfg["input"]["invert_y "] = "0";
|
||||
@ -457,39 +512,15 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_duplicate) {
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(tempFile.path());
|
||||
|
||||
BOOST_CHECK(!config.isValid());
|
||||
const auto &parseResult = config.getParseResult();
|
||||
BOOST_CHECK(!parseResult.isValid());
|
||||
BOOST_CHECK_EQUAL(parseResult.type(),
|
||||
GameConfig::ParseResult::ErrorType::INVALIDINPUTFILE);
|
||||
RWConfigParser::ParseResult::INVALIDINPUTFILE);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_invalid_required_missing) {
|
||||
// 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) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_invalid_wrong_type) {
|
||||
// Test wrong data type
|
||||
auto cfg = getValidConfig();
|
||||
cfg["input"]["invert_y"] = "d";
|
||||
@ -497,41 +528,20 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) {
|
||||
TempFile tempFile;
|
||||
tempFile.append(cfg);
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
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(),
|
||||
GameConfig::ParseResult::ErrorType::INVALIDCONTENT);
|
||||
RWConfigParser::ParseResult::INVALIDCONTENT);
|
||||
|
||||
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing().size(), 0);
|
||||
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 1);
|
||||
BOOST_REQUIRE_EQUAL(parseResult.getKeysInvalidData().size(), 1);
|
||||
|
||||
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData()[0], "input.invert_y");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_config_invalid_empty) {
|
||||
// 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) {
|
||||
BOOST_AUTO_TEST_CASE(test_configParser_invalid_nodir) {
|
||||
// Test reading non-existing configuration file in non-existing directory
|
||||
TempDir tempDir;
|
||||
TempFile tempFile(tempDir);
|
||||
@ -539,29 +549,133 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_nodir) {
|
||||
BOOST_CHECK(!tempDir.exists());
|
||||
BOOST_CHECK(!tempFile.exists());
|
||||
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
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(),
|
||||
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
|
||||
TempFile tempFile;
|
||||
|
||||
BOOST_CHECK(!tempFile.exists());
|
||||
GameConfig config;
|
||||
config.loadFile(tempFile.path());
|
||||
RWConfigParser cfgParser;
|
||||
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(),
|
||||
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()
|
||||
|
@ -1,11 +1,13 @@
|
||||
#include "test_Globals.hpp"
|
||||
|
||||
#include <GameConfig.hpp>
|
||||
#include <RWConfig.hpp>
|
||||
|
||||
#if RW_TEST_WITH_DATA
|
||||
std::string Global::getGamePath() {
|
||||
GameConfig config;
|
||||
config.loadFile(GameConfig::getDefaultConfigPath() / "openrw.ini");
|
||||
return config.getGameDataPath().string(); //FIXME: use path
|
||||
rwfs::path configPath = RWConfigParser::getDefaultConfigPath() / "openrw.ini";
|
||||
RWConfigParser cfgParser;
|
||||
auto [cfgLayer, parseResult] = cfgParser.loadFile(configPath);
|
||||
BOOST_REQUIRE(parseResult.isValid());
|
||||
return *cfgLayer.gamedataPath;
|
||||
}
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user