1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-01-30 21:15:36 +00:00

Merge branch 'portable-launcher' into 'master'

Portable Launcher (plus a whole slew of bugs fixes for problems I found that I suspect aren't on the tracker)

Closes #6846

See merge request OpenMW/openmw!3925
This commit is contained in:
psi29a 2024-04-10 09:53:07 +00:00
commit 8037a6e765
34 changed files with 665 additions and 249 deletions

View file

@ -49,6 +49,7 @@
Bug #6754: Beast to Non-beast transformation mod is not working on OpenMW Bug #6754: Beast to Non-beast transformation mod is not working on OpenMW
Bug #6758: Main menu background video can be stopped by opening the options menu Bug #6758: Main menu background video can be stopped by opening the options menu
Bug #6807: Ultimate Galleon is not working properly Bug #6807: Ultimate Galleon is not working properly
Bug #6846: Launcher only works with default config paths
Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands
Bug #6894: Added item combines with equipped stack instead of creating a new unequipped stack Bug #6894: Added item combines with equipped stack instead of creating a new unequipped stack
Bug #6932: Creatures flee from my followers and we have to chase after them Bug #6932: Creatures flee from my followers and we have to chase after them

View file

@ -146,7 +146,9 @@ namespace
dataDirs.insert(dataDirs.begin(), resDir / "vfs"); dataDirs.insert(dataDirs.begin(), resDir / "vfs");
const Files::Collections fileCollections(dataDirs); const Files::Collections fileCollections(dataDirs);
const auto& archives = variables["fallback-archive"].as<StringsVector>(); const auto& archives = variables["fallback-archive"].as<StringsVector>();
const auto& contentFiles = variables["content"].as<StringsVector>(); StringsVector contentFiles{ "builtin.omwscripts" };
const auto& configContentFiles = variables["content"].as<StringsVector>();
contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end());
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap); Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);

View file

