From 1eb783aae348f906056cee17cb65dfd507429901 Mon Sep 17 00:00:00 2001 From: Tannin Date: Sat, 31 May 2014 13:43:48 +0200 Subject: - added a new mod type that represents files handled externally (i.e. DLCs) as mods in MO - hashes of file names in bsa files are no longer checked all the time - author and description is now read from esp files - rewrote the code that fixes modlists after a rename, should be a bit more robust - fixes to qt 5 and msvc 2013 compatibility - started to update the tutorial (not done yet!) - bugfix: counter for the problems badge wasn't calculated correctly --- src/bbcode.cpp | 15 +- src/browserdialog.cpp | 14 +- src/browserdialog.h | 4 +- src/directoryrefresher.cpp | 68 ++++-- src/directoryrefresher.h | 21 +- src/downloadmanager.cpp | 2 +- src/logbuffer.cpp | 51 +++-- src/mainwindow.cpp | 105 ++++++---- src/mainwindow.h | 3 +- src/mainwindow.ui | 38 ++-- src/modinfo.cpp | 329 ++++++++++++++++++++---------- src/modinfo.h | 165 ++++++++++++--- src/persistentcookiejar.cpp | 2 + src/pluginlist.cpp | 14 +- src/pluginlist.h | 2 + src/profile.cpp | 18 +- src/shared/directoryentry.cpp | 12 +- src/shared/directoryentry.h | 3 +- src/tutorials/tutorial_firststeps_main.js | 22 +- src/tutorials/tutorials_mainwindow.qml | 1 - 20 files changed, 623 insertions(+), 266 deletions(-) diff --git a/src/bbcode.cpp b/src/bbcode.cpp index 455f4767..f87b5d85 100644 --- a/src/bbcode.cpp +++ b/src/bbcode.cpp @@ -160,10 +160,17 @@ private: m_TagMap["youtube"] = std::make_pair(QRegExp("\\[youtube\\](.*)\\[/youtube\\]"), "http://www.youtube.com/v/\\1"); - m_ColorMap = boost::assign::map_list_of("red", "FF0000")("green", "00FF00")("blue", "0000FF") - ("black", "000000")("gray", "7F7F7F")("white", "FFFFFF") - ("yellow", "FFFF00")("cyan", "00FFFF")("magenta", "FF00FF") - ("brown", "A52A2A")("orange", "FFCC00"); + m_ColorMap.insert(std::make_pair("red", "FF0000")); + m_ColorMap.insert(std::make_pair("green", "00FF00")); + m_ColorMap.insert(std::make_pair("blue", "0000FF")); + m_ColorMap.insert(std::make_pair("black", "000000")); + m_ColorMap.insert(std::make_pair("gray", "7F7F7F")); + m_ColorMap.insert(std::make_pair("white", "FFFFFF")); + m_ColorMap.insert(std::make_pair("yellow", "FFFF00")); + m_ColorMap.insert(std::make_pair("cyan", "00FFFF")); + m_ColorMap.insert(std::make_pair("magenta", "FF00FF")); + m_ColorMap.insert(std::make_pair("brown", "A52A2A")); + m_ColorMap.insert(std::make_pair("orange", "FFCC00")); // make all patterns non-greedy and case-insensitive for (TagMap::iterator iter = m_TagMap.begin(); iter != m_TagMap.end(); ++iter) { diff --git a/src/browserdialog.cpp b/src/browserdialog.cpp index a832f1a3..81a5db68 100644 --- a/src/browserdialog.cpp +++ b/src/browserdialog.cpp @@ -255,16 +255,16 @@ void BrowserDialog::on_searchEdit_returnPressed() } } -void BrowserDialog::on_browserTabWidget_currentChanged(QWidget *current) +void BrowserDialog::on_refreshBtn_clicked() +{ + getCurrentView()->reload(); +} + +void BrowserDialog::on_browserTabWidget_currentChanged(int index) { - BrowserView *currentView = qobject_cast(current); + BrowserView *currentView = qobject_cast(ui->browserTabWidget->widget(index)); if (currentView != NULL) { ui->backBtn->setEnabled(currentView->history()->canGoBack()); ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); } } - -void BrowserDialog::on_refreshBtn_clicked() -{ - getCurrentView()->reload(); -} diff --git a/src/browserdialog.h b/src/browserdialog.h index 060d596c..bd27661b 100644 --- a/src/browserdialog.h +++ b/src/browserdialog.h @@ -99,10 +99,10 @@ private slots: void startSearch(); - void on_browserTabWidget_currentChanged(QWidget *arg1); - void on_refreshBtn_clicked(); + void on_browserTabWidget_currentChanged(int index); + private: QString guessFileName(const QString &url); diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp index 21d1f811..52e16be4 100644 --- a/src/directoryrefresher.cpp +++ b/src/directoryrefresher.cpp @@ -20,8 +20,10 @@ along with Mod Organizer. If not, see . #include "directoryrefresher.h" #include "utility.h" #include "report.h" +#include "modinfo.h" #include #include +#include using namespace MOBase; @@ -50,8 +52,15 @@ void DirectoryRefresher::setMods(const std::vector &managedArchives) { QMutexLocker locker(&m_RefreshLock); - m_Mods = mods; - m_ManagedArchives = managedArchives; + + m_Mods.clear(); + for (auto mod = mods.begin(); mod != mods.end(); ++mod) { + QString name = std::get<0>(*mod); + ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(name)); + m_Mods.push_back(EntryInfo(name, std::get<1>(*mod), info->stealFiles(), info->archives(), std::get<2>(*mod))); + } + + m_EnabledArchives = managedArchives; } @@ -68,18 +77,51 @@ void DirectoryRefresher::cleanStructure(DirectoryEntry *structure) } } -void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory) +void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure + , const QString &modName + , int priority + , const QString &directory + , const QStringList &stealFiles + , const QStringList &archives) { std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory)); - - directoryStructure->addFromOrigin(ToWString(modName), directoryW, priority); - QDir dir(directory); + std::wstring modNameW = ToWString(modName); + + + if (stealFiles.length() > 0) { + // instead of adding all the files of the target directory, we just change the root of the specified + // files to this mod + directoryStructure->createOrigin(modNameW, directoryW, priority); + foreach (const QString &filename, stealFiles) { + QFileInfo fileInfo(filename); + FileEntry::Ptr file = directoryStructure->findFile(ToWString(fileInfo.fileName())); + if (file.get() != NULL) { + if (file->getOrigin() == 0) { + // replace data as the origin on this bsa + file->removeOrigin(0); + file->addOrigin(directoryStructure->getOriginByName(modNameW).getID(), + file->getFileTime(), L""); + } + } + } + } else { + directoryStructure->addFromOrigin(modNameW, directoryW, priority); + } +/* QDir dir(directory); QFileInfoList bsaFiles = dir.entryInfoList(QStringList("*.bsa"), QDir::Files); foreach (QFileInfo file, bsaFiles) { - if (m_ManagedArchives.find(file.fileName()) != m_ManagedArchives.end()) { + if (m_EnabledArchives.find(file.fileName()) != m_EnabledArchives.end()) { directoryStructure->addFromBSA(ToWString(modName), directoryW, ToWString(QDir::toNativeSeparators(file.absoluteFilePath())), priority); } + }*/ + + foreach (const QString &archive, archives) { + QFileInfo fileInfo(archive); + if (m_EnabledArchives.find(fileInfo.fileName()) != m_EnabledArchives.end()) { + directoryStructure->addFromBSA(modNameW, directoryW, + ToWString(QDir::toNativeSeparators(fileInfo.absoluteFilePath())), priority); + } } } @@ -91,23 +133,23 @@ void DirectoryRefresher::refresh() m_DirectoryStructure = new DirectoryEntry(L"data", NULL, 0); + std::wstring dataDirectory = GameInfo::instance().getGameDirectory() + L"\\data"; + m_DirectoryStructure->addFromOrigin(L"data", dataDirectory, 0); + // TODO what was the point of having the priority in this tuple? the list is already sorted by priority - std::vector >::const_iterator iter = m_Mods.begin(); + auto iter = m_Mods.begin(); //TODO i is the priority here, where higher = more important. the input vector is also sorted by priority but inverted! for (int i = 1; iter != m_Mods.end(); ++iter, ++i) { - QString modName = std::get<0>(*iter); +qDebug("%s - %d", qPrintable(iter->modName), i); try { - addModToStructure(m_DirectoryStructure, modName, i, std::get<1>(*iter)); + addModToStructure(m_DirectoryStructure, iter->modName, i, iter->absolutePath, iter->stealFiles, iter->archives); } catch (const std::exception &e) { emit error(tr("failed to read bsa: %1").arg(e.what())); } emit progress((i * 100) / m_Mods.size() + 1); } - std::wstring dataDirectory = GameInfo::instance().getGameDirectory() + L"\\data"; - m_DirectoryStructure->addFromOrigin(L"data", dataDirectory, 0); - emit progress(100); cleanStructure(m_DirectoryStructure); diff --git a/src/directoryrefresher.h b/src/directoryrefresher.h index 691448fa..f828d793 100644 --- a/src/directoryrefresher.h +++ b/src/directoryrefresher.h @@ -23,6 +23,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include #include #include #include @@ -83,7 +84,7 @@ public: * @param directory * @param priorityDir */ - void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory); + void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory, const QStringList &stealFiles, const QStringList &archives); public slots: @@ -100,8 +101,22 @@ signals: private: - std::vector > m_Mods; - std::set m_ManagedArchives; + struct EntryInfo { + EntryInfo(const QString &modName, const QString &absolutePath, + const QStringList &stealFiles, const QStringList &archives, int priority) + : modName(modName), absolutePath(absolutePath), stealFiles(stealFiles) + , archives(archives), priority(priority) {} + QString modName; + QString absolutePath; + QStringList stealFiles; + QStringList archives; + int priority; + }; + +private: + + std::vector m_Mods; + std::set m_EnabledArchives; MOShared::DirectoryEntry *m_DirectoryStructure; QMutex m_RefreshLock; diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 5200bb4f..e3cab528 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -26,8 +26,8 @@ along with Mod Organizer. If not, see . #include "utility.h" #include "json.h" #include "selectiondialog.h" +#include "bbcode.h" #include -#include #include #include #include diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp index 1bf9cd85..ef0549b0 100644 --- a/src/logbuffer.cpp +++ b/src/logbuffer.cpp @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see . #include "report.h" #include #include +#include #include #include @@ -108,6 +109,17 @@ void LogBuffer::init(int messageCount, QtMsgType minMsgType, const QString &outp #endif } +char LogBuffer::msgTypeID(QtMsgType type) +{ + switch (type) { + case QtDebugMsg: return 'D'; + case QtWarningMsg: return 'W'; + case QtCriticalMsg: return 'C'; + case QtFatalMsg: return 'F'; + default: return '?'; + } +} + #if QT_VERSION >= QT_VERSION_CHECK(5,0,0) void LogBuffer::log(QtMsgType type, const QMessageLogContext &context, const QString &message) @@ -116,24 +128,31 @@ void LogBuffer::log(QtMsgType type, const QMessageLogContext &context, const QSt if (!s_Instance.isNull()) { s_Instance->logMessage(type, message); } - fprintf(stdout, "(%s:%u) %s\n", context.file, context.line, qPrintable(message)); +// fprintf(stdout, "(%s:%u) %s\n", context.file, context.line, qPrintable(message)); + if (type == QtDebugMsg) { + fprintf(stdout, "%s [%c] %s\n", qPrintable(QTime::currentTime().toString()), msgTypeID(type), qPrintable(message)); + } else { + fprintf(stdout, "%s [%c] (%s:%u) %s\n", qPrintable(QTime::currentTime().toString()), msgTypeID(type), + context.file, context.line, qPrintable(message)); + } fflush(stdout); } #else - -char LogBuffer::msgTypeID(QtMsgType type) +void LogBuffer::log(QtMsgType type, const char *message) { - switch (type) { - case QtDebugMsg: return 'D'; - case QtWarningMsg: return 'W'; - case QtCriticalMsg: return 'C'; - case QtFatalMsg: return 'F'; - default: return '?'; + QMutexLocker guard(&s_Mutex); + if (!s_Instance.isNull()) { + s_Instance->logMessage(type, message); } + fprintf(stdout, "%s [%c] %s\n", qPrintable(QTime::currentTime().toString()), msgTypeID(type), message); + fflush(stdout); } +#endif + + QModelIndex LogBuffer::index(int row, int column, const QModelIndex&) const { return createIndex(row, column, row); @@ -162,7 +181,7 @@ QVariant LogBuffer::data(const QModelIndex &index, int role) const { unsigned offset = m_NumMessages < m_Messages.size() ? 0 : m_NumMessages - m_Messages.size(); - unsigned int msgIndex = (offset + index.row()) % m_Messages.size(); + unsigned int msgIndex = (offset + index.row() + 1) % m_Messages.size(); switch (role) { case Qt::DisplayRole: { if (index.column() == 0) { @@ -200,18 +219,6 @@ QVariant LogBuffer::data(const QModelIndex &index, int role) const return QVariant(); } -void LogBuffer::log(QtMsgType type, const char *message) -{ - QMutexLocker guard(&s_Mutex); - if (!s_Instance.isNull()) { - s_Instance->logMessage(type, message); - } - fprintf(stdout, "%s [%c] %s\n", qPrintable(QTime::currentTime().toString()), msgTypeID(type), message); - fflush(stdout); -} - -#endif - void LogBuffer::writeNow() { QMutexLocker guard(&s_Mutex); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1e8d512a..496d0d9e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -538,11 +538,9 @@ void MainWindow::updateProblemsButton() ui->actionProblems->setIconText(tr("Problems")); ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); -// QPixmap mergedIcon(64, 64); QPixmap mergedIcon = QPixmap(":/MO/gui/warning").scaled(64, 64); { QPainter painter(&mergedIcon); -// painter.setBrush(QBrush(Qt::transparent)); std::string badgeName = std::string(":/MO/gui/badge_") + (numProblems < 10 ? std::to_string(static_cast(numProblems)) : "more"); painter.drawPixmap(32, 32, 32, 32, QPixmap(badgeName.c_str())); } @@ -588,11 +586,11 @@ bool MainWindow::errorReported(QString &logFile) int MainWindow::checkForProblems() { + int numProblems = 0; foreach (IPluginDiagnose *diagnose, m_DiagnosisPlugins) { - std::vector activeProblems = diagnose->activeProblems(); - return activeProblems.size(); + numProblems += diagnose->activeProblems().size(); } - return 0; + return numProblems; } void MainWindow::about() @@ -672,7 +670,11 @@ void MainWindow::saveArchiveList() for (int j = 0; j < tlItem->childCount(); ++j) { QTreeWidgetItem *item = tlItem->child(j); if (item->checkState(0) == Qt::Checked) { - archiveFile->write(item->text(0).toUtf8().append("\r\n")); + // in managed mode, "register" all enabled archives, otherwise register only the files registered in the ini + if (ui->manageArchivesBox->isChecked() + || item->data(0, Qt::UserRole).toBool()) { + archiveFile->write(item->text(0).toUtf8().append("\r\n")); + } } } } @@ -1624,11 +1626,11 @@ bool MainWindow::refreshProfiles(bool selectProfile) } } - -std::set MainWindow::managedArchives() +std::set MainWindow::enabledArchives() { std::set result; + QFile archiveFile(m_CurrentProfile->getArchivesFileName()); if (archiveFile.open(QIODevice::ReadOnly)) { while (!archiveFile.atEnd()) { @@ -1645,7 +1647,7 @@ void MainWindow::refreshDirectoryStructure() m_DirectoryUpdate = true; std::vector > activeModList = m_CurrentProfile->getActiveMods(); - m_DirectoryRefresher.setMods(activeModList, managedArchives()); + m_DirectoryRefresher.setMods(activeModList, enabledArchives()); statusBar()->show(); m_RefreshProgress->setRange(0, 100); @@ -1847,7 +1849,7 @@ void MainWindow::refreshBSAList() m_ActiveArchives.clear(); - auto iter = managedArchives(); + auto iter = enabledArchives(); m_ActiveArchives = toStringList(iter.begin(), iter.end()); if (m_ActiveArchives.isEmpty()) { m_ActiveArchives = m_DefaultArchives; @@ -1871,6 +1873,7 @@ void MainWindow::refreshBSAList() if (index == -1) { index = 0xFFFF; } + QString basename = filename.left(filename.indexOf(".")); QStringList strings(filename); bool isArchive = false; int origin = current->getOrigin(isArchive); @@ -1880,11 +1883,23 @@ void MainWindow::refreshBSAList() newItem->setData(1, Qt::UserRole, origin); newItem->setFlags(newItem->flags() & ~Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable); newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked); - if (m_Settings.forceEnableCoreFiles() && m_DefaultArchives.contains(filename)) { + newItem->setData(0, Qt::UserRole, false); + if (m_Settings.forceEnableCoreFiles() + && m_DefaultArchives.contains(filename)) { + newItem->setCheckState(0, Qt::Checked); + newItem->setDisabled(true); + newItem->setData(0, Qt::UserRole, true); + } else if ((m_PluginList.state(basename + ".esp") == IPluginList::STATE_ACTIVE) + || (m_PluginList.state(basename + ".esm") == IPluginList::STATE_ACTIVE)) { newItem->setCheckState(0, Qt::Checked); newItem->setDisabled(true); } else { - newItem->setCheckState(0, (index != 0xFFFF) ? Qt::Checked : Qt::Unchecked); + if (ui->manageArchivesBox->isChecked()) { + newItem->setCheckState(0, (index != 0xFFFF) ? Qt::Checked : Qt::Unchecked); + } else { + newItem->setCheckState(0, Qt::Unchecked); + newItem->setDisabled(true); + } } if (index < 0) index = 0; @@ -1909,6 +1924,7 @@ void MainWindow::refreshBSAList() ui->bsaList->addTopLevelItem(subItem); } subItem->addChild(iter->second); + subItem->setExpanded(true); } checkBSAList(); @@ -2630,7 +2646,9 @@ void MainWindow::modStatusChanged(unsigned int index) m_DirectoryRefresher.addModToStructure(m_DirectoryStructure , modInfo->name() , m_CurrentProfile->getModPriority(index) - , modInfo->absolutePath()); + , modInfo->absolutePath() + , modInfo->stealFiles() + , modInfo->archives()); DirectoryRefresher::cleanStructure(m_DirectoryStructure); } else { if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { @@ -2728,35 +2746,32 @@ void MainWindow::renameModInList(QFile &modList, const QString &oldName, const Q while (!modList.atEnd()) { QByteArray line = modList.readLine(); + if (line.length() == 0) { - break; + // ignore empty lines + qWarning("mod list contained invalid data: empty line"); + continue; } - if (line.at(0) == '#') { + + char spec = line.at(0); + if (spec == '#') { + // don't touch comments outBuffer.write(line); continue; } - QString modName = QString::fromUtf8(line.constData()).trimmed(); - if (modName.isEmpty()) { - break; - } - bool disabled = false; - if (modName.at(0) == '-') { - disabled = true; - modName = modName.mid(1); - } else if (modName.at(0) == '+') { - modName = modName.mid(1); + QString modName = QString::fromUtf8(line).mid(1).trimmed(); + + if (modName.isEmpty()) { + // file broken? + qWarning("mod list contained invalid data: missing mod name"); + continue; } + outBuffer.write(QByteArray(1, spec)); if (modName == oldName) { modName = newName; } - - if (disabled) { - outBuffer.write("-"); - } else { - outBuffer.write("+"); - } outBuffer.write(modName.toUtf8().constData()); outBuffer.write("\r\n"); } @@ -3130,6 +3145,9 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, dialog->raise(); dialog->activateWindow(); connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int))); + } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { + // nop - no menu for this file type + return; } else { modInfo->saveMeta(); ModInfoDialog dialog(modInfo, m_DirectoryStructure, this); @@ -3159,9 +3177,12 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); origin.enable(false); - m_DirectoryRefresher.addModToStructure(m_DirectoryStructure, - modInfo->name(), m_CurrentProfile->getModPriority(index), - modInfo->absolutePath()); + m_DirectoryRefresher.addModToStructure(m_DirectoryStructure + , modInfo->name() + , m_CurrentProfile->getModPriority(index) + , modInfo->absolutePath() + , modInfo->stealFiles() + , modInfo->archives()); DirectoryRefresher::cleanStructure(m_DirectoryStructure); refreshLists(); } @@ -3728,6 +3749,8 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) { menu->addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked())); menu->addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked())); + } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { + // nop, nothing to do with this mod } else { QMenu *addRemoveCategoriesMenu = new QMenu(tr("Add/Remove Categories")); populateMenuCategories(addRemoveCategoriesMenu, 0); @@ -3786,8 +3809,10 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) menu->addAction(tr("Open in explorer"), this, SLOT(openExplorer_clicked())); } - QAction *infoAction = menu->addAction(tr("Information..."), this, SLOT(information_clicked())); - menu->setDefaultAction(infoAction); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) { + QAction *infoAction = menu->addAction(tr("Information..."), this, SLOT(information_clicked())); + menu->setDefaultAction(infoAction); + } } menu->exec(modList->mapToGlobal(pos)); @@ -4818,7 +4843,7 @@ void MainWindow::extractBSATriggered() QString originPath = QDir::fromNativeSeparators(ToQString(m_DirectoryStructure->getOriginByName(ToWString(item->text(1))).getPath())); QString archivePath = QString("%1\\%2").arg(originPath).arg(item->text(0)); - BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData()); + BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData(), true); if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) { reportError(tr("failed to read %1: %2").arg(archivePath).arg(result)); return; @@ -4902,6 +4927,7 @@ void MainWindow::on_actionProblems_triggered() ProblemsDialog problems(m_DiagnosisPlugins, this); if (problems.hasProblems()) { problems.exec(); + updateProblemsButton(); } } @@ -5410,3 +5436,8 @@ void MainWindow::on_managedArchiveLabel_linkHovered(const QString&) QToolTip::showText(QCursor::pos(), ui->managedArchiveLabel->toolTip()); } + +void MainWindow::on_manageArchivesBox_toggled(bool) +{ + refreshBSAList(); +} diff --git a/src/mainwindow.h b/src/mainwindow.h index bbbbea2f..f55a7f8f 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -285,7 +285,7 @@ private: QMenu *modListContextMenu(); - std::set managedArchives(); + std::set enabledArchives(); private: @@ -593,6 +593,7 @@ private slots: // ui slots void on_categoriesAndBtn_toggled(bool checked); void on_categoriesOrBtn_toggled(bool checked); void on_managedArchiveLabel_linkHovered(const QString &link); + void on_manageArchivesBox_toggled(bool checked); }; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 1bb70d64..aaf7e63a 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -126,7 +126,7 @@ - If checked, all mods that match one of the selected categories are displayed. + If checked, all mods that match at least one of the selected categories are displayed. Or @@ -899,17 +899,31 @@ p, li { white-space: pre-wrap; } 6 - - - <html><head/><body><p><span style=" font-weight:600;">Managed</span> Archives are always loaded and the priority of their mod (left pane) applies to them. MO will also provide conflict information for managed archives.<br/><span style=" font-weight:600;">Unmanaged</span> Archives are only loaded if there is a plugin of the same name (&quot;Plugins&quot; tab) with the same priority as that plugin!</p><p><span style=" font-style:italic;">If you don't understand this it's safest to leave all archives unchecked except the grayed out ones.</span></p></body></html> - - - <html><head/><body><p>Check an archive to have MO manage it. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">read more</span></a>)</p></body></html> - - - true - - + + + + + + + + true + + + + + + + <html><head/><body><p>BSAs are bundles of game assets (textures, scripts, ...). By default, the engine loads these bundles in a separate step from loose files. MO can manage those archives to align their load order with that of loose files:</p><p>If archives are <span style=" font-weight:600;">managed</span>, their load order is specified by the priority of the corresponding mod (left pane), the same as the loose files. You can manually enable any BSA that has no corresponding plugin active.<br/></p><p>If archives are <span style=" font-weight:600;">not managed</span> their load order is specified by the priority of the corresponding plugin (right pane, plugins tab). You can then not manually enable BSAs where the plugin isn't active.</p><p>In either case you can not disable archives if there is a matching plugin, the game will load them no matter what.</p></body></html> + + + <html><head/><body><p>Have MO manage archives (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">read more</span></a>)</p></body></html> + + + true + + + + diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 12463559..87321b69 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -73,6 +73,16 @@ ModInfo::Ptr ModInfo::createFrom(const QDir &dir, DirectoryEntry **directoryStru } +ModInfo::Ptr ModInfo::createFromPlugin(const QString &espName, const QStringList &bsaNames + , DirectoryEntry ** directoryStructure) +{ + QMutexLocker locker(&s_Mutex); + ModInfo::Ptr result = ModInfo::Ptr(new ModInfoForeign(espName, bsaNames, directoryStructure)); + s_Collection.push_back(result); + return result; +} + + void ModInfo::createFromOverwrite() { QMutexLocker locker(&s_Mutex); @@ -179,12 +189,29 @@ void ModInfo::updateFromDisc(const QString &modDirectory, DirectoryEntry **direc QMutexLocker lock(&s_Mutex); s_Collection.clear(); s_NextID = 0; - // list all directories in the mod directory and make a mod out of each - QDir mods(QDir::fromNativeSeparators(modDirectory)); - mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); - QDirIterator modIter(mods); - while (modIter.hasNext()) { - createFrom(QDir(modIter.next()), directoryStructure); + + { // list all directories in the mod directory and make a mod out of each + QDir mods(QDir::fromNativeSeparators(modDirectory)); + mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); + QDirIterator modIter(mods); + while (modIter.hasNext()) { + createFrom(QDir(modIter.next()), directoryStructure); + } + } + + { // list plugins in the data directory and make a foreign-managed mod out of each + QDir dataDir(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory())) + "/data"); + foreach (const QFileInfo &file, dataDir.entryInfoList(QStringList() << "*.esp" << "*.esm")) { + if ((file.baseName() != "Update") + && (file.baseName() != ToQString(GameInfo::instance().getGameName()))) { + QStringList archives; + foreach (const QString archiveName, dataDir.entryList(QStringList() << file.baseName() + "*.bsa")) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + + createFromPlugin(file.fileName(), archives, directoryStructure); + } + } } createFromOverwrite(); @@ -293,18 +320,136 @@ void ModInfo::testValid() } +ModInfoWithConflictInfo::ModInfoWithConflictInfo(DirectoryEntry **directoryStructure) + : m_DirectoryStructure(directoryStructure) {} + +void ModInfoWithConflictInfo::clearCaches() +{ + m_LastConflictCheck = QTime(); +} + +std::vector ModInfoWithConflictInfo::getFlags() const +{ + std::vector result; + switch (isConflicted()) { + case CONFLICT_MIXED: { + result.push_back(ModInfo::FLAG_CONFLICT_MIXED); + } break; + case CONFLICT_OVERWRITE: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); + } break; + case CONFLICT_OVERWRITTEN: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); + } break; + case CONFLICT_REDUNDANT: { + result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); + } break; + default: { /* NOP */ } + } + return result; +} + + +ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isConflicted() const +{ + // this is costy so cache the result + QTime now = QTime::currentTime(); + if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) { + bool overwrite = false; + bool overwritten = false; + bool regular = false; + + int dataID = 0; + if ((*m_DirectoryStructure)->originExists(L"data")) { + dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); + } + + std::wstring name = ToWString(this->name()); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { + const std::vector &alternatives = (*iter)->getAlternatives(); + if (alternatives.size() == 0) { + // no alternatives -> no conflict + regular = true; + } else { + for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { + // don't treat files overwritten in data as "conflict" + if (*altIter != dataID) { + bool ignore = false; + if ((*iter)->getOrigin(ignore) == origin.getID()) { + overwrite = true; + break; + } else { + overwritten = true; + break; + } + } else if (alternatives.size() == 1) { + // only alternative is data -> no conflict + regular = true; + } + } + } + } + } + + m_LastConflictCheck = QTime::currentTime(); + + if (overwrite && overwritten) m_CurrentConflictState = CONFLICT_MIXED; + else if (overwrite) m_CurrentConflictState = CONFLICT_OVERWRITE; + else if (overwritten) { + if (!regular) { + m_CurrentConflictState = CONFLICT_REDUNDANT; + } else { + m_CurrentConflictState = CONFLICT_OVERWRITTEN; + } + } + else m_CurrentConflictState = CONFLICT_NONE; + } + + return m_CurrentConflictState; +} + + +bool ModInfoWithConflictInfo::isRedundant() const +{ + std::wstring name = ToWString(this->name()); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + bool ignore = false; + for (auto iter = files.begin(); iter != files.end(); ++iter) { + if ((*iter)->getOrigin(ignore) == origin.getID()) { + return false; + } + } + return true; + } else { + return false; + } +} + + + ModInfoRegular::ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure) - : ModInfo(), m_Name(path.dirName()), m_Path(path.absolutePath()), m_MetaInfoChanged(false), - m_EndorsedState(ENDORSED_UNKNOWN), m_DirectoryStructure(directoryStructure) + : ModInfoWithConflictInfo(directoryStructure) + , m_Name(path.dirName()) + , m_Path(path.absolutePath()) + , m_MetaInfoChanged(false) + , m_EndorsedState(ENDORSED_UNKNOWN) { testValid(); m_CreationTime = QFileInfo(path.absolutePath()).created(); // read out the meta-file for information readMeta(); - connect(&m_NexusBridge, SIGNAL(descriptionAvailable(int,QVariant,QVariant)), this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(endorsementToggled(int,QVariant,QVariant)), this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(requestFailed(int,int,QVariant,QString)), this, SLOT(nxmRequestFailed(int,int,QVariant,QString))); + connect(&m_NexusBridge, SIGNAL(descriptionAvailable(int,QVariant,QVariant)) + , this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(endorsementToggled(int,QVariant,QVariant)) + , this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(requestFailed(int,int,QVariant,QString)) + , this, SLOT(nxmRequestFailed(int,int,QVariant,QString))); } @@ -612,11 +757,6 @@ void ModInfoRegular::endorse(bool doEndorse) } } -void ModInfoRegular::clearCaches() -{ - m_LastConflictCheck = QTime(); -} - QString ModInfoRegular::absolutePath() const { @@ -636,22 +776,7 @@ void ModInfoRegular::ignoreUpdate(bool ignore) std::vector ModInfoRegular::getFlags() const { - std::vector result; - switch (isConflicted()) { - case CONFLICT_MIXED: { - result.push_back(ModInfo::FLAG_CONFLICT_MIXED); - } break; - case CONFLICT_OVERWRITE: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); - } break; - case CONFLICT_OVERWRITTEN: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); - } break; - case CONFLICT_REDUNDANT: { - result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); - } break; - default: { /* NOP */ } - } + std::vector result = ModInfoWithConflictInfo::getFlags(); if ((m_NexusID != -1) && (endorsedState() == ENDORSED_FALSE)) { result.push_back(ModInfo::FLAG_NOTENDORSED); } @@ -712,90 +837,20 @@ ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const return m_EndorsedState; } -ModInfoRegular::EConflictType ModInfoRegular::isConflicted() const +QDateTime ModInfoRegular::getLastNexusQuery() const { - // this is costy so cache the result - QTime now = QTime::currentTime(); - if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) { - bool overwrite = false; - bool overwritten = false; - bool regular = false; - - int dataID = 0; - if ((*m_DirectoryStructure)->originExists(L"data")) { - dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); - } - - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { - const std::vector &alternatives = (*iter)->getAlternatives(); - if (alternatives.size() == 0) { - // no alternatives -> no conflict - regular = true; - } else { - for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { - // don't treat files overwritten in data as "conflict" - if (*altIter != dataID) { - bool ignore = false; - if ((*iter)->getOrigin(ignore) == origin.getID()) { - overwrite = true; - break; - } else { - overwritten = true; - break; - } - } else if (alternatives.size() == 1) { - // only alternative is data -> no conflict - regular = true; - } - } - } - } - } - - m_LastConflictCheck = QTime::currentTime(); - - if (overwrite && overwritten) m_CurrentConflictState = CONFLICT_MIXED; - else if (overwrite) m_CurrentConflictState = CONFLICT_OVERWRITE; - else if (overwritten) { - if (!regular) { - m_CurrentConflictState = CONFLICT_REDUNDANT; - } else { - m_CurrentConflictState = CONFLICT_OVERWRITTEN; - } - } - else m_CurrentConflictState = CONFLICT_NONE; - } - - return m_CurrentConflictState; + return m_LastNexusQuery; } -bool ModInfoRegular::isRedundant() const +QStringList ModInfoRegular::archives() const { - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - bool ignore = false; - for (auto iter = files.begin(); iter != files.end(); ++iter) { - if ((*iter)->getOrigin(ignore) == origin.getID()) { - return false; - } - } - return true; - } else { - return false; + QStringList result; + QDir dir(this->absolutePath()); + foreach (const QString &archive, dir.entryList(QStringList("*.bsa"))) { + result.append(this->absolutePath() + "/" + archive); } -} - - -QDateTime ModInfoRegular::getLastNexusQuery() const -{ - return m_LastNexusQuery; + return result; } std::vector ModInfoRegular::getIniTweaks() const @@ -874,9 +929,61 @@ int ModInfoOverwrite::getHighlight() const return (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; } - QString ModInfoOverwrite::getDescription() const { return tr("This pseudo mod contains files from the virtual data tree that got " "modified (i.e. by the construction kit)"); } + +QStringList ModInfoOverwrite::archives() const +{ + QStringList result; + QDir dir(this->absolutePath()); + foreach (const QString &archive, dir.entryList(QStringList("*.bsa"))) { + result.append(this->absolutePath() + "/" + archive); + } + return result; +} + +QString ModInfoForeign::name() const +{ + return m_Name; +} + +QDateTime ModInfoForeign::creationTime() const +{ + return m_CreationTime; +} + +QString ModInfoForeign::absolutePath() const +{ + return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory())) + "/data"; +} + +std::vector ModInfoForeign::getFlags() const +{ + std::vector result = ModInfoWithConflictInfo::getFlags(); + result.push_back(FLAG_FOREIGN); + + return result; +} + +int ModInfoForeign::getHighlight() const +{ + return 0; +} + +QString ModInfoForeign::getDescription() const +{ + return tr("This pseudo mod represents content managed outside MO. It isn't modified by MO."); +} + +ModInfoForeign::ModInfoForeign(const QString &referenceFile, const QStringList &archives, + DirectoryEntry **directoryStructure) + : ModInfoWithConflictInfo(directoryStructure) + , m_ReferenceFile(referenceFile) + , m_Archives(archives) +{ + m_CreationTime = QFileInfo(referenceFile).created(); + m_Name = QFileInfo(m_ReferenceFile).baseName(); +} diff --git a/src/modinfo.h b/src/modinfo.h index 8b37b915..21267068 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -59,6 +59,7 @@ public: FLAG_INVALID, FLAG_BACKUP, FLAG_OVERWRITE, + FLAG_FOREIGN, FLAG_NOTENDORSED, FLAG_NOTES, FLAG_CONFLICT_OVERWRITE, @@ -160,6 +161,14 @@ public: */ static ModInfo::Ptr createFrom(const QDir &dir, MOShared::DirectoryEntry **directoryStructure); + /** + * @brief create a new "foreign-managed" mod from a tuple of plugin and archives + * @param espName name of the plugin + * @param bsaNames names of archives + * @return a new mod + */ + static ModInfo::Ptr createFromPlugin(const QString &espName, const QStringList &bsaNames, MOShared::DirectoryEntry **directoryStructure); + virtual bool isRegular() const { return false; } virtual bool isEmpty() const { return false; } @@ -339,6 +348,11 @@ public: */ virtual int getFixedPriority() const = 0; + /** + * @return true if the mod is always enabled + */ + virtual bool alwaysEnabled() const { return false; } + /** * @return true if the mod can be updated */ @@ -389,6 +403,16 @@ public: */ virtual QDateTime getLastNexusQuery() const = 0; + /** + * @return a list of files that, if they exist in the data directory are treated as files in THIS mod + */ + virtual QStringList stealFiles() const { return QStringList(); } + + /** + * @return a list of archives belonging to this mod (as absolute file paths) + */ + virtual QStringList archives() const = 0; + /** * @brief test if the mod belongs to the specified category * @@ -481,6 +505,50 @@ private: }; +class ModInfoWithConflictInfo : public ModInfo +{ + +public: + + ModInfoWithConflictInfo(MOShared::DirectoryEntry **directoryStructure); + + std::vector getFlags() const; + + /** + * @brief clear all caches held for this mod + */ + virtual void clearCaches(); +private: + + enum EConflictType { + CONFLICT_NONE, + CONFLICT_OVERWRITE, + CONFLICT_OVERWRITTEN, + CONFLICT_MIXED, + CONFLICT_REDUNDANT + }; + +private: + + /** + * @return true if there is a conflict for files in this mod + */ + EConflictType isConflicted() const; + + /** + * @return true if this mod is completely replaced by others + */ + bool isRedundant() const; + +private: + + MOShared::DirectoryEntry **m_DirectoryStructure; + + mutable EConflictType m_CurrentConflictState; + mutable QTime m_LastConflictCheck; + +}; + /** * @brief Represents meta information about a single mod. @@ -489,7 +557,7 @@ private: * to manage the mod collection * **/ -class ModInfoRegular : public ModInfo +class ModInfoRegular : public ModInfoWithConflictInfo { Q_OBJECT @@ -643,11 +711,6 @@ public: */ virtual void endorse(bool doEndorse); - /** - * @brief clear all caches held for this mod - */ - virtual void clearCaches(); - /** * @brief getter for the mod name * @@ -746,7 +809,9 @@ public: /** * @return last time nexus was queried for infos on this mod */ - QDateTime getLastNexusQuery() const; + virtual QDateTime getLastNexusQuery() const; + + virtual QStringList archives() const; /** * @brief stores meta information back to disk @@ -754,15 +819,6 @@ public: virtual void saveMeta(); void readMeta(); -private: - - enum EConflictType { - CONFLICT_NONE, - CONFLICT_OVERWRITE, - CONFLICT_OVERWRITTEN, - CONFLICT_MIXED, - CONFLICT_REDUNDANT - }; private slots: @@ -770,18 +826,6 @@ private slots: void nxmEndorsementToggled(int, QVariant userData, QVariant resultData); void nxmRequestFailed(int modID, int fileID, QVariant userData, const QString &errorMessage); -private: - - /** - * @return true if there is a conflict for files in this mod - */ - EConflictType isConflicted() const; - - /** - * @return true if this mod is completely replaced by others - */ - bool isRedundant() const; - protected: ModInfoRegular(const QDir &path, MOShared::DirectoryEntry **directoryStructure); @@ -807,11 +851,6 @@ private: NexusBridge m_NexusBridge; - MOShared::DirectoryEntry **m_DirectoryStructure; - - mutable EConflictType m_CurrentConflictState; - mutable QTime m_LastConflictCheck; - }; @@ -887,6 +926,7 @@ public: virtual QString getDescription() const; virtual QDateTime getLastNexusQuery() const { return QDateTime(); } virtual QString getNexusDescription() const { return QString(); } + virtual QStringList archives() const; private: @@ -898,4 +938,63 @@ private: }; + +class ModInfoForeign : public ModInfoWithConflictInfo +{ + + Q_OBJECT + + friend class ModInfo; + +public: + + virtual bool updateAvailable() const { return false; } + virtual bool updateIgnored() const { return false; } + virtual bool downgradeAvailable() const { return false; } + virtual bool updateNXMInfo() { return false; } + virtual void setCategory(int, bool) {} + virtual bool setName(const QString&) { return false; } + virtual void setNotes(const QString&) {} + virtual void setNexusID(int) {} + virtual void setNewestVersion(const MOBase::VersionInfo&) {} + virtual void ignoreUpdate(bool) {} + virtual void setNexusDescription(const QString&) {} + virtual void addNexusCategory(int) {} + virtual void setIsEndorsed(bool) {} + virtual void setNeverEndorse() {} + virtual bool remove() { return false; } + virtual void endorse(bool) {} + virtual bool isEmpty() const { return false; } + virtual QString name() const; + virtual QString notes() const { return ""; } + virtual QDateTime creationTime() const; + virtual QString absolutePath() const; + virtual MOBase::VersionInfo getNewestVersion() const { return ""; } + virtual QString getInstallationFile() const { return ""; } + virtual int getNexusID() const { return -1; } + virtual std::vector getIniTweaks() const { return std::vector(); } + virtual std::vector getFlags() const; + virtual int getHighlight() const; + virtual QString getDescription() const; + virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual QString getNexusDescription() const { return QString(); } + virtual int getFixedPriority() const { return INT_MIN; } + virtual QStringList archives() const { return m_Archives; } + virtual QStringList stealFiles() const { return m_Archives + QStringList(m_ReferenceFile); } + virtual bool alwaysEnabled() const { return true; } + +protected: + + ModInfoForeign(const QString &referenceFile, const QStringList &archives, MOShared::DirectoryEntry **directoryStructure); + +private: + + QString m_Name; + QString m_ReferenceFile; + QStringList m_Archives; + QDateTime m_CreationTime; + int m_Priority; + +}; + #endif // MODINFO_H diff --git a/src/persistentcookiejar.cpp b/src/persistentcookiejar.cpp index 7b9694a2..6ca0de39 100644 --- a/src/persistentcookiejar.cpp +++ b/src/persistentcookiejar.cpp @@ -1,5 +1,7 @@ #include "persistentcookiejar.h" #include +#include +#include PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *parent) diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index 24504f92..b1d2a446 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -773,15 +773,21 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const if (m_ESPs[index].m_ForceEnabled) { toolTip += tr("This plugin can't be disabled (enforced by the game)"); } else { - QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); + QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); + if (m_ESPs[index].m_Author.size() > 0) { + text += "
" + tr("Author") + ": " + m_ESPs[index].m_Author; + } + if (m_ESPs[index].m_Description.size() > 0) { + text += "
" + tr("Description") + ": " + m_ESPs[index].m_Description; + } if (m_ESPs[index].m_MasterUnset.size() > 0) { - text += "
" + tr("Missing Masters") + ": " + SetJoin(m_ESPs[index].m_MasterUnset, ", ") + ""; + text += "
" + tr("Missing Masters") + ": " + SetJoin(m_ESPs[index].m_MasterUnset, ", ") + ""; } std::set enabledMasters; std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(), m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(), std::inserter(enabledMasters, enabledMasters.end())); - text += "
" + tr("Enabled Masters") + ": " + SetJoin(enabledMasters, ", "); + text += "
" + tr("Enabled Masters") + ": " + SetJoin(enabledMasters, ", "); if (m_ESPs[index].m_HasIni) { text += "
There is an ini file connected to this esp. Its settings will be added to your game settings, overwriting " "in case of conflicts."; @@ -1093,6 +1099,8 @@ PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled, FILETIME time, ESP::File file(ToWString(fullPath)); m_IsMaster = file.isMaster(); m_IsDummy = file.isDummy(); + m_Author = QString::fromLatin1(file.author().c_str()); + m_Description = QString::fromLatin1(file.description().c_str()); std::set masters = file.masters(); for (auto iter = masters.begin(); iter != masters.end(); ++iter) { m_Masters.insert(QString(iter->c_str())); diff --git a/src/pluginlist.h b/src/pluginlist.h index ecd9c654..bb988281 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -244,6 +244,8 @@ private: QString m_OriginName; bool m_IsMaster; bool m_IsDummy; + QString m_Author; + QString m_Description; bool m_HasIni; std::set m_Masters; mutable std::set m_MasterUnset; diff --git a/src/profile.cpp b/src/profile.cpp index 8d02c047..2d982790 100644 --- a/src/profile.cpp +++ b/src/profile.cpp @@ -162,8 +162,11 @@ void Profile::writeModlistNow(bool onlyOnTimer) const unsigned int index = m_ModIndexByPriority[i]; if (index != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - if (modInfo->getFixedPriority() == INT_MIN) { - if (m_ModStatus[index].m_Enabled) { + std::vector flags = modInfo->getFlags(); + if ((modInfo->getFixedPriority() == INT_MIN)) { + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { + file->write("*"); + } else if (m_ModStatus[index].m_Enabled) { file->write("+"); } else { file->write("-"); @@ -255,7 +258,8 @@ void Profile::refreshModStatus() } else if (line.at(0) == '-') { enabled = false; modName = QString::fromUtf8(line.mid(1).trimmed().constData()); - } else if (line.at(0) == '+') { + } else if ((line.at(0) == '+') + || (line.at(0) == '*')) { modName = QString::fromUtf8(line.mid(1).trimmed().constData()); } else { modName = QString::fromUtf8(line.trimmed().constData()); @@ -269,8 +273,9 @@ void Profile::refreshModStatus() unsigned int modindex = ModInfo::getIndex(modName); if (modindex != UINT_MAX) { ModInfo::Ptr info = ModInfo::getByIndex(modindex); - if ((modindex < m_ModStatus.size()) && (info->getFixedPriority() == INT_MIN)) { - m_ModStatus[modindex].m_Enabled = enabled; + if ((modindex < m_ModStatus.size()) + && (info->getFixedPriority() == INT_MIN)) { + m_ModStatus[modindex].m_Enabled = enabled || info->alwaysEnabled(); if (m_ModStatus[modindex].m_Priority == -1) { if (static_cast(index) >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); @@ -295,9 +300,10 @@ void Profile::refreshModStatus() // give priorities to mods not referenced in the profile for (size_t i = 0; i < m_ModStatus.size(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - if (!modInfo->canBeEnabled()) { + if (modInfo->getFixedPriority() == INT_MAX) { continue; } + if (m_ModStatus[i].m_Priority != -1) { m_ModStatus[i].m_Priority = numKnownMods - m_ModStatus[i].m_Priority - 1; } else { diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index 6311ba25..b7863284 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -443,10 +443,12 @@ FilesOrigin &DirectoryEntry::createOrigin(const std::wstring &originName, const void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority) { FilesOrigin &origin = createOrigin(originName, directory, priority); - boost::scoped_array buffer(new wchar_t[MAXPATH_UNICODE + 1]); - memset(buffer.get(), L'\0', MAXPATH_UNICODE + 1); - int offset = _snwprintf(buffer.get(), MAXPATH_UNICODE, L"%ls", directory.c_str()); - addFiles(origin, buffer.get(), offset); + if (directory.length() != 0) { + boost::scoped_array buffer(new wchar_t[MAXPATH_UNICODE + 1]); + memset(buffer.get(), L'\0', MAXPATH_UNICODE + 1); + int offset = _snwprintf(buffer.get(), MAXPATH_UNICODE, L"%ls", directory.c_str()); + addFiles(origin, buffer.get(), offset); + } m_Populated = true; } @@ -461,7 +463,7 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di } BSA::Archive archive; - BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str()); + BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false); if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { std::ostringstream stream; stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError(); diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h index f691603f..7b656b05 100644 --- a/src/shared/directoryentry.h +++ b/src/shared/directoryentry.h @@ -273,6 +273,8 @@ public: bool hasContentsFromOrigin(int originID) const; + FilesOrigin &createOrigin(const std::wstring &originName, const std::wstring &directory, int priority); + private: DirectoryEntry(const DirectoryEntry &reference); @@ -292,7 +294,6 @@ private: origin.addFile(file->getIndex()); } - FilesOrigin &createOrigin(const std::wstring &originName, const std::wstring &directory, int priority); void addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset); void addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName); diff --git a/src/tutorials/tutorial_firststeps_main.js b/src/tutorials/tutorial_firststeps_main.js index e952c95a..97330a48 100644 --- a/src/tutorials/tutorial_firststeps_main.js +++ b/src/tutorials/tutorial_firststeps_main.js @@ -3,7 +3,8 @@ function getTutorialSteps() { return [ function() { - tutorial.text = qsTr("Welcome to the ModOrganizer Tutorial! This will guide you through the most important features of the program.") + tutorial.text = qsTr("Welcome to the ModOrganizer Tutorial! This will guide you through the most common features of MO." + + "\nPlease go along with the tutorial because some things can't be demonstrated if you skip something.") waitForClick() }, @@ -37,13 +38,26 @@ function getTutorialSteps() }, function() { - tutorial.text = qsTr("This list displays all mods installed through MO. The first point in our agenda will be adding some stuff to it.") + tutorial.text = qsTr("This list displays all mods installed through MO. It also displays installed DLCs and some mods " + + "installed outside MO but you have limited control over those in MO.") highlightItem("modList", false) waitForClick() }, function() { - tutorial.text = qsTr("There are a few ways to get mods into ModOrganizer. You can use your regular browser to send download from nexusmods to MO." + tutorial.text = qsTr("Before we start installing mods, let's have a quick look at the settings.") + highlightAction("actionSettings", true) + tutorialControl.waitForAction("actionSettings") + }, + + function() { + tutorial.text = qsTr("Now it's time to install a few mods!" + + "Please go along with this because we need a few mods installed to demonstrate other features") + waitForClick() + }, + + function() { + tutorial.text = qsTr("There are a few ways to get mods into ModOrganizer. You can use your regular browser to send download from Nexus to MO. " + "Click on \"Nexus\" to open the appropriate nexusmods page. This will also register ModOrganizer as the downloader " + "for \"nxm links\" for the game MO is managing. \"nxm links\" are the green buttons on Nexus saying \"Download with Manager\".") highlightAction("actionNexus", true) @@ -51,7 +65,7 @@ function getTutorialSteps() }, function() { - tutorial.text = qsTr("Downloads will appear here. Double click one to install it.") + tutorial.text = qsTr("Downloads will appear here. Please download at least one mod, then double click it to install.") applicationWindow.modInstalled.connect(nextStep) highlightItem("downloadView", true) }, diff --git a/src/tutorials/tutorials_mainwindow.qml b/src/tutorials/tutorials_mainwindow.qml index 6b4e6b30..0012b686 100644 --- a/src/tutorials/tutorials_mainwindow.qml +++ b/src/tutorials/tutorials_mainwindow.qml @@ -1,6 +1,5 @@ import QtQuick 1.1 - TutorialOverlay { id: tutorial offsetBottom: 50 -- cgit v1.3.1