diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d0910d22..a181a35098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Bug #6579: OpenMW compilation error when using OSG doubles for BoundingSphere Feature #890: OpenMW-CS: Column filtering Feature #1465: "Reset" argument for AI functions + Feature #2491: Ability to make OpenMW "portable" Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console Feature #3616: Allow Zoom levels on the World Map diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index e81368c607..bc5d34bc03 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -25,6 +25,7 @@ int main(int argc, char** argv) ("encoding", boost::program_options::value()->default_value("win1252"), "encoding of the save file") ; p_desc.add("mwsave", 1).add("output", 1); + Files::ConfigurationManager::addCommonOptions(desc); bpo::variables_map variables; diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index d335d7cded..167f6b9c26 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -414,57 +414,23 @@ bool Launcher::MainDialog::setupGameData() bool Launcher::MainDialog::setupGraphicsSettings() { - // This method is almost a copy of OMW::Engine::loadSettings(). They should definitely - // remain consistent, and possibly be merged into a shared component. At the very least - // the filenames should be in the CfgMgr component. - - // Ensure to clear previous settings in case we had already loaded settings. - mEngineSettings.clear(); - - // Create the settings manager and load default settings file - const std::string localDefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); - const std::string globalDefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); - std::string defaultPath; - - // Prefer the defaults.bin in the current directory. - if (boost::filesystem::exists(localDefault)) - defaultPath = localDefault; - else if (boost::filesystem::exists(globalDefault)) - defaultPath = globalDefault; - // Something's very wrong if we can't find the file at all. - else { - cfgError(tr("Error reading OpenMW configuration file"), - tr("
Could not find defaults.bin