@ -142,7 +142,7 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C
ui.setupUi(this); ui.setupUi(this);
setObjectName("DataFilesPage"); setObjectName("DataFilesPage");
mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true); mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true);
const QString encoding = mGameSettings.value("encoding", "win1252"); const QString encoding = mGameSettings.value("encoding", { "win1252" }).value;
mSelector->setEncoding(encoding); mSelector->setEncoding(encoding);
QVector<std::pair<QString, QString>> languages = { { "English", tr("English") }, { "French", tr("French") }, QVector<std::pair<QString, QString>> languages = { { "English", tr("English") }, { "French", tr("French") },
@ -163,11 +163,11 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C
connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); }); connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); });
connect(ui.directoryUpButton, &QPushButton::released, this, [this]() { this->moveDirectory(-1); }); connect(ui.directoryUpButton, &QPushButton::released, this, [this]() { this->moveDirectory(-1); });
connect(ui.directoryDownButton, &QPushButton::released, this, [this]() { this->moveDirectory(1); }); connect(ui.directoryDownButton, &QPushButton::released, this, [this]() { this->moveDirectory(1); });
connect(ui.directoryRemoveButton, &QPushButton::released, this, [this]() { this->removeDirectory(); }); connect(ui.directoryRemoveButton, &QPushButton::released, this, &DataFilesPage::removeDirectory);
connect(ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveArchives(-1); }); connect(ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveArchives(-1); });
connect(ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchives(1); }); connect(ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchives(1); });
connect( connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories);
ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); }); connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives);
buildView(); buildView();
loadSettings(); loadSettings();
@ -271,65 +271,79 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
ui.archiveListWidget->clear(); ui.archiveListWidget->clear();
ui.directoryListWidget->clear(); ui.directoryListWidget->clear();
QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName); QList<Config::SettingValue> directories = mGameSettings.getDataDirs();
if (directories.isEmpty()) QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName);
directories = mGameSettings.getDataDirs(); if (!contentModelDirectories.isEmpty())
{
directories.erase(std::remove_if(directories.begin(), directories.end(),
[&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }),
directories.end());
for (const auto& dir : contentModelDirectories)
directories.push_back({ dir });
}
mDataLocal = mGameSettings.getDataLocal(); mDataLocal = mGameSettings.getDataLocal();
if (!mDataLocal.isEmpty()) if (!mDataLocal.isEmpty())
directories.insert(0, mDataLocal); directories.insert(0, { mDataLocal });
const auto& globalDataDir = mGameSettings.getGlobalDataDir(); const auto& resourcesVfs = mGameSettings.getResourcesVfs();
if (!globalDataDir.empty()) if (!resourcesVfs.isEmpty())
directories.insert(0, Files::pathToQString(globalDataDir)); directories.insert(0, { resourcesVfs });
std::unordered_set<QString> visitedDirectories; std::unordered_set<QString> visitedDirectories;
for (const QString& currentDir : directories) for (const Config::SettingValue& currentDir : directories)
{ {
// normalize user supplied directories: resolve symlink, convert to native separator, make absolute if (!visitedDirectories.insert(currentDir.value).second)
const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir)).canonicalPath();
if (!visitedDirectories.insert(canonicalDirPath).second)
continue; continue;
// add new achives files presents in current directory // add new achives files presents in current directory
addArchivesFromDir(currentDir); addArchivesFromDir(currentDir.value);
QString tooltip; QStringList tooltip;
// add content files presents in current directory // add content files presents in current directory
mSelector->addFiles(currentDir, mNewDataDirs.contains(canonicalDirPath)); mSelector->addFiles(currentDir.value, mNewDataDirs.contains(currentDir.value));
// add current directory to list // add current directory to list
ui.directoryListWidget->addItem(currentDir); ui.directoryListWidget->addItem(currentDir.originalRepresentation);
auto row = ui.directoryListWidget->count() - 1; auto row = ui.directoryListWidget->count() - 1;
auto* item = ui.directoryListWidget->item(row); auto* item = ui.directoryListWidget->item(row);
item->setData(Qt::UserRole, QVariant::fromValue(currentDir));
if (currentDir.value != currentDir.originalRepresentation)
tooltip << tr("Resolved as %1").arg(currentDir.value);
// Display new content with custom formatting // Display new content with custom formatting
if (mNewDataDirs.contains(canonicalDirPath)) if (mNewDataDirs.contains(currentDir.value))
{ {
tooltip += tr("Will be added to the current profile"); tooltip << tr("Will be added to the current profile");
QFont font = item->font(); QFont font = item->font();
font.setBold(true); font.setBold(true);
font.setItalic(true); font.setItalic(true);
item->setFont(font); item->setFont(font);
} }
// deactivate data-local and global data directory: they are always included // deactivate data-local and resources/vfs: they are always included
if (currentDir == mDataLocal || Files::pathFromQString(currentDir) == globalDataDir) // same for ones from non-user config files
if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs
|| !mGameSettings.isUserSetting(currentDir))
{ {
auto flags = item->flags(); auto flags = item->flags();
item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled));
if (currentDir.value == mDataLocal)
tooltip << tr("This is the data-local directory and cannot be disabled");
else if (currentDir.value == resourcesVfs)
tooltip << tr("This directory is part of OpenMW and cannot be disabled");
else
tooltip << tr("This directory is enabled in an openmw.cfg other than the user one");
} }
// Add a "data file" icon if the directory contains a content file // Add a "data file" icon if the directory contains a content file
if (mSelector->containsDataFiles(currentDir)) if (mSelector->containsDataFiles(currentDir.value))
{ {
item->setIcon(QIcon(":/images/openmw-plugin.png")); item->setIcon(QIcon(":/images/openmw-plugin.png"));
if (!tooltip.isEmpty())
tooltip += "\n";
tooltip += tr("Contains content file(s)"); tooltip << tr("Contains content file(s)");
} }
else else
{ {
@ -339,19 +353,26 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
auto emptyIcon = QIcon(pixmap); auto emptyIcon = QIcon(pixmap);
item->setIcon(emptyIcon); item->setIcon(emptyIcon);
} }
item->setToolTip(tooltip); item->setToolTip(tooltip.join('\n'));
} }
mSelector->sortFiles(); mSelector->sortFiles();
QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName); QList<Config::SettingValue> selectedArchives = mGameSettings.getArchiveList();
if (selectedArchives.isEmpty()) QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName);
selectedArchives = mGameSettings.getArchiveList(); if (contentModelSelectedArchives.isEmpty())
{
selectedArchives.erase(std::remove_if(selectedArchives.begin(), selectedArchives.end(),
[&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }),
selectedArchives.end());
for (const auto& dir : contentModelSelectedArchives)
selectedArchives.push_back({ dir });
}
// sort and tick BSA according to profile // sort and tick BSA according to profile
int row = 0; int row = 0;
for (const auto& archive : selectedArchives) for (const auto& archive : selectedArchives)
{ {
const auto match = ui.archiveListWidget->findItems(archive, Qt::MatchExactly); const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchExactly);
if (match.isEmpty()) if (match.isEmpty())
continue; continue;
const auto name = match[0]->text(); const auto name = match[0]->text();
@ -359,9 +380,25 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
ui.archiveListWidget->takeItem(oldrow); ui.archiveListWidget->takeItem(oldrow);
ui.archiveListWidget->insertItem(row, name); ui.archiveListWidget->insertItem(row, name);
ui.archiveListWidget->item(row)->setCheckState(Qt::Checked); ui.archiveListWidget->item(row)->setCheckState(Qt::Checked);
ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(archive));
if (!mGameSettings.isUserSetting(archive))
{
auto flags = ui.archiveListWidget->item(row)->flags();
ui.archiveListWidget->item(row)->setFlags(
flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled));
ui.archiveListWidget->item(row)->setToolTip(
tr("This archive is enabled in an openmw.cfg other than the user one"));
}
row++; row++;
} }
QStringList nonUserContent;
for (const auto& content : mGameSettings.getContentList())
{
if (!mGameSettings.isUserSetting(content))
nonUserContent.push_back(content.value);
}
mSelector->setNonUserContent(nonUserContent);
mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName));
} }
@ -389,7 +426,19 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile)
{ {
fileNames.append(item->fileName()); fileNames.append(item->fileName());
} }
mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); QStringList dirNames;
for (const auto& dir : dirList)
{
if (mGameSettings.isUserSetting(dir))
dirNames.push_back(dir.originalRepresentation);
}
QStringList archiveNames;
for (const auto& archive : selectedArchivePaths())
{
if (mGameSettings.isUserSetting(archive))
archiveNames.push_back(archive.originalRepresentation);
}
mLauncherSettings.setContentList(profileName, dirNames, archiveNames, fileNames);
mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames);
QString language(mSelector->languageBox()->currentData().toString()); QString language(mSelector->languageBox()->currentData().toString());
@ -398,38 +447,38 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile)
if (language == QLatin1String("Polish")) if (language == QLatin1String("Polish"))
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); mGameSettings.setValue(QLatin1String("encoding"), { "win1250" });
} }
else if (language == QLatin1String("Russian")) else if (language == QLatin1String("Russian"))
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); mGameSettings.setValue(QLatin1String("encoding"), { "win1251" });
} }
else else
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); mGameSettings.setValue(QLatin1String("encoding"), { "win1252" });
} }
} }
QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const QList<Config::SettingValue> Launcher::DataFilesPage::selectedDirectoriesPaths() const
{ {
QStringList dirList; QList<Config::SettingValue> dirList;
for (int i = 0; i < ui.directoryListWidget->count(); ++i) for (int i = 0; i < ui.directoryListWidget->count(); ++i)
{ {
const QListWidgetItem* item = ui.directoryListWidget->item(i); const QListWidgetItem* item = ui.directoryListWidget->item(i);
if (item->flags() & Qt::ItemIsEnabled) if (item->flags() & Qt::ItemIsEnabled)
dirList.append(item->text()); dirList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
} }
return dirList; return dirList;
} }
QStringList Launcher::DataFilesPage::selectedArchivePaths() const QList<Config::SettingValue> Launcher::DataFilesPage::selectedArchivePaths() const
{ {
QStringList archiveList; QList<Config::SettingValue> archiveList;
for (int i = 0; i < ui.archiveListWidget->count(); ++i) for (int i = 0; i < ui.archiveListWidget->count(); ++i)
{ {
const QListWidgetItem* item = ui.archiveListWidget->item(i); const QListWidgetItem* item = ui.archiveListWidget->item(i);
if (item->checkState() == Qt::Checked) if (item->checkState() == Qt::Checked)
archiveList.append(item->text()); archiveList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
} }
return archiveList; return archiveList;
} }
@ -583,7 +632,20 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered()
if (profile.isEmpty()) if (profile.isEmpty())
return; return;
mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths()); const auto& dirList = selectedDirectoriesPaths();
QStringList dirNames;
for (const auto& dir : dirList)
{
if (mGameSettings.isUserSetting(dir))
dirNames.push_back(dir.originalRepresentation);
}
QStringList archiveNames;
for (const auto& archive : selectedArchivePaths())
{
if (mGameSettings.isUserSetting(archive))
archiveNames.push_back(archive.originalRepresentation);
}
mLauncherSettings.setContentList(profile, dirNames, archiveNames, selectedFilePaths());
addProfile(profile, true); addProfile(profile, true);
} }
@ -650,6 +712,9 @@ void Launcher::DataFilesPage::addSubdirectories(bool append)
if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty()) if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty())
return; return;
ui.directoryListWidget->addItem(rootPath); ui.directoryListWidget->addItem(rootPath);
auto row = ui.directoryListWidget->count() - 1;
auto* item = ui.directoryListWidget->item(row);
item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ rootPath }));
mNewDataDirs.push_back(rootPath); mNewDataDirs.push_back(rootPath);
refreshDataFilesView(); refreshDataFilesView();
return; return;
@ -679,8 +744,11 @@ void Launcher::DataFilesPage::addSubdirectories(bool append)
const auto* dir = select.dirListWidget->item(i); const auto* dir = select.dirListWidget->item(i);
if (dir->checkState() == Qt::Checked) if (dir->checkState() == Qt::Checked)
{ {
ui.directoryListWidget->insertItem(selectedRow++, dir->text()); ui.directoryListWidget->insertItem(selectedRow, dir->text());
auto* item = ui.directoryListWidget->item(selectedRow);
item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ dir->text() }));
mNewDataDirs.push_back(dir->text()); mNewDataDirs.push_back(dir->text());
++selectedRow;
} }
} }
@ -702,6 +770,21 @@ void Launcher::DataFilesPage::sortDirectories()
} }
} }
void Launcher::DataFilesPage::sortArchives()
{
// Ensure disabled entries (aka ones from non-user config files) are always at the top.
for (auto i = 1; i < ui.archiveListWidget->count(); ++i)
{
if (!(ui.archiveListWidget->item(i)->flags() & Qt::ItemIsEnabled)
&& (ui.archiveListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled))
{
const auto item = ui.archiveListWidget->takeItem(i);
ui.archiveListWidget->insertItem(i - 1, item);
ui.archiveListWidget->setCurrentRow(i);
}
}
}
void Launcher::DataFilesPage::moveDirectory(int step) void Launcher::DataFilesPage::moveDirectory(int step)
{ {
int selectedRow = ui.directoryListWidget->currentRow(); int selectedRow = ui.directoryListWidget->currentRow();
@ -784,9 +867,8 @@ bool Launcher::DataFilesPage::moveArchive(QListWidgetItem* listItem, int step)
if (selectedRow == -1 || newRow < 0 || newRow > ui.archiveListWidget->count() - 1) if (selectedRow == -1 || newRow < 0 || newRow > ui.archiveListWidget->count() - 1)
return false; return false;
const QListWidgetItem* item = ui.archiveListWidget->takeItem(selectedRow); QListWidgetItem* item = ui.archiveListWidget->takeItem(selectedRow);
ui.archiveListWidget->insertItem(newRow, item);
addArchive(item->text(), item->checkState(), newRow);
ui.archiveListWidget->setCurrentRow(newRow); ui.archiveListWidget->setCurrentRow(newRow);
return true; return true;
} }
@ -797,6 +879,7 @@ void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState sel
row = ui.archiveListWidget->count(); row = ui.archiveListWidget->count();
ui.archiveListWidget->insertItem(row, name); ui.archiveListWidget->insertItem(row, name);
ui.archiveListWidget->item(row)->setCheckState(selected); ui.archiveListWidget->item(row)->setCheckState(selected);
ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ name }));
if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ??? if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ???
{ {
auto item = ui.archiveListWidget->item(row); auto item = ui.archiveListWidget->item(row);

View file

@ -25,6 +25,7 @@ namespace ContentSelectorView
namespace Config namespace Config
{ {
class GameSettings; class GameSettings;
struct SettingValue;
class LauncherSettings; class LauncherSettings;
} }
@ -73,6 +74,7 @@ namespace Launcher
void updateCloneProfileOkButton(const QString& text); void updateCloneProfileOkButton(const QString& text);
void addSubdirectories(bool append); void addSubdirectories(bool append);
void sortDirectories(); void sortDirectories();
void sortArchives();
void removeDirectory(); void removeDirectory();
void moveArchives(int step); void moveArchives(int step);
void moveDirectory(int step); void moveDirectory(int step);
@ -146,8 +148,8 @@ namespace Launcher
* @return the file paths of all selected content files * @return the file paths of all selected content files
*/ */
QStringList selectedFilePaths() const; QStringList selectedFilePaths() const;
QStringList selectedArchivePaths() const; QList<Config::SettingValue> selectedArchivePaths() const;
QStringList selectedDirectoriesPaths() const; QList<Config::SettingValue> selectedDirectoriesPaths() const;
}; };
} }
#endif #endif

View file

@ -37,9 +37,9 @@ Launcher::ImportPage::ImportPage(const Files::ConfigurationManager& cfg, Config:
// Detect Morrowind configuration files // Detect Morrowind configuration files
QStringList iniPaths; QStringList iniPaths;
for (const QString& path : mGameSettings.getDataDirs()) for (const auto& path : mGameSettings.getDataDirs())
{ {
QDir dir(path); QDir dir(path.value);
dir.setPath(dir.canonicalPath()); // Resolve symlinks dir.setPath(dir.canonicalPath()); // Resolve symlinks
if (dir.exists(QString("Morrowind.ini"))) if (dir.exists(QString("Morrowind.ini")))
@ -125,7 +125,7 @@ void Launcher::ImportPage::on_importerButton_clicked()
arguments.append(QString("--fonts")); arguments.append(QString("--fonts"));
arguments.append(QString("--encoding")); arguments.append(QString("--encoding"));
arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); arguments.append(mGameSettings.value(QString("encoding"), { "win1252" }).value);
arguments.append(QString("--ini")); arguments.append(QString("--ini"));
arguments.append(settingsComboBox->currentText()); arguments.append(settingsComboBox->currentText());
arguments.append(QString("--cfg")); arguments.append(QString("--cfg"));

View file

@ -292,7 +292,7 @@ bool Launcher::MainDialog::setupLauncherSettings()
if (!QFile::exists(path)) if (!QFile::exists(path))
return true; return true;
Log(Debug::Verbose) << "Loading config file: " << path.toUtf8().constData(); Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData();
QFile file(path); QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
@ -320,7 +320,7 @@ bool Launcher::MainDialog::setupGameSettings()
QFile file; QFile file;
auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, bool), auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, const QString&, bool),
bool ignoreContent = false) -> std::optional<bool> { bool ignoreContent = false) -> std::optional<bool> {
file.setFileName(path); file.setFileName(path);
if (file.exists()) if (file.exists())
@ -337,7 +337,7 @@ bool Launcher::MainDialog::setupGameSettings()
QTextStream stream(&file); QTextStream stream(&file);
Misc::ensureUtf8Encoding(stream); Misc::ensureUtf8Encoding(stream);
(mGameSettings.*reader)(stream, ignoreContent); (mGameSettings.*reader)(stream, QFileInfo(path).dir().path(), ignoreContent);
file.close(); file.close();
return true; return true;
} }
@ -349,29 +349,24 @@ bool Launcher::MainDialog::setupGameSettings()
if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readUserFile)) if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readUserFile))
return false; return false;
// Now the rest - priority: user > local > global for (const auto& path : Files::getActiveConfigPathsQString(mCfgMgr))
if (auto result = loadFile(Files::getLocalConfigPathQString(mCfgMgr), &Config::GameSettings::readFile, true))
{ {
// Load global if local wasn't found Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData();
if (!*result && !loadFile(Files::getGlobalConfigPathQString(mCfgMgr), &Config::GameSettings::readFile, true)) if (!loadFile(path, &Config::GameSettings::readFile))
return false; return false;
} }
else
return false;
if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readFile))
return false;
return true; return true;
} }
bool Launcher::MainDialog::setupGameData() bool Launcher::MainDialog::setupGameData()
{ {
QStringList dataDirs; bool foundData = false;
// Check if the paths actually contain data files // Check if the paths actually contain data files
for (const QString& path3 : mGameSettings.getDataDirs()) for (const auto& path3 : mGameSettings.getDataDirs())
{ {
QDir dir(path3); QDir dir(path3.value);
QStringList filters; QStringList filters;
filters << "*.esp" filters << "*.esp"
<< "*.esm" << "*.esm"
@ -379,10 +374,13 @@ bool Launcher::MainDialog::setupGameData()
<< "*.omwaddon"; << "*.omwaddon";
if (!dir.entryList(filters).isEmpty()) if (!dir.entryList(filters).isEmpty())
dataDirs.append(path3); {
foundData = true;
break;
}
} }
if (dataDirs.isEmpty()) if (!foundData)
{ {
QMessageBox msgBox; QMessageBox msgBox;
msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setWindowTitle(tr("Error detecting Morrowind installation"));

View file

@ -345,7 +345,7 @@ bool Launcher::SettingsPage::loadSettings()
{ {
loadSettingBool(Settings::input().mGrabCursor, *grabCursorCheckBox); loadSettingBool(Settings::input().mGrabCursor, *grabCursorCheckBox);
bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; bool skipMenu = mGameSettings.value("skip-menu").value.toInt() == 1;
if (skipMenu) if (skipMenu)
{ {
skipMenuCheckBox->setCheckState(Qt::Checked); skipMenuCheckBox->setCheckState(Qt::Checked);
@ -353,8 +353,8 @@ bool Launcher::SettingsPage::loadSettings()
startDefaultCharacterAtLabel->setEnabled(skipMenu); startDefaultCharacterAtLabel->setEnabled(skipMenu);
startDefaultCharacterAtField->setEnabled(skipMenu); startDefaultCharacterAtField->setEnabled(skipMenu);
startDefaultCharacterAtField->setText(mGameSettings.value("start")); startDefaultCharacterAtField->setText(mGameSettings.value("start").value);
runScriptAfterStartupField->setText(mGameSettings.value("script-run")); runScriptAfterStartupField->setText(mGameSettings.value("script-run").value);
} }
return true; return true;
} }
@ -541,17 +541,17 @@ void Launcher::SettingsPage::saveSettings()
saveSettingBool(*grabCursorCheckBox, Settings::input().mGrabCursor); saveSettingBool(*grabCursorCheckBox, Settings::input().mGrabCursor);
int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked;
if (skipMenu != mGameSettings.value("skip-menu").toInt()) if (skipMenu != mGameSettings.value("skip-menu").value.toInt())
mGameSettings.setValue("skip-menu", QString::number(skipMenu)); mGameSettings.setValue("skip-menu", { QString::number(skipMenu) });
QString startCell = startDefaultCharacterAtField->text(); QString startCell = startDefaultCharacterAtField->text();
if (startCell != mGameSettings.value("start")) if (startCell != mGameSettings.value("start").value)
{ {
mGameSettings.setValue("start", startCell); mGameSettings.setValue("start", { startCell });
} }
QString scriptRun = runScriptAfterStartupField->text(); QString scriptRun = runScriptAfterStartupField->text();
if (scriptRun != mGameSettings.value("script-run")) if (scriptRun != mGameSettings.value("script-run").value)
mGameSettings.setValue("script-run", scriptRun); mGameSettings.setValue("script-run", { scriptRun });
} }
} }

View file

@ -165,7 +165,9 @@ namespace NavMeshTool
dataDirs.insert(dataDirs.begin(), resDir / "vfs"); dataDirs.insert(dataDirs.begin(), resDir / "vfs");
const Files::Collections fileCollections(dataDirs); const Files::Collections fileCollections(dataDirs);
const auto& archives = variables["fallback-archive"].as<StringsVector>(); const auto& archives = variables["fallback-archive"].as<StringsVector>();
const auto& contentFiles = variables["content"].as<StringsVector>(); StringsVector contentFiles{ "builtin.omwscripts" };
const auto& configContentFiles = variables["content"].as<StringsVector>();
contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end());
const std::size_t threadsNumber = variables["threads"].as<std::size_t>(); const std::size_t threadsNumber = variables["threads"].as<std::size_t>();
if (threadsNumber < 1) if (threadsNumber < 1)

View file

@ -93,7 +93,6 @@ void CSMDoc::Runner::start(bool delayed)
arguments << "--data=\"" + Files::pathToQString(mProjectPath.parent_path()) + "\""; arguments << "--data=\"" + Files::pathToQString(mProjectPath.parent_path()) + "\"";
arguments << "--replace=content"; arguments << "--replace=content";
arguments << "--content=builtin.omwscripts";
for (const auto& mContentFile : mContentFiles) for (const auto& mContentFile : mContentFiles)
{ {

View file

@ -109,7 +109,8 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati
Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting...";
return false; return false;
} }
std::set<std::string> contentDedupe; engine.addContentFile("builtin.omwscripts");
std::set<std::string> contentDedupe{ "builtin.omwscripts" };
for (const auto& contentFile : content) for (const auto& contentFile : content)
{ {
if (!contentDedupe.insert(contentFile).second) if (!contentDedupe.insert(contentFile).second)

View file

@ -18,7 +18,7 @@ Wizard::InstallationPage::InstallationPage(QWidget* parent, Config::GameSettings
mFinished = false; mFinished = false;
mThread = std::make_unique<QThread>(); mThread = std::make_unique<QThread>();
mUnshield = std::make_unique<UnshieldWorker>(mGameSettings.value("morrowind-bsa-filesize").toLongLong()); mUnshield = std::make_unique<UnshieldWorker>(mGameSettings.value("morrowind-bsa-filesize").value.toLongLong());
mUnshield->moveToThread(mThread.get()); mUnshield->moveToThread(mThread.get());
connect(mThread.get(), &QThread::started, mUnshield.get(), &UnshieldWorker::extract); connect(mThread.get(), &QThread::started, mUnshield.get(), &UnshieldWorker::extract);

View file

@ -47,7 +47,7 @@ int main(int argc, char* argv[])
l10n::installQtTranslations(app, "wizard", resourcesPath); l10n::installQtTranslations(app, "wizard", resourcesPath);
Wizard::MainWizard wizard; Wizard::MainWizard wizard(std::move(configurationManager));
wizard.show(); wizard.show();
return app.exec(); return app.exec();

View file

@ -24,11 +24,14 @@
#include "installationpage.hpp" #include "installationpage.hpp"
#endif #endif
#include <algorithm>
using namespace Process; using namespace Process;
Wizard::MainWizard::MainWizard(QWidget* parent) Wizard::MainWizard::MainWizard(Files::ConfigurationManager&& cfgMgr, QWidget* parent)
: QWizard(parent) : QWizard(parent)
, mInstallations() , mInstallations()
, mCfgMgr(cfgMgr)
, mError(false) , mError(false)
, mGameSettings(mCfgMgr) , mGameSettings(mCfgMgr)
{ {
@ -166,16 +169,13 @@ void Wizard::MainWizard::setupGameSettings()
QTextStream stream(&file); QTextStream stream(&file);
Misc::ensureUtf8Encoding(stream); Misc::ensureUtf8Encoding(stream);
mGameSettings.readUserFile(stream); mGameSettings.readUserFile(stream, QFileInfo(path).dir().path());
} }
file.close(); file.close();
// Now the rest // Now the rest
QStringList paths; QStringList paths = Files::getActiveConfigPathsQString(mCfgMgr);
paths.append(Files::getUserConfigPathQString(mCfgMgr));
paths.append(QLatin1String("openmw.cfg"));
paths.append(Files::getGlobalConfigPathQString(mCfgMgr));
for (const QString& path2 : paths) for (const QString& path2 : paths)
{ {
@ -198,7 +198,7 @@ void Wizard::MainWizard::setupGameSettings()
QTextStream stream(&file); QTextStream stream(&file);
Misc::ensureUtf8Encoding(stream); Misc::ensureUtf8Encoding(stream);
mGameSettings.readFile(stream); mGameSettings.readFile(stream, QFileInfo(path2).dir().path());
} }
file.close(); file.close();
} }
@ -243,11 +243,11 @@ void Wizard::MainWizard::setupLauncherSettings()
void Wizard::MainWizard::setupInstallations() void Wizard::MainWizard::setupInstallations()
{ {
// Check if the paths actually contain a Morrowind installation // Check if the paths actually contain a Morrowind installation
for (const QString& path : mGameSettings.getDataDirs()) for (const auto& path : mGameSettings.getDataDirs())
{ {
if (findFiles(QLatin1String("Morrowind"), path)) if (findFiles(QLatin1String("Morrowind"), path.value))
addInstallation(path); addInstallation(path.value);
} }
} }
@ -334,10 +334,12 @@ void Wizard::MainWizard::addInstallation(const QString& path)
mInstallations.insert(QDir::toNativeSeparators(path), install); mInstallations.insert(QDir::toNativeSeparators(path), install);
// Add it to the openmw.cfg too // Add it to the openmw.cfg too
if (!mGameSettings.getDataDirs().contains(path)) const auto& dataDirs = mGameSettings.getDataDirs();
if (std::none_of(
dataDirs.begin(), dataDirs.end(), [&](const Config::SettingValue& dir) { return dir.value == path; }))
{ {
mGameSettings.setMultiValue(QLatin1String("data"), path); mGameSettings.setMultiValue(QLatin1String("data"), { path });
mGameSettings.addDataDir(path); mGameSettings.addDataDir({ path });
} }
} }
@ -396,15 +398,15 @@ void Wizard::MainWizard::writeSettings()
if (language == QLatin1String("Polish")) if (language == QLatin1String("Polish"))
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); mGameSettings.setValue(QLatin1String("encoding"), { "win1250" });
} }
else if (language == QLatin1String("Russian")) else if (language == QLatin1String("Russian"))
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); mGameSettings.setValue(QLatin1String("encoding"), { "win1251" });
} }
else else
{ {
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); mGameSettings.setValue(QLatin1String("encoding"), { "win1252" });
} }
// Write the installation path so that openmw can find them // Write the installation path so that openmw can find them
@ -412,7 +414,7 @@ void Wizard::MainWizard::writeSettings()
// Make sure the installation path is the last data= entry // Make sure the installation path is the last data= entry
mGameSettings.removeDataDir(path); mGameSettings.removeDataDir(path);
mGameSettings.addDataDir(path); mGameSettings.addDataDir({ path });
QString userPath(Files::pathToQString(mCfgMgr.getUserConfigPath())); QString userPath(Files::pathToQString(mCfgMgr.getUserConfigPath()));
QDir dir(userPath); QDir dir(userPath);

