mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-29 17:45:34 +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:
commit
8037a6e765
34 changed files with 665 additions and 249 deletions
|
@ -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('\"'))
|
||||
const QString& local = mSettings.value(QString("data-local")).value;
|
||||
|
||||
if (!local.isEmpty() && QDir(local).exists())
|
||||
{
|
||||
local.remove(0, 1);
|
||||
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);
|
||||
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
|
||||
{
|
||||
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*(.+)$");
|
||||
|
||||
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;
|
||||
|
||||
inline void removeDataDir(const QString& dir)
|
||||
QString getResourcesVfs() const;
|
||||
|
||||
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 mDataDirs;
|
||||
QStringList mContexts;
|
||||
|
||||
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><b>Author:</b> %1<br/><b>Format version:</b> %2<br/><b>Modified:</b> %3<br/><b>Path:</b><br/>%4<br/><br/><b>Description:</b><br/>%5<br/><br/><b>Dependencies: </b>%6<br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is part of OpenMW.</b><br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is enabled in a config file other than the user one.</b><br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ContentSelectorView::ContentSelector</name>
|
||||
|
|
|
@ -29,6 +29,14 @@
|
|||
<source><b>Author:</b> %1<br/><b>Format version:</b> %2<br/><b>Modified:</b> %3<br/><b>Path:</b><br/>%4<br/><br/><b>Description:</b><br/>%5<br/><br/><b>Dependencies: </b>%6<br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is part of OpenMW.</b><br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is enabled in a config file other than the user one.</b><br/></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ContentSelectorView::ContentSelector</name>
|
||||
|
|
|
@ -29,6 +29,14 @@
|
|||
<source><b>Author:</b> %1<br/><b>Format version:</b> %2<br/><b>Modified:</b> %3<br/><b>Path:</b><br/>%4<br/><br/><b>Description:</b><br/>%5<br/><br/><b>Dependencies: </b>%6<br/></source>
|
||||
<translation><b>Автор:</b> %1<br/><b>Версия формата данных:</b> %2<br/><b>Дата изменения:</b> %3<br/><b>Путь к файлу:</b><br/>%4<br/><br/><b>Описание:</b><br/>%5<br/><br/><b>Зависимости: </b>%6<br/></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is part of OpenMW.</b><br/></source>
|
||||
<translation><br/><b>Этот контентный файл не может быть отключен, потому что он является частью OpenMW.</b><br/></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><br/><b>This content file cannot be disabled because it is enabled in a config file other than the user one.</b><br/></source>
|
||||
<translation><br/><b>Этот контентный файл не может быть отключен, потому что он включен в конфигурационном файле, не являющемся пользовательским.</b><br/></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>&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>&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>&Uncheck Selected</source>
|
||||
<translation>&Отключить выбранные</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…
Reference in a new issue