Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
23 kB · 635 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636#include "gamesettings.hpp"
#include <QDir>#include <QProgressDialog>#include <QRegularExpression>
#include <components/files/configurationmanager.hpp>#include <components/files/qtconversion.hpp>#include <components/misc/utf8qtextstream.hpp>
const char Config::GameSettings::sArchiveKey[] = "fallback-archive";const char Config::GameSettings::sContentKey[] = "content";const char Config::GameSettings::sDirectoryKey[] = "data";
namespace{ template <typename T> QList<T> reverse(QList<T> values) { std::reverse(values.begin(), values.end()); return values; }}
Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) : mCfgMgr(cfg){}
void Config::GameSettings::validatePaths(){ QList<SettingValue> paths = mSettings.values(QString("data"));
mDataDirs.clear();
QProgressDialog progressBar("Validating paths", {}, 0, static_cast<int>(paths.size() + 1)); progressBar.setWindowModality(Qt::WindowModal); progressBar.setValue(0);
for (const auto& dataDir : paths) { if (QDir(dataDir.value).exists()) { SettingValue copy = dataDir; copy.value = QDir(dataDir.value).canonicalPath(); mDataDirs.append(copy); } progressBar.setValue(progressBar.value() + 1); }
// Do the same for data-local const QString& local = mSettings.value(QString("data-local")).value;
progressBar.setValue(progressBar.value() + 1);
if (!local.isEmpty() && QDir(local).exists()) { mDataLocal = QDir(local).canonicalPath(); }}
QString Config::GameSettings::getResourcesVfs() const{ QString resources = mSettings.value(QString("resources"), { "./resources", "", "" }).value; resources += "/vfs"; return QFileInfo(resources).canonicalFilePath();}
QList<Config::SettingValue> Config::GameSettings::values( const QString& key, const QList<SettingValue>& defaultValues) const{ if (!mSettings.values(key).isEmpty()) return mSettings.values(key); return defaultValues;}
bool Config::GameSettings::containsValue(const QString& key, const QString& value) const{ auto [itr, end] = mSettings.equal_range(key); while (itr != end) { if (itr->value == value) return true; ++itr; } return false;}
bool Config::GameSettings::readFile(QTextStream& stream, const QString& context, bool ignoreContent){ if (readFile(stream, mSettings, context, ignoreContent)) { mContexts.push_back(context); return true; } return false;}
bool Config::GameSettings::readUserFile(QTextStream& stream, const QString& context, bool ignoreContent){ return readFile(stream, mUserSettings, context, ignoreContent);}
bool Config::GameSettings::readFile( QTextStream& stream, QMultiMap<QString, SettingValue>& settings, const QString& context, bool ignoreContent){ QMultiMap<QString, SettingValue> cache; QRegularExpression replaceRe("^\\s*replace\\s*=\\s*(.+)$"); QRegularExpression keyRe("^([^=]+)\\s*=\\s*(.+)$");
auto initialPos = stream.pos();
while (!stream.atEnd()) { QString line = stream.readLine();
if (line.isEmpty() || line.startsWith("#")) continue;
QRegularExpressionMatch match = replaceRe.match(line); if (match.hasMatch()) { QString key = match.captured(1).trimmed(); // Replace composing entries with a replace= line if (key == QLatin1String("config") || key == QLatin1String("replace") || key == QLatin1String("data") || key == QLatin1String("fallback-archive") || key == QLatin1String("content") || key == QLatin1String("groundcover") || key == QLatin1String("fallback")) settings.remove(key); } }
stream.seek(initialPos);
while (!stream.atEnd()) { QString line = stream.readLine();
if (line.isEmpty() || line.startsWith("#")) continue;
QRegularExpressionMatch match = keyRe.match(line); if (match.hasMatch()) { QString key = match.captured(1).trimmed(); SettingValue value{ match.captured(2).trimmed(), value.value, context };
// Don't remove composing entries if (key != QLatin1String("config") && key != QLatin1String("replace") && key != QLatin1String("data") && key != QLatin1String("fallback-archive") && key != QLatin1String("content") && key != QLatin1String("groundcover") && key != QLatin1String("fallback")) settings.remove(key);
if (key == QLatin1String("config") || key == QLatin1String("user-data") || key == QLatin1String("resources") || key == QLatin1String("data") || key == QLatin1String("data-local") || key == QLatin1String("load-savegame")) { // Path line (e.g. 'data=...'), so needs processing to deal with ampersands and quotes // The following is based on boost::io::detail::quoted_manip.hpp, but we don't actually use // boost::filesystem::path anymore, and use a custom class MaybeQuotedPath which uses Boost-like quoting // rules but internally stores as a std::filesystem::path. // Caution: This is intentional behaviour to duplicate how Boost and what we replaced it with worked, // and we rely on that. QChar delim = '\"'; QChar escape = '&';
if (value.value.at(0) == delim) { QString valueOriginal = value.value; value.value = "";
for (QString::const_iterator it = valueOriginal.begin() + 1; it != valueOriginal.end(); ++it) { if (*it == escape) ++it; else if (*it == delim) break; value.value += *it; } value.originalRepresentation = value.value; }
value = processPathSettingValue(value); } if (ignoreContent && (key == QLatin1String("content") || key == QLatin1String("data"))) continue;
auto containsValue = [&](const QMultiMap<QString, SettingValue>& map) { for (auto [itr, end] = map.equal_range(key); itr != end; ++itr) { if (itr->value == value.value) return true; } return false; };
if (!containsValue(cache) && !containsValue(settings)) { cache.insert(key, value); } } }
if (settings.isEmpty()) { settings = std::move(cache); // This is the first time we read a file validatePaths(); return true; }
// Merge the changed keys with those which didn't settings.unite(cache); validatePaths();
return true;}
bool Config::GameSettings::writeFile(QTextStream& stream){ // Iterate in reverse order to preserve insertion order auto i = mUserSettings.end(); while (i != mUserSettings.begin()) { i--;
// path lines (e.g. 'data=...') need quotes and ampersands escaping to match how boost::filesystem::path uses // boost::io::quoted // We don't actually use boost::filesystem::path anymore, but use a custom class MaybeQuotedPath which uses // Boost-like quoting rules but internally stores as a std::filesystem::path. // Caution: This is intentional behaviour to duplicate how Boost and what we replaced it with worked, and we // rely on that. if (i.key() == QLatin1String("config") || i.key() == QLatin1String("user-data") || i.key() == QLatin1String("resources") || i.key() == QLatin1String("data") || i.key() == QLatin1String("data-local") || i.key() == QLatin1String("load-savegame")) { stream << i.key() << "=";
// Equivalent to stream << std::quoted(i.value(), '"', '&'), which won't work on QStrings. QChar delim = '\"'; QChar escape = '&'; QString string = i.value().originalRepresentation;
stream << delim; for (auto& it : string) { if (it == delim || it == escape) stream << escape; stream << it; } stream << delim;
stream << '\n'; continue; }
stream << i.key() << "=" << i.value().originalRepresentation << "\n"; }
return true;}
bool Config::GameSettings::isOrderedLine(const QString& line){ return line.contains(QRegularExpression("^\\s*fallback-archive\\s*=")) || line.contains(QRegularExpression("^\\s*fallback\\s*=")) || line.contains(QRegularExpression("^\\s*data\\s*=")) || line.contains(QRegularExpression("^\\s*data-local\\s*=")) || line.contains(QRegularExpression("^\\s*resources\\s*=")) || line.contains(QRegularExpression("^\\s*groundcover\\s*=")) || line.contains(QRegularExpression("^\\s*content\\s*="));}
// Policy://// - Always ignore a line beginning with '#' or empty lines; added above a config// entry.//// - If a line in file exists with matching key and value, then replace the line with that of mUserSettings.// - else if only the key matches, remove comment//// - If there is no corresponding line in file, add at the end//// - Removed content items are saved as comments if the item had any comments.// Content items prepended with '##' are considered previously removed.//bool Config::GameSettings::writeFileWithComments(QFile& file){ QTextStream stream(&file); Misc::ensureUtf8Encoding(stream);
// slurp std::vector<QString> fileCopy; QString line = stream.readLine(); while (!line.isNull()) { fileCopy.push_back(line); line = stream.readLine(); } stream.seek(0);
// empty file, no comments to keep if (fileCopy.empty()) return writeFile(stream);
QMultiMap<QString, SettingValue> existingSettings; QString context = QFileInfo(file).absoluteDir().path(); if (readFile(stream, existingSettings, context)) { // don't use QMultiMap operator== as mUserSettings may have blank context fields // don't use one std::equal with custom predicate as (until Qt 6.4) there was no key-value iterator if (std::equal(existingSettings.keyBegin(), existingSettings.keyEnd(), mUserSettings.keyBegin(), mUserSettings.keyEnd()) && std::equal(existingSettings.cbegin(), existingSettings.cend(), mUserSettings.cbegin(), [](const SettingValue& l, const SettingValue& r) { return l.originalRepresentation == r.originalRepresentation; })) { // The existing file already contains what we need, don't risk scrambling comments and formatting return true; } } stream.seek(0);
// start // | // | +----------------------------------------------------------+ // | | | // v v | // skip non-"ordered" lines (remove "ordered" lines) | // | ^ | // | | | // | non-"ordered" line, write saved comments | // | ^ | // v | | // blank or comment line, save in temp buffer <--------+ | // | | | | // | +------- comment line ------+ | // v (special processing '##') | // "ordered" line | // | | // v | // save in a separate map of comments keyed by "ordered" line | // | | // +----------------------------------------------------------+ // // QRegularExpression settingRegex("^([^=]+)\\s*=\\s*(.+?)\\s*$"); std::vector<QString> comments; auto commentStart = fileCopy.end(); std::map<QString, std::vector<QString>> commentsMap; for (auto iter = fileCopy.begin(); iter != fileCopy.end(); ++iter) { if (isOrderedLine(*iter)) { // save in a separate map of comments keyed by "ordered" line if (!comments.empty()) { QRegularExpressionMatch match = settingRegex.match(*iter); if (match.hasMatch()) { commentsMap[match.captured(1) + "=" + match.captured(2)] = comments; comments.clear(); commentStart = fileCopy.end(); } // else do nothing, malformed line }
*iter = QString(); // "ordered" lines to be removed later } else if ((*iter).isEmpty() || (*iter).contains(QRegularExpression("^\\s*#"))) { // comment line, save in temp buffer if (comments.empty()) commentStart = iter;
// special removed content processing if ((*iter).contains(QRegularExpression("^##content\\s*="))) { if (!comments.empty()) { commentsMap[*iter] = comments; comments.clear(); commentStart = fileCopy.end(); } } else comments.push_back(*iter);
*iter = QString(); // assume to be deleted later } else { QRegularExpressionMatch match = settingRegex.match(*iter);
// blank or non-"ordered" line, write saved comments if (!comments.empty() && match.hasMatch() && settingRegex.captureCount() >= 2 && mUserSettings.find(match.captured(1)) != mUserSettings.end()) { if (commentStart == fileCopy.end()) throw std::runtime_error( "Config::GameSettings: failed to parse settings - iterator is past of end of settings file");
for (const auto& comment : comments) { *commentStart = comment; ++commentStart; } comments.clear(); commentStart = fileCopy.end(); }
// keep blank lines and non-"ordered" lines other than comments
// look for a key in the line if (!match.hasMatch() || settingRegex.captureCount() < 2) { // no key or no value found in line, replace with a null string which will be removed later *iter = QString(); comments.clear(); commentStart = fileCopy.end(); continue; }
// look for a matching key in user settings *iter = QString(); // assume no match QString key = match.captured(1); QString keyVal = match.captured(1) + "=" + match.captured(2); QMultiMap<QString, SettingValue>::const_iterator i = mUserSettings.find(key); while (i != mUserSettings.end() && i.key() == key) { // todo: does this need to handle paths? QString settingLine = i.key() + "=" + i.value().originalRepresentation; QRegularExpressionMatch keyMatch = settingRegex.match(settingLine); if (keyMatch.hasMatch()) { if ((keyMatch.captured(1) + "=" + keyMatch.captured(2)) == keyVal) { *iter = std::move(settingLine); break; } } ++i; } } }
// comments at top of file for (auto& iter : fileCopy) { if (iter.isNull()) continue;
// Below is based on readFile() code, if that changes corresponding change may be // required (for example duplicates may be inserted if the rules don't match) if (/*(*iter).isEmpty() ||*/ iter.contains(QRegularExpression("^\\s*#"))) { stream << iter << "\n"; continue; } }
// Iterate in reverse order to preserve insertion order QString settingLine; auto it = mUserSettings.end(); while (it != mUserSettings.begin()) { it--;
// path lines (e.g. 'data=...') need quotes and ampersands escaping to match how boost::filesystem::path uses // boost::io::quoted // We don't actually use boost::filesystem::path anymore, but use a custom class MaybeQuotedPath which uses // Boost-like quoting rules but internally stores as a std::filesystem::path. // Caution: This is intentional behaviour to duplicate how Boost and what we replaced it with worked, and we // rely on that. if (it.key() == QLatin1String("config") || it.key() == QLatin1String("user-data") || it.key() == QLatin1String("resources") || it.key() == QLatin1String("data") || it.key() == QLatin1String("data-local") || it.key() == QLatin1String("load-savegame")) { settingLine = it.key() + "=";
// Equivalent to settingLine += std::quoted(it.value(), '"', '&'), which won't work on QStrings. QChar delim = '\"'; QChar escape = '&'; QString string = it.value().originalRepresentation;
settingLine += delim; for (auto& iter : string) { if (iter == delim || iter == escape) settingLine += escape; settingLine += iter; } settingLine += delim; } else settingLine = it.key() + "=" + it.value().originalRepresentation;
QRegularExpressionMatch match = settingRegex.match(settingLine); if (match.hasMatch()) { auto i = commentsMap.find(match.captured(1) + "=" + match.captured(2));
// check if previous removed content item with comments if (i == commentsMap.end()) i = commentsMap.find("##" + match.captured(1) + "=" + match.captured(2));
if (i != commentsMap.end()) { std::vector<QString> cLines = i->second; for (const auto& cLine : cLines) stream << cLine << "\n";
commentsMap.erase(i); } }
stream << settingLine << "\n"; }
// flush any removed settings if (!commentsMap.empty()) { auto i = commentsMap.begin(); for (; i != commentsMap.end(); ++i) { if (i->first.contains(QRegularExpression("^\\s*content\\s*="))) { std::vector<QString> cLines = i->second; for (const auto& cLine : cLines) stream << cLine << "\n";
// mark the content line entry for future preocessing stream << "##" << i->first << "\n";
// commentsMap.erase(i); } } }
// flush any end comments if (!comments.empty()) { for (const auto& comment : comments) stream << comment << "\n"; }
file.resize(file.pos());
return true;}
bool Config::GameSettings::hasMaster(){ bool result = false; QList<SettingValue> content = mSettings.values(QString(Config::GameSettings::sContentKey)); for (int i = 0; i < content.count(); ++i) { if (content.at(i).value.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive) || content.at(i).value.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive)) { result = true; break; } }
return result;}
void Config::GameSettings::setContentList( const QList<SettingValue>& dirNames, const QList<SettingValue>& archiveNames, const QStringList& fileNames){ remove(sDirectoryKey); for (auto const& item : dirNames) setMultiValue(sDirectoryKey, item); remove(sArchiveKey); for (auto const& item : archiveNames) setMultiValue(sArchiveKey, item); remove(sContentKey); for (auto const& item : fileNames) setMultiValue(sContentKey, { item });}
QList<Config::SettingValue> Config::GameSettings::getDataDirs() const{ return reverse(mDataDirs);}
QList<Config::SettingValue> Config::GameSettings::getArchiveList() const{ // QMap returns multiple rows in LIFO order, so need to reverse return reverse(values(sArchiveKey));}
QList<Config::SettingValue> Config::GameSettings::getContentList() const{ // QMap returns multiple rows in LIFO order, so need to reverse return reverse(values(sContentKey));}
bool Config::GameSettings::isUserSetting(const SettingValue& settingValue) const{ return settingValue.context.isEmpty() || settingValue.context == getUserContext();}
Config::SettingValue Config::GameSettings::processPathSettingValue(const SettingValue& value){ std::filesystem::path path = Files::pathFromQString(value.value); std::filesystem::path basePath = Files::pathFromQString(value.context.isEmpty() ? getUserContext() : value.context); mCfgMgr.processPath(path, basePath); return SettingValue{ Files::pathToQString(path), value.originalRepresentation, value.context };}
void Config::GameSettings::clear(){ mSettings.clear(); mContexts.clear(); mUserSettings.clear(); mDataDirs.clear(); mDataLocal.clear();}
QDataStream& Config::operator<<(QDataStream& out, const SettingValue& settingValue){ out << settingValue.value; out << settingValue.originalRepresentation; out << settingValue.context; return out;}
QDataStream& Config::operator>>(QDataStream& in, SettingValue& settingValue){ in >> settingValue.value; in >> settingValue.originalRepresentation; in >> settingValue.context; return in;}