\ - The problem may be due to an incomplete installation of OpenMW.
\ - Reinstalling OpenMW may resolve the problem.")); + mEngineSettings.clear(); // Ensure to clear previous settings in case we had already loaded settings. + try + { + boost::program_options::variables_map variables; + boost::program_options::options_description desc; + mCfgMgr.addCommonOptions(desc); + mCfgMgr.readConfiguration(variables, desc, true); + mEngineSettings.load(mCfgMgr); + return true; + } + catch (std::exception& e) + { + cfgError(tr("Error reading OpenMW configuration files"), + tr("
The problem may be due to an incomplete installation of OpenMW.
\ + Reinstalling OpenMW may resolve the problem.
") + e.what()); return false; } - - // Load the default settings, report any parsing errors. - try { - mEngineSettings.loadDefault(defaultPath); - } - catch (std::exception& e) { - std::string msg = std::string("
Error reading defaults.bin

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); - return false; - } - - // Load user settings if they exist - const std::string userPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - // User settings are not required to exist, so if they don't we're done. - if (!boost::filesystem::exists(userPath)) return true; - - try { - mEngineSettings.loadUser(userPath); - } - catch (std::exception& e) { - std::string msg = std::string("
Error reading settings.cfg

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); - return false; - } - - return true; } void Launcher::MainDialog::loadSettings() diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 8e46170e63..f89e80e542 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -84,27 +84,11 @@ namespace NavMeshTool ("process-interior-cells", bpo::value()->implicit_value(true) ->default_value(false), "build navmesh for interior cells") ; + Files::ConfigurationManager::addCommonOptions(result); return result; } - void loadSettings(const Files::ConfigurationManager& config, Settings::Manager& settings) - { - const std::string localDefault = (config.getLocalPath() / "defaults.bin").string(); - const std::string globalDefault = (config.getGlobalPath() / "defaults.bin").string(); - - if (boost::filesystem::exists(localDefault)) - settings.loadDefault(localDefault); - else if (boost::filesystem::exists(globalDefault)) - settings.loadDefault(globalDefault); - else - throw std::runtime_error("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); - - const std::string settingsPath = (config.getUserConfigPath() / "settings.cfg").string(); - if (boost::filesystem::exists(settingsPath)) - settings.loadUser(settingsPath); - } - int runNavMeshTool(int argc, char *argv[]) { bpo::options_description desc = makeOptionsDescription(); @@ -165,7 +149,7 @@ namespace NavMeshTool VFS::registerArchives(&vfs, fileCollections, archives, true); Settings::Manager settings; - loadSettings(config, settings); + settings.load(config); const osg::Vec3f agentHalfExtents = Settings::Manager::getVector3("default actor pathfind half extents", "Game"); diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 862d1c545f..1d5934fe5d 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,7 @@ using namespace Fallback; CS::Editor::Editor (int argc, char **argv) -: mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), +: mConfigVariables(readConfiguration()), mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), mPid(""), mLock(), mMerge (mDocumentManager), mIpcServerName ("org.openmw.OpenCS"), mServer(nullptr), mClientSocket(nullptr) { @@ -82,7 +83,7 @@ CS::Editor::~Editor () remove(mPid.string().c_str())); // ignore any error } -std::pair > CS::Editor::readConfig(bool quiet) +boost::program_options::variables_map CS::Editor::readConfiguration() { boost::program_options::variables_map variables; boost::program_options::options_description desc("Syntax: openmw-cs \nAllowed options"); @@ -101,10 +102,19 @@ std::pair > CS::Editor::readConfi ->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)") ("script-blacklist-use", boost::program_options::value()->implicit_value(true) ->default_value(true), "enable script blacklisting"); + Files::ConfigurationManager::addCommonOptions(desc); boost::program_options::notify(variables); mCfgMgr.readConfiguration(variables, desc, false); + setupLogging(mCfgMgr.getLogPath().string(), "OpenMW-CS"); + + return variables; +} + +std::pair > CS::Editor::readConfig(bool quiet) +{ + boost::program_options::variables_map& variables = mConfigVariables; Fallback::Map::init(variables["fallback"].as().mMap); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 1c93427613..7b258c8049 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -40,6 +40,7 @@ namespace CS Q_OBJECT Files::ConfigurationManager mCfgMgr; + boost::program_options::variables_map mConfigVariables; CSMPrefs::State mSettingsState; CSMDoc::DocumentManager mDocumentManager; CSVDoc::StartupDialogue mStartup; @@ -58,6 +59,8 @@ namespace CS Files::PathContainer mDataDirs; std::string mEncodingName; + boost::program_options::variables_map readConfiguration(); + ///< Calls mCfgMgr.readConfiguration; should be used before initialization of mSettingsState as it depends on the configuration. std::pair > readConfig(bool quiet=false); ///< \return data paths diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index c7d57a256e..1e6e718983 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -79,5 +79,5 @@ int runApplication(int argc, char *argv[]) int main(int argc, char *argv[]) { - return wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); + return wrapApplication(&runApplication, argc, argv, "OpenMW-CS", false); } diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 06dc1f4a2e..ee0cbbed5a 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -16,22 +16,7 @@ CSMPrefs::State *CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::load() { - // default settings file - boost::filesystem::path local = mConfigurationManager.getLocalPath() / mDefaultConfigFile; - boost::filesystem::path global = mConfigurationManager.getGlobalPath() / mDefaultConfigFile; - - if (boost::filesystem::exists (local)) - mSettings.loadDefault (local.string()); - else if (boost::filesystem::exists (global)) - mSettings.loadDefault (global.string()); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"" + mDefaultConfigFile + "\" was properly installed."); - - // user settings file - boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; - - if (boost::filesystem::exists (user)) - mSettings.loadUser (user.string()); + mSettings.load(mConfigurationManager); } void CSMPrefs::State::declare() diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 9e2e1aedad..cd32cb3b0d 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -518,28 +518,6 @@ void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) mNewGame = newGame; } -std::string OMW::Engine::loadSettings (Settings::Manager & settings) -{ - // Create the settings manager and load default settings file - const std::string localdefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); - const std::string globaldefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); - - // prefer local - if (boost::filesystem::exists(localdefault)) - settings.loadDefault(localdefault); - else if (boost::filesystem::exists(globaldefault)) - settings.loadDefault(globaldefault); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); - - // load user settings if they exist - std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - if (boost::filesystem::exists(settingspath)) - settings.loadUser(settingspath); - - return settingspath; -} - void OMW::Engine::createWindow(Settings::Manager& settings) { int screen = settings.getInt("screen", "Video"); @@ -975,8 +953,7 @@ void OMW::Engine::go() // Load settings Settings::Manager settings; - std::string settingspath; - settingspath = loadSettings (settings); + std::string settingspath = settings.load(mCfgMgr); MWClass::registerClasses(); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d5bed4ba25..456fe23b60 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -40,6 +40,7 @@ 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; @@ -61,9 +62,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat return false; } - bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); cfgMgr.readConfiguration(variables, desc); - Files::mergeComposingVariables(variables, composingVariables, desc); + + setupLogging(cfgMgr.getLogPath().string(), "OpenMW"); Version::Version v = Version::getOpenmwVersion(variables["resources"].as().string()); Log(Debug::Info) << v.describe(); @@ -230,7 +231,7 @@ extern "C" int SDL_main(int argc, char**argv) int main(int argc, char**argv) #endif { - return wrapApplication(&runApplication, argc, argv, "OpenMW"); + return wrapApplication(&runApplication, argc, argv, "OpenMW", false); } // Platform specific for Windows when there is no console built into the executable. diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index 10003cfdfd..bf1a40f7d9 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -58,6 +58,7 @@ struct ContentFileTest : public ::testing::Test ("content", boost::program_options::value>()->default_value(std::vector(), "") ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") ("data-local", boost::program_options::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), "")); + Files::ConfigurationManager::addCommonOptions(desc); boost::program_options::notify(variables); diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 0e372942f2..c84bb39d3d 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -137,61 +137,65 @@ namespace Debug } static std::unique_ptr rawStdout = nullptr; +static std::unique_ptr rawStderr = nullptr; +static boost::filesystem::ofstream logfile; + +#if defined(_WIN32) && defined(_DEBUG) +static boost::iostreams::stream_buffer sb; +#else +static boost::iostreams::stream_buffer coutsb; +static boost::iostreams::stream_buffer cerrsb; +#endif std::ostream& getRawStdout() { return rawStdout ? *rawStdout : std::cout; } -int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName) +// Redirect cout and cerr to the log file +void setupLogging(const std::string& logDir, const std::string& appName, std::ios_base::openmode mode) +{ +#if defined(_WIN32) && defined(_DEBUG) + // Redirect cout and cerr to VS debug output when running in debug mode + sb.open(Debug::DebugOutput()); + std::cout.rdbuf(&sb); + std::cerr.rdbuf(&sb); +#else + const std::string logName = Misc::StringUtils::lowerCase(appName) + ".log"; + logfile.open(boost::filesystem::path(logDir) / logName, mode); + + coutsb.open(Debug::Tee(logfile, *rawStdout)); + cerrsb.open(Debug::Tee(logfile, *rawStderr)); + + std::cout.rdbuf(&coutsb); + std::cerr.rdbuf(&cerrsb); +#endif +} + +int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], + const std::string& appName, bool autoSetupLogging) { #if defined _WIN32 (void)Debug::attachParentConsole(); #endif rawStdout = std::make_unique(std::cout.rdbuf()); - - // Some objects used to redirect cout and cerr - // Scope must be here, so this still works inside the catch block for logging exceptions - std::streambuf* cout_rdbuf = std::cout.rdbuf (); - std::streambuf* cerr_rdbuf = std::cerr.rdbuf (); - -#if defined(_WIN32) && defined(_DEBUG) - boost::iostreams::stream_buffer sb; -#else - boost::iostreams::stream_buffer coutsb; - boost::iostreams::stream_buffer cerrsb; - std::ostream oldcout(cout_rdbuf); - std::ostream oldcerr(cerr_rdbuf); -#endif - - const std::string logName = Misc::StringUtils::lowerCase(appName) + ".log"; - boost::filesystem::ofstream logfile; + rawStderr = std::make_unique(std::cerr.rdbuf()); int ret = 0; try { Files::ConfigurationManager cfgMgr; -#if defined(_WIN32) && defined(_DEBUG) - // Redirect cout and cerr to VS debug output when running in debug mode - sb.open(Debug::DebugOutput()); - std::cout.rdbuf (&sb); - std::cerr.rdbuf (&sb); -#else - // Redirect cout and cerr to the log file - // If we are collecting a stack trace, append to existing log file - std::ios_base::openmode mode = std::ios::out; - if(argc == 2 && strcmp(argv[1], crash_switch) == 0) - mode |= std::ios::app; + if (autoSetupLogging) + { + std::ios_base::openmode mode = std::ios::out; - logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName), mode); + // If we are collecting a stack trace, append to existing log file + if (argc == 2 && strcmp(argv[1], crash_switch) == 0) + mode |= std::ios::app; - coutsb.open (Debug::Tee(logfile, oldcout)); - cerrsb.open (Debug::Tee(logfile, oldcerr)); - - std::cout.rdbuf (&coutsb); - std::cerr.rdbuf (&cerrsb); -#endif + setupLogging(cfgMgr.getLogPath().string(), appName, mode); + } #if defined(_WIN32) const std::string crashLogName = Misc::StringUtils::lowerCase(appName) + "-crash.dmp"; @@ -217,8 +221,8 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c } // Restore cout and cerr - std::cout.rdbuf(cout_rdbuf); - std::cerr.rdbuf(cerr_rdbuf); + std::cout.rdbuf(rawStdout->rdbuf()); + std::cerr.rdbuf(rawStderr->rdbuf()); Debug::CurrentDebugLevel = Debug::NoLevel; return ret; diff --git a/components/debug/debugging.hpp b/components/debug/debugging.hpp index d8849cd896..a2c5ae9e6c 100644 --- a/components/debug/debugging.hpp +++ b/components/debug/debugging.hpp @@ -133,11 +133,16 @@ namespace Debug std::map mColors; }; #endif + + } // Can be used to print messages without timestamps std::ostream& getRawStdout(); -int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], const std::string& appName); +void setupLogging(const std::string& logDir, const std::string& appName, std::ios_base::openmode = std::ios::out); + +int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, char *argv[], + const std::string& appName, bool autoSetupLogging = true); #endif diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index c2cd44960f..87f4cd4943 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -20,6 +20,7 @@ static const char* const applicationName = "openmw"; #endif const char* const localToken = "?local?"; +const char* const userConfigToken = "?userconfig?"; const char* const userDataToken = "?userdata?"; const char* const globalToken = "?global?"; @@ -29,18 +30,10 @@ ConfigurationManager::ConfigurationManager(bool silent) { setupTokensMapping(); - boost::filesystem::create_directories(mFixedPath.getUserConfigPath()); - boost::filesystem::create_directories(mFixedPath.getUserDataPath()); - + // Initialize with fixed paths, will be overridden in `readConfiguration`. mLogPath = mFixedPath.getUserConfigPath(); - + mUserDataPath = mFixedPath.getUserDataPath(); mScreenshotPath = mFixedPath.getUserDataPath() / "screenshots"; - - // probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails - boost::system::error_code dirErr; - if (!boost::filesystem::create_directories(mScreenshotPath, dirErr) && !boost::filesystem::is_directory(mScreenshotPath)) { - mScreenshotPath = mFixedPath.getUserDataPath(); - } } ConfigurationManager::~ConfigurationManager() @@ -50,6 +43,7 @@ ConfigurationManager::~ConfigurationManager() void ConfigurationManager::setupTokensMapping() { mTokensMapping.insert(std::make_pair(localToken, &FixedPath<>::getLocalPath)); + mTokensMapping.insert(std::make_pair(userConfigToken, &FixedPath<>::getUserConfigPath)); mTokensMapping.insert(std::make_pair(userDataToken, &FixedPath<>::getUserDataPath)); mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); } @@ -57,31 +51,129 @@ void ConfigurationManager::setupTokensMapping() void ConfigurationManager::readConfiguration(boost::program_options::variables_map& variables, boost::program_options::options_description& description, bool quiet) { + using ParsedConfigFile = bpo::basic_parsed_options; bool silent = mSilent; mSilent = quiet; - - // User config has the highest priority. - auto composingVariables = separateComposingVariables(variables, description); - loadConfig(mFixedPath.getUserConfigPath(), variables, description); - mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); - // read either local or global config depending on type of installation - composingVariables = separateComposingVariables(variables, description); - bool loaded = loadConfig(mFixedPath.getLocalPath(), variables, description); - mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); - if (!loaded) + std::optional config = loadConfig(mFixedPath.getLocalPath(), description); + if (config) + mActiveConfigPaths.push_back(mFixedPath.getLocalPath()); + else { - composingVariables = separateComposingVariables(variables, description); - loadConfig(mFixedPath.getGlobalConfigPath(), variables, description); + mActiveConfigPaths.push_back(mFixedPath.getGlobalConfigPath()); + config = loadConfig(mFixedPath.getGlobalConfigPath(), description); + } + if (!config) + { + if (!quiet) + Log(Debug::Error) << "Neither local config nor global config are available."; + mSilent = silent; + return; + } + + std::stack extraConfigDirs; + addExtraConfigDirs(extraConfigDirs, variables); + addExtraConfigDirs(extraConfigDirs, *config); + + std::vector parsedOptions{*std::move(config)}; + std::set alreadyParsedPaths; // needed to prevent infinite loop in case of a circular link + alreadyParsedPaths.insert(boost::filesystem::path(mActiveConfigPaths.front())); + + while (!extraConfigDirs.empty()) + { + boost::filesystem::path path = extraConfigDirs.top(); + extraConfigDirs.pop(); + if (alreadyParsedPaths.count(path) > 0) + { + if (!quiet) + Log(Debug::Warning) << "Repeated config dir: " << path; + continue; + } + alreadyParsedPaths.insert(path); + mActiveConfigPaths.push_back(path); + config = loadConfig(path, description); + if (!config) + continue; + addExtraConfigDirs(extraConfigDirs, *config); + parsedOptions.push_back(*std::move(config)); + } + + for (auto it = parsedOptions.rbegin(); it != parsedOptions.rend(); ++it) + { + auto composingVariables = separateComposingVariables(variables, description); + boost::program_options::store(std::move(*it), variables); mergeComposingVariables(variables, composingVariables, description); - boost::program_options::notify(variables); + } + + mLogPath = mActiveConfigPaths.back(); + mUserDataPath = variables["user-data"].as(); + if (mUserDataPath.empty()) + { + if (!quiet) + Log(Debug::Warning) << "Error: `user-data` is not specified"; + mUserDataPath = mFixedPath.getUserDataPath(); + } + processPath(mUserDataPath, true); + mScreenshotPath = mUserDataPath / "screenshots"; + + boost::filesystem::create_directories(getUserConfigPath()); + boost::filesystem::create_directories(mScreenshotPath); + + // probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails + 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(); + Log(Debug::Info) << "User data dir: " << mUserDataPath.string(); + Log(Debug::Info) << "Screenshots dir: " << mScreenshotPath.string(); } mSilent = silent; } +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 +{ + 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) +{ + description.add_options() + ("config", bpo::value()->multitoken()->composing(), "additional config directories") + ("user-data", bpo::value(), + "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) { @@ -126,7 +218,7 @@ void mergeComposingVariables(boost::program_options::variables_map& first, boost continue; } - if (replacedVariables.count(name)) + if (replacedVariables.count(name) || firstPosition->second.defaulted() || firstPosition->second.empty()) { firstPosition->second = second[name]; continue; @@ -142,7 +234,6 @@ void mergeComposingVariables(boost::program_options::variables_map& first, boost { auto& firstPathContainer = boost::any_cast(firstValue); const auto& secondPathContainer = boost::any_cast(secondValue); - firstPathContainer.insert(firstPathContainer.end(), secondPathContainer.begin(), secondPathContainer.end()); } else if (firstValue.type() == typeid(std::vector)) @@ -165,57 +256,60 @@ 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::processPaths(Files::PathContainer& dataDirs, bool create) +void ConfigurationManager::processPath(boost::filesystem::path& path, bool create) const +{ + std::string str = path.string(); + + // Do nothing if the path doesn't start with a token + if (str.empty() || str[0] != '?') + return; + + std::string::size_type pos = str.find('?', 1); + if (pos != std::string::npos && pos != 0) + { + auto tokenIt = mTokensMapping.find(str.substr(0, pos + 1)); + if (tokenIt != mTokensMapping.end()) + { + boost::filesystem::path tempPath(((mFixedPath).*(tokenIt->second))()); + if (pos < str.length() - 1) + { + // There is something after the token, so we should + // append it to the path + tempPath /= str.substr(pos + 1, str.length() - pos); + } + + path = tempPath; + } + else + { + if (!mSilent) + Log(Debug::Warning) << "Path starts with unknown token: " << path; + 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 { - std::string path; for (Files::PathContainer::iterator it = dataDirs.begin(); it != dataDirs.end(); ++it) { - path = it->string(); - - // Check if path contains a token - if (!path.empty() && *path.begin() == '?') - { - std::string::size_type pos = path.find('?', 1); - if (pos != std::string::npos && pos != 0) - { - TokensMappingContainer::iterator tokenIt = mTokensMapping.find(path.substr(0, pos + 1)); - if (tokenIt != mTokensMapping.end()) - { - boost::filesystem::path tempPath(((mFixedPath).*(tokenIt->second))()); - if (pos < path.length() - 1) - { - // There is something after the token, so we should - // append it to the path - tempPath /= path.substr(pos + 1, path.length() - pos); - } - - *it = tempPath; - } - else - { - // Clean invalid / unknown token, it will be removed outside the loop - (*it).clear(); - } - } - } - + processPath(*it, create); if (!boost::filesystem::is_directory(*it)) { - if (create) - { - try - { - boost::filesystem::create_directories (*it); - } - catch (...) {} - - if (boost::filesystem::is_directory(*it)) - continue; - } - + if (!mSilent) + Log(Debug::Warning) << "No such dir: " << *it; (*it).clear(); } } @@ -224,8 +318,8 @@ void ConfigurationManager::processPaths(Files::PathContainer& dataDirs, bool cre std::bind(&boost::filesystem::path::empty, std::placeholders::_1)), dataDirs.end()); } -bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, - boost::program_options::variables_map& variables, +std::optional> ConfigurationManager::loadConfig( + const boost::filesystem::path& path, boost::program_options::options_description& description) { boost::filesystem::path cfgFile(path); @@ -238,20 +332,11 @@ bool ConfigurationManager::loadConfig(const boost::filesystem::path& path, boost::filesystem::ifstream configFileStream(cfgFile); if (configFileStream.is_open()) - { - parseConfig(configFileStream, variables, description); - - return true; - } - else - { - if (!mSilent) - Log(Debug::Error) << "Loading failed."; - - return false; - } + return Files::parse_config_file(configFileStream, description, true); + else if (!mSilent) + Log(Debug::Error) << "Loading failed."; } - return false; + return std::nullopt; } const boost::filesystem::path& ConfigurationManager::getGlobalPath() const @@ -261,12 +346,15 @@ const boost::filesystem::path& ConfigurationManager::getGlobalPath() const const boost::filesystem::path& ConfigurationManager::getUserConfigPath() const { - return mFixedPath.getUserConfigPath(); + if (mActiveConfigPaths.empty()) + return mFixedPath.getUserConfigPath(); + else + return mActiveConfigPaths.back(); } const boost::filesystem::path& ConfigurationManager::getUserDataPath() const { - return mFixedPath.getUserDataPath(); + return mUserDataPath; } const boost::filesystem::path& ConfigurationManager::getLocalPath() const @@ -344,4 +432,4 @@ PathContainer asPathContainer(const MaybeQuotedPathContainer& MaybeQuotedPathCon return PathContainer(MaybeQuotedPathContainer.begin(), MaybeQuotedPathContainer.end()); } -} /* namespace Cfg */ +} /* namespace Files */ diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 4b641c12fd..49844cca41 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -2,6 +2,8 @@ #define COMPONENTS_FILES_CONFIGURATIONMANAGER_HPP #include +#include +#include #include @@ -25,39 +27,51 @@ struct ConfigurationManager void readConfiguration(boost::program_options::variables_map& variables, boost::program_options::options_description& description, bool quiet=false); - void processPaths(Files::PathContainer& dataDirs, bool create = 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. /**< Fixed paths */ const boost::filesystem::path& getGlobalPath() const; - const boost::filesystem::path& getUserConfigPath() const; const boost::filesystem::path& getLocalPath() const; const boost::filesystem::path& getGlobalDataPath() const; + const boost::filesystem::path& getUserConfigPath() const; const boost::filesystem::path& getUserDataPath() const; const boost::filesystem::path& getLocalDataPath() const; const boost::filesystem::path& getInstallPath() const; + const std::vector& getActiveConfigPaths() const { return mActiveConfigPaths; } const boost::filesystem::path& getCachePath() const; const boost::filesystem::path& getLogPath() const; const boost::filesystem::path& getScreenshotPath() const; + static void addCommonOptions(boost::program_options::options_description& description); + private: typedef Files::FixedPath<> FixedPathType; typedef const boost::filesystem::path& (FixedPathType::*path_type_f)() const; typedef std::map TokensMappingContainer; - bool loadConfig(const boost::filesystem::path& path, - boost::program_options::variables_map& variables, + std::optional> loadConfig( + const boost::filesystem::path& path, boost::program_options::options_description& description); + 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(); + std::vector mActiveConfigPaths; + FixedPathType mFixedPath; boost::filesystem::path mLogPath; + boost::filesystem::path mUserDataPath; boost::filesystem::path mScreenshotPath; TokensMappingContainer mTokensMapping; diff --git a/components/settings/parser.cpp b/components/settings/parser.cpp index f2419dfdd6..dd1b8c1a20 100644 --- a/components/settings/parser.cpp +++ b/components/settings/parser.cpp @@ -9,7 +9,8 @@ #include -void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, bool base64Encoded) +void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, + bool base64Encoded, bool overrideExisting) { mFile = file; boost::filesystem::ifstream fstream; @@ -73,7 +74,9 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat std::string value = line.substr(valueBegin); Misc::StringUtils::trim(value); - if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) + if (overrideExisting) + settings[std::make_pair(currentCategory, setting)] = value; + else if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); } } diff --git a/components/settings/parser.hpp b/components/settings/parser.hpp index 45b1a18f72..c934fbea07 100644 --- a/components/settings/parser.hpp +++ b/components/settings/parser.hpp @@ -10,7 +10,8 @@ namespace Settings class SettingsFileParser { public: - void loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, bool base64encoded = false); + void loadSettingsFile(const std::string& file, CategorySettingValueMap& settings, + bool base64encoded = false, bool overrideExisting = false); void saveSettingsFile(const std::string& file, const CategorySettingValueMap& settings); diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 7fa625e4ab..b8ff700492 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -3,6 +3,7 @@ #include +#include #include namespace Settings @@ -19,16 +20,33 @@ void Manager::clear() mChangedSettings.clear(); } -void Manager::loadDefault(const std::string &file) +std::string Manager::load(const Files::ConfigurationManager& cfgMgr) { SettingsFileParser parser; - parser.loadSettingsFile(file, mDefaultSettings, true); -} + const std::vector& paths = cfgMgr.getActiveConfigPaths(); + if (paths.empty()) + throw std::runtime_error("No config dirs! ConfigurationManager::readConfiguration must be called first."); -void Manager::loadUser(const std::string &file) -{ - SettingsFileParser parser; - parser.loadSettingsFile(file, mUserSettings); + // Create the settings manager and load default settings file. + const std::string defaultsBin = (paths.front() / "defaults.bin").string(); + if (!boost::filesystem::exists(defaultsBin)) + throw std::runtime_error ("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); + parser.loadSettingsFile(defaultsBin, mDefaultSettings, true, false); + + // Load "settings.cfg" from every config dir except the last one as additional default settings. + for (int i = 0; i < static_cast(paths.size()) - 1; ++i) + { + const std::string additionalDefaults = (paths[i] / "settings.cfg").string(); + if (boost::filesystem::exists(additionalDefaults)) + parser.loadSettingsFile(additionalDefaults, mDefaultSettings, false, true); + } + + // Load "settings.cfg" from the last config as user settings if they exist. This path will be used to save modified settings. + std::string settingspath = (paths.back() / "settings.cfg").string(); + if (boost::filesystem::exists(settingspath)) + parser.loadSettingsFile(settingspath, mUserSettings, false, false); + + return settingspath; } void Manager::saveUser(const std::string &file) diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index a4b1cf3a54..04831ef171 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -9,6 +9,11 @@ #include #include +namespace Files +{ + struct ConfigurationManager; +} + namespace Settings { /// @@ -26,11 +31,8 @@ namespace Settings void clear(); ///< clears all settings and default settings - void loadDefault (const std::string& file); - ///< load file as the default settings (can be overridden by user settings) - - void loadUser (const std::string& file); - ///< load file as user settings + std::string load(const Files::ConfigurationManager& cfgMgr); + ///< load settings from all active config dirs. Returns the path of the last loaded file. void saveUser (const std::string& file); ///< save user settings to file diff --git a/files/openmw.cfg b/files/openmw.cfg index f524911489..d1ecd6f8a3 100644 --- a/files/openmw.cfg +++ b/files/openmw.cfg @@ -4,6 +4,8 @@ content=builtin.omwscripts data-local="?userdata?data" +user-data="?userdata?" +config="?userconfig?" resources=${OPENMW_RESOURCE_FILES} script-blacklist=Museum script-blacklist=MockChangeScript diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local index bed9b9b10a..f928113002 100644 --- a/files/openmw.cfg.local +++ b/files/openmw.cfg.local @@ -4,6 +4,8 @@ content=builtin.omwscripts data-local="?userdata?data" +user-data="?userdata?" +config="?userconfig?" resources=./resources script-blacklist=Museum script-blacklist=MockChangeScript