From c7ab67c2c1ff9193b72e3694e81fdb7b827a9462 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 26 Apr 2022 22:36:03 +0200 Subject: [PATCH] Allow relative paths in openmw.cfg; support --replace=config. --- apps/bulletobjecttool/main.cpp | 2 +- apps/navmeshtool/main.cpp | 2 +- apps/opencs/editor.cpp | 8 +- apps/openmw/main.cpp | 4 +- apps/openmw/options.cpp | 4 +- apps/openmw_test_suite/mwworld/test_store.cpp | 8 +- components/config/gamesettings.cpp | 4 +- components/files/configurationmanager.cpp | 199 ++++++++++-------- components/files/configurationmanager.hpp | 30 +-- 9 files changed, 137 insertions(+), 124 deletions(-) diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index c80883fe78..36ec8df9ed 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -141,7 +141,7 @@ namespace if (!local.empty()) dataDirs.push_back(std::move(local)); - config.processPaths(dataDirs); + config.filterOutNonExistingPaths(dataDirs); const auto fsStrict = variables["fs-strict"].as(); const auto resDir = variables["resources"].as(); diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 643c14af0a..e024a35530 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -133,7 +133,7 @@ namespace NavMeshTool if (!local.empty()) dataDirs.push_back(std::move(local)); - config.processPaths(dataDirs); + config.filterOutNonExistingPaths(dataDirs); const auto fsStrict = variables["fs-strict"].as(); const auto resDir = variables["resources"].as(); diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 40defda4e2..d8672cd84b 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -137,10 +137,12 @@ std::pair > CS::Editor::readConfi Files::PathContainer::value_type local(variables["data-local"].as()); if (!local.empty()) + { + boost::filesystem::create_directories(local); dataLocal.push_back(local); - - mCfgMgr.processPaths (dataDirs); - mCfgMgr.processPaths (dataLocal, true); + } + mCfgMgr.filterOutNonExistingPaths(dataDirs); + mCfgMgr.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) mLocal = dataLocal[0]; diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 1dfa2a6494..a0967ec14b 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -37,8 +37,6 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat typedef std::vector StringsVector; bpo::options_description desc = OpenMW::makeOptionsDescription(); - Files::ConfigurationManager::addCommonOptions(desc); - bpo::variables_map variables; Files::parseArgs(argc, argv, variables, desc); @@ -82,7 +80,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat if (!local.empty()) dataDirs.push_back(local); - cfgMgr.processPaths(dataDirs); + cfgMgr.filterOutNonExistingPaths(dataDirs); engine.setResourceDir(variables["resources"].as()); engine.setDataDirs(dataDirs); diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index dbf910b70f..f557e42282 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -16,14 +16,12 @@ namespace OpenMW bpo::options_description makeOptionsDescription() { bpo::options_description desc("Syntax: openmw \nAllowed options"); + Files::ConfigurationManager::addCommonOptions(desc); desc.add_options() ("help", "print help message") ("version", "print version information and quit") - ("replace", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended") - ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") ->multitoken()->composing(), "set data directories (later directories have higher priority)") diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index c15e238c9b..ab1fdd4995 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -71,12 +71,14 @@ struct ContentFileTest : public ::testing::Test } Files::PathContainer::value_type local(variables["data-local"].as()); - if (!local.empty()) { + if (!local.empty()) + { + boost::filesystem::create_directories(local); dataLocal.push_back(local); } - mConfigurationManager.processPaths (dataDirs); - mConfigurationManager.processPaths (dataLocal, true); + mConfigurationManager.filterOutNonExistingPaths(dataDirs); + mConfigurationManager.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 6253d53f45..0ec13abe36 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -28,7 +28,7 @@ void Config::GameSettings::validatePaths() } // Parse the data dirs to convert the tokenized paths - mCfgMgr.processPaths(dataDirs); + mCfgMgr.processPaths(dataDirs, /*basePath=*/""); mDataDirs.clear(); for (auto & dataDir : dataDirs) { @@ -54,7 +54,7 @@ void Config::GameSettings::validatePaths() QByteArray bytes = local.toUtf8(); dataDirs.push_back(Files::PathContainer::value_type(std::string(bytes.constData(), bytes.length()))); - mCfgMgr.processPaths(dataDirs); + mCfgMgr.processPaths(dataDirs, /*basePath=*/""); if (!dataDirs.empty()) { QString path = QString::fromUtf8(dataDirs.front().string().c_str()); diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 80426586fe..74fe03b3e2 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -11,6 +11,8 @@ namespace Files { +namespace bpo = boost::program_options; + static const char* const openmwCfgFile = "openmw.cfg"; #if defined(_WIN32) || defined(__WINDOWS__) @@ -31,7 +33,6 @@ ConfigurationManager::ConfigurationManager(bool silent) setupTokensMapping(); // Initialize with fixed paths, will be overridden in `readConfiguration`. - mLogPath = mFixedPath.getUserConfigPath(); mUserDataPath = mFixedPath.getUserDataPath(); mScreenshotPath = mFixedPath.getUserDataPath() / "screenshots"; } @@ -48,14 +49,25 @@ void ConfigurationManager::setupTokensMapping() mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); } -void ConfigurationManager::readConfiguration(boost::program_options::variables_map& variables, - boost::program_options::options_description& description, bool quiet) +static bool hasReplaceConfig(const bpo::variables_map& variables) +{ + if (variables["replace"].empty()) + return false; + for (const std::string& var : variables["replace"].as>()) + { + if (var == "config") + return true; + } + return false; +} + +void ConfigurationManager::readConfiguration(bpo::variables_map& variables, + const bpo::options_description& description, bool quiet) { - using ParsedConfigFile = bpo::basic_parsed_options; bool silent = mSilent; mSilent = quiet; - std::optional config = loadConfig(mFixedPath.getLocalPath(), description); + std::optional config = loadConfig(mFixedPath.getLocalPath(), description); if (config) mActiveConfigPaths.push_back(mFixedPath.getLocalPath()); else @@ -73,9 +85,10 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m std::stack extraConfigDirs; addExtraConfigDirs(extraConfigDirs, variables); - addExtraConfigDirs(extraConfigDirs, *config); + if (!hasReplaceConfig(variables)) + addExtraConfigDirs(extraConfigDirs, *config); - std::vector parsedOptions{*std::move(config)}; + std::vector parsedConfigs{*std::move(config)}; std::set alreadyParsedPaths; // needed to prevent infinite loop in case of a circular link alreadyParsedPaths.insert(boost::filesystem::path(mActiveConfigPaths.front())); @@ -92,20 +105,35 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m alreadyParsedPaths.insert(path); mActiveConfigPaths.push_back(path); config = loadConfig(path, description); - if (!config) - continue; - addExtraConfigDirs(extraConfigDirs, *config); - parsedOptions.push_back(*std::move(config)); + if (config && hasReplaceConfig(*config) && parsedConfigs.size() > 1) + { + mActiveConfigPaths.resize(1); + parsedConfigs.resize(1); + Log(Debug::Info) << "Skipping previous configs except " << (mActiveConfigPaths.front() / "openmw.cfg") << + " due to replace=config in " << (path / "openmw.cfg"); + } + mActiveConfigPaths.push_back(path); + if (config) + { + addExtraConfigDirs(extraConfigDirs, *config); + parsedConfigs.push_back(*std::move(config)); + } } - for (auto it = parsedOptions.rbegin(); it != parsedOptions.rend(); ++it) + for (auto it = parsedConfigs.rbegin(); it != parsedConfigs.rend(); ++it) { auto composingVariables = separateComposingVariables(variables, description); - boost::program_options::store(std::move(*it), variables); + for (auto& [k, v] : *it) + { + auto it = variables.find(k); + if (it == variables.end()) + variables.insert({k, v}); + else if (it->second.defaulted()) + it->second = v; + } mergeComposingVariables(variables, composingVariables, description); } - mLogPath = mActiveConfigPaths.back(); mUserDataPath = variables["user-data"].as(); if (mUserDataPath.empty()) { @@ -113,7 +141,6 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m Log(Debug::Warning) << "Error: `user-data` is not specified"; mUserDataPath = mFixedPath.getUserDataPath(); } - processPath(mUserDataPath, true); mScreenshotPath = mUserDataPath / "screenshots"; boost::filesystem::create_directories(getUserConfigPath()); @@ -123,18 +150,6 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m if (!boost::filesystem::is_directory(mScreenshotPath)) mScreenshotPath = mUserDataPath; - if (!quiet && !variables["replace"].empty()) - { - for (const std::string& var : variables["replace"].as>()) - { - if (var == "config") - { - Log(Debug::Warning) << "replace=config is not allowed and was ignored"; - break; - } - } - } - if (!quiet) { Log(Debug::Info) << "Logs dir: " << getUserConfigPath().string(); @@ -146,42 +161,30 @@ void ConfigurationManager::readConfiguration(boost::program_options::variables_m } void ConfigurationManager::addExtraConfigDirs(std::stack& dirs, - const bpo::basic_parsed_options& options) const -{ - boost::program_options::variables_map variables; - boost::program_options::store(options, variables); - boost::program_options::notify(variables); - addExtraConfigDirs(dirs, variables); -} - -void ConfigurationManager::addExtraConfigDirs(std::stack& dirs, - const boost::program_options::variables_map& variables) const + const bpo::variables_map& variables) const { auto configIt = variables.find("config"); if (configIt == variables.end()) return; Files::PathContainer newDirs = asPathContainer(configIt->second.as()); - processPaths(newDirs); for (auto it = newDirs.rbegin(); it != newDirs.rend(); ++it) dirs.push(*it); } -void ConfigurationManager::addCommonOptions(boost::program_options::options_description& description) +void ConfigurationManager::addCommonOptions(bpo::options_description& description) { - Files::MaybeQuotedPath defaultUserData; - static_cast(defaultUserData) = boost::filesystem::path("?userdata?"); - description.add_options() ("config", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "") ->multitoken()->composing(), "additional config directories") - ("user-data", bpo::value()->default_value(defaultUserData, ""), + ("replace", bpo::value>()->default_value(std::vector(), "")->multitoken()->composing(), + "settings where the values from the current source should replace those from lower-priority sources instead of being appended") + ("user-data", bpo::value()->default_value(Files::MaybeQuotedPath(), ""), "set user data directory (used for saves, screenshots, etc)"); } -boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map & variables, - boost::program_options::options_description& description) +bpo::variables_map separateComposingVariables(bpo::variables_map & variables, const bpo::options_description& description) { - boost::program_options::variables_map composingVariables; + bpo::variables_map composingVariables; for (auto itr = variables.begin(); itr != variables.end();) { if (description.find(itr->first, false).semantic()->is_composing()) @@ -195,8 +198,8 @@ boost::program_options::variables_map separateComposingVariables(boost::program_ return composingVariables; } -void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, - boost::program_options::options_description& description) +void mergeComposingVariables(bpo::variables_map& first, bpo::variables_map& second, + const bpo::options_description& description) { // There are a few places this assumes all variables are present in second, but it's never crashed in the wild, so it looks like that's guaranteed. std::set replacedVariables; @@ -260,16 +263,18 @@ void mergeComposingVariables(boost::program_options::variables_map& first, boost Log(Debug::Error) << "Unexpected composing variable type. Curse boost and their blasted arcane templates."; } } - boost::program_options::notify(first); } -void ConfigurationManager::processPath(boost::filesystem::path& path, bool create) const +void ConfigurationManager::processPath(boost::filesystem::path& path, const boost::filesystem::path& basePath) const { std::string str = path.string(); - // Do nothing if the path doesn't start with a token if (str.empty() || str[0] != '?') + { + if (!path.is_absolute()) + path = basePath / path; return; + } std::string::size_type pos = str.find('?', 1); if (pos != std::string::npos && pos != 0) @@ -294,37 +299,49 @@ void ConfigurationManager::processPath(boost::filesystem::path& path, bool creat path.clear(); } } - - if (!boost::filesystem::is_directory(path) && create) - { - try - { - boost::filesystem::create_directories(path); - } - catch (...) {} - } } -void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, bool create) const +void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, const boost::filesystem::path& basePath) const { - for (Files::PathContainer::iterator it = dataDirs.begin(); it != dataDirs.end(); ++it) - { - processPath(*it, create); - if (!boost::filesystem::is_directory(*it)) - { - if (!mSilent) - Log(Debug::Warning) << "No such dir: " << *it; - (*it).clear(); - } - } - - dataDirs.erase(std::remove_if(dataDirs.begin(), dataDirs.end(), - std::bind(&boost::filesystem::path::empty, std::placeholders::_1)), dataDirs.end()); + for (auto& path : dataDirs) + processPath(path, basePath); } -std::optional> ConfigurationManager::loadConfig( - const boost::filesystem::path& path, - boost::program_options::options_description& description) +void ConfigurationManager::processPaths(boost::program_options::variables_map& variables, const boost::filesystem::path& basePath) const +{ + for (auto& [name, var] : variables) + { + if (var.defaulted()) + continue; + if (var.value().type() == typeid(MaybeQuotedPathContainer)) + { + auto& pathContainer = boost::any_cast(var.value()); + for (MaybeQuotedPath& path : pathContainer) + processPath(path, basePath); + } + else if (var.value().type() == typeid(MaybeQuotedPath)) + { + boost::filesystem::path& path = boost::any_cast(var.value()); + processPath(path, basePath); + } + } +} + +void ConfigurationManager::filterOutNonExistingPaths(Files::PathContainer& dataDirs) const +{ + dataDirs.erase(std::remove_if(dataDirs.begin(), dataDirs.end(), + [this](const boost::filesystem::path& p) + { + bool exists = boost::filesystem::is_directory(p); + if (!exists && !mSilent) + Log(Debug::Warning) << "No such dir: " << p; + return !exists; + }), + dataDirs.end()); +} + +std::optional ConfigurationManager::loadConfig( + const boost::filesystem::path& path, const bpo::options_description& description) const { boost::filesystem::path cfgFile(path); cfgFile /= std::string(openmwCfgFile); @@ -336,7 +353,12 @@ std::optional> ConfigurationManager::loadConfig( boost::filesystem::ifstream configFileStream(cfgFile); if (configFileStream.is_open()) - return Files::parse_config_file(configFileStream, description, true); + { + bpo::variables_map variables; + bpo::store(Files::parse_config_file(configFileStream, description, true), variables); + processPaths(variables, path); + return variables; + } else if (!mSilent) Log(Debug::Error) << "Loading failed."; } @@ -381,32 +403,23 @@ const boost::filesystem::path& ConfigurationManager::getInstallPath() const return mFixedPath.getInstallPath(); } -const boost::filesystem::path& ConfigurationManager::getLogPath() const -{ - return mLogPath; -} - const boost::filesystem::path& ConfigurationManager::getScreenshotPath() const { return mScreenshotPath; } -void parseArgs(int argc, const char* const argv[], boost::program_options::variables_map& variables, - boost::program_options::options_description& description) +void parseArgs(int argc, const char* const argv[], bpo::variables_map& variables, + const bpo::options_description& description) { - boost::program_options::store( - boost::program_options::command_line_parser(argc, argv).options(description).allow_unregistered().run(), + bpo::store( + bpo::command_line_parser(argc, argv).options(description).allow_unregistered().run(), variables ); } -void parseConfig(std::istream& stream, boost::program_options::variables_map& variables, - boost::program_options::options_description& description) +void parseConfig(std::istream& stream, bpo::variables_map& variables, const bpo::options_description& description) { - boost::program_options::store( - Files::parse_config_file(stream, description, true), - variables - ); + bpo::store(Files::parse_config_file(stream, description, true), variables); } std::istream& operator>> (std::istream& istream, MaybeQuotedPath& MaybeQuotedPath) diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 49844cca41..f45df612ef 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -25,11 +25,14 @@ struct ConfigurationManager virtual ~ConfigurationManager(); void readConfiguration(boost::program_options::variables_map& variables, - boost::program_options::options_description& description, bool quiet=false); + const boost::program_options::options_description& description, bool quiet=false); - void processPath(boost::filesystem::path& path, bool create = false) const; - void processPaths(Files::PathContainer& dataDirs, bool create = false) const; - ///< \param create Try creating the directory, if it does not exist. + void filterOutNonExistingPaths(Files::PathContainer& dataDirs) const; + + // Replaces tokens (`?local?`, `?global?`, etc.) in paths. Adds `basePath` prefix for relative paths. + void processPath(boost::filesystem::path& path, const boost::filesystem::path& basePath) const; + void processPaths(Files::PathContainer& dataDirs, const boost::filesystem::path& basePath) const; + void processPaths(boost::program_options::variables_map& variables, const boost::filesystem::path& basePath) const; /**< Fixed paths */ const boost::filesystem::path& getGlobalPath() const; @@ -44,7 +47,7 @@ struct ConfigurationManager const boost::filesystem::path& getCachePath() const; - const boost::filesystem::path& getLogPath() const; + const boost::filesystem::path& getLogPath() const { return getUserConfigPath(); } const boost::filesystem::path& getScreenshotPath() const; static void addCommonOptions(boost::program_options::options_description& description); @@ -55,14 +58,12 @@ struct ConfigurationManager typedef const boost::filesystem::path& (FixedPathType::*path_type_f)() const; typedef std::map TokensMappingContainer; - std::optional> loadConfig( + std::optional loadConfig( const boost::filesystem::path& path, - boost::program_options::options_description& description); + const boost::program_options::options_description& description) const; void addExtraConfigDirs(std::stack& dirs, const boost::program_options::variables_map& variables) const; - void addExtraConfigDirs(std::stack& dirs, - const boost::program_options::basic_parsed_options& options) const; void setupTokensMapping(); @@ -70,7 +71,6 @@ struct ConfigurationManager FixedPathType mFixedPath; - boost::filesystem::path mLogPath; boost::filesystem::path mUserDataPath; boost::filesystem::path mScreenshotPath; @@ -80,16 +80,16 @@ struct ConfigurationManager }; boost::program_options::variables_map separateComposingVariables(boost::program_options::variables_map& variables, - boost::program_options::options_description& description); + const boost::program_options::options_description& description); void mergeComposingVariables(boost::program_options::variables_map& first, boost::program_options::variables_map& second, - boost::program_options::options_description& description); + const boost::program_options::options_description& description); void parseArgs(int argc, const char* const argv[], boost::program_options::variables_map& variables, - boost::program_options::options_description& description); + const boost::program_options::options_description& description); void parseConfig(std::istream& stream, boost::program_options::variables_map& variables, - boost::program_options::options_description& description); + const boost::program_options::options_description& description); class MaybeQuotedPath : public boost::filesystem::path { @@ -101,6 +101,6 @@ typedef std::vector MaybeQuotedPathContainer; PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathContainer); -} /* namespace Cfg */ +} /* namespace Files */ #endif /* COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP */