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
master
psi29a 3 weeks ago
commit 8037a6e765

@ -49,6 +49,7 @@
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 #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 #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

@ -146,7 +146,9 @@ namespace
dataDirs.insert(dataDirs.begin(), resDir / "vfs");
const Files::Collections fileCollections(dataDirs);
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);

@ -142,7 +142,7 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C
ui.setupUi(this);
setObjectName("DataFilesPage");
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);
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.directoryUpButton, &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.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchives(1); });
connect(
ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); });
connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories);
connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives);
buildView();
loadSettings();
@ -271,65 +271,79 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
ui.archiveListWidget->clear();
ui.directoryListWidget->clear();
QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName);
if (directories.isEmpty())
directories = mGameSettings.getDataDirs();
QList<Config::SettingValue> directories = mGameSettings.getDataDirs();
QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName);
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();
if (!mDataLocal.isEmpty())
directories.insert(0, mDataLocal);
directories.insert(0, { mDataLocal });
const auto& globalDataDir = mGameSettings.getGlobalDataDir();
if (!globalDataDir.empty())
directories.insert(0, Files::pathToQString(globalDataDir));
const auto& resourcesVfs = mGameSettings.getResourcesVfs();
if (!resourcesVfs.isEmpty())
directories.insert(0, { resourcesVfs });
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
const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir)).canonicalPath();
if (!visitedDirectories.insert(canonicalDirPath).second)
if (!visitedDirectories.insert(currentDir.value).second)
continue;
// add new achives files presents in current directory
addArchivesFromDir(currentDir);
addArchivesFromDir(currentDir.value);
QString tooltip;
QStringList tooltip;
// 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
ui.directoryListWidget->addItem(currentDir);
ui.directoryListWidget->addItem(currentDir.originalRepresentation);
auto row = ui.directoryListWidget->count() - 1;
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
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();
font.setBold(true);
font.setItalic(true);
item->setFont(font);
}
// deactivate data-local and global data directory: they are always included
if (currentDir == mDataLocal || Files::pathFromQString(currentDir) == globalDataDir)
// deactivate data-local and resources/vfs: they are always included
// same for ones from non-user config files
if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs
|| !mGameSettings.isUserSetting(currentDir))
{
auto flags = item->flags();
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
if (mSelector->containsDataFiles(currentDir))
if (mSelector->containsDataFiles(currentDir.value))
{
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
{
@ -339,19 +353,26 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
auto emptyIcon = QIcon(pixmap);
item->setIcon(emptyIcon);
}
item->setToolTip(tooltip);
item->setToolTip(tooltip.join('\n'));
}
mSelector->sortFiles();
QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName);
if (selectedArchives.isEmpty())
selectedArchives = mGameSettings.getArchiveList();
QList<Config::SettingValue> selectedArchives = mGameSettings.getArchiveList();
QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName);
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
int row = 0;
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())
continue;
const auto name = match[0]->text();
@ -359,9 +380,25 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
ui.archiveListWidget->takeItem(oldrow);
ui.archiveListWidget->insertItem(row, name);
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++;
}
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));
}
@ -389,7 +426,19 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile)
{
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);
QString language(mSelector->languageBox()->currentData().toString());
@ -398,38 +447,38 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile)
if (language == QLatin1String("Polish"))
{
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250"));
mGameSettings.setValue(QLatin1String("encoding"), { "win1250" });
}
else if (language == QLatin1String("Russian"))
{
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251"));
mGameSettings.setValue(QLatin1String("encoding"), { "win1251" });
}
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)
{
const QListWidgetItem* item = ui.directoryListWidget->item(i);
if (item->flags() & Qt::ItemIsEnabled)
dirList.append(item->text());
dirList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
}
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)
{
const QListWidgetItem* item = ui.archiveListWidget->item(i);
if (item->checkState() == Qt::Checked)
archiveList.append(item->text());
archiveList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
}
return archiveList;
}
@ -583,7 +632,20 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered()
if (profile.isEmpty())
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);
}
@ -650,6 +712,9 @@ void Launcher::DataFilesPage::addSubdirectories(bool append)
if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty())
return;
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);
refreshDataFilesView();
return;
@ -679,8 +744,11 @@ void Launcher::DataFilesPage::addSubdirectories(bool append)
const auto* dir = select.dirListWidget->item(i);
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());
++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)
{
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)
return false;
const QListWidgetItem* item = ui.archiveListWidget->takeItem(selectedRow);
addArchive(item->text(), item->checkState(), newRow);
QListWidgetItem* item = ui.archiveListWidget->takeItem(selectedRow);
ui.archiveListWidget->insertItem(newRow, item);
ui.archiveListWidget->setCurrentRow(newRow);
return true;
}
@ -797,6 +879,7 @@ void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState sel
row = ui.archiveListWidget->count();
ui.archiveListWidget->insertItem(row, name);
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 ???
{
auto item = ui.archiveListWidget->item(row);

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

@ -37,9 +37,9 @@ Launcher::ImportPage::ImportPage(const Files::ConfigurationManager& cfg, Config:
// Detect Morrowind configuration files
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
if (dir.exists(QString("Morrowind.ini")))
@ -125,7 +125,7 @@ void Launcher::ImportPage::on_importerButton_clicked()
arguments.append(QString("--fonts"));
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(settingsComboBox->currentText());
arguments.append(QString("--cfg"));

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

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

@ -165,7 +165,9 @@ namespace NavMeshTool
dataDirs.insert(dataDirs.begin(), resDir / "vfs");
const Files::Collections fileCollections(dataDirs);
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>();
if (threadsNumber < 1)

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

@ -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...";
return false;
}
std::set<std::string> contentDedupe;
engine.addContentFile("builtin.omwscripts");
std::set<std::string> contentDedupe{ "builtin.omwscripts" };
for (const auto& contentFile : content)
{
if (!contentDedupe.insert(contentFile).second)

@ -18,7 +18,7 @@ Wizard::InstallationPage::InstallationPage(QWidget* parent, Config::GameSettings
mFinished = false;
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());
connect(mThread.get(), &QThread::started, mUnshield.get(), &UnshieldWorker::extract);

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

@ -24,11 +24,14 @@
#include "installationpage.hpp"
#endif
#include <algorithm>
using namespace Process;
Wizard::MainWizard::MainWizard(QWidget* parent)
Wizard::MainWizard::MainWizard(Files::ConfigurationManager&& cfgMgr, QWidget* parent)
: QWizard(parent)
, mInstallations()
, mCfgMgr(cfgMgr)
, mError(false)
, mGameSettings(mCfgMgr)
{
@ -166,16 +169,13 @@ void Wizard::MainWizard::setupGameSettings()
QTextStream stream(&file);
Misc::ensureUtf8Encoding(stream);
mGameSettings.readUserFile(stream);
mGameSettings.readUserFile(stream, QFileInfo(path).dir().path());
}
file.close();
// Now the rest
QStringList paths;
paths.append(Files::getUserConfigPathQString(mCfgMgr));
paths.append(QLatin1String("openmw.cfg"));
paths.append(Files::getGlobalConfigPathQString(mCfgMgr));
QStringList paths = Files::getActiveConfigPathsQString(mCfgMgr);
for (const QString& path2 : paths)
{
@ -198,7 +198,7 @@ void Wizard::MainWizard::setupGameSettings()
QTextStream stream(&file);
Misc::ensureUtf8Encoding(stream);
mGameSettings.readFile(stream);
mGameSettings.readFile(stream, QFileInfo(path2).dir().path());
}
file.close();
}
@ -243,11 +243,11 @@ void Wizard::MainWizard::setupLauncherSettings()
void Wizard::MainWizard::setupInstallations()
{
// 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))
addInstallation(path);
if (findFiles(QLatin1String("Morrowind"), path.value))
addInstallation(path.value);
}
}
@ -334,10 +334,12 @@ void Wizard::MainWizard::addInstallation(const QString& path)
mInstallations.insert(QDir::toNativeSeparators(path), install);
// 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.addDataDir(path);
mGameSettings.setMultiValue(QLatin1String("data"), { path });
mGameSettings.addDataDir({ path });
}
}
@ -396,15 +398,15 @@ void Wizard::MainWizard::writeSettings()
if (language == QLatin1String("Polish"))
{
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250"));
mGameSettings.setValue(QLatin1String("encoding"), { "win1250" });
}
else if (language == QLatin1String("Russian"))
{
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251"));
mGameSettings.setValue(QLatin1String("encoding"), { "win1251" });
}
else
{
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252"));
mGameSettings.setValue(QLatin1String("encoding"), { "win1252" });
}
// 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
mGameSettings.removeDataDir(path);
mGameSettings.addDataDir(path);
mGameSettings.addDataDir({ path });
QString userPath(Files::pathToQString(mCfgMgr.getUserConfigPath()));
QDir dir(userPath);

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

@ -13,7 +13,8 @@ const char Config::GameSettings::sDirectoryKey[] = "data";
namespace
{
QStringList reverse(QStringList values)
template <typename T>
QList<T> reverse(QList<T> values)
{
std::reverse(values.begin(), values.end());
return values;
@ -23,83 +24,111 @@ namespace
Config::GameSettings::GameSettings(const Files::ConfigurationManager& 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()
{
QStringList paths = mSettings.values(QString("data"));
Files::PathContainer dataDirs;
QList<SettingValue> paths = mSettings.values(QString("data"));
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();
for (const auto& dataDir : dataDirs)
for (const auto& dataDir : paths)
{
if (is_directory(dataDir))
mDataDirs.append(Files::pathToQString(dataDir));
if (QDir(dataDir.value).exists())
{
SettingValue copy = dataDir;
copy.value = QDir(dataDir.value).canonicalPath();
mDataDirs.append(copy);
}
}
// Do the same for data-local
QString local = mSettings.value(QString("data-local"));
if (local.length() && local.at(0) == QChar('\"'))
{
local.remove(0, 1);
local.chop(1);
}
if (local.isEmpty())
return;
dataDirs.clear();
dataDirs.emplace_back(Files::pathFromQString(local));
const QString& local = mSettings.value(QString("data-local")).value;
mCfgMgr.processPaths(dataDirs, /*basePath=*/"");
if (!dataDirs.empty())
if (!local.isEmpty() && QDir(local).exists())
{
const auto& path = dataDirs.front();
if (is_directory(path))
mDataLocal = Files::pathToQString(path);
mDataLocal = QDir(local).canonicalPath();
}
}
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)
const auto& path = mCfgMgr.getGlobalDataPath();
if (std::filesystem::exists(path))
return std::filesystem::canonical(path);
return {};
QString resources = mSettings.value(QString("resources"), { "./resources", "", "" }).value;
resources += "/vfs";
return QFileInfo(resources).canonicalFilePath();
}
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())
return mSettings.values(key);
return defaultValues;
}
bool Config::GameSettings::readFile(QTextStream& stream, bool ignoreContent)
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)
{
return readFile(stream, mSettings, ignoreContent);
if (readFile(stream, mSettings, context, ignoreContent))
{
mContexts.push_back(context);
return true;
}
return false;
}
bool Config::GameSettings::readUserFile(QTextStream& stream, bool ignoreContent)
bool Config::GameSettings::readUserFile(QTextStream& stream, const QString& context, bool ignoreContent)
{
return readFile(stream, mUserSettings, ignoreContent);
return readFile(stream, mUserSettings, context, ignoreContent);
}
bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap<QString, QString>& settings, bool ignoreContent)
bool Config::GameSettings::readFile(
QTextStream& stream, QMultiMap<QString, SettingValue>& settings, const QString& context, bool ignoreContent)
{
QMultiMap<QString, QString> cache;
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("script-blacklist")
|| key == QLatin1String("fallback"))
settings.remove(key);
}
}
stream.seek(initialPos);
while (!stream.atEnd())
{
QString line = stream.readLine();
@ -111,27 +140,32 @@ bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap<QString, QStr
if (match.hasMatch())
{
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
if (key != QLatin1String("data") && key != QLatin1String("fallback-archive")
&& key != QLatin1String("content") && key != QLatin1String("groundcover")
&& key != QLatin1String("script-blacklist"))
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);
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"))
{
// 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
// work as there are too may QStrings involved
// 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.at(0) == delim)
if (value.value.at(0) == delim)
{
QString valueOriginal = value;
value = "";
QString valueOriginal = value.value;
value.value = "";
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;
else if (*it == delim)
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")))
continue;
QStringList values = cache.values(key);
QList<SettingValue> values = cache.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);
}
@ -184,15 +232,16 @@ bool Config::GameSettings::writeFile(QTextStream& stream)
// 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("data") || i.key() == QLatin1String("data-local")
|| i.key() == QLatin1String("resources") || i.key() == QLatin1String("load-savegame"))
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();
QString string = i.value().originalRepresentation;
stream << delim;
for (auto& it : string)
@ -207,7 +256,7 @@ bool Config::GameSettings::writeFile(QTextStream& stream)
continue;
}
stream << i.key() << "=" << i.value() << "\n";
stream << i.key() << "=" << i.value().originalRepresentation << "\n";
}
return true;
@ -362,10 +411,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
*iter = QString(); // assume no match
QString key = match.captured(1);
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)
{
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);
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.
// 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("data") || it.key() == QLatin1String("data-local")
|| it.key() == QLatin1String("resources") || it.key() == QLatin1String("load-savegame"))
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();
QString string = it.value().originalRepresentation;
settingLine += delim;
for (auto& iter : string)
@ -428,7 +479,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
settingLine += delim;
}
else
settingLine = it.key() + "=" + it.value();
settingLine = it.key() + "=" + it.value().originalRepresentation;
QRegularExpressionMatch match = settingRegex.match(settingLine);
if (match.hasMatch())
@ -487,11 +538,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file)
bool Config::GameSettings::hasMaster()
{
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)
{
if (content.at(i).endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive)
|| content.at(i).endsWith(QLatin1String(".esm"), Qt::CaseInsensitive))
if (content.at(i).value.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive)
|| content.at(i).value.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive))
{
result = true;
break;
@ -502,40 +553,62 @@ bool Config::GameSettings::hasMaster()
}
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(key);
for (auto const& item : list)
setMultiValue(key, item);
};
reset(sDirectoryKey, dirNames);
reset(sArchiveKey, archiveNames);
reset(sContentKey, 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 });
}
QStringList Config::GameSettings::getDataDirs() const
QList<Config::SettingValue> Config::GameSettings::getDataDirs() const
{
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
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
return reverse(values(sContentKey));
}
bool Config::GameSettings::isUserSetting(const SettingValue& settingValue) const
{
return settingValue.context.isEmpty() || settingValue.context == getUserContext();
}
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;
}

@ -17,33 +17,48 @@ namespace Files
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
{
public:
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.insert(key, value);
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))
mSettings.insert(key, value);
values = mUserSettings.values(key);
if (!values.contains(value))
mUserSettings.insert(key, value);
if (isUserSetting(value))
{
values = mUserSettings.values(key);
if (!values.contains(value))
mUserSettings.insert(key, value);
}
}
inline void remove(const QString& key)
@ -52,35 +67,50 @@ namespace Config
mUserSettings.remove(key);
}
QStringList getDataDirs() const;
std::filesystem::path getGlobalDataDir() const;
QList<SettingValue> getDataDirs() const;
QString getResourcesVfs() const;
inline void removeDataDir(const QString& dir)
inline void removeDataDir(const QString& existingDir)
{
if (!dir.isEmpty())
mDataDirs.removeAll(dir);
if (!existingDir.isEmpty())
{
// 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);
}
inline QString getDataLocal() const { return mDataLocal; }
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, QMultiMap<QString, QString>& settings, bool ignoreContent = false);
bool readUserFile(QTextStream& stream, bool ignoreContent = false);
bool readFile(QTextStream& stream, const QString& context, bool ignoreContent = false);
bool readFile(QTextStream& stream, QMultiMap<QString, SettingValue>& settings, const QString& context,
bool ignoreContent = false);
bool readUserFile(QTextStream& stream, const QString& context, bool ignoreContent = false);
bool writeFile(QTextStream& stream);
bool writeFileWithComments(QFile& file);
QStringList getArchiveList() const;
void setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames);
QStringList getContentList() const;
QList<SettingValue> getArchiveList() const;
void setContentList(
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();
@ -88,10 +118,12 @@ namespace Config
const Files::ConfigurationManager& mCfgMgr;
void validatePaths();
QMultiMap<QString, QString> mSettings;
QMultiMap<QString, QString> mUserSettings;
QMultiMap<QString, SettingValue> mSettings;
QMultiMap<QString, SettingValue> mUserSettings;
QStringList mContexts;
QStringList mDataDirs;
QList<SettingValue> mDataDirs;
QString mDataLocal;
static const char sArchiveKey[];
@ -100,5 +132,11 @@ namespace Config
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

@ -223,9 +223,25 @@ QStringList Config::LauncherSettings::getContentLists()
void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
{
// obtain content list from game settings (if present)
QStringList dirs(gameSettings.getDataDirs());
const QStringList archives(gameSettings.getArchiveList());
const QStringList files(gameSettings.getContentList());
QList<SettingValue> dirs(gameSettings.getDataDirs());
dirs.erase(std::remove_if(
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 (dirs.isEmpty() || files.isEmpty())
@ -233,17 +249,28 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
return;
}
// global and local data directories are not part of any profile
const auto globalDataDir = Files::pathToQString(gameSettings.getGlobalDataDir());
// local data directory and resources/vfs are not part of any profile
const auto resourcesVfs = gameSettings.getResourcesVfs();
const auto dataLocal = gameSettings.getDataLocal();
dirs.removeAll(globalDataDir);
dirs.removeAll(dataLocal);
dirs.erase(
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
for (const QString& listName : getContentLists())
{
if (files == getContentListFiles(listName) && archives == getArchiveList(listName)
&& dirs == getDataDirectoryList(listName))
const auto& listDirs = 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);
return;
@ -253,7 +280,10 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
// otherwise, add content list
QString newContentListName(makeNewContentListName());
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,

@ -109,6 +109,9 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index
if (!file)
return Qt::NoItemFlags;
if (file->builtIn() || file->fromAnotherConfigFile())
return Qt::ItemIsEnabled;
// game files can always be checked
if (file == mGameFile)
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
@ -130,7 +133,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index
continue;
noGameFiles = false;
if (mCheckedFiles.contains(depFile))
if (depFile->builtIn() || depFile->fromAnotherConfigFile() || mCheckedFiles.contains(depFile))
{
gamefileChecked = true;
break;
@ -217,7 +220,8 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int
if (file == mGameFile)
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:
@ -279,7 +283,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const
{
int checkValue = value.toInt();
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;
success = true;
@ -374,6 +383,13 @@ bool ContentSelectorModel::ContentModel::dropMimeData(
else if (parent.isValid())
beginRow = parent.row();
int firstModifiable = 0;
while (item(firstModifiable)->builtIn() || item(firstModifiable)->fromAnotherConfigFile())
++firstModifiable;
if (beginRow < firstModifiable)
return false;
QByteArray encodedData = data->data(mMimeType);
QDataStream stream(&encodedData, QIODevice::ReadOnly);
@ -434,10 +450,6 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
{
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()));
bool add = file == nullptr;
std::unique_ptr<EsmFile> newFile;
@ -453,6 +465,11 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
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))
{
file->setDate(info.lastModified());
@ -583,15 +600,20 @@ void ContentSelectorModel::ContentModel::setCurrentGameFile(const EsmFile* file)
void ContentSelectorModel::ContentModel::sortFiles()
{
emit layoutAboutToBeChanged();
int firstModifiable = 0;
while (mFiles.at(firstModifiable)->builtIn() || mFiles.at(firstModifiable)->fromAnotherConfigFile())
++firstModifiable;
// Dependency sort
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);
if (moved.find(file) == moved.end())
{
int index = -1;
for (int j = 0; j < i; ++j)
for (int j = firstModifiable; j < i; ++j)
{
const QStringList& gameFiles = mFiles.at(j)->gameFiles();
// 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;
}
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
{
return mPluginsWithLoadOrderError.contains(file->filePath());
@ -654,6 +698,7 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL
{
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
const EsmFile* file = item(filepath);
int filePosition = indexFromItem(file).row();
@ -751,7 +796,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath,
const EsmFile* file = item(filepath);
if (!file)
if (!file || file->builtIn() || file->fromAnotherConfigFile())
return false;
if (checkState)

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

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

@ -26,7 +26,9 @@ namespace ContentSelectorModel
FileProperty_DateModified = 3,
FileProperty_FilePath = 4,
FileProperty_Description = 5,
FileProperty_GameFile = 6
FileProperty_BuiltIn = 6,
FileProperty_FromAnotherConfigFile = 7,
FileProperty_GameFile = 8,
};
EsmFile(const QString& fileName = QString(), ModelItem* parent = nullptr);
@ -40,6 +42,8 @@ namespace ContentSelectorModel
void setFilePath(const QString& path);
void setGameFiles(const QStringList& gameFiles);
void setDescription(const QString& description);
void setBuiltIn(bool builtIn);
void setFromAnotherConfigFile(bool fromAnotherConfigFile);
void addGameFile(const QString& name) { mGameFiles.append(name); }
QVariant fileProperty(const FileProperty prop) const;
@ -49,18 +53,29 @@ namespace ContentSelectorModel
QDateTime modified() const { return mModified; }
QString formatVersion() const { return mVersion; }
QString filePath() const { return mPath; }
bool builtIn() const { return mBuiltIn; }
bool fromAnotherConfigFile() const { return mFromAnotherConfigFile; }
/// @note Contains file names, not paths.
const QStringList& gameFiles() const { return mGameFiles; }
QString description() const { return mDescription; }
QString toolTip() const
{
return mTooltipTemlate.arg(mAuthor)
.arg(mVersion)
.arg(mModified.toString(Qt::ISODate))
.arg(mPath)
.arg(mDescription)
.arg(mGameFiles.join(", "));
QString tooltip = mTooltipTemlate.arg(mAuthor)
.arg(mVersion)
.arg(mModified.toString(Qt::ISODate))
.arg(mPath)
.arg(mDescription)
.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;
@ -82,6 +97,8 @@ namespace ContentSelectorModel
QStringList mGameFiles;
QString mDescription;
QString mToolTip;
bool mBuiltIn = false;
bool mFromAnotherConfigFile = false;
};
}

@ -123,6 +123,11 @@ void ContentSelectorView::ContentSelector::buildContextMenu()
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)
{
clearCheckStates();
@ -336,4 +341,4 @@ void ContentSelectorView::ContentSelector::slotSearchFilterTextChanged(const QSt
void ContentSelectorView::ContentSelector::slotRowsMoved()
{
ui->addonView->selectionModel()->clearSelection();
}
}

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

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

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

@ -22,6 +22,16 @@ namespace Files
{
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

@ -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>
<translation type="unfinished"></translation>
</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>
<name>ContentSelectorView::ContentSelector</name>

@ -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>
<translation type="unfinished"></translation>
</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>
<name>ContentSelectorView::ContentSelector</name>

@ -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>
<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>
<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>
<name>ContentSelectorView::ContentSelector</name>

@ -370,6 +370,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source>
<translation type="unfinished"></translation>
</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>
<name>Launcher::GraphicsPage</name>

@ -370,6 +370,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source>
<translation type="unfinished"></translation>
</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>
<name>Launcher::GraphicsPage</name>

@ -372,6 +372,26 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>&amp;Uncheck Selected</source>
<translation>&amp;Отключить выбранные</translation>
</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>
<name>Launcher::GraphicsPage</name>

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

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

Loading…
Cancel
Save