View file

@ -45,7 +45,7 @@ namespace Wizard
Page_Conclusion Page_Conclusion
}; };
MainWizard(QWidget* parent = nullptr); MainWizard(Files::ConfigurationManager&& cfgMgr, QWidget* parent = nullptr);
~MainWizard() override; ~MainWizard() override;
bool findFiles(const QString& name, const QString& path); bool findFiles(const QString& name, const QString& path);

View file

@ -13,7 +13,8 @@ const char Config::GameSettings::sDirectoryKey[] = "data";
namespace namespace
{ {
QStringList reverse(QStringList values) template <typename T>
QList<T> reverse(QList<T> values)
{ {
std::reverse(values.begin(), values.end()); std::reverse(values.begin(), values.end());
return values; return values;
@ -23,83 +24,111 @@ namespace
Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg)
: mCfgMgr(cfg) : mCfgMgr(cfg)
{ {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// this needs calling once so Qt can see its stream operators, which it needs when dragging and dropping
// it's automatic with Qt 6
qRegisterMetaTypeStreamOperators<SettingValue>("Config::SettingValue");
#endif
} }
void Config::GameSettings::validatePaths() void Config::GameSettings::validatePaths()
{ {
QStringList paths = mSettings.values(QString("data")); QList<SettingValue> paths = mSettings.values(QString("data"));
Files::PathContainer dataDirs;
for (const QString& path : paths)
{
dataDirs.emplace_back(Files::pathFromQString(path));
}
// Parse the data dirs to convert the tokenized paths
mCfgMgr.processPaths(dataDirs, /*basePath=*/"");
mDataDirs.clear(); mDataDirs.clear();
for (const auto& dataDir : dataDirs) for (const auto& dataDir : paths)
{ {
if (is_directory(dataDir)) if (QDir(dataDir.value).exists())
mDataDirs.append(Files::pathToQString(dataDir)); {
SettingValue copy = dataDir;
copy.value = QDir(dataDir.value).canonicalPath();
mDataDirs.append(copy);
}
} }
// Do the same for data-local // Do the same for data-local
QString local = mSettings.value(QString("data-local")); const QString& local = mSettings.value(QString("data-local")).value;
if (local.length() && local.at(0) == QChar('\"'))
if (!local.isEmpty() && QDir(local).exists())
{ {
local.remove(0, 1); mDataLocal = QDir(local).canonicalPath();
local.chop(1);
}
if (local.isEmpty())
return;
dataDirs.clear();
dataDirs.emplace_back(Files::pathFromQString(local));
mCfgMgr.processPaths(dataDirs, /*basePath=*/"");
if (!dataDirs.empty())
{
const auto& path = dataDirs.front();
if (is_directory(path))
mDataLocal = Files::pathToQString(path);
} }
} }
std::filesystem::path Config::GameSettings::getGlobalDataDir() const QString Config::GameSettings::getResourcesVfs() const
{ {
// global data dir may not exists if OpenMW is not installed (ie if run from build directory) QString resources = mSettings.value(QString("resources"), { "./resources", "", "" }).value;
const auto& path = mCfgMgr.getGlobalDataPath(); resources += "/vfs";
if (std::filesystem::exists(path)) return QFileInfo(resources).canonicalFilePath();
return std::filesystem::canonical(path);
return {};
} }
QStringList Config::GameSettings::values(const QString& key, const QStringList& defaultValues) const QList<Config::SettingValue> Config::GameSettings::values(
const QString& key, const QList<SettingValue>& defaultValues) const
{ {
if (!mSettings.values(key).isEmpty()) if (!mSettings.values(key).isEmpty())
return mSettings.values(key); return mSettings.values(key);
return defaultValues; return defaultValues;
} }
bool Config::GameSettings::readFile(QTextStream& stream, bool ignoreContent) bool Config::GameSettings::containsValue(const QString& key, const QString& value) const
{ {
return readFile(stream, mSettings, ignoreContent); auto [itr, end] = mSettings.equal_range(key);
while (itr != end)
{
if (itr->value == value)
return true;
++itr;
}
return false;
} }
bool Config::GameSettings::readUserFile(QTextStream& stream, bool ignoreContent) bool Config::GameSettings::readFile(QTextStream& stream, const QString& context, bool ignoreContent)
{ {
return readFile(stream, mUserSettings, ignoreContent); if (readFile(stream, mSettings, context, ignoreContent))
{
mContexts.push_back(context);
return true;
}
return false;
} }
bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap<QString, QString>& settings, bool ignoreContent) bool Config::GameSettings::readUserFile(QTextStream& stream, const QString& context, bool ignoreContent)
{ {
QMultiMap<QString, QString> cache; 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*(.+)$"); 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("script-blacklist")
|| key == QLatin1String("fallback"))
settings.remove(key);
}
}
stream.seek(initialPos);
while (!stream.atEnd()) while (!stream.atEnd())
{ {
QString line = stream.readLine(); QString line = stream.readLine();
@ -111,27 +140,32 @@ bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap<QString, QStr
if (match.hasMatch()) if (match.hasMatch())
{ {
QString key = match.captured(1).trimmed(); QString key = match.captured(1).trimmed();
QString value = match.captured(2).trimmed(); SettingValue value{ match.captured(2).trimmed(), value.value, context };
// Don't remove composing entries // Don't remove composing entries
if (key != QLatin1String("data") && key != QLatin1String("fallback-archive") if (key != QLatin1String("config") && key != QLatin1String("replace") && key != QLatin1String("data")
&& key != QLatin1String("content") && key != QLatin1String("groundcover") && key != QLatin1String("fallback-archive") && key != QLatin1String("content")
&& key != QLatin1String("script-blacklist")) && key != QLatin1String("groundcover") && key != QLatin1String("script-blacklist")
&& key != QLatin1String("fallback"))
settings.remove(key); settings.remove(key);
if (key == QLatin1String("data") || key == QLatin1String("data-local") || key == QLatin1String("resources") if (key == QLatin1String("config") || key == QLatin1String("user-data") || key == QLatin1String("resources")
|| key == QLatin1String("data") || key == QLatin1String("data-local")
|| key == QLatin1String("load-savegame")) || key == QLatin1String("load-savegame"))
{ {
// Path line (e.g. 'data=...'), so needs processing to deal with ampersands and quotes // 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 calling those functions did not // The following is based on boost::io::detail::quoted_manip.hpp, but we don't actually use
// work as there are too may QStrings involved // 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 delim = '\"';
QChar escape = '&'; QChar escape = '&';
if (value.at(0) == delim) if (value.value.at(0) == delim)
{ {
QString valueOriginal = value; QString valueOriginal = value.value;
value = ""; value.value = "";
for (QString::const_iterator it = valueOriginal.begin() + 1; it != valueOriginal.end(); ++it) for (QString::const_iterator it = valueOriginal.begin() + 1; it != valueOriginal.end(); ++it)
{ {
@ -139,17 +173,31 @@ bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap<QString, QStr
++it; ++it;
else if (*it == delim) else if (*it == delim)
break; break;
value += *it; value.value += *it;
} }
value.originalRepresentation = value.value;
} }
std::filesystem::path path = Files::pathFromQString(value.value);
mCfgMgr.processPath(path, Files::pathFromQString(context));
value.value = Files::pathToQString(path);
} }
if (ignoreContent && (key == QLatin1String("content") || key == QLatin1String("data"))) if (ignoreContent && (key == QLatin1String("content") || key == QLatin1String("data")))
continue; continue;
QStringList values = cache.values(key); QList<SettingValue> values = cache.values(key);
values.append(settings.values(key)); values.append(settings.values(key));
if (!values.contains(value)) bool exists = false;
for (const auto& existingValue : values)
{
if (existingValue.value == value.value)
{
exists = true;
break;
}
}
if (!exists)
{ {
cache.insert(key, value); cache.insert(key, value);
} }
@ -184,15 +232,16 @@ bool Config::GameSettings::writeFile(QTextStream& stream)
// Boost-like quoting rules but internally stores as a std::filesystem::path. // 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 // Caution: This is intentional behaviour to duplicate how Boost and what we replaced it with worked, and we
// rely on that. // rely on that.
if (i.key() == QLatin1String("data") || i.key() == QLatin1String("data-local") if (i.key() == QLatin1String("config") || i.key() == QLatin1String("user-data")
|| i.key() == QLatin1String("resources") || i.key() == QLatin1String("load-savegame")) || i.key() == QLatin1String("resources") || i.key() == QLatin1String("data")
|| i.key() == QLatin1String("data-local") || i.key() == QLatin1String("load-savegame"))
{ {
stream << i.key() << "="; stream << i.key() << "=";
// Equivalent to stream << std::quoted(i.value(), '"', '&'), which won't work on QStrings. // Equivalent to stream << std::quoted(i.value(), '"', '&'), which won't work on QStrings.
QChar delim = '\"'; QChar delim = '\"';
QChar escape = '&'; QChar escape = '&';
QString string = i.value(); QString string = i.value().originalRepresentation;
stream << delim; stream << delim;
for (auto& it : string) for (auto& it : string)
@ -207,7 +256,7 @@ bool Config::GameSettings::writeFile(QTextStream& stream)
continue; continue;
} }
stream << i.key() << "=" << i.value() << "\n"; stream << i.key() << "=" << i.value().originalRepresentation << "\n";
} }
return true; return true;
@ -362,10 +411,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
*iter = QString(); // assume no match *iter = QString(); // assume no match
QString key = match.captured(1); QString key = match.captured(1);
QString keyVal = match.captured(1) + "=" + match.captured(2); QString keyVal = match.captured(1) + "=" + match.captured(2);
QMultiMap<QString, QString>::const_iterator i = mUserSettings.find(key); QMultiMap<QString, SettingValue>::const_iterator i = mUserSettings.find(key);
while (i != mUserSettings.end() && i.key() == key) while (i != mUserSettings.end() && i.key() == key)
{ {
QString settingLine = i.key() + "=" + i.value(); // todo: does this need to handle paths?
QString settingLine = i.key() + "=" + i.value().originalRepresentation;
QRegularExpressionMatch keyMatch = settingRegex.match(settingLine); QRegularExpressionMatch keyMatch = settingRegex.match(settingLine);
if (keyMatch.hasMatch()) if (keyMatch.hasMatch())
{ {
@ -408,15 +458,16 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
// Boost-like quoting rules but internally stores as a std::filesystem::path. // 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 // Caution: This is intentional behaviour to duplicate how Boost and what we replaced it with worked, and we
// rely on that. // rely on that.
if (it.key() == QLatin1String("data") || it.key() == QLatin1String("data-local") if (it.key() == QLatin1String("config") || it.key() == QLatin1String("user-data")
|| it.key() == QLatin1String("resources") || it.key() == QLatin1String("load-savegame")) || it.key() == QLatin1String("resources") || it.key() == QLatin1String("data")
|| it.key() == QLatin1String("data-local") || it.key() == QLatin1String("load-savegame"))
{ {
settingLine = it.key() + "="; settingLine = it.key() + "=";
// Equivalent to settingLine += std::quoted(it.value(), '"', '&'), which won't work on QStrings. // Equivalent to settingLine += std::quoted(it.value(), '"', '&'), which won't work on QStrings.
QChar delim = '\"'; QChar delim = '\"';
QChar escape = '&'; QChar escape = '&';
QString string = it.value(); QString string = it.value().originalRepresentation;
settingLine += delim; settingLine += delim;
for (auto& iter : string) for (auto& iter : string)
@ -428,7 +479,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
settingLine += delim; settingLine += delim;
} }
else else
settingLine = it.key() + "=" + it.value(); settingLine = it.key() + "=" + it.value().originalRepresentation;
QRegularExpressionMatch match = settingRegex.match(settingLine); QRegularExpressionMatch match = settingRegex.match(settingLine);
if (match.hasMatch()) if (match.hasMatch())
@ -487,11 +538,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
bool Config::GameSettings::hasMaster() bool Config::GameSettings::hasMaster()
{ {
bool result = false; bool result = false;
QStringList content = mSettings.values(QString(Config::GameSettings::sContentKey)); QList<SettingValue> content = mSettings.values(QString(Config::GameSettings::sContentKey));
for (int i = 0; i < content.count(); ++i) for (int i = 0; i < content.count(); ++i)
{ {
if (content.at(i).endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive) if (content.at(i).value.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive)
|| content.at(i).endsWith(QLatin1String(".esm"), Qt::CaseInsensitive)) || content.at(i).value.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive))
{ {
result = true; result = true;
break; break;
@ -502,40 +553,62 @@ bool Config::GameSettings::hasMaster()
} }
void Config::GameSettings::setContentList( void Config::GameSettings::setContentList(
const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames) const QList<SettingValue>& dirNames, const QList<SettingValue>& archiveNames, const QStringList& fileNames)
{ {
auto const reset = [this](const char* key, const QStringList& list) { remove(sDirectoryKey);
remove(key); for (auto const& item : dirNames)
for (auto const& item : list) setMultiValue(sDirectoryKey, item);
setMultiValue(key, item); remove(sArchiveKey);
}; for (auto const& item : archiveNames)
setMultiValue(sArchiveKey, item);
reset(sDirectoryKey, dirNames); remove(sContentKey);
reset(sArchiveKey, archiveNames); for (auto const& item : fileNames)
reset(sContentKey, fileNames); setMultiValue(sContentKey, { item });
} }
QStringList Config::GameSettings::getDataDirs() const QList<Config::SettingValue> Config::GameSettings::getDataDirs() const
{ {
return reverse(mDataDirs); return reverse(mDataDirs);
} }
QStringList Config::GameSettings::getArchiveList() const QList<Config::SettingValue> Config::GameSettings::getArchiveList() const
{ {
// QMap returns multiple rows in LIFO order, so need to reverse // QMap returns multiple rows in LIFO order, so need to reverse
return reverse(values(sArchiveKey)); return reverse(values(sArchiveKey));
} }
QStringList Config::GameSettings::getContentList() const QList<Config::SettingValue> Config::GameSettings::getContentList() const
{ {
// QMap returns multiple rows in LIFO order, so need to reverse // QMap returns multiple rows in LIFO order, so need to reverse
return reverse(values(sContentKey)); return reverse(values(sContentKey));
} }
bool Config::GameSettings::isUserSetting(const SettingValue& settingValue) const
{
return settingValue.context.isEmpty() || settingValue.context == getUserContext();
}
void Config::GameSettings::clear() void Config::GameSettings::clear()
{ {
mSettings.clear(); mSettings.clear();
mContexts.clear();
mUserSettings.clear(); mUserSettings.clear();
mDataDirs.clear(); mDataDirs.clear();
mDataLocal.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;
}

View file

@ -17,33 +17,48 @@ namespace Files
namespace Config namespace Config
{ {
struct SettingValue
{
QString value = "";
// value as found in openmw.cfg, e.g. relative path with ?slug?
QString originalRepresentation = value;
// path of openmw.cfg, e.g. to resolve relative paths
QString context = "";
friend std::strong_ordering operator<=>(const SettingValue&, const SettingValue&) = default;
};
class GameSettings class GameSettings
{ {
public: public:
explicit GameSettings(const Files::ConfigurationManager& cfg); explicit GameSettings(const Files::ConfigurationManager& cfg);
inline QString value(const QString& key, const QString& defaultValue = QString()) inline SettingValue value(const QString& key, const SettingValue& defaultValue = {})
{ {
return mSettings.value(key).isEmpty() ? defaultValue : mSettings.value(key); return mSettings.contains(key) ? mSettings.value(key) : defaultValue;
} }
inline void setValue(const QString& key, const QString& value) inline void setValue(const QString& key, const SettingValue& value)
{ {
mSettings.remove(key); mSettings.remove(key);
mSettings.insert(key, value); mSettings.insert(key, value);
mUserSettings.remove(key); mUserSettings.remove(key);
mUserSettings.insert(key, value); if (isUserSetting(value))
mUserSettings.insert(key, value);
} }
inline void setMultiValue(const QString& key, const QString& value) inline void setMultiValue(const QString& key, const SettingValue& value)
{ {
QStringList values = mSettings.values(key); QList<SettingValue> values = mSettings.values(key);
if (!values.contains(value)) if (!values.contains(value))
mSettings.insert(key, value); mSettings.insert(key, value);
values = mUserSettings.values(key); if (isUserSetting(value))
if (!values.contains(value)) {
mUserSettings.insert(key, value); values = mUserSettings.values(key);
if (!values.contains(value))
mUserSettings.insert(key, value);
}
} }
inline void remove(const QString& key) inline void remove(const QString& key)
@ -52,35 +67,50 @@ namespace Config
mUserSettings.remove(key); mUserSettings.remove(key);
} }
QStringList getDataDirs() const; QList<SettingValue> getDataDirs() const;
std::filesystem::path getGlobalDataDir() const;
inline void removeDataDir(const QString& dir) QString getResourcesVfs() const;
inline void removeDataDir(const QString& existingDir)
{ {
if (!dir.isEmpty()) if (!existingDir.isEmpty())
mDataDirs.removeAll(dir); {
// non-user settings can't be removed as we can't edit the openmw.cfg they're in
mDataDirs.erase(
std::remove_if(mDataDirs.begin(), mDataDirs.end(),
[&](const SettingValue& dir) { return isUserSetting(dir) && dir.value == existingDir; }),
mDataDirs.end());
}
} }
inline void addDataDir(const QString& dir)
inline void addDataDir(const SettingValue& dir)
{ {
if (!dir.isEmpty()) if (!dir.value.isEmpty())
mDataDirs.append(dir); mDataDirs.append(dir);
} }
inline QString getDataLocal() const { return mDataLocal; } inline QString getDataLocal() const { return mDataLocal; }
bool hasMaster(); bool hasMaster();
QStringList values(const QString& key, const QStringList& defaultValues = QStringList()) const; QList<SettingValue> values(const QString& key, const QList<SettingValue>& defaultValues = {}) const;
bool containsValue(const QString& key, const QString& value) const;
bool readFile(QTextStream& stream, bool ignoreContent = false); bool readFile(QTextStream& stream, const QString& context, bool ignoreContent = false);
bool readFile(QTextStream& stream, QMultiMap<QString, QString>& settings, bool ignoreContent = false); bool readFile(QTextStream& stream, QMultiMap<QString, SettingValue>& settings, const QString& context,
bool readUserFile(QTextStream& stream, bool ignoreContent = false); bool ignoreContent = false);
bool readUserFile(QTextStream& stream, const QString& context, bool ignoreContent = false);
bool writeFile(QTextStream& stream); bool writeFile(QTextStream& stream);
bool writeFileWithComments(QFile& file); bool writeFileWithComments(QFile& file);
QStringList getArchiveList() const; QList<SettingValue> getArchiveList() const;
void setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); void setContentList(
QStringList getContentList() const; const QList<SettingValue>& dirNames, const QList<SettingValue>& archiveNames, const QStringList& fileNames);
QList<SettingValue> getContentList() const;
const QString& getUserContext() const { return mContexts.back(); }
bool isUserSetting(const SettingValue& settingValue) const;
void clear(); void clear();
@ -88,10 +118,12 @@ namespace Config
const Files::ConfigurationManager& mCfgMgr; const Files::ConfigurationManager& mCfgMgr;
void validatePaths(); void validatePaths();
QMultiMap<QString, QString> mSettings; QMultiMap<QString, SettingValue> mSettings;
QMultiMap<QString, QString> mUserSettings; QMultiMap<QString, SettingValue> mUserSettings;
QStringList mDataDirs; QStringList mContexts;
QList<SettingValue> mDataDirs;
QString mDataLocal; QString mDataLocal;
static const char sArchiveKey[]; static const char sArchiveKey[];
@ -100,5 +132,11 @@ namespace Config
static bool isOrderedLine(const QString& line); static bool isOrderedLine(const QString& line);
}; };
QDataStream& operator<<(QDataStream& out, const SettingValue& settingValue);
QDataStream& operator>>(QDataStream& in, SettingValue& settingValue);
} }
Q_DECLARE_METATYPE(Config::SettingValue)
#endif // GAMESETTINGS_HPP #endif // GAMESETTINGS_HPP

View file

@ -223,9 +223,25 @@ QStringList Config::LauncherSettings::getContentLists()
void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
{ {
// obtain content list from game settings (if present) // obtain content list from game settings (if present)
QStringList dirs(gameSettings.getDataDirs()); QList<SettingValue> dirs(gameSettings.getDataDirs());
const QStringList archives(gameSettings.getArchiveList()); dirs.erase(std::remove_if(
const QStringList files(gameSettings.getContentList()); dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return !gameSettings.isUserSetting(dir); }),
dirs.end());
// archives and content files aren't preprocessed, so we don't need to track their original form
const QList<SettingValue> archivesOriginal(gameSettings.getArchiveList());
QStringList archives;
for (const auto& archive : archivesOriginal)
{
if (gameSettings.isUserSetting(archive))
archives.push_back(archive.value);
}
const QList<SettingValue> filesOriginal(gameSettings.getContentList());
QStringList files;
for (const auto& file : filesOriginal)
{
if (gameSettings.isUserSetting(file))
files.push_back(file.value);
}
// if openmw.cfg has no content, exit so we don't create an empty content list. // if openmw.cfg has no content, exit so we don't create an empty content list.
if (dirs.isEmpty() || files.isEmpty()) if (dirs.isEmpty() || files.isEmpty())
@ -233,17 +249,28 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
return; return;
} }
// global and local data directories are not part of any profile // local data directory and resources/vfs are not part of any profile
const auto globalDataDir = Files::pathToQString(gameSettings.getGlobalDataDir()); const auto resourcesVfs = gameSettings.getResourcesVfs();
const auto dataLocal = gameSettings.getDataLocal(); const auto dataLocal = gameSettings.getDataLocal();
dirs.removeAll(globalDataDir); dirs.erase(
dirs.removeAll(dataLocal); std::remove_if(dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return dir.value == resourcesVfs; }),
dirs.end());
dirs.erase(
std::remove_if(dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return dir.value == dataLocal; }),
dirs.end());
// if any existing profile in launcher matches the content list, make that profile the default // if any existing profile in launcher matches the content list, make that profile the default
for (const QString& listName : getContentLists()) for (const QString& listName : getContentLists())
{ {
if (files == getContentListFiles(listName) && archives == getArchiveList(listName) const auto& listDirs = getDataDirectoryList(listName);
&& dirs == getDataDirectoryList(listName)) if (dirs.length() != listDirs.length())
continue;
for (int i = 0; i < dirs.length(); ++i)
{
if (dirs[i].value != listDirs[i])
continue;
}
if (files == getContentListFiles(listName) && archives == getArchiveList(listName))
{ {
setCurrentContentListName(listName); setCurrentContentListName(listName);
return; return;
@ -253,7 +280,10 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
// otherwise, add content list // otherwise, add content list
QString newContentListName(makeNewContentListName()); QString newContentListName(makeNewContentListName());
setCurrentContentListName(newContentListName); setCurrentContentListName(newContentListName);
setContentList(newContentListName, dirs, archives, files); QStringList newListDirs;
for (const auto& dir : dirs)
newListDirs.push_back(dir.value);
setContentList(newContentListName, newListDirs, archives, files);
} }
void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& dirNames, void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& dirNames,

View file

@ -109,6 +109,9 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index
if (!file) if (!file)
return Qt::NoItemFlags; return Qt::NoItemFlags;
if (file->builtIn() || file->fromAnotherConfigFile())
return Qt::ItemIsEnabled;
// game files can always be checked // game files can always be checked
if (file == mGameFile) if (file == mGameFile)
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
@ -130,7 +133,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index
continue; continue;
noGameFiles = false; noGameFiles = false;
if (mCheckedFiles.contains(depFile)) if (depFile->builtIn() || depFile->fromAnotherConfigFile() || mCheckedFiles.contains(depFile))
{ {
gamefileChecked = true; gamefileChecked = true;
break; break;
@ -217,7 +220,8 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int
if (file == mGameFile) if (file == mGameFile)
return QVariant(); return QVariant();
return mCheckedFiles.contains(file) ? Qt::Checked : Qt::Unchecked; return (file->builtIn() || file->fromAnotherConfigFile() || mCheckedFiles.contains(file)) ? Qt::Checked
: Qt::Unchecked;
} }
case Qt::UserRole: case Qt::UserRole:
@ -279,7 +283,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const
{ {
int checkValue = value.toInt(); int checkValue = value.toInt();
bool setState = false; bool setState = false;
if (checkValue == Qt::Checked && !mCheckedFiles.contains(file)) if (file->builtIn() || file->fromAnotherConfigFile())
{
setState = false;
success = false;
}
else if (checkValue == Qt::Checked && !mCheckedFiles.contains(file))
{ {
setState = true; setState = true;
success = true; success = true;
@ -374,6 +383,13 @@ bool ContentSelectorModel::ContentModel::dropMimeData(
else if (parent.isValid()) else if (parent.isValid())
beginRow = parent.row(); beginRow = parent.row();
int firstModifiable = 0;
while (item(firstModifiable)->builtIn() || item(firstModifiable)->fromAnotherConfigFile())
++firstModifiable;
if (beginRow < firstModifiable)
return false;
QByteArray encodedData = data->data(mMimeType); QByteArray encodedData = data->data(mMimeType);
QDataStream stream(&encodedData, QIODevice::ReadOnly); QDataStream stream(&encodedData, QIODevice::ReadOnly);
@ -434,10 +450,6 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
{ {
QFileInfo info(dir.absoluteFilePath(path2)); QFileInfo info(dir.absoluteFilePath(path2));
// Enabled by default in system openmw.cfg; shouldn't be shown in content list.
if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0)
continue;
EsmFile* file = const_cast<EsmFile*>(item(info.fileName())); EsmFile* file = const_cast<EsmFile*>(item(info.fileName()));
bool add = file == nullptr; bool add = file == nullptr;
std::unique_ptr<EsmFile> newFile; std::unique_ptr<EsmFile> newFile;
@ -453,6 +465,11 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
file->setGameFiles({}); file->setGameFiles({});
} }
if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0)
file->setBuiltIn(true);
file->setFromAnotherConfigFile(mNonUserContent.contains(info.fileName().toLower()));
if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive))
{ {
file->setDate(info.lastModified()); file->setDate(info.lastModified());
@ -583,15 +600,20 @@ void ContentSelectorModel::ContentModel::setCurrentGameFile(const EsmFile* file)
void ContentSelectorModel::ContentModel::sortFiles() void ContentSelectorModel::ContentModel::sortFiles()
{ {
emit layoutAboutToBeChanged(); emit layoutAboutToBeChanged();
int firstModifiable = 0;
while (mFiles.at(firstModifiable)->builtIn() || mFiles.at(firstModifiable)->fromAnotherConfigFile())
++firstModifiable;
// Dependency sort // Dependency sort
std::unordered_set<const EsmFile*> moved; std::unordered_set<const EsmFile*> moved;
for (int i = mFiles.size() - 1; i > 0;) for (int i = mFiles.size() - 1; i > firstModifiable;)
{ {
const auto file = mFiles.at(i); const auto file = mFiles.at(i);
if (moved.find(file) == moved.end()) if (moved.find(file) == moved.end())
{ {
int index = -1; int index = -1;
for (int j = 0; j < i; ++j) for (int j = firstModifiable; j < i; ++j)
{ {
const QStringList& gameFiles = mFiles.at(j)->gameFiles(); const QStringList& gameFiles = mFiles.at(j)->gameFiles();
// All addon files are implicitly dependent on the game file // All addon files are implicitly dependent on the game file
@ -641,6 +663,28 @@ void ContentSelectorModel::ContentModel::setNew(const QString& filepath, bool is
mNewFiles[filepath] = isNew; mNewFiles[filepath] = isNew;
} }
void ContentSelectorModel::ContentModel::setNonUserContent(const QStringList& fileList)
{
mNonUserContent.clear();
for (const auto& file : fileList)
mNonUserContent.insert(file.toLower());
for (auto* file : mFiles)
file->setFromAnotherConfigFile(mNonUserContent.contains(file->fileName().toLower()));
int insertPosition = 0;
while (mFiles.at(insertPosition)->builtIn())
++insertPosition;
for (const auto& filepath : fileList)
{
const EsmFile* file = item(filepath);
int filePosition = indexFromItem(file).row();
mFiles.move(filePosition, insertPosition++);
}
sortFiles();
}
bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile* file) const bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile* file) const
{ {
return mPluginsWithLoadOrderError.contains(file->filePath()); return mPluginsWithLoadOrderError.contains(file->filePath());
@ -654,6 +698,7 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL
{ {
if (setCheckState(filepath, true)) if (setCheckState(filepath, true))
{ {
// setCheckState already gracefully handles builtIn and fromAnotherConfigFile
// as necessary, move plug-ins in visible list to match sequence of supplied filelist // as necessary, move plug-ins in visible list to match sequence of supplied filelist
const EsmFile* file = item(filepath); const EsmFile* file = item(filepath);
int filePosition = indexFromItem(file).row(); int filePosition = indexFromItem(file).row();
@ -751,7 +796,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath,
const EsmFile* file = item(filepath); const EsmFile* file = item(filepath);
if (!file) if (!file || file->builtIn() || file->fromAnotherConfigFile())
return false; return false;
if (checkState) if (checkState)

View file

@ -62,6 +62,7 @@ namespace ContentSelectorModel
bool setCheckState(const QString& filepath, bool isChecked); bool setCheckState(const QString& filepath, bool isChecked);
bool isNew(const QString& filepath) const; bool isNew(const QString& filepath) const;
void setNew(const QString& filepath, bool isChecked); void setNew(const QString& filepath, bool isChecked);
void setNonUserContent(const QStringList& fileList);
void setContentList(const QStringList& fileList); void setContentList(const QStringList& fileList);
ContentFileList checkedItems() const; ContentFileList checkedItems() const;
void uncheckAll(); void uncheckAll();
@ -85,7 +86,7 @@ namespace ContentSelectorModel
const EsmFile* mGameFile; const EsmFile* mGameFile;
ContentFileList mFiles; ContentFileList mFiles;
QStringList mArchives; QSet<QString> mNonUserContent;
std::set<const EsmFile*> mCheckedFiles; std::set<const EsmFile*> mCheckedFiles;
QHash<QString, bool> mNewFiles; QHash<QString, bool> mNewFiles;
QSet<QString> mPluginsWithLoadOrderError; QSet<QString> mPluginsWithLoadOrderError;

View file

@ -41,6 +41,16 @@ void ContentSelectorModel::EsmFile::setDescription(const QString& description)
mDescription = description; mDescription = description;
} }
void ContentSelectorModel::EsmFile::setBuiltIn(bool builtIn)
{
mBuiltIn = builtIn;
}
void ContentSelectorModel::EsmFile::setFromAnotherConfigFile(bool fromAnotherConfigFile)
{
mFromAnotherConfigFile = fromAnotherConfigFile;
}
bool ContentSelectorModel::EsmFile::isGameFile() const bool ContentSelectorModel::EsmFile::isGameFile() const
{ {
return (mGameFiles.size() == 0) return (mGameFiles.size() == 0)
@ -76,6 +86,14 @@ QVariant ContentSelectorModel::EsmFile::fileProperty(const FileProperty prop) co
return mDescription; return mDescription;
break; break;
case FileProperty_BuiltIn:
return mBuiltIn;
break;
case FileProperty_FromAnotherConfigFile:
return mFromAnotherConfigFile;
break;
case FileProperty_GameFile: case FileProperty_GameFile:
return mGameFiles; return mGameFiles;
break; break;
@ -113,6 +131,15 @@ void ContentSelectorModel::EsmFile::setFileProperty(const FileProperty prop, con
mDescription = value; mDescription = value;
break; break;
// todo: check these work
case FileProperty_BuiltIn:
mBuiltIn = value == "true";
break;
case FileProperty_FromAnotherConfigFile:
mFromAnotherConfigFile = value == "true";
break;
case FileProperty_GameFile: case FileProperty_GameFile:
mGameFiles << value; mGameFiles << value;
break; break;

View file

@ -26,7 +26,9 @@ namespace ContentSelectorModel
FileProperty_DateModified = 3, FileProperty_DateModified = 3,
FileProperty_FilePath = 4, FileProperty_FilePath = 4,
FileProperty_Description = 5, FileProperty_Description = 5,
FileProperty_GameFile = 6 FileProperty_BuiltIn = 6,
FileProperty_FromAnotherConfigFile = 7,
FileProperty_GameFile = 8,
}; };
EsmFile(const QString& fileName = QString(), ModelItem* parent = nullptr); EsmFile(const QString& fileName = QString(), ModelItem* parent = nullptr);
@ -40,6 +42,8 @@ namespace ContentSelectorModel
void setFilePath(const QString& path); void setFilePath(const QString& path);
void setGameFiles(const QStringList& gameFiles); void setGameFiles(const QStringList& gameFiles);
void setDescription(const QString& description); void setDescription(const QString& description);
void setBuiltIn(bool builtIn);
void setFromAnotherConfigFile(bool fromAnotherConfigFile);
void addGameFile(const QString& name) { mGameFiles.append(name); } void addGameFile(const QString& name) { mGameFiles.append(name); }
QVariant fileProperty(const FileProperty prop) const; QVariant fileProperty(const FileProperty prop) const;
@ -49,18 +53,29 @@ namespace ContentSelectorModel
QDateTime modified() const { return mModified; } QDateTime modified() const { return mModified; }
QString formatVersion() const { return mVersion; } QString formatVersion() const { return mVersion; }
QString filePath() const { return mPath; } QString filePath() const { return mPath; }
bool builtIn() const { return mBuiltIn; }
bool fromAnotherConfigFile() const { return mFromAnotherConfigFile; }
/// @note Contains file names, not paths. /// @note Contains file names, not paths.
const QStringList& gameFiles() const { return mGameFiles; } const QStringList& gameFiles() const { return mGameFiles; }
QString description() const { return mDescription; } QString description() const { return mDescription; }
QString toolTip() const QString toolTip() const
{ {
return mTooltipTemlate.arg(mAuthor) QString tooltip = mTooltipTemlate.arg(mAuthor)
.arg(mVersion) .arg(mVersion)
.arg(mModified.toString(Qt::ISODate)) .arg(mModified.toString(Qt::ISODate))
.arg(mPath) .arg(mPath)
.arg(mDescription) .arg(mDescription)
.arg(mGameFiles.join(", ")); .arg(mGameFiles.join(", "));
if (mBuiltIn)
tooltip += tr("<br/><b>This content file cannot be disabled because it is part of OpenMW.</b><br/>");
else if (mFromAnotherConfigFile)
tooltip += tr(
"<br/><b>This content file cannot be disabled because it is enabled in a config file other than "
"the user one.</b><br/>");
return tooltip;
} }
bool isGameFile() const; bool isGameFile() const;
@ -82,6 +97,8 @@ namespace ContentSelectorModel
QStringList mGameFiles; QStringList mGameFiles;
QString mDescription; QString mDescription;
QString mToolTip; QString mToolTip;
bool mBuiltIn = false;
bool mFromAnotherConfigFile = false;
}; };
} }

View file

@ -123,6 +123,11 @@ void ContentSelectorView::ContentSelector::buildContextMenu()
mContextMenu->addAction(tr("&Copy Path(s) to Clipboard"), this, SLOT(slotCopySelectedItemsPaths())); mContextMenu->addAction(tr("&Copy Path(s) to Clipboard"), this, SLOT(slotCopySelectedItemsPaths()));
} }
void ContentSelectorView::ContentSelector::setNonUserContent(const QStringList& fileList)
{
mContentModel->setNonUserContent(fileList);
}
void ContentSelectorView::ContentSelector::setProfileContent(const QStringList& fileList) void ContentSelectorView::ContentSelector::setProfileContent(const QStringList& fileList)
{ {
clearCheckStates(); clearCheckStates();

View file

@ -40,6 +40,7 @@ namespace ContentSelectorView
void sortFiles(); void sortFiles();
bool containsDataFiles(const QString& path); bool containsDataFiles(const QString& path);
void clearFiles(); void clearFiles();
void setNonUserContent(const QStringList& fileList);
void setProfileContent(const QStringList& fileList); void setProfileContent(const QStringList& fileList);
void clearCheckStates(); void clearCheckStates();

View file

@ -68,6 +68,9 @@ namespace Files
bool silent = mSilent; bool silent = mSilent;
mSilent = quiet; mSilent = quiet;
// ensure defaults are present
bpo::store(bpo::parsed_options(&description), variables);
std::optional<bpo::variables_map> config = loadConfig(mFixedPath.getLocalPath(), description); std::optional<bpo::variables_map> config = loadConfig(mFixedPath.getLocalPath(), description);
if (config) if (config)
mActiveConfigPaths.push_back(mFixedPath.getLocalPath()); mActiveConfigPaths.push_back(mFixedPath.getLocalPath());
@ -411,11 +414,6 @@ namespace Files
return mFixedPath.getLocalPath(); return mFixedPath.getLocalPath();
} }
const std::filesystem::path& ConfigurationManager::getGlobalDataPath() const
{
return mFixedPath.getGlobalDataPath();
}
const std::filesystem::path& ConfigurationManager::getCachePath() const const std::filesystem::path& ConfigurationManager::getCachePath() const
{ {
return mFixedPath.getCachePath(); return mFixedPath.getCachePath();

View file

@ -45,7 +45,6 @@ namespace Files
const std::filesystem::path& getGlobalPath() const; const std::filesystem::path& getGlobalPath() const;
const std::filesystem::path& getLocalPath() const; const std::filesystem::path& getLocalPath() const;
const std::filesystem::path& getGlobalDataPath() const;
const std::filesystem::path& getUserConfigPath() const; const std::filesystem::path& getUserConfigPath() const;
const std::filesystem::path& getUserDataPath() const; const std::filesystem::path& getUserDataPath() const;
const std::filesystem::path& getLocalDataPath() const; const std::filesystem::path& getLocalDataPath() const;

View file

@ -22,6 +22,16 @@ namespace Files
{ {
return Files::pathToQString(cfgMgr.getGlobalPath() / openmwCfgFile); return Files::pathToQString(cfgMgr.getGlobalPath() / openmwCfgFile);
} }
inline QStringList getActiveConfigPathsQString(const Files::ConfigurationManager& cfgMgr)
{
const auto& activePaths = cfgMgr.getActiveConfigPaths();
QStringList result;
result.reserve(static_cast<int>(activePaths.size()));
for (const auto& path : activePaths)
result.append(Files::pathToQString(path / openmwCfgFile));
return result;
}
} }
#endif // OPENMW_COMPONENTS_FILES_QTCONFIGPATH_H #endif // OPENMW_COMPONENTS_FILES_QTCONFIGPATH_H

View file

@ -29,6 +29,14 @@
<source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source> <source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is part of OpenMW.&lt;/b&gt;&lt;br/&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is enabled in a config file other than the user one.&lt;/b&gt;&lt;br/&gt;</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>ContentSelectorView::ContentSelector</name> <name>ContentSelectorView::ContentSelector</name>

View file

@ -29,6 +29,14 @@
<source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source> <source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is part of OpenMW.&lt;/b&gt;&lt;br/&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is enabled in a config file other than the user one.&lt;/b&gt;&lt;br/&gt;</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>ContentSelectorView::ContentSelector</name> <name>ContentSelectorView::ContentSelector</name>

View file

@ -29,6 +29,14 @@
<source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source> <source>&lt;b&gt;Author:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Format version:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Modified:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Path:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Dependencies: &lt;/b&gt;%6&lt;br/&gt;</source>
<translation>&lt;b&gt;Автор:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Версия формата данных:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Дата изменения:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Путь к файлу:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Описание:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Зависимости: &lt;/b&gt;%6&lt;br/&gt;</translation> <translation>&lt;b&gt;Автор:&lt;/b&gt; %1&lt;br/&gt;&lt;b&gt;Версия формата данных:&lt;/b&gt; %2&lt;br/&gt;&lt;b&gt;Дата изменения:&lt;/b&gt; %3&lt;br/&gt;&lt;b&gt;Путь к файлу:&lt;/b&gt;&lt;br/&gt;%4&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Описание:&lt;/b&gt;&lt;br/&gt;%5&lt;br/&gt;&lt;br/&gt;&lt;b&gt;Зависимости: &lt;/b&gt;%6&lt;br/&gt;</translation>
</message> </message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is part of OpenMW.&lt;/b&gt;&lt;br/&gt;</source>
<translation>&lt;br/&gt;&lt;b&gt;Этот контентный файл не может быть отключен, потому что он является частью OpenMW.&lt;/b&gt;&lt;br/&gt;</translation>
</message>
<message>
<source>&lt;br/&gt;&lt;b&gt;This content file cannot be disabled because it is enabled in a config file other than the user one.&lt;/b&gt;&lt;br/&gt;</source>
<translation>&lt;br/&gt;&lt;b&gt;Этот контентный файл не может быть отключен, потому что он включен в конфигурационном файле, не являющемся пользовательским.&lt;/b&gt;&lt;br/&gt;</translation>
</message>
</context> </context>
<context> <context>
<name>ContentSelectorView::ContentSelector</name> <name>ContentSelectorView::ContentSelector</name>

View file

@ -370,6 +370,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source> <source>&amp;Uncheck Selected</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Resolved as %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This is the data-local directory and cannot be disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This directory is part of OpenMW and cannot be disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This directory is enabled in an openmw.cfg other than the user one</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This archive is enabled in an openmw.cfg other than the user one</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Launcher::GraphicsPage</name> <name>Launcher::GraphicsPage</name>

View file

@ -370,6 +370,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source> <source>&amp;Uncheck Selected</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Resolved as %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This is the data-local directory and cannot be disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This directory is part of OpenMW and cannot be disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This directory is enabled in an openmw.cfg other than the user one</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This archive is enabled in an openmw.cfg other than the user one</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Launcher::GraphicsPage</name> <name>Launcher::GraphicsPage</name>

View file

@ -372,6 +372,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source> <source>&amp;Uncheck Selected</source>
<translation>&amp;Отключить выбранные</translation> <translation>&amp;Отключить выбранные</translation>
</message> </message>
<message>
<source>Resolved as %1</source>
<translation>Путь разрешен как %1</translation>
</message>
<message>
<source>This is the data-local directory and cannot be disabled</source>
<translation>Это директория data-loсal, и она не может быть отключена</translation>
</message>
<message>
<source>This directory is part of OpenMW and cannot be disabled</source>
<translation>Это директория является частью OpenMW и не может быть отключена</translation>
</message>
<message>
<source>This directory is enabled in an openmw.cfg other than the user one</source>
<translation>Это директория включена в openmw.cfg, не являющемся пользовательским</translation>
</message>
<message>
<source>This archive is enabled in an openmw.cfg other than the user one</source>
<translation>Этот архив включен в openmw.cfg, не являющемся пользовательским</translation>
</message>
</context> </context>
<context> <context>
<name>Launcher::GraphicsPage</name> <name>Launcher::GraphicsPage</name>

View file

@ -2,7 +2,6 @@
# Modifications should be done on the user openmw.cfg file instead # Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
content=builtin.omwscripts
data-local="?userdata?data" data-local="?userdata?data"
user-data="?userdata?" user-data="?userdata?"
config="?userconfig?" config="?userconfig?"

View file

@ -2,7 +2,6 @@
# Modifications should be done on the user openmw.cfg file instead # Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
content=builtin.omwscripts
data-local="?userdata?data" data-local="?userdata?data"
user-data="?userdata?" user-data="?userdata?"
config="?userconfig?" config="?userconfig